Compare commits
2 Commits
1b5d0fbf7d
...
2bbcae2e7a
| Author | SHA1 | Date | |
|---|---|---|---|
| 2bbcae2e7a | |||
| 4d63b17e68 |
@ -10,9 +10,11 @@ https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
|
||||
# asgi.py
|
||||
|
||||
import os
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings")
|
||||
|
||||
import django
|
||||
|
||||
django.setup()
|
||||
|
||||
|
||||
@ -30,11 +32,17 @@ from django.core.asgi import get_asgi_application
|
||||
# # "websocket": AuthMiddlewareStack(URLRouter(routing.websocket_urlpatterns)),
|
||||
# }
|
||||
# )
|
||||
application = ProtocolTypeRouter({
|
||||
"http": AuthMiddlewareStack(
|
||||
URLRouter([
|
||||
path("sse/notifications/", NotificationSSEApp()),
|
||||
re_path(r"", get_asgi_application()), # All other routes go to Django
|
||||
])
|
||||
),
|
||||
})
|
||||
application = ProtocolTypeRouter(
|
||||
{
|
||||
"http": AuthMiddlewareStack(
|
||||
URLRouter(
|
||||
[
|
||||
path("sse/notifications/", NotificationSSEApp()),
|
||||
re_path(
|
||||
r"", get_asgi_application()
|
||||
), # All other routes go to Django
|
||||
]
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@ -56,7 +56,7 @@ from .models import (
|
||||
DealerSettings,
|
||||
Tasks,
|
||||
Recall,
|
||||
Ticket
|
||||
Ticket,
|
||||
)
|
||||
from django_ledger import models as ledger_models
|
||||
from django.forms import (
|
||||
@ -146,9 +146,16 @@ class StaffForm(forms.ModelForm):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Staff
|
||||
fields = ["first_name","last_name", "arabic_name", "phone_number", "address", "logo", "group"]
|
||||
fields = [
|
||||
"first_name",
|
||||
"last_name",
|
||||
"arabic_name",
|
||||
"phone_number",
|
||||
"address",
|
||||
"logo",
|
||||
"group",
|
||||
]
|
||||
|
||||
|
||||
# Dealer Form
|
||||
@ -439,13 +446,15 @@ class CarFinanceForm(forms.ModelForm):
|
||||
marked_price = cleaned_data.get("marked_price")
|
||||
|
||||
if cost_price > marked_price:
|
||||
raise forms.ValidationError({"cost_price": "Cost price should not be greater than marked price"})
|
||||
raise forms.ValidationError(
|
||||
{"cost_price": "Cost price should not be greater than marked price"}
|
||||
)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
class Meta:
|
||||
model = Car
|
||||
fields = ["cost_price","marked_price"]
|
||||
fields = ["cost_price", "marked_price"]
|
||||
|
||||
|
||||
class CarLocationForm(forms.ModelForm):
|
||||
@ -1168,7 +1177,7 @@ class ScheduleForm(forms.ModelForm):
|
||||
scheduled_at = forms.DateTimeField(
|
||||
widget=DateTimeInput(attrs={"type": "datetime-local"})
|
||||
)
|
||||
reminder = forms.BooleanField(help_text=_("Send a reminder?"),required=False)
|
||||
reminder = forms.BooleanField(help_text=_("Send a reminder?"), required=False)
|
||||
|
||||
class Meta:
|
||||
model = Schedule
|
||||
@ -1289,6 +1298,7 @@ class OpportunityForm(forms.ModelForm):
|
||||
if self.instance and self.instance.pk:
|
||||
self.fields["probability"].initial = self.instance.probability
|
||||
|
||||
|
||||
class OpportunityStageForm(forms.ModelForm):
|
||||
"""
|
||||
Represents a form for creating or editing Opportunity instances.
|
||||
@ -1305,17 +1315,13 @@ class OpportunityStageForm(forms.ModelForm):
|
||||
:type Meta.fields: list
|
||||
"""
|
||||
|
||||
|
||||
class Meta:
|
||||
model = Opportunity
|
||||
fields = [
|
||||
"stage",
|
||||
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
class InvoiceModelCreateForm(InvoiceModelCreateFormBase):
|
||||
"""
|
||||
Represents a form for creating an Invoice model that inherits from a base
|
||||
@ -1633,8 +1639,7 @@ class PermissionForm(forms.ModelForm):
|
||||
"django_ledger.billmodeldjango_ledger.itemmodel",
|
||||
"django_ledger.invoicemodel",
|
||||
"django_ledger.vendormodel",
|
||||
"django_ledger.journalentrymodel"
|
||||
"django_ledger.purchaseordermodel",
|
||||
"django_ledger.journalentrymodeldjango_ledger.purchaseordermodel",
|
||||
]
|
||||
|
||||
permissions = cache.get(
|
||||
@ -2138,91 +2143,115 @@ class VatRateForm(forms.ModelForm):
|
||||
class CustomSetPasswordForm(SetPasswordForm):
|
||||
new_password1 = forms.CharField(
|
||||
label="New Password",
|
||||
widget=forms.PasswordInput(attrs={'class': 'form-control', 'placeholder': 'New Password'})
|
||||
widget=forms.PasswordInput(
|
||||
attrs={"class": "form-control", "placeholder": "New Password"}
|
||||
),
|
||||
)
|
||||
new_password2 = forms.CharField(
|
||||
label="Confirm New Password",
|
||||
widget=forms.PasswordInput(attrs={'class': 'form-control', 'placeholder': 'Confirm New Password'})
|
||||
widget=forms.PasswordInput(
|
||||
attrs={"class": "form-control", "placeholder": "Confirm New Password"}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# forms.py
|
||||
class RecallFilterForm(forms.Form):
|
||||
make = forms.ModelChoiceField(
|
||||
queryset=CarMake.objects.all(),
|
||||
required=False,
|
||||
label=_("Make"),
|
||||
widget=forms.Select(attrs={'class': 'form-control'})
|
||||
widget=forms.Select(attrs={"class": "form-control"}),
|
||||
)
|
||||
model = forms.ModelChoiceField(
|
||||
queryset=CarModel.objects.none(),
|
||||
required=False,
|
||||
label=_("Model"),
|
||||
widget=forms.Select(attrs={'class': 'form-control'})
|
||||
widget=forms.Select(attrs={"class": "form-control"}),
|
||||
)
|
||||
serie = forms.ModelChoiceField(
|
||||
queryset=CarSerie.objects.none(),
|
||||
required=False,
|
||||
label=_("Series"),
|
||||
widget=forms.Select(attrs={'class': 'form-control'})
|
||||
widget=forms.Select(attrs={"class": "form-control"}),
|
||||
)
|
||||
trim = forms.ModelChoiceField(
|
||||
queryset=CarTrim.objects.none(),
|
||||
required=False,
|
||||
label=_("Trim"),
|
||||
widget=forms.Select(attrs={'class': 'form-control'})
|
||||
widget=forms.Select(attrs={"class": "form-control"}),
|
||||
)
|
||||
year_from = forms.IntegerField(
|
||||
required=False,
|
||||
label=_("From Year"),
|
||||
widget=forms.NumberInput(attrs={"class": "form-control"}),
|
||||
)
|
||||
year_to = forms.IntegerField(
|
||||
required=False,
|
||||
label=_("To Year"),
|
||||
widget=forms.NumberInput(attrs={"class": "form-control"}),
|
||||
)
|
||||
year_from = forms.IntegerField(required=False, label=_("From Year"),
|
||||
widget=forms.NumberInput(attrs={'class': 'form-control'}))
|
||||
year_to = forms.IntegerField(required=False, label=_("To Year"),
|
||||
widget=forms.NumberInput(attrs={'class': 'form-control'}))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
make_id = kwargs.pop('make_id', None)
|
||||
model_id = kwargs.pop('model_id', None)
|
||||
serie_id = kwargs.pop('serie_id', None)
|
||||
make_id = kwargs.pop("make_id", None)
|
||||
model_id = kwargs.pop("model_id", None)
|
||||
serie_id = kwargs.pop("serie_id", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if make_id:
|
||||
self.fields['model'].queryset = CarModel.objects.filter(id_car_make_id=make_id)
|
||||
self.fields["model"].queryset = CarModel.objects.filter(
|
||||
id_car_make_id=make_id
|
||||
)
|
||||
if model_id:
|
||||
self.fields['serie'].queryset = CarSerie.objects.filter(id_car_model_id=model_id)
|
||||
self.fields["serie"].queryset = CarSerie.objects.filter(
|
||||
id_car_model_id=model_id
|
||||
)
|
||||
if serie_id:
|
||||
self.fields['trim'].queryset = CarTrim.objects.filter(id_car_serie_id=serie_id)
|
||||
self.fields["trim"].queryset = CarTrim.objects.filter(
|
||||
id_car_serie_id=serie_id
|
||||
)
|
||||
|
||||
|
||||
class RecallCreateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Recall
|
||||
fields = ['title', 'description', 'make', 'model', 'serie', 'trim', 'year_from', 'year_to']
|
||||
fields = [
|
||||
"title",
|
||||
"description",
|
||||
"make",
|
||||
"model",
|
||||
"serie",
|
||||
"trim",
|
||||
"year_from",
|
||||
"year_to",
|
||||
]
|
||||
widgets = {
|
||||
'make': forms.Select(attrs={'class': 'form-control'}),
|
||||
'model': forms.Select(attrs={'class': 'form-control'}),
|
||||
'serie': forms.Select(attrs={'class': 'form-control'}),
|
||||
'trim': forms.Select(attrs={'class': 'form-control'}),
|
||||
'title': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'description': forms.Textarea(attrs={'class': 'form-control'}),
|
||||
'year_from': forms.NumberInput(attrs={'class': 'form-control'}),
|
||||
'year_to': forms.NumberInput(attrs={'class': 'form-control'}),
|
||||
"make": forms.Select(attrs={"class": "form-control"}),
|
||||
"model": forms.Select(attrs={"class": "form-control"}),
|
||||
"serie": forms.Select(attrs={"class": "form-control"}),
|
||||
"trim": forms.Select(attrs={"class": "form-control"}),
|
||||
"title": forms.TextInput(attrs={"class": "form-control"}),
|
||||
"description": forms.Textarea(attrs={"class": "form-control"}),
|
||||
"year_from": forms.NumberInput(attrs={"class": "form-control"}),
|
||||
"year_to": forms.NumberInput(attrs={"class": "form-control"}),
|
||||
}
|
||||
|
||||
|
||||
class TicketForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Ticket
|
||||
fields = ['subject', 'description', 'priority']
|
||||
fields = ["subject", "description", "priority"]
|
||||
widgets = {
|
||||
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 10}),
|
||||
"description": forms.Textarea(attrs={"class": "form-control", "rows": 10}),
|
||||
}
|
||||
|
||||
|
||||
class TicketResolutionForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Ticket
|
||||
fields = ['status', 'resolution_notes']
|
||||
fields = ["status", "resolution_notes"]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Limit status choices to resolution options
|
||||
self.fields['status'].choices = [
|
||||
('resolved', 'Resolved'),
|
||||
('closed', 'Closed')
|
||||
]
|
||||
self.fields["status"].choices = [("resolved", "Resolved"), ("closed", "Closed")]
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import logging
|
||||
from inventory.models import Dealer
|
||||
from .utils import get_accounts_data,create_account
|
||||
from .utils import get_accounts_data, create_account
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def check_create_coa_accounts(task):
|
||||
logger.info("Checking if all accounts are created")
|
||||
instance = task.kwargs["dealer"]
|
||||
@ -17,7 +18,8 @@ def check_create_coa_accounts(task):
|
||||
logger.info(f"Default account does not exist: {account_data['code']}")
|
||||
create_account(entity, coa, account_data)
|
||||
|
||||
|
||||
def print_results(task):
|
||||
dealer= task.kwargs["dealer"]
|
||||
print("HOOK: ",dealer)
|
||||
print("HOOK: ",dealer.pk)
|
||||
dealer = task.kwargs["dealer"]
|
||||
print("HOOK: ", dealer)
|
||||
print("HOOK: ", dealer.pk)
|
||||
|
||||
@ -8,11 +8,12 @@ from django.core.management.base import BaseCommand
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django_ledger.models import InvoiceModel,EstimateModel
|
||||
from inventory.models import ExtraInfo,Notification,CustomGroup
|
||||
from django_ledger.models import InvoiceModel, EstimateModel
|
||||
from inventory.models import ExtraInfo, Notification, CustomGroup
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Handles invoices due date reminders"
|
||||
|
||||
@ -33,27 +34,30 @@ class Command(BaseCommand):
|
||||
|
||||
def invocie_expiration_reminders(self):
|
||||
"""Queue email reminders for expiring plans"""
|
||||
reminder_days = getattr(settings, 'INVOICE_PAST_DUE_REMIND', [3, 7, 14])
|
||||
reminder_days = getattr(settings, "INVOICE_PAST_DUE_REMIND", [3, 7, 14])
|
||||
today = timezone.now().date()
|
||||
|
||||
for days in reminder_days:
|
||||
target_date = today + timedelta(days=days)
|
||||
expiring_plans = InvoiceModel.objects.filter(
|
||||
date_due=target_date
|
||||
).select_related('customer','ce_model')
|
||||
).select_related("customer", "ce_model")
|
||||
|
||||
for inv in expiring_plans:
|
||||
# dealer = inv.customer.customer_set.first().dealer
|
||||
subject = f"Your invoice is due in {days} days"
|
||||
message = render_to_string('emails/invoice_past_due_reminder.txt', {
|
||||
'customer_name': inv.customer.customer_name,
|
||||
'invoice_number': inv.invoice_number,
|
||||
'amount_due': inv.amount_due,
|
||||
'days_past_due': inv.due_in_days(),
|
||||
'SITE_NAME': settings.SITE_NAME
|
||||
})
|
||||
message = render_to_string(
|
||||
"emails/invoice_past_due_reminder.txt",
|
||||
{
|
||||
"customer_name": inv.customer.customer_name,
|
||||
"invoice_number": inv.invoice_number,
|
||||
"amount_due": inv.amount_due,
|
||||
"days_past_due": inv.due_in_days(),
|
||||
"SITE_NAME": settings.SITE_NAME,
|
||||
},
|
||||
)
|
||||
send_email(
|
||||
'noreply@yourdomain.com',
|
||||
"noreply@yourdomain.com",
|
||||
inv.customer.email,
|
||||
subject,
|
||||
message,
|
||||
@ -65,21 +69,24 @@ class Command(BaseCommand):
|
||||
"""Queue email reminders for expiring plans"""
|
||||
today = timezone.now().date()
|
||||
expiring_plans = InvoiceModel.objects.filter(
|
||||
date_due__lte = today
|
||||
).select_related('customer','ce_model')
|
||||
date_due__lte=today
|
||||
).select_related("customer", "ce_model")
|
||||
|
||||
# Send email
|
||||
for inv in expiring_plans:
|
||||
dealer = inv.customer.customer_set.first().dealer
|
||||
|
||||
subject = f"Your invoice is past due"
|
||||
message = render_to_string('emails/invoice_past_due.txt', {
|
||||
'customer_name': inv.customer.customer_name,
|
||||
'invoice_number': inv.invoice_number,
|
||||
'amount_due': inv.amount_due,
|
||||
'days_past_due': (today - inv.date_due).days,
|
||||
'SITE_NAME': settings.SITE_NAME
|
||||
})
|
||||
message = render_to_string(
|
||||
"emails/invoice_past_due.txt",
|
||||
{
|
||||
"customer_name": inv.customer.customer_name,
|
||||
"invoice_number": inv.invoice_number,
|
||||
"amount_due": inv.amount_due,
|
||||
"days_past_due": (today - inv.date_due).days,
|
||||
"SITE_NAME": settings.SITE_NAME,
|
||||
},
|
||||
)
|
||||
|
||||
# send notification to accountatnt
|
||||
recipients = (
|
||||
@ -90,24 +97,28 @@ class Command(BaseCommand):
|
||||
)
|
||||
for rec in recipients:
|
||||
Notification.objects.create(
|
||||
user=rec,
|
||||
message=_(
|
||||
"""
|
||||
user=rec,
|
||||
message=_(
|
||||
"""
|
||||
Invoice {invoice_number} is past due,please your
|
||||
<a href="{url}" target="_blank">View</a>.
|
||||
"""
|
||||
).format(
|
||||
invoice_number=inv.invoice_number,
|
||||
url=reverse(
|
||||
"invoice_detail",
|
||||
kwargs={"dealer_slug": dealer.slug, "entity_slug": dealer.entity.slug, "pk": inv.pk},
|
||||
).format(
|
||||
invoice_number=inv.invoice_number,
|
||||
url=reverse(
|
||||
"invoice_detail",
|
||||
kwargs={
|
||||
"dealer_slug": dealer.slug,
|
||||
"entity_slug": dealer.entity.slug,
|
||||
"pk": inv.pk,
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
# send email to customer
|
||||
send_email(
|
||||
'noreply@yourdomain.com',
|
||||
"noreply@yourdomain.com",
|
||||
inv.customer.email,
|
||||
subject,
|
||||
message,
|
||||
@ -131,4 +142,4 @@ class Command(BaseCommand):
|
||||
# created__lt=cutoff,
|
||||
# status=Order.STATUS.NEW
|
||||
# ).delete()
|
||||
# self.stdout.write(f"Cleaned up {count} old incomplete orders")
|
||||
# self.stdout.write(f"Cleaned up {count} old incomplete orders")
|
||||
|
||||
@ -2,9 +2,11 @@ from decimal import Decimal
|
||||
import random
|
||||
from django.core.management.base import BaseCommand
|
||||
from inventory.models import Car
|
||||
from django_ledger.models import EntityModel,InvoiceModel,ItemModel
|
||||
from django_ledger.models import EntityModel, InvoiceModel, ItemModel
|
||||
from inventory.utils import CarFinanceCalculator
|
||||
from rich import print
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = ""
|
||||
|
||||
@ -14,27 +16,43 @@ class Command(BaseCommand):
|
||||
admin = e.admin
|
||||
# estimate = e.get_estimates().first()
|
||||
# e.create_invoice(coa_model=e.get_default_coa(), customer_model=customer, terms="net_30")
|
||||
i=InvoiceModel.objects.first()
|
||||
i = InvoiceModel.objects.first()
|
||||
|
||||
calc = CarFinanceCalculator(i)
|
||||
data = calc.get_finance_data()
|
||||
for car_data in data['cars']:
|
||||
car = i.get_itemtxs_data()[0].filter(
|
||||
item_model__car__vin=car_data['vin']
|
||||
).first().item_model.car
|
||||
calc = CarFinanceCalculator(i)
|
||||
data = calc.get_finance_data()
|
||||
for car_data in data["cars"]:
|
||||
car = (
|
||||
i.get_itemtxs_data()[0]
|
||||
.filter(item_model__car__vin=car_data["vin"])
|
||||
.first()
|
||||
.item_model.car
|
||||
)
|
||||
print("car", car)
|
||||
qty = Decimal(car_data['quantity'])
|
||||
qty = Decimal(car_data["quantity"])
|
||||
print("qty", qty)
|
||||
|
||||
# amounts from calculator
|
||||
net_car_price = Decimal(car_data['total']) # after discount
|
||||
net_add_price = Decimal(data['total_additionals']) # per car or split however you want
|
||||
vat_amount = Decimal(data['total_vat_amount']) * qty # prorate if multi-qty
|
||||
net_car_price = Decimal(car_data["total"]) # after discount
|
||||
net_add_price = Decimal(
|
||||
data["total_additionals"]
|
||||
) # per car or split however you want
|
||||
vat_amount = Decimal(data["total_vat_amount"]) * qty # prorate if multi-qty
|
||||
# grand_total = net_car_price + net_add_price + vat_amount
|
||||
grand_total = Decimal(data['grand_total'])
|
||||
cost_total = Decimal(car_data['cost_price']) * qty
|
||||
grand_total = Decimal(data["grand_total"])
|
||||
cost_total = Decimal(car_data["cost_price"]) * qty
|
||||
|
||||
print("net_car_price", net_car_price, "net_add_price", net_add_price, "vat_amount", vat_amount, "grand_total", grand_total, "cost_total", cost_total)
|
||||
print(
|
||||
"net_car_price",
|
||||
net_car_price,
|
||||
"net_add_price",
|
||||
net_add_price,
|
||||
"vat_amount",
|
||||
vat_amount,
|
||||
"grand_total",
|
||||
grand_total,
|
||||
"cost_total",
|
||||
cost_total,
|
||||
)
|
||||
|
||||
# acc_cars = e.get_coa_accounts().get(name="Inventory (Cars)")
|
||||
# acc_sales = e.get_coa_accounts().get(name="Car Sales")
|
||||
@ -76,4 +94,4 @@ class Command(BaseCommand):
|
||||
# operation=InvoiceModel.ITEMIZE_APPEND)
|
||||
# print(i.amount_due)
|
||||
|
||||
# i.save()
|
||||
# i.save()
|
||||
|
||||
@ -2,8 +2,11 @@ from django.core.management.base import BaseCommand
|
||||
from django.contrib.auth import get_user_model
|
||||
import datetime
|
||||
from inventory.models import Dealer
|
||||
from plans.models import Plan, Order,PlanPricing
|
||||
from plans.models import Plan, Order, PlanPricing
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = ""
|
||||
|
||||
@ -25,4 +28,4 @@ class Command(BaseCommand):
|
||||
)
|
||||
|
||||
order.complete_order()
|
||||
print(user.userplan)
|
||||
print(user.userplan)
|
||||
|
||||
@ -11,6 +11,7 @@ from inventory.tasks import send_bilingual_reminder, handle_email_result
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Handles subscription plan maintenance tasks"
|
||||
|
||||
@ -30,17 +31,18 @@ class Command(BaseCommand):
|
||||
|
||||
def send_expiration_reminders(self):
|
||||
"""Queue email reminders for expiring plans"""
|
||||
reminder_days = getattr(settings, 'PLANS_EXPIRATION_REMIND', [3, 7, 14])
|
||||
reminder_days = getattr(settings, "PLANS_EXPIRATION_REMIND", [3, 7, 14])
|
||||
today = timezone.now().date()
|
||||
|
||||
for days in reminder_days:
|
||||
target_date = today + timedelta(days=days)
|
||||
expiring_plans = UserPlan.objects.filter(
|
||||
active=True,
|
||||
expire=target_date
|
||||
).select_related('user', 'plan')
|
||||
active=True, expire=target_date
|
||||
).select_related("user", "plan")
|
||||
|
||||
self.stdout.write(f"Queuing {days}-day reminders for {expiring_plans.count()} plans")
|
||||
self.stdout.write(
|
||||
f"Queuing {days}-day reminders for {expiring_plans.count()} plans"
|
||||
)
|
||||
|
||||
for user_plan in expiring_plans:
|
||||
# Queue email task
|
||||
@ -50,14 +52,13 @@ class Command(BaseCommand):
|
||||
user_plan.plan_id,
|
||||
user_plan.expire,
|
||||
days,
|
||||
hook=handle_email_result
|
||||
hook=handle_email_result,
|
||||
)
|
||||
|
||||
def deactivate_expired_plans(self):
|
||||
"""Deactivate plans that have expired (synchronous)"""
|
||||
expired_plans = UserPlan.objects.filter(
|
||||
active=True,
|
||||
expire__lt=timezone.now().date()
|
||||
active=True, expire__lt=timezone.now().date()
|
||||
)
|
||||
count = expired_plans.update(active=False)
|
||||
self.stdout.write(f"Deactivated {count} expired plans")
|
||||
@ -66,7 +67,6 @@ class Command(BaseCommand):
|
||||
"""Delete incomplete orders older than 30 days"""
|
||||
cutoff = timezone.now() - timedelta(days=30)
|
||||
count, _ = Order.objects.filter(
|
||||
created__lt=cutoff,
|
||||
status=Order.STATUS.NEW
|
||||
created__lt=cutoff, status=Order.STATUS.NEW
|
||||
).delete()
|
||||
self.stdout.write(f"Cleaned up {count} old incomplete orders")
|
||||
self.stdout.write(f"Cleaned up {count} old incomplete orders")
|
||||
|
||||
@ -5,5 +5,10 @@ from django_q.tasks import async_task, result
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **kwargs):
|
||||
from inventory.models import Dealer
|
||||
|
||||
instance = Dealer.objects.first()
|
||||
async_task(func="inventory.tasks.test_task",dealer=instance,hook="inventory.hooks.print_results")
|
||||
async_task(
|
||||
func="inventory.tasks.test_task",
|
||||
dealer=instance,
|
||||
hook="inventory.hooks.print_results",
|
||||
)
|
||||
|
||||
@ -3,21 +3,24 @@ import json, random, string, decimal
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.test import Client
|
||||
from django.contrib.auth import get_user_model
|
||||
from plans.models import Order, PlanPricing, AbstractOrder, UserPlan, BillingInfo,Plan
|
||||
from plans.models import Order, PlanPricing, AbstractOrder, UserPlan, BillingInfo, Plan
|
||||
from inventory.tasks import create_user_dealer
|
||||
from inventory import models # adjust import to your app
|
||||
from inventory import models # adjust import to your app
|
||||
from django_q.tasks import async_task
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Seed a full dealership via the real signup & downstream views"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--count', type=int, default=1, help='Number of dealers to seed')
|
||||
parser.add_argument(
|
||||
"--count", type=int, default=1, help="Number of dealers to seed"
|
||||
)
|
||||
|
||||
def handle(self, *args, **opts):
|
||||
count = opts['count']
|
||||
count = opts["count"]
|
||||
client = Client() # lives inside management command
|
||||
|
||||
for n in range(6, 9):
|
||||
@ -43,7 +46,16 @@ class Command(BaseCommand):
|
||||
"address": f"Street {n}, Riyadh",
|
||||
}
|
||||
|
||||
dealer = create_user_dealer(payload['email'], payload['password'], payload['name'], payload['arabic_name'], payload['phone_number'], payload['crn'], payload['vrn'], payload['address'])
|
||||
dealer = create_user_dealer(
|
||||
payload["email"],
|
||||
payload["password"],
|
||||
payload["name"],
|
||||
payload["arabic_name"],
|
||||
payload["phone_number"],
|
||||
payload["crn"],
|
||||
payload["vrn"],
|
||||
payload["address"],
|
||||
)
|
||||
user = dealer.user
|
||||
self._assign_random_plan(user)
|
||||
self._services(dealer)
|
||||
@ -61,7 +73,7 @@ class Command(BaseCommand):
|
||||
|
||||
return payload["email"]
|
||||
|
||||
def _assign_random_plan(self,user):
|
||||
def _assign_random_plan(self, user):
|
||||
"""
|
||||
Pick a random Plan and create + initialize a UserPlan for the user.
|
||||
"""
|
||||
@ -72,14 +84,13 @@ class Command(BaseCommand):
|
||||
plan = random.choice(plans)
|
||||
|
||||
user_plan, created = UserPlan.objects.get_or_create(
|
||||
user=user,
|
||||
defaults={'plan': plan, 'active': True}
|
||||
user=user, defaults={"plan": plan, "active": True}
|
||||
)
|
||||
if created:
|
||||
user_plan.initialize()
|
||||
return user_plan
|
||||
|
||||
def _services(self,dealer):
|
||||
def _services(self, dealer):
|
||||
additional_services = [
|
||||
{
|
||||
"name": "Vehicle registration transfer assistance",
|
||||
@ -114,5 +125,5 @@ class Command(BaseCommand):
|
||||
price=additional_service["price"],
|
||||
description=additional_service["description"],
|
||||
dealer=dealer,
|
||||
uom="Unit"
|
||||
uom="Unit",
|
||||
)
|
||||
|
||||
@ -4,11 +4,31 @@ import json, random, string, decimal
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.test import Client
|
||||
from django.contrib.auth import get_user_model
|
||||
from plans.models import Order, PlanPricing, AbstractOrder, UserPlan, BillingInfo,Plan
|
||||
from plans.models import Order, PlanPricing, AbstractOrder, UserPlan, BillingInfo, Plan
|
||||
from inventory.services import decodevin
|
||||
from inventory.tasks import create_user_dealer
|
||||
from inventory.models import AdditionalServices, Car, CarColors, CarFinance, CarMake, CustomGroup, Customer, Dealer, ExteriorColors, InteriorColors, Lead, UnitOfMeasure,Vendor,Staff
|
||||
from django_ledger.models import PurchaseOrderModel,ItemTransactionModel,ItemModel,EntityModel
|
||||
from inventory.models import (
|
||||
AdditionalServices,
|
||||
Car,
|
||||
CarColors,
|
||||
CarFinance,
|
||||
CarMake,
|
||||
CustomGroup,
|
||||
Customer,
|
||||
Dealer,
|
||||
ExteriorColors,
|
||||
InteriorColors,
|
||||
Lead,
|
||||
UnitOfMeasure,
|
||||
Vendor,
|
||||
Staff,
|
||||
)
|
||||
from django_ledger.models import (
|
||||
PurchaseOrderModel,
|
||||
ItemTransactionModel,
|
||||
ItemModel,
|
||||
EntityModel,
|
||||
)
|
||||
from django_q.tasks import async_task
|
||||
from faker import Faker
|
||||
from appointment.models import Appointment, AppointmentRequest, Service, StaffMember
|
||||
@ -16,6 +36,7 @@ from appointment.models import Appointment, AppointmentRequest, Service, StaffMe
|
||||
User = get_user_model()
|
||||
fake = Faker()
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Seed a full dealership via the real signup & downstream views"
|
||||
|
||||
@ -31,7 +52,6 @@ class Command(BaseCommand):
|
||||
# self._create_randome_services(dealer)
|
||||
# self._create_random_lead(dealer)
|
||||
|
||||
|
||||
# dealer = Dealer.objects.get(name="Dealer #6")
|
||||
# coa_model = dealer.entity.get_default_coa()
|
||||
# inventory_account = dealer.entity.get_all_accounts().get(name="Inventory (Cars)")
|
||||
@ -43,20 +63,32 @@ class Command(BaseCommand):
|
||||
self.stdout.write(self.style.SUCCESS(f"✅ PO created for {dealers}"))
|
||||
|
||||
def _create_random_po(self, dealer):
|
||||
for i in range(random.randint(1,70)):
|
||||
for i in range(random.randint(1, 70)):
|
||||
try:
|
||||
e: EntityModel = dealer.entity
|
||||
e.create_purchase_order(po_title=f"Test PO {random.randint(1,9999)}-{i}")
|
||||
e.create_purchase_order(
|
||||
po_title=f"Test PO {random.randint(1, 9999)}-{i}"
|
||||
)
|
||||
except Exception as e:
|
||||
self.stderr.write(self.style.ERROR(f"Error : {e}"))
|
||||
|
||||
def _create_random_vendors(self, dealer):
|
||||
for i in range(random.randint(1,50)):
|
||||
for i in range(random.randint(1, 50)):
|
||||
try:
|
||||
name = fake.name()
|
||||
n = random.randint(1,9999)
|
||||
phone = f"05678{random.randint(0,9)}{random.randint(0,9)}{random.randint(0,9)}{random.randint(0,9)}{random.randint(0,9)}"
|
||||
Vendor.objects.create(dealer=dealer, name=f"{name}{n}", arabic_name=f"{name}{n}", email=f"{name}{n}@tenhal.sa", phone_number=phone,crn=f"CRN {n}", vrn=f"VRN {n}", address=f"Address {fake.address()}",contact_person=f"Contact Person {name}{n}")
|
||||
n = random.randint(1, 9999)
|
||||
phone = f"05678{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}"
|
||||
Vendor.objects.create(
|
||||
dealer=dealer,
|
||||
name=f"{name}{n}",
|
||||
arabic_name=f"{name}{n}",
|
||||
email=f"{name}{n}@tenhal.sa",
|
||||
phone_number=phone,
|
||||
crn=f"CRN {n}",
|
||||
vrn=f"VRN {n}",
|
||||
address=f"Address {fake.address()}",
|
||||
contact_person=f"Contact Person {name}{n}",
|
||||
)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
@ -65,7 +97,9 @@ class Command(BaseCommand):
|
||||
name = f"{fake.name()}{i}"
|
||||
email = fake.email()
|
||||
password = "Tenhal@123"
|
||||
user = User.objects.create_user(username=email, email=email, password=password)
|
||||
user = User.objects.create_user(
|
||||
username=email, email=email, password=password
|
||||
)
|
||||
user.is_staff = True
|
||||
user.save()
|
||||
|
||||
@ -74,17 +108,24 @@ class Command(BaseCommand):
|
||||
# for service in services:
|
||||
# staff_member.services_offered.add(service)
|
||||
|
||||
staff = Staff.objects.create(dealer=dealer,user=user,name=name,arabic_name=name,phone_number=fake.phone_number(),active=True)
|
||||
staff = Staff.objects.create(
|
||||
dealer=dealer,
|
||||
user=user,
|
||||
name=name,
|
||||
arabic_name=name,
|
||||
phone_number=fake.phone_number(),
|
||||
active=True,
|
||||
)
|
||||
|
||||
groups = CustomGroup.objects.filter(dealer=dealer)
|
||||
random_group = random.choice(list(groups))
|
||||
staff.add_group(random_group.group)
|
||||
# for i in range(random.randint(1,15)):
|
||||
# n = random.randint(1,9999)
|
||||
# phone = f"05678{random.randint(1,9999)}{random.randint(1,9999)}{random.randint(1,9999)}{random.randint(1,9999)}{random.randint(1,9999)}"
|
||||
# Vendor.objects.create(dealer=dealer, name=f"{fake.name}", arabic_name=f"{fake.first_name_female()} {fake.last_name_female()}", email=f"vendor{n}@test.com", phone_number=phone,crn=f"CRN {n}", vrn=f"VRN {n}", address=f"Address {fake.address()}",contact_person=f"Contact Person {n}")
|
||||
# n = random.randint(1,9999)
|
||||
# phone = f"05678{random.randint(1,9999)}{random.randint(1,9999)}{random.randint(1,9999)}{random.randint(1,9999)}{random.randint(1,9999)}"
|
||||
# Vendor.objects.create(dealer=dealer, name=f"{fake.name}", arabic_name=f"{fake.first_name_female()} {fake.last_name_female()}", email=f"vendor{n}@test.com", phone_number=phone,crn=f"CRN {n}", vrn=f"VRN {n}", address=f"Address {fake.address()}",contact_person=f"Contact Person {n}")
|
||||
|
||||
def _create_random_cars(self,dealer):
|
||||
def _create_random_cars(self, dealer):
|
||||
vendors = Vendor.objects.filter(dealer=dealer).all()
|
||||
|
||||
vin_list = [
|
||||
@ -103,18 +144,20 @@ class Command(BaseCommand):
|
||||
]
|
||||
for vin in vin_list:
|
||||
try:
|
||||
for _ in range(random.randint(1,2)):
|
||||
for _ in range(random.randint(1, 2)):
|
||||
vin = f"{vin[:-4]}{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}"
|
||||
result = decodevin(vin)
|
||||
make = CarMake.objects.get(name=result["maker"])
|
||||
model = make.carmodel_set.filter(name__contains=result["model"]).first()
|
||||
model = make.carmodel_set.filter(
|
||||
name__contains=result["model"]
|
||||
).first()
|
||||
if not model or model == "":
|
||||
model = random.choice(make.carmodel_set.all())
|
||||
year = result["modelYear"]
|
||||
serie = random.choice(model.carserie_set.all())
|
||||
trim = random.choice(serie.cartrim_set.all())
|
||||
vendor = random.choice(vendors)
|
||||
print(make, model, serie, trim, vendor,vin)
|
||||
print(make, model, serie, trim, vendor, vin)
|
||||
car = Car.objects.create(
|
||||
vin=vin,
|
||||
id_car_make=make,
|
||||
@ -128,9 +171,12 @@ class Command(BaseCommand):
|
||||
mileage=0,
|
||||
)
|
||||
print(car)
|
||||
cp=random.randint(10000, 100000)
|
||||
cp = random.randint(10000, 100000)
|
||||
CarFinance.objects.create(
|
||||
car=car, cost_price=cp, selling_price=0,marked_price=cp+random.randint(2000, 7000)
|
||||
car=car,
|
||||
cost_price=cp,
|
||||
selling_price=0,
|
||||
marked_price=cp + random.randint(2000, 7000),
|
||||
)
|
||||
CarColors.objects.create(
|
||||
car=car,
|
||||
@ -141,8 +187,8 @@ class Command(BaseCommand):
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
def _create_random_customers(self,dealer):
|
||||
for i in range(random.randint(1,60)):
|
||||
def _create_random_customers(self, dealer):
|
||||
for i in range(random.randint(1, 60)):
|
||||
try:
|
||||
c = Customer(
|
||||
dealer=dealer,
|
||||
@ -161,7 +207,7 @@ class Command(BaseCommand):
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
def _create_randome_services(self,dealer):
|
||||
def _create_randome_services(self, dealer):
|
||||
additional_services = [
|
||||
{
|
||||
"name": "Vehicle registration transfer assistance",
|
||||
@ -196,12 +242,11 @@ class Command(BaseCommand):
|
||||
price=additional_service["price"],
|
||||
description=additional_service["description"],
|
||||
dealer=dealer,
|
||||
uom=uom
|
||||
uom=uom,
|
||||
)
|
||||
|
||||
|
||||
def _create_random_lead(self,dealer):
|
||||
for i in range(random.randint(1,60)):
|
||||
def _create_random_lead(self, dealer):
|
||||
for i in range(random.randint(1, 60)):
|
||||
try:
|
||||
first_name = fake.name()
|
||||
last_name = fake.last_name()
|
||||
@ -224,7 +269,7 @@ class Command(BaseCommand):
|
||||
id_car_model=model,
|
||||
source="website",
|
||||
channel="website",
|
||||
staff=staff
|
||||
staff=staff,
|
||||
)
|
||||
c = Customer(
|
||||
dealer=dealer,
|
||||
@ -243,4 +288,4 @@ class Command(BaseCommand):
|
||||
lead.customer = c
|
||||
lead.save()
|
||||
except Exception as e:
|
||||
pass
|
||||
pass
|
||||
|
||||
@ -152,11 +152,22 @@ class DealerSlugMiddleware:
|
||||
|
||||
def process_view(self, request, view_func, view_args, view_kwargs):
|
||||
paths = [
|
||||
"/ar/signup/", "/en/signup/", "/ar/login/", "/en/login/",
|
||||
"/ar/logout/", "/en/logout/", "/en/ledger/", "/ar/ledger/",
|
||||
"/en/notifications/", "/ar/notifications/", "/en/appointment/",
|
||||
"/ar/appointment/", "/en/feature/recall/","/ar/feature/recall/",
|
||||
"/ar/help_center/", "/en/help_center/",
|
||||
"/ar/signup/",
|
||||
"/en/signup/",
|
||||
"/ar/login/",
|
||||
"/en/login/",
|
||||
"/ar/logout/",
|
||||
"/en/logout/",
|
||||
"/en/ledger/",
|
||||
"/ar/ledger/",
|
||||
"/en/notifications/",
|
||||
"/ar/notifications/",
|
||||
"/en/appointment/",
|
||||
"/ar/appointment/",
|
||||
"/en/feature/recall/",
|
||||
"/ar/feature/recall/",
|
||||
"/ar/help_center/",
|
||||
"/en/help_center/",
|
||||
]
|
||||
print("------------------------------------")
|
||||
print(request.path in paths)
|
||||
|
||||
@ -42,12 +42,14 @@ from django_ledger.models import (
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
|
||||
# from appointment.models import StaffMember
|
||||
from plans.quota import get_user_quota
|
||||
from plans.models import UserPlan
|
||||
from django.db.models import Q
|
||||
from imagekit.models import ImageSpecField
|
||||
from imagekit.processors import ResizeToFill
|
||||
|
||||
# from plans.models import AbstractPlan
|
||||
# from simple_history.models import HistoricalRecords
|
||||
from plans.models import Invoice
|
||||
@ -229,7 +231,9 @@ class CarMake(models.Model, LocalizedNameMixin):
|
||||
name = models.CharField(max_length=255, blank=True, null=True)
|
||||
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True)
|
||||
arabic_name = models.CharField(max_length=255, blank=True, null=True)
|
||||
logo = models.ImageField(_("logo"), upload_to="car_make", blank=True, null=True,default="user-logo.jpg")
|
||||
logo = models.ImageField(
|
||||
_("logo"), upload_to="car_make", blank=True, null=True, default="user-logo.jpg"
|
||||
)
|
||||
is_sa_import = models.BooleanField(default=False)
|
||||
car_type = models.SmallIntegerField(choices=CarType.choices, blank=True, null=True)
|
||||
|
||||
@ -589,7 +593,7 @@ class AdditionalServices(models.Model, LocalizedNameMixin):
|
||||
"price_": str(self.price_),
|
||||
"taxable": self.taxable,
|
||||
"uom": self.uom,
|
||||
"service_tax":str(self.service_tax)
|
||||
"service_tax": str(self.service_tax),
|
||||
}
|
||||
|
||||
@property
|
||||
@ -604,9 +608,7 @@ class AdditionalServices(models.Model, LocalizedNameMixin):
|
||||
@property
|
||||
def service_tax(self):
|
||||
vat = VatRate.objects.filter(dealer=self.dealer, is_active=True).first()
|
||||
return (
|
||||
Decimal(self.price * vat.rate)
|
||||
)
|
||||
return Decimal(self.price * vat.rate)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Additional Services")
|
||||
@ -683,10 +685,13 @@ class Car(Base):
|
||||
)
|
||||
#
|
||||
additional_services = models.ManyToManyField(
|
||||
AdditionalServices, related_name="additionals", blank=True,null=True
|
||||
AdditionalServices, related_name="additionals", blank=True, null=True
|
||||
)
|
||||
cost_price = models.DecimalField(
|
||||
max_digits=14, decimal_places=2, verbose_name=_("Cost Price"),default=Decimal("0.00")
|
||||
max_digits=14,
|
||||
decimal_places=2,
|
||||
verbose_name=_("Cost Price"),
|
||||
default=Decimal("0.00"),
|
||||
)
|
||||
selling_price = models.DecimalField(
|
||||
max_digits=14,
|
||||
@ -710,7 +715,7 @@ class Car(Base):
|
||||
remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks"))
|
||||
mileage = models.IntegerField(blank=True, null=True, verbose_name=_("Mileage"))
|
||||
receiving_date = models.DateTimeField(verbose_name=_("Receiving Date"))
|
||||
sold_date=models.DateTimeField(verbose_name=_("Sold Date"),null=True,blank=True)
|
||||
sold_date = models.DateTimeField(verbose_name=_("Sold Date"), null=True, blank=True)
|
||||
hash = models.CharField(
|
||||
max_length=64, blank=True, null=True, verbose_name=_("Hash")
|
||||
)
|
||||
@ -773,6 +778,7 @@ class Car(Base):
|
||||
@property
|
||||
def logo(self):
|
||||
return getattr(self.id_car_make, "logo", "")
|
||||
|
||||
# @property
|
||||
# def additional_services(self):
|
||||
# return self.additional_services.all()
|
||||
@ -787,9 +793,15 @@ class Car(Base):
|
||||
)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@property
|
||||
def invoice(self):
|
||||
return self.item_model.invoicemodel_set.first if self.item_model.invoicemodel_set.first() else None
|
||||
return (
|
||||
self.item_model.invoicemodel_set.first
|
||||
if self.item_model.invoicemodel_set.first()
|
||||
else None
|
||||
)
|
||||
|
||||
def get_transfer(self):
|
||||
return self.transfer_logs.filter(active=True).first()
|
||||
|
||||
@ -873,39 +885,54 @@ class Car(Base):
|
||||
car=self, exterior=exterior, interior=interior
|
||||
)
|
||||
self.save()
|
||||
|
||||
@property
|
||||
def logo(self):
|
||||
return self.id_car_make.logo.url if self.id_car_make.logo else None
|
||||
|
||||
#
|
||||
@property
|
||||
def get_additional_services_amount(self):
|
||||
return sum([Decimal(x.price) for x in self.additional_services.all()])
|
||||
|
||||
@property
|
||||
def get_additional_services_amount_(self):
|
||||
return sum([Decimal(x.price_) for x in self.additional_services.all()])
|
||||
|
||||
@property
|
||||
def get_additional_services_vat(self):
|
||||
vat = VatRate.objects.filter(dealer=self.dealer,is_active=True).first()
|
||||
return sum([Decimal((x.price)*(vat.rate)) for x in self.additional_services.filter(taxable=True)])
|
||||
vat = VatRate.objects.filter(dealer=self.dealer, is_active=True).first()
|
||||
return sum(
|
||||
[
|
||||
Decimal((x.price) * (vat.rate))
|
||||
for x in self.additional_services.filter(taxable=True)
|
||||
]
|
||||
)
|
||||
|
||||
def get_additional_services(self):
|
||||
vat = VatRate.objects.filter(dealer=self.dealer,is_active=True).first()
|
||||
return {"services": [[x,((x.price)*(vat.rate) if x.taxable else 0)] for x in self.additional_services.all()],
|
||||
"total_":self.get_additional_services_amount_,
|
||||
"total":self.get_additional_services_amount,
|
||||
"services_vat":self.get_additional_services_vat}
|
||||
vat = VatRate.objects.filter(dealer=self.dealer, is_active=True).first()
|
||||
return {
|
||||
"services": [
|
||||
[x, ((x.price) * (vat.rate) if x.taxable else 0)]
|
||||
for x in self.additional_services.all()
|
||||
],
|
||||
"total_": self.get_additional_services_amount_,
|
||||
"total": self.get_additional_services_amount,
|
||||
"services_vat": self.get_additional_services_vat,
|
||||
}
|
||||
|
||||
@property
|
||||
def final_price(self):
|
||||
return Decimal(self.marked_price -self.discount)
|
||||
return Decimal(self.marked_price - self.discount)
|
||||
|
||||
@property
|
||||
def vat_amount(self):
|
||||
vat = VatRate.objects.filter(dealer=self.dealer,is_active=True).first()
|
||||
vat = VatRate.objects.filter(dealer=self.dealer, is_active=True).first()
|
||||
return Decimal(self.final_price) * (vat.rate)
|
||||
|
||||
@property
|
||||
def total_services_and_car_vat(self):
|
||||
return self.vat_amount+self.get_additional_services()['services_vat']
|
||||
return self.vat_amount + self.get_additional_services()["services_vat"]
|
||||
|
||||
@property
|
||||
def final_price_plus_vat(self):
|
||||
@ -913,29 +940,32 @@ class Car(Base):
|
||||
|
||||
@property
|
||||
def final_price_plus_services_plus_vat(self):
|
||||
return Decimal(self.final_price_plus_vat) + Decimal(self.get_additional_services()['total_']) #total services with vat and car_sell price with vat
|
||||
return Decimal(self.final_price_plus_vat) + Decimal(
|
||||
self.get_additional_services()["total_"]
|
||||
) # total services with vat and car_sell price with vat
|
||||
|
||||
# to be used after invoice is created
|
||||
@property
|
||||
def invoice(self):
|
||||
return self.item_model.invoicemodel_set.first() or None
|
||||
|
||||
@property
|
||||
def estimate(self):
|
||||
return getattr(self.invoice,'ce_model',None)
|
||||
return getattr(self.invoice, "ce_model", None)
|
||||
|
||||
@property
|
||||
def discount(self):
|
||||
if not self.estimate:
|
||||
return 0
|
||||
try:
|
||||
instance = ExtraInfo.objects.get(
|
||||
dealer=self.dealer,
|
||||
content_type=ContentType.objects.get_for_model(EstimateModel),
|
||||
object_id=self.estimate.pk,
|
||||
)
|
||||
return Decimal(instance.data.get('discount',0))
|
||||
dealer=self.dealer,
|
||||
content_type=ContentType.objects.get_for_model(EstimateModel),
|
||||
object_id=self.estimate.pk,
|
||||
)
|
||||
return Decimal(instance.data.get("discount", 0))
|
||||
except ExtraInfo.DoesNotExist:
|
||||
return Decimal(0)
|
||||
|
||||
|
||||
return Decimal(0)
|
||||
|
||||
# def get_discount_amount(self,estimate,user):
|
||||
# try:
|
||||
@ -961,10 +991,6 @@ class Car(Base):
|
||||
# return round(self.total_discount + self.vat_amount + self.total_additionals, 2)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class CarTransfer(models.Model):
|
||||
car = models.ForeignKey(
|
||||
"Car",
|
||||
@ -1001,7 +1027,7 @@ class CarTransfer(models.Model):
|
||||
|
||||
@property
|
||||
def total_price(self):
|
||||
return self.quantity * self.car.total_vat # TODO : check later
|
||||
return self.quantity * self.car.total_vat # TODO : check later
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Car Transfer Log")
|
||||
@ -1311,7 +1337,11 @@ class Dealer(models.Model, LocalizedNameMixin):
|
||||
)
|
||||
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
|
||||
name = models.CharField(max_length=255, verbose_name=_("English Name"))
|
||||
phone_number = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator()])
|
||||
phone_number = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("Phone Number"),
|
||||
validators=[SaudiPhoneNumberValidator()],
|
||||
)
|
||||
address = models.CharField(
|
||||
max_length=200, blank=True, null=True, verbose_name=_("Address")
|
||||
)
|
||||
@ -1365,6 +1395,7 @@ class Dealer(models.Model, LocalizedNameMixin):
|
||||
@property
|
||||
def customers(self):
|
||||
return models.Customer.objects.filter(dealer=self)
|
||||
|
||||
@property
|
||||
def user_quota(self):
|
||||
try:
|
||||
@ -1415,6 +1446,7 @@ class Dealer(models.Model, LocalizedNameMixin):
|
||||
def invoices(self):
|
||||
return Invoice.objects.filter(order__user=self.user)
|
||||
|
||||
|
||||
class StaffTypes(models.TextChoices):
|
||||
# MANAGER = "manager", _("Manager")
|
||||
INVENTORY = "inventory", _("Inventory")
|
||||
@ -1429,15 +1461,17 @@ class Staff(models.Model):
|
||||
# staff_member = models.OneToOneField(
|
||||
# StaffMember, on_delete=models.CASCADE, related_name="staff"
|
||||
# )
|
||||
user = models.OneToOneField(
|
||||
User, on_delete=models.CASCADE, related_name="staff"
|
||||
)
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="staff")
|
||||
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="staff")
|
||||
first_name = models.CharField(max_length=255, verbose_name=_("First Name"))
|
||||
last_name = models.CharField(max_length=255, verbose_name=_("Last Name"))
|
||||
|
||||
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
|
||||
phone_number = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator()])
|
||||
phone_number = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("Phone Number"),
|
||||
validators=[SaudiPhoneNumberValidator()],
|
||||
)
|
||||
staff_type = models.CharField(
|
||||
choices=StaffTypes.choices, max_length=255, verbose_name=_("Staff Type")
|
||||
)
|
||||
@ -1445,7 +1479,11 @@ class Staff(models.Model):
|
||||
max_length=200, blank=True, null=True, verbose_name=_("Address")
|
||||
)
|
||||
logo = models.ImageField(
|
||||
upload_to="logos/staff", blank=True, null=True, verbose_name=_("Image"),default="default-image/user.jpg"
|
||||
upload_to="logos/staff",
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_("Image"),
|
||||
default="default-image/user.jpg",
|
||||
)
|
||||
thumbnail = ImageSpecField(
|
||||
source="logo",
|
||||
@ -1480,6 +1518,7 @@ class Staff(models.Model):
|
||||
@property
|
||||
def fullname(self):
|
||||
return self.first_name + " " + self.last_name
|
||||
|
||||
def deactivate_account(self):
|
||||
self.active = False
|
||||
self.user.is_active = False
|
||||
@ -1544,8 +1583,7 @@ class Staff(models.Model):
|
||||
permissions = []
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=['dealer', 'user'],
|
||||
name='unique_staff_email_per_dealer'
|
||||
fields=["dealer", "user"], name="unique_staff_email_per_dealer"
|
||||
)
|
||||
]
|
||||
|
||||
@ -1648,7 +1686,11 @@ class Customer(models.Model):
|
||||
CustomerModel, on_delete=models.SET_NULL, null=True
|
||||
)
|
||||
user = models.OneToOneField(
|
||||
User, on_delete=models.CASCADE, related_name="customer_profile", null=True, blank=True
|
||||
User,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="customer_profile",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
title = models.CharField(
|
||||
choices=Title.choices, default=Title.NA, max_length=10, verbose_name=_("Title")
|
||||
@ -1676,7 +1718,11 @@ class Customer(models.Model):
|
||||
)
|
||||
active = models.BooleanField(default=True, verbose_name=_("Active"))
|
||||
image = models.ImageField(
|
||||
upload_to="customers/", blank=True, null=True, verbose_name=_("Image"),default="default-image/user-jpg"
|
||||
upload_to="customers/",
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_("Image"),
|
||||
default="default-image/user-jpg",
|
||||
)
|
||||
thumbnail = ImageSpecField(
|
||||
source="image",
|
||||
@ -1708,8 +1754,7 @@ class Customer(models.Model):
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=['dealer', 'email'],
|
||||
name='unique_customer_email_per_dealer'
|
||||
fields=["dealer", "email"], name="unique_customer_email_per_dealer"
|
||||
)
|
||||
]
|
||||
verbose_name = _("Customer")
|
||||
@ -1779,13 +1824,13 @@ class Customer(models.Model):
|
||||
user, created = User.objects.get_or_create(
|
||||
username=self.email,
|
||||
defaults={
|
||||
'email': self.email,
|
||||
'first_name': self.first_name,
|
||||
'last_name': self.last_name,
|
||||
'password': make_random_password(),
|
||||
'is_staff': False,
|
||||
'is_superuser': False,
|
||||
'is_active': False if for_lead else True,
|
||||
"email": self.email,
|
||||
"first_name": self.first_name,
|
||||
"last_name": self.last_name,
|
||||
"password": make_random_password(),
|
||||
"is_staff": False,
|
||||
"is_superuser": False,
|
||||
"is_active": False if for_lead else True,
|
||||
},
|
||||
)
|
||||
self.user = user
|
||||
@ -1822,7 +1867,11 @@ class Organization(models.Model, LocalizedNameMixin):
|
||||
CustomerModel, on_delete=models.SET_NULL, null=True
|
||||
)
|
||||
user = models.OneToOneField(
|
||||
User, on_delete=models.CASCADE, related_name="organization_profile", null=True, blank=True
|
||||
User,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="organization_profile",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
name = models.CharField(max_length=255, verbose_name=_("Name"))
|
||||
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
|
||||
@ -1831,12 +1880,20 @@ class Organization(models.Model, LocalizedNameMixin):
|
||||
)
|
||||
vrn = models.CharField(max_length=15, verbose_name=_("VAT Registration Number"))
|
||||
email = models.EmailField(verbose_name=_("Email"))
|
||||
phone_number = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator()])
|
||||
phone_number = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("Phone Number"),
|
||||
validators=[SaudiPhoneNumberValidator()],
|
||||
)
|
||||
address = models.CharField(
|
||||
max_length=200, blank=True, null=True, verbose_name=_("Address")
|
||||
)
|
||||
logo = models.ImageField(
|
||||
upload_to="logos", blank=True, null=True, verbose_name=_("Logo"),default="default-image/user.jpg"
|
||||
upload_to="logos",
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_("Logo"),
|
||||
default="default-image/user.jpg",
|
||||
)
|
||||
thumbnail = ImageSpecField(
|
||||
source="logo",
|
||||
@ -1965,7 +2022,11 @@ class Representative(models.Model, LocalizedNameMixin):
|
||||
id_number = models.CharField(
|
||||
max_length=10, unique=True, verbose_name=_("ID Number")
|
||||
)
|
||||
phone_number = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator()])
|
||||
phone_number = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("Phone Number"),
|
||||
validators=[SaudiPhoneNumberValidator()],
|
||||
)
|
||||
email = models.EmailField(max_length=255, verbose_name=_("Email Address"))
|
||||
address = models.CharField(
|
||||
max_length=200, blank=True, null=True, verbose_name=_("Address")
|
||||
@ -1985,7 +2046,11 @@ class Lead(models.Model):
|
||||
first_name = models.CharField(max_length=50, verbose_name=_("First Name"))
|
||||
last_name = models.CharField(max_length=50, verbose_name=_("Last Name"))
|
||||
email = models.EmailField(verbose_name=_("Email"))
|
||||
phone_number = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator()])
|
||||
phone_number = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("Phone Number"),
|
||||
validators=[SaudiPhoneNumberValidator()],
|
||||
)
|
||||
address = models.CharField(
|
||||
max_length=200, blank=True, null=True, verbose_name=_("Address")
|
||||
)
|
||||
@ -2175,8 +2240,9 @@ class Lead(models.Model):
|
||||
.order_by("-updated")
|
||||
.first()
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("lead_detail", args=[self.dealer.slug,self.slug])
|
||||
return reverse("lead_detail", args=[self.dealer.slug, self.slug])
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
@ -2246,6 +2312,7 @@ class Schedule(models.Model):
|
||||
@property
|
||||
def duration(self):
|
||||
return (self.end_time - self.start_time).seconds
|
||||
|
||||
@property
|
||||
def schedule_past_date(self):
|
||||
if self.scheduled_at < now():
|
||||
@ -2255,6 +2322,7 @@ class Schedule(models.Model):
|
||||
@property
|
||||
def get_purpose(self):
|
||||
return self.purpose.replace("_", " ").title()
|
||||
|
||||
class Meta:
|
||||
ordering = ["-scheduled_at"]
|
||||
verbose_name = _("Schedule")
|
||||
@ -2673,11 +2741,19 @@ class Vendor(models.Model, LocalizedNameMixin):
|
||||
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 = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator()])
|
||||
phone_number = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("Phone Number"),
|
||||
validators=[SaudiPhoneNumberValidator()],
|
||||
)
|
||||
email = models.EmailField(max_length=255, verbose_name=_("Email Address"))
|
||||
address = models.CharField(max_length=200, verbose_name=_("Address"))
|
||||
logo = models.ImageField(
|
||||
upload_to="logos/vendors", blank=True, null=True, verbose_name=_("Logo"),default="default-image/user.jpg"
|
||||
upload_to="logos/vendors",
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_("Logo"),
|
||||
default="default-image/user.jpg",
|
||||
)
|
||||
thumbnail = ImageSpecField(
|
||||
source="logo",
|
||||
@ -3225,7 +3301,6 @@ class CustomGroup(models.Model):
|
||||
"activity",
|
||||
"payment",
|
||||
"vendor",
|
||||
|
||||
],
|
||||
other_perms=[
|
||||
"view_car",
|
||||
@ -3236,8 +3311,7 @@ class CustomGroup(models.Model):
|
||||
"view_saleorder",
|
||||
"view_leads",
|
||||
"view_opportunity",
|
||||
'view_customer'
|
||||
|
||||
"view_customer",
|
||||
],
|
||||
)
|
||||
self.set_permissions(
|
||||
@ -3533,7 +3607,7 @@ class ExtraInfo(models.Model):
|
||||
return f"ExtraInfo for {self.content_object} ({self.content_type})"
|
||||
|
||||
@classmethod
|
||||
def get_sale_orders(cls, staff=None, is_dealer=False,dealer=None):
|
||||
def get_sale_orders(cls, staff=None, is_dealer=False, dealer=None):
|
||||
if not staff and not is_dealer:
|
||||
return []
|
||||
|
||||
@ -3546,11 +3620,13 @@ class ExtraInfo(models.Model):
|
||||
content_type=content_type,
|
||||
related_content_type=related_content_type,
|
||||
related_object_id__isnull=False,
|
||||
).union(cls.objects.filter(
|
||||
dealer=dealer,
|
||||
content_type=ContentType.objects.get_for_model(EstimateModel),
|
||||
related_content_type=ContentType.objects.get_for_model(User),
|
||||
))
|
||||
).union(
|
||||
cls.objects.filter(
|
||||
dealer=dealer,
|
||||
content_type=ContentType.objects.get_for_model(EstimateModel),
|
||||
related_content_type=ContentType.objects.get_for_model(User),
|
||||
)
|
||||
)
|
||||
else:
|
||||
qs = cls.objects.filter(
|
||||
dealer=dealer,
|
||||
@ -3559,7 +3635,17 @@ class ExtraInfo(models.Model):
|
||||
related_object_id=staff.pk,
|
||||
)
|
||||
# qs = qs.select_related("customer","estimate","invoice")
|
||||
data = SaleOrder.objects.filter(pk__in=[x.content_object.sale_orders.select_related("customer","estimate","invoice").first().pk for x in qs if x.content_object.sale_orders.first()])
|
||||
data = SaleOrder.objects.filter(
|
||||
pk__in=[
|
||||
x.content_object.sale_orders.select_related(
|
||||
"customer", "estimate", "invoice"
|
||||
)
|
||||
.first()
|
||||
.pk
|
||||
for x in qs
|
||||
if x.content_object.sale_orders.first()
|
||||
]
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
@ -3572,7 +3658,7 @@ class ExtraInfo(models.Model):
|
||||
# ]
|
||||
|
||||
@classmethod
|
||||
def get_invoices(cls, staff=None, is_dealer=False,dealer=None):
|
||||
def get_invoices(cls, staff=None, is_dealer=False, dealer=None):
|
||||
if not staff and not is_dealer:
|
||||
return []
|
||||
|
||||
@ -3585,11 +3671,13 @@ class ExtraInfo(models.Model):
|
||||
content_type=content_type,
|
||||
related_content_type=related_content_type,
|
||||
related_object_id__isnull=False,
|
||||
).union(cls.objects.filter(
|
||||
dealer=dealer,
|
||||
content_type=content_type,
|
||||
related_content_type=ContentType.objects.get_for_model(User),
|
||||
))
|
||||
).union(
|
||||
cls.objects.filter(
|
||||
dealer=dealer,
|
||||
content_type=content_type,
|
||||
related_content_type=ContentType.objects.get_for_model(User),
|
||||
)
|
||||
)
|
||||
else:
|
||||
qs = cls.objects.filter(
|
||||
dealer=dealer,
|
||||
@ -3608,32 +3696,16 @@ class Recall(models.Model):
|
||||
title = models.CharField(max_length=200, verbose_name=_("Recall Title"))
|
||||
description = models.TextField(verbose_name=_("Description"))
|
||||
make = models.ForeignKey(
|
||||
CarMake,
|
||||
models.DO_NOTHING,
|
||||
verbose_name=_("Make"),
|
||||
null=True,
|
||||
blank=True
|
||||
CarMake, models.DO_NOTHING, verbose_name=_("Make"), null=True, blank=True
|
||||
)
|
||||
model = models.ForeignKey(
|
||||
CarModel,
|
||||
models.DO_NOTHING,
|
||||
verbose_name=_("Model"),
|
||||
null=True,
|
||||
blank=True
|
||||
CarModel, models.DO_NOTHING, verbose_name=_("Model"), null=True, blank=True
|
||||
)
|
||||
serie = models.ForeignKey(
|
||||
CarSerie,
|
||||
models.DO_NOTHING,
|
||||
verbose_name=_("Series"),
|
||||
null=True,
|
||||
blank=True
|
||||
CarSerie, models.DO_NOTHING, verbose_name=_("Series"), null=True, blank=True
|
||||
)
|
||||
trim = models.ForeignKey(
|
||||
CarTrim,
|
||||
models.DO_NOTHING,
|
||||
verbose_name=_("Trim"),
|
||||
null=True,
|
||||
blank=True
|
||||
CarTrim, models.DO_NOTHING, verbose_name=_("Trim"), null=True, blank=True
|
||||
)
|
||||
year_from = models.IntegerField(verbose_name=_("From Year"), null=True, blank=True)
|
||||
year_to = models.IntegerField(verbose_name=_("To Year"), null=True, blank=True)
|
||||
@ -3643,7 +3715,7 @@ class Recall(models.Model):
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("Created By")
|
||||
verbose_name=_("Created By"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -3653,11 +3725,16 @@ class Recall(models.Model):
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
class RecallNotification(models.Model):
|
||||
recall = models.ForeignKey(Recall, on_delete=models.CASCADE, related_name='notifications')
|
||||
dealer = models.ForeignKey("Dealer", on_delete=models.CASCADE, related_name='recall_notifications')
|
||||
recall = models.ForeignKey(
|
||||
Recall, on_delete=models.CASCADE, related_name="notifications"
|
||||
)
|
||||
dealer = models.ForeignKey(
|
||||
"Dealer", on_delete=models.CASCADE, related_name="recall_notifications"
|
||||
)
|
||||
sent_at = models.DateTimeField(auto_now_add=True)
|
||||
cars_affected = models.ManyToManyField(Car, related_name='recall_notifications')
|
||||
cars_affected = models.ManyToManyField(Car, related_name="recall_notifications")
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Recall Notification")
|
||||
@ -3666,27 +3743,30 @@ class RecallNotification(models.Model):
|
||||
def __str__(self):
|
||||
return f"Notification for {self.dealer} about {self.recall}"
|
||||
|
||||
|
||||
class Ticket(models.Model):
|
||||
STATUS_CHOICES = [
|
||||
('open', 'Open'),
|
||||
('in_progress', 'In Progress'),
|
||||
('resolved', 'Resolved'),
|
||||
('closed', 'Closed'),
|
||||
("open", "Open"),
|
||||
("in_progress", "In Progress"),
|
||||
("resolved", "Resolved"),
|
||||
("closed", "Closed"),
|
||||
]
|
||||
|
||||
PRIORITY_CHOICES = [
|
||||
('low', 'Low'),
|
||||
('medium', 'Medium'),
|
||||
('high', 'High'),
|
||||
('critical', 'Critical'),
|
||||
("low", "Low"),
|
||||
("medium", "Medium"),
|
||||
("high", "High"),
|
||||
("critical", "Critical"),
|
||||
]
|
||||
|
||||
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name='tickets')
|
||||
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="tickets")
|
||||
subject = models.CharField(max_length=200)
|
||||
description = models.TextField()
|
||||
resolution_notes = models.TextField(blank=True, null=True)
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='open')
|
||||
priority = models.CharField(max_length=20, choices=PRIORITY_CHOICES, default='medium')
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="open")
|
||||
priority = models.CharField(
|
||||
max_length=20, choices=PRIORITY_CHOICES, default="medium"
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
@ -3697,7 +3777,7 @@ class Ticket(models.Model):
|
||||
Returns None if ticket isn't resolved/closed.
|
||||
Returns timedelta if resolved/closed.
|
||||
"""
|
||||
if self.status in ['resolved', 'closed'] and self.created_at:
|
||||
if self.status in ["resolved", "closed"] and self.created_at:
|
||||
return self.updated_at - self.created_at
|
||||
return None
|
||||
|
||||
@ -3729,9 +3809,11 @@ class Ticket(models.Model):
|
||||
|
||||
|
||||
class CarImage(models.Model):
|
||||
car = models.OneToOneField('Car', on_delete=models.CASCADE, related_name='generated_image')
|
||||
car = models.OneToOneField(
|
||||
"Car", on_delete=models.CASCADE, related_name="generated_image"
|
||||
)
|
||||
image_hash = models.CharField(max_length=64, unique=True)
|
||||
image = models.ImageField(upload_to='car_images/', null=True, blank=True)
|
||||
image = models.ImageField(upload_to="car_images/", null=True, blank=True)
|
||||
is_generating = models.BooleanField(default=False)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
@ -3752,5 +3834,5 @@ class CarImage(models.Model):
|
||||
async_task(
|
||||
generate_car_image_task,
|
||||
self.id,
|
||||
task_name=f"generate_car_image_{self.car.vin}"
|
||||
)
|
||||
task_name=f"generate_car_image_{self.car.vin}",
|
||||
)
|
||||
|
||||
@ -87,6 +87,7 @@ from inventory.models import Notification
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@database_sync_to_async
|
||||
def get_user(user_id):
|
||||
User = get_user_model()
|
||||
@ -95,24 +96,24 @@ def get_user(user_id):
|
||||
except User.DoesNotExist:
|
||||
return AnonymousUser()
|
||||
|
||||
|
||||
@database_sync_to_async
|
||||
def get_notifications(user, last_id):
|
||||
notifications = Notification.objects.filter(
|
||||
user=user,
|
||||
id__gt=last_id,
|
||||
is_read=False
|
||||
user=user, id__gt=last_id, is_read=False
|
||||
).order_by("created")
|
||||
|
||||
return [
|
||||
{
|
||||
'id': n.id,
|
||||
'message': n.message,
|
||||
'created': n.created.isoformat(), # Convert datetime to string
|
||||
'is_read': n.is_read
|
||||
"id": n.id,
|
||||
"message": n.message,
|
||||
"created": n.created.isoformat(), # Convert datetime to string
|
||||
"is_read": n.is_read,
|
||||
}
|
||||
for n in notifications
|
||||
]
|
||||
|
||||
|
||||
class NotificationSSEApp:
|
||||
async def __call__(self, scope, receive, send):
|
||||
if scope["type"] != "http":
|
||||
@ -143,15 +144,17 @@ class NotificationSSEApp:
|
||||
|
||||
for notification in notifications:
|
||||
await self._send_notification(send, notification)
|
||||
if notification['id'] > last_id:
|
||||
last_id = notification['id']
|
||||
if notification["id"] > last_id:
|
||||
last_id = notification["id"]
|
||||
|
||||
# Send keep-alive comment every 15 seconds
|
||||
await send({
|
||||
"type": "http.response.body",
|
||||
"body": b":keep-alive\n\n",
|
||||
"more_body": True
|
||||
})
|
||||
await send(
|
||||
{
|
||||
"type": "http.response.body",
|
||||
"body": b":keep-alive\n\n",
|
||||
"more_body": True,
|
||||
}
|
||||
)
|
||||
|
||||
# await asyncio.sleep(3)
|
||||
|
||||
@ -161,16 +164,18 @@ class NotificationSSEApp:
|
||||
await self._close_connection(send)
|
||||
|
||||
async def _send_headers(self, send):
|
||||
await send({
|
||||
"type": "http.response.start",
|
||||
"status": 200,
|
||||
"headers": [
|
||||
(b"content-type", b"text/event-stream"),
|
||||
(b"cache-control", b"no-cache"),
|
||||
(b"connection", b"keep-alive"),
|
||||
(b"x-accel-buffering", b"no"),
|
||||
]
|
||||
})
|
||||
await send(
|
||||
{
|
||||
"type": "http.response.start",
|
||||
"status": 200,
|
||||
"headers": [
|
||||
(b"content-type", b"text/event-stream"),
|
||||
(b"cache-control", b"no-cache"),
|
||||
(b"connection", b"keep-alive"),
|
||||
(b"x-accel-buffering", b"no"),
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
async def _send_notification(self, send, notification):
|
||||
try:
|
||||
@ -179,27 +184,25 @@ class NotificationSSEApp:
|
||||
f"event: notification\n"
|
||||
f"data: {json.dumps(notification)}\n\n"
|
||||
)
|
||||
await send({
|
||||
"type": "http.response.body",
|
||||
"body": event_str.encode("utf-8"),
|
||||
"more_body": True
|
||||
})
|
||||
await send(
|
||||
{
|
||||
"type": "http.response.body",
|
||||
"body": event_str.encode("utf-8"),
|
||||
"more_body": True,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error sending notification: {e}")
|
||||
|
||||
async def _send_response(self, send, status, body):
|
||||
await send({
|
||||
"type": "http.response.start",
|
||||
"status": status,
|
||||
"headers": [(b"content-type", b"text/plain")]
|
||||
})
|
||||
await send({
|
||||
"type": "http.response.body",
|
||||
"body": body
|
||||
})
|
||||
await send(
|
||||
{
|
||||
"type": "http.response.start",
|
||||
"status": status,
|
||||
"headers": [(b"content-type", b"text/plain")],
|
||||
}
|
||||
)
|
||||
await send({"type": "http.response.body", "body": body})
|
||||
|
||||
async def _close_connection(self, send):
|
||||
await send({
|
||||
"type": "http.response.body",
|
||||
"body": b""
|
||||
})
|
||||
await send({"type": "http.response.body", "body": b""})
|
||||
|
||||
@ -20,7 +20,12 @@ from django.contrib import messages
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django_ledger.models import ItemTransactionModel,InvoiceModel,LedgerModel,EntityModel
|
||||
from django_ledger.models import (
|
||||
ItemTransactionModel,
|
||||
InvoiceModel,
|
||||
LedgerModel,
|
||||
EntityModel,
|
||||
)
|
||||
from django.views.generic.detail import DetailView
|
||||
from django.views.generic.edit import CreateView
|
||||
from django_ledger.forms.chart_of_accounts import (
|
||||
@ -35,17 +40,28 @@ from django_ledger.forms.purchase_order import (
|
||||
get_po_itemtxs_formset_class,
|
||||
)
|
||||
from django_ledger.views.purchase_order import PurchaseOrderModelModelViewQuerySetMixIn
|
||||
from django_ledger.models import PurchaseOrderModel, EstimateModel, BillModel, ChartOfAccountModel
|
||||
from django_ledger.models import (
|
||||
PurchaseOrderModel,
|
||||
EstimateModel,
|
||||
BillModel,
|
||||
ChartOfAccountModel,
|
||||
)
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from django.views.generic.edit import UpdateView
|
||||
from django.views.generic.base import RedirectView
|
||||
from django.views.generic.list import ListView
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_ledger.forms.invoice import (BaseInvoiceModelUpdateForm, InvoiceModelCreateForEstimateForm,
|
||||
get_invoice_itemtxs_formset_class,
|
||||
DraftInvoiceModelUpdateForm, InReviewInvoiceModelUpdateForm,
|
||||
ApprovedInvoiceModelUpdateForm, PaidInvoiceModelUpdateForm,
|
||||
AccruedAndApprovedInvoiceModelUpdateForm, InvoiceModelCreateForm)
|
||||
from django_ledger.forms.invoice import (
|
||||
BaseInvoiceModelUpdateForm,
|
||||
InvoiceModelCreateForEstimateForm,
|
||||
get_invoice_itemtxs_formset_class,
|
||||
DraftInvoiceModelUpdateForm,
|
||||
InReviewInvoiceModelUpdateForm,
|
||||
ApprovedInvoiceModelUpdateForm,
|
||||
PaidInvoiceModelUpdateForm,
|
||||
AccruedAndApprovedInvoiceModelUpdateForm,
|
||||
InvoiceModelCreateForm,
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -71,7 +87,11 @@ class PurchaseOrderModelUpdateView(
|
||||
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["entity_slug"] = dealer.entity.slug
|
||||
context["po_ready_to_fulfill"] = [item for item in po_model.get_itemtxs_data()[0] if item.po_item_status == 'received']
|
||||
context["po_ready_to_fulfill"] = [
|
||||
item
|
||||
for item in po_model.get_itemtxs_data()[0]
|
||||
if item.po_item_status == "received"
|
||||
]
|
||||
|
||||
if not itemtxs_formset:
|
||||
itemtxs_qs = self.get_po_itemtxs_qs(po_model)
|
||||
@ -776,12 +796,12 @@ class InventoryListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
|
||||
|
||||
class InvoiceModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
||||
slug_url_kwarg = 'invoice_pk'
|
||||
slug_field = 'uuid'
|
||||
context_object_name = 'invoice'
|
||||
slug_url_kwarg = "invoice_pk"
|
||||
slug_field = "uuid"
|
||||
context_object_name = "invoice"
|
||||
# template_name = 'inventory/sales/invoices/invoice_update.html'
|
||||
form_class = BaseInvoiceModelUpdateForm
|
||||
http_method_names = ['get', 'post']
|
||||
http_method_names = ["get", "post"]
|
||||
|
||||
action_update_items = False
|
||||
|
||||
@ -802,115 +822,137 @@ class InvoiceModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Update
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form_class = self.get_form_class()
|
||||
if self.request.method == 'POST' and self.action_update_items:
|
||||
if self.request.method == "POST" and self.action_update_items:
|
||||
return form_class(
|
||||
entity_slug=self.kwargs['entity_slug'],
|
||||
entity_slug=self.kwargs["entity_slug"],
|
||||
user_model=self.request.dealer.user,
|
||||
instance=self.object
|
||||
instance=self.object,
|
||||
)
|
||||
return form_class(
|
||||
entity_slug=self.kwargs['entity_slug'],
|
||||
entity_slug=self.kwargs["entity_slug"],
|
||||
user_model=self.request.dealer.user,
|
||||
**self.get_form_kwargs()
|
||||
**self.get_form_kwargs(),
|
||||
)
|
||||
|
||||
def get_context_data(self, itemtxs_formset=None, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
invoice_model: InvoiceModel = self.object
|
||||
title = f'Invoice {invoice_model.invoice_number}'
|
||||
context['page_title'] = title
|
||||
context['header_title'] = title
|
||||
title = f"Invoice {invoice_model.invoice_number}"
|
||||
context["page_title"] = title
|
||||
context["header_title"] = title
|
||||
|
||||
ledger_model: LedgerModel = self.object.ledger
|
||||
|
||||
if not invoice_model.is_configured():
|
||||
messages.add_message(
|
||||
request=self.request,
|
||||
message=f'Invoice {invoice_model.invoice_number} must have all accounts configured.',
|
||||
message=f"Invoice {invoice_model.invoice_number} must have all accounts configured.",
|
||||
level=messages.ERROR,
|
||||
extra_tags='is-danger'
|
||||
extra_tags="is-danger",
|
||||
)
|
||||
|
||||
if not invoice_model.is_paid():
|
||||
if ledger_model.locked:
|
||||
messages.add_message(self.request,
|
||||
messages.ERROR,
|
||||
f'Warning! This invoice is locked. Must unlock before making any changes.',
|
||||
extra_tags='is-danger')
|
||||
messages.add_message(
|
||||
self.request,
|
||||
messages.ERROR,
|
||||
f"Warning! This invoice is locked. Must unlock before making any changes.",
|
||||
extra_tags="is-danger",
|
||||
)
|
||||
|
||||
if ledger_model.locked:
|
||||
messages.add_message(self.request,
|
||||
messages.ERROR,
|
||||
f'Warning! This Invoice is Locked. Must unlock before making any changes.',
|
||||
extra_tags='is-danger')
|
||||
messages.add_message(
|
||||
self.request,
|
||||
messages.ERROR,
|
||||
f"Warning! This Invoice is Locked. Must unlock before making any changes.",
|
||||
extra_tags="is-danger",
|
||||
)
|
||||
|
||||
if not ledger_model.is_posted():
|
||||
messages.add_message(self.request,
|
||||
messages.INFO,
|
||||
f'This Invoice has not been posted. Must post to see ledger changes.',
|
||||
extra_tags='is-info')
|
||||
messages.add_message(
|
||||
self.request,
|
||||
messages.INFO,
|
||||
f"This Invoice has not been posted. Must post to see ledger changes.",
|
||||
extra_tags="is-info",
|
||||
)
|
||||
|
||||
if not itemtxs_formset:
|
||||
itemtxs_qs = invoice_model.itemtransactionmodel_set.all().select_related('item_model')
|
||||
itemtxs_qs, itemtxs_agg = invoice_model.get_itemtxs_data(queryset=itemtxs_qs)
|
||||
invoice_itemtxs_formset_class = get_invoice_itemtxs_formset_class(invoice_model)
|
||||
itemtxs_formset = invoice_itemtxs_formset_class(
|
||||
entity_slug=self.kwargs['entity_slug'],
|
||||
user_model=self.request.dealer.user,
|
||||
invoice_model=invoice_model,
|
||||
itemtxs_qs = invoice_model.itemtransactionmodel_set.all().select_related(
|
||||
"item_model"
|
||||
)
|
||||
itemtxs_qs, itemtxs_agg = invoice_model.get_itemtxs_data(
|
||||
queryset=itemtxs_qs
|
||||
)
|
||||
invoice_itemtxs_formset_class = get_invoice_itemtxs_formset_class(
|
||||
invoice_model
|
||||
)
|
||||
itemtxs_formset = invoice_itemtxs_formset_class(
|
||||
entity_slug=self.kwargs["entity_slug"],
|
||||
user_model=self.request.dealer.user,
|
||||
invoice_model=invoice_model,
|
||||
queryset=itemtxs_qs,
|
||||
)
|
||||
else:
|
||||
itemtxs_qs, itemtxs_agg = invoice_model.get_itemtxs_data(queryset=itemtxs_formset.queryset)
|
||||
itemtxs_qs, itemtxs_agg = invoice_model.get_itemtxs_data(
|
||||
queryset=itemtxs_formset.queryset
|
||||
)
|
||||
|
||||
context['itemtxs_formset'] = itemtxs_formset
|
||||
context['total_amount__sum'] = itemtxs_agg['total_amount__sum']
|
||||
context["itemtxs_formset"] = itemtxs_formset
|
||||
context["total_amount__sum"] = itemtxs_agg["total_amount__sum"]
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
entity_slug = self.kwargs['entity_slug']
|
||||
invoice_pk = self.kwargs['invoice_pk']
|
||||
return reverse('invoice_detail',
|
||||
kwargs={
|
||||
'dealer_slug': self.request.dealer.slug,
|
||||
'entity_slug': entity_slug,
|
||||
'pk': invoice_pk
|
||||
})
|
||||
entity_slug = self.kwargs["entity_slug"]
|
||||
invoice_pk = self.kwargs["invoice_pk"]
|
||||
return reverse(
|
||||
"invoice_detail",
|
||||
kwargs={
|
||||
"dealer_slug": self.request.dealer.slug,
|
||||
"entity_slug": entity_slug,
|
||||
"pk": invoice_pk,
|
||||
},
|
||||
)
|
||||
|
||||
# def get_queryset(self):
|
||||
# qs = super().get_queryset()
|
||||
# return qs.prefetch_related('itemtransactionmodel_set')
|
||||
def get_queryset(self):
|
||||
if self.queryset is None:
|
||||
self.queryset = InvoiceModel.objects.for_entity(
|
||||
entity_slug=self.kwargs['entity_slug'],
|
||||
user_model=self.request.user
|
||||
).select_related('customer', 'ledger').order_by('-created')
|
||||
return super().get_queryset().prefetch_related('itemtransactionmodel_set')
|
||||
|
||||
self.queryset = (
|
||||
InvoiceModel.objects.for_entity(
|
||||
entity_slug=self.kwargs["entity_slug"], user_model=self.request.user
|
||||
)
|
||||
.select_related("customer", "ledger")
|
||||
.order_by("-created")
|
||||
)
|
||||
return super().get_queryset().prefetch_related("itemtransactionmodel_set")
|
||||
|
||||
def form_valid(self, form):
|
||||
invoice_model: InvoiceModel = form.save(commit=False)
|
||||
if invoice_model.can_migrate():
|
||||
invoice_model.migrate_state(
|
||||
user_model=self.request.dealer.user,
|
||||
entity_slug=self.kwargs['entity_slug']
|
||||
entity_slug=self.kwargs["entity_slug"],
|
||||
)
|
||||
messages.add_message(self.request,
|
||||
messages.SUCCESS,
|
||||
f'Invoice {self.object.invoice_number} successfully updated.',
|
||||
extra_tags='is-success')
|
||||
messages.add_message(
|
||||
self.request,
|
||||
messages.SUCCESS,
|
||||
f"Invoice {self.object.invoice_number} successfully updated.",
|
||||
extra_tags="is-success",
|
||||
)
|
||||
return super().form_valid(form)
|
||||
|
||||
def get(self, request, entity_slug, invoice_pk, *args, **kwargs):
|
||||
if self.action_update_items:
|
||||
return HttpResponseRedirect(
|
||||
redirect_to=reverse('invoice_update',
|
||||
kwargs={
|
||||
'dealer_slug': request.dealer.slug,
|
||||
'entity_slug': entity_slug,
|
||||
'pk': invoice_pk
|
||||
})
|
||||
redirect_to=reverse(
|
||||
"invoice_update",
|
||||
kwargs={
|
||||
"dealer_slug": request.dealer.slug,
|
||||
"entity_slug": entity_slug,
|
||||
"pk": invoice_pk,
|
||||
},
|
||||
)
|
||||
)
|
||||
return super(InvoiceModelUpdateView, self).get(request, *args, **kwargs)
|
||||
|
||||
@ -922,18 +964,22 @@ class InvoiceModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Update
|
||||
queryset = self.get_queryset()
|
||||
invoice_model = self.get_object(queryset=queryset)
|
||||
self.object = invoice_model
|
||||
invoice_itemtxs_formset_class = get_invoice_itemtxs_formset_class(invoice_model)
|
||||
itemtxs_formset = invoice_itemtxs_formset_class(request.POST,
|
||||
user_model=self.request.dealer.user,
|
||||
invoice_model=invoice_model,
|
||||
entity_slug=entity_slug)
|
||||
invoice_itemtxs_formset_class = get_invoice_itemtxs_formset_class(
|
||||
invoice_model
|
||||
)
|
||||
itemtxs_formset = invoice_itemtxs_formset_class(
|
||||
request.POST,
|
||||
user_model=self.request.dealer.user,
|
||||
invoice_model=invoice_model,
|
||||
entity_slug=entity_slug,
|
||||
)
|
||||
|
||||
if not invoice_model.can_edit_items():
|
||||
messages.add_message(
|
||||
request,
|
||||
message=f'Cannot update items once Invoice is {invoice_model.get_invoice_status_display()}',
|
||||
message=f"Cannot update items once Invoice is {invoice_model.get_invoice_status_display()}",
|
||||
level=messages.ERROR,
|
||||
extra_tags='is-danger'
|
||||
extra_tags="is-danger",
|
||||
)
|
||||
context = self.get_context_data(itemtxs_formset=itemtxs_formset)
|
||||
return self.render_to_response(context=context)
|
||||
@ -941,8 +987,12 @@ class InvoiceModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Update
|
||||
if itemtxs_formset.has_changed():
|
||||
if itemtxs_formset.is_valid():
|
||||
itemtxs_list = itemtxs_formset.save(commit=False)
|
||||
entity_qs = EntityModel.objects.for_user(user_model=self.request.dealer.user)
|
||||
entity_model: EntityModel = get_object_or_404(entity_qs, slug__exact=entity_slug)
|
||||
entity_qs = EntityModel.objects.for_user(
|
||||
user_model=self.request.dealer.user
|
||||
)
|
||||
entity_model: EntityModel = get_object_or_404(
|
||||
entity_qs, slug__exact=entity_slug
|
||||
)
|
||||
|
||||
for itemtxs in itemtxs_list:
|
||||
itemtxs.invoice_model_id = invoice_model.uuid
|
||||
@ -953,53 +1003,72 @@ class InvoiceModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Update
|
||||
invoice_model.get_state(commit=True)
|
||||
invoice_model.clean()
|
||||
invoice_model.save(
|
||||
update_fields=['amount_due',
|
||||
'amount_receivable',
|
||||
'amount_unearned',
|
||||
'amount_earned',
|
||||
'updated']
|
||||
update_fields=[
|
||||
"amount_due",
|
||||
"amount_receivable",
|
||||
"amount_unearned",
|
||||
"amount_earned",
|
||||
"updated",
|
||||
]
|
||||
)
|
||||
|
||||
invoice_model.migrate_state(
|
||||
entity_slug=entity_slug,
|
||||
user_model=self.request.user,
|
||||
raise_exception=False,
|
||||
itemtxs_qs=itemtxs_qs
|
||||
itemtxs_qs=itemtxs_qs,
|
||||
)
|
||||
|
||||
messages.add_message(request,
|
||||
message=f'Items for Invoice {invoice_model.invoice_number} saved.',
|
||||
level=messages.SUCCESS,
|
||||
extra_tags='is-success')
|
||||
messages.add_message(
|
||||
request,
|
||||
message=f"Items for Invoice {invoice_model.invoice_number} saved.",
|
||||
level=messages.SUCCESS,
|
||||
extra_tags="is-success",
|
||||
)
|
||||
return HttpResponseRedirect(
|
||||
redirect_to=reverse('django_ledger:invoice-update',
|
||||
kwargs={
|
||||
'entity_slug': entity_slug,
|
||||
'invoice_pk': invoice_pk
|
||||
})
|
||||
redirect_to=reverse(
|
||||
"django_ledger:invoice-update",
|
||||
kwargs={
|
||||
"entity_slug": entity_slug,
|
||||
"invoice_pk": invoice_pk,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
# if not valid, return formset with errors...
|
||||
return self.render_to_response(context=self.get_context_data(itemtxs_formset=itemtxs_formset))
|
||||
return self.render_to_response(
|
||||
context=self.get_context_data(itemtxs_formset=itemtxs_formset)
|
||||
)
|
||||
return super(InvoiceModelUpdateView, self).post(request, **kwargs)
|
||||
|
||||
|
||||
|
||||
class ChartOfAccountModelModelBaseViewMixIn(LoginRequiredMixin, PermissionRequiredMixin):
|
||||
class ChartOfAccountModelModelBaseViewMixIn(
|
||||
LoginRequiredMixin, PermissionRequiredMixin
|
||||
):
|
||||
queryset = None
|
||||
permission_required = []
|
||||
|
||||
def get_queryset(self):
|
||||
if self.queryset is None:
|
||||
entity_model = self.request.dealer.entity
|
||||
self.queryset = entity_model.chartofaccountmodel_set.all().order_by('-updated')
|
||||
self.queryset = entity_model.chartofaccountmodel_set.all().order_by(
|
||||
"-updated"
|
||||
)
|
||||
return super().get_queryset()
|
||||
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
return reverse('coa-list', kwargs={'dealer_slug': self.request.dealer.slug,
|
||||
'entity_slug': self.request.entity.slug})
|
||||
return reverse(
|
||||
"coa-list",
|
||||
kwargs={
|
||||
"dealer_slug": self.request.dealer.slug,
|
||||
"entity_slug": self.request.entity.slug,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class ChartOfAccountModelListView(ChartOfAccountModelModelBaseViewMixIn, ListView):
|
||||
template_name = 'chart_of_accounts/coa_list.html'
|
||||
context_object_name = 'coa_list'
|
||||
template_name = "chart_of_accounts/coa_list.html"
|
||||
context_object_name = "coa_list"
|
||||
inactive = False
|
||||
|
||||
def get_queryset(self):
|
||||
@ -1010,84 +1079,116 @@ class ChartOfAccountModelListView(ChartOfAccountModelModelBaseViewMixIn, ListVie
|
||||
|
||||
def get_context_data(self, *, object_list=None, **kwargs):
|
||||
context = super().get_context_data(object_list=None, **kwargs)
|
||||
context['inactive'] = self.inactive
|
||||
context['header_subtitle'] = self.request.entity.name
|
||||
context['header_subtitle_icon'] = 'gravity-ui:hierarchy'
|
||||
context['page_title'] = 'Inactive Chart of Account List' if self.inactive else 'Chart of Accounts List'
|
||||
context['header_title'] = 'Inactive Chart of Account List' if self.inactive else 'Chart of Accounts List'
|
||||
context["inactive"] = self.inactive
|
||||
context["header_subtitle"] = self.request.entity.name
|
||||
context["header_subtitle_icon"] = "gravity-ui:hierarchy"
|
||||
context["page_title"] = (
|
||||
"Inactive Chart of Account List"
|
||||
if self.inactive
|
||||
else "Chart of Accounts List"
|
||||
)
|
||||
context["header_title"] = (
|
||||
"Inactive Chart of Account List"
|
||||
if self.inactive
|
||||
else "Chart of Accounts List"
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
class ChartOfAccountModelCreateView(ChartOfAccountModelModelBaseViewMixIn, CreateView):
|
||||
template_name = 'chart_of_accounts/coa_create.html'
|
||||
template_name = "chart_of_accounts/coa_create.html"
|
||||
extra_context = {
|
||||
'header_title': _('Create Chart of Accounts'),
|
||||
'page_title': _('Create Chart of Account'),
|
||||
"header_title": _("Create Chart of Accounts"),
|
||||
"page_title": _("Create Chart of Account"),
|
||||
}
|
||||
|
||||
def get_initial(self):
|
||||
return {
|
||||
'entity': self.request.entity,
|
||||
"entity": self.request.entity,
|
||||
}
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
return ChartOfAccountsModelCreateForm(
|
||||
entity_model=self.request.entity,
|
||||
**self.get_form_kwargs()
|
||||
entity_model=self.request.entity, **self.get_form_kwargs()
|
||||
)
|
||||
|
||||
def get_context_data(self, *, object_list=None, **kwargs):
|
||||
context = super().get_context_data(object_list=None, **kwargs)
|
||||
context['header_subtitle'] = f'New Chart of Accounts: {self.request.entity.name}'
|
||||
context['header_subtitle_icon'] = 'gravity-ui:hierarchy'
|
||||
context["header_subtitle"] = (
|
||||
f"New Chart of Accounts: {self.request.entity.name}"
|
||||
)
|
||||
context["header_subtitle_icon"] = "gravity-ui:hierarchy"
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('coa-list', kwargs={'dealer_slug': self.request.dealer.slug,
|
||||
'entity_slug': self.request.entity.slug})
|
||||
return reverse(
|
||||
"coa-list",
|
||||
kwargs={
|
||||
"dealer_slug": self.request.dealer.slug,
|
||||
"entity_slug": self.request.entity.slug,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class ChartOfAccountModelUpdateView(ChartOfAccountModelModelBaseViewMixIn, UpdateView):
|
||||
context_object_name = 'coa_model'
|
||||
slug_url_kwarg = 'coa_slug'
|
||||
template_name = 'chart_of_accounts/coa_update.html'
|
||||
context_object_name = "coa_model"
|
||||
slug_url_kwarg = "coa_slug"
|
||||
template_name = "chart_of_accounts/coa_update.html"
|
||||
form_class = ChartOfAccountsModelUpdateForm
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
chart_of_accounts_model: ChartOfAccountModel = self.object
|
||||
context['page_title'] = f'Update Chart of Account {chart_of_accounts_model.name}'
|
||||
context['header_title'] = f'Update Chart of Account {chart_of_accounts_model.name}'
|
||||
context["page_title"] = (
|
||||
f"Update Chart of Account {chart_of_accounts_model.name}"
|
||||
)
|
||||
context["header_title"] = (
|
||||
f"Update Chart of Account {chart_of_accounts_model.name}"
|
||||
)
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('coa-list', kwargs={'dealer_slug': self.request.dealer.slug,
|
||||
'entity_slug': self.request.entity.slug})
|
||||
return reverse(
|
||||
"coa-list",
|
||||
kwargs={
|
||||
"dealer_slug": self.request.dealer.slug,
|
||||
"entity_slug": self.request.entity.slug,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class CharOfAccountModelActionView(ChartOfAccountModelModelBaseViewMixIn,
|
||||
RedirectView,
|
||||
SingleObjectMixin):
|
||||
http_method_names = ['get']
|
||||
slug_url_kwarg = 'coa_slug'
|
||||
class CharOfAccountModelActionView(
|
||||
ChartOfAccountModelModelBaseViewMixIn, RedirectView, SingleObjectMixin
|
||||
):
|
||||
http_method_names = ["get"]
|
||||
slug_url_kwarg = "coa_slug"
|
||||
action_name = None
|
||||
commit = True
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
kwargs['user_model'] = self.request.user
|
||||
kwargs["user_model"] = self.request.user
|
||||
if not self.action_name:
|
||||
raise ImproperlyConfigured('View attribute action_name is required.')
|
||||
response = super(CharOfAccountModelActionView, self).get(request, *args, **kwargs)
|
||||
raise ImproperlyConfigured("View attribute action_name is required.")
|
||||
response = super(CharOfAccountModelActionView, self).get(
|
||||
request, *args, **kwargs
|
||||
)
|
||||
coa_model: ChartOfAccountModel = self.get_object()
|
||||
|
||||
try:
|
||||
getattr(coa_model, self.action_name)(commit=self.commit, **kwargs)
|
||||
messages.add_message(request, level=messages.SUCCESS, extra_tags='is-success',
|
||||
message=_('Successfully updated {} Default Chart of Account to '.format(
|
||||
request.entity.name) +
|
||||
'{}'.format(coa_model.name)))
|
||||
messages.add_message(
|
||||
request,
|
||||
level=messages.SUCCESS,
|
||||
extra_tags="is-success",
|
||||
message=_(
|
||||
"Successfully updated {} Default Chart of Account to ".format(
|
||||
request.entity.name
|
||||
)
|
||||
+ "{}".format(coa_model.name)
|
||||
),
|
||||
)
|
||||
except ValidationError as e:
|
||||
messages.add_message(request,
|
||||
message=e.message,
|
||||
level=messages.ERROR,
|
||||
extra_tags='is-danger')
|
||||
messages.add_message(
|
||||
request, message=e.message, level=messages.ERROR, extra_tags="is-danger"
|
||||
)
|
||||
return response
|
||||
|
||||
@ -28,6 +28,7 @@ from plans.models import UserPlan
|
||||
from plans.signals import order_completed, activate_user_plan
|
||||
from inventory.tasks import send_email
|
||||
from django.conf import settings
|
||||
|
||||
# logging
|
||||
import logging
|
||||
|
||||
@ -177,7 +178,11 @@ def create_ledger_entity(sender, instance, created, **kwargs):
|
||||
entity.create_uom(name=u[1], unit_abbr=u[0])
|
||||
|
||||
# Create COA accounts, background task
|
||||
async_task(func="inventory.tasks.create_coa_accounts",dealer=instance,hook="inventory.hooks.check_create_coa_accounts")
|
||||
async_task(
|
||||
func="inventory.tasks.create_coa_accounts",
|
||||
dealer=instance,
|
||||
hook="inventory.hooks.check_create_coa_accounts",
|
||||
)
|
||||
# async_task('inventory.tasks.check_create_coa_accounts', instance, schedule_type='O', schedule_time=timedelta(seconds=20))
|
||||
|
||||
# create_settings(instance.pk)
|
||||
@ -273,14 +278,14 @@ def create_item_model(sender, instance, created, **kwargs):
|
||||
else:
|
||||
instance.item_model.default_amount = instance.marked_price
|
||||
|
||||
# inventory = entity.create_item_inventory(
|
||||
# name=instance.vin,
|
||||
# uom_model=uom,
|
||||
# item_type=ItemModel.ITEM_TYPE_LUMP_SUM
|
||||
# )
|
||||
# inventory.additional_info = {}
|
||||
# inventory.additional_info.update({"car_info": instance.to_dict()})
|
||||
# inventory.save()
|
||||
# inventory = entity.create_item_inventory(
|
||||
# name=instance.vin,
|
||||
# uom_model=uom,
|
||||
# item_type=ItemModel.ITEM_TYPE_LUMP_SUM
|
||||
# )
|
||||
# inventory.additional_info = {}
|
||||
# inventory.additional_info.update({"car_info": instance.to_dict()})
|
||||
# inventory.save()
|
||||
# else:
|
||||
# instance.item_model.additional_info.update({"car_info": instance.to_dict()})
|
||||
# instance.item_model.save()
|
||||
@ -1039,7 +1044,11 @@ def po_fullfilled_notification(sender, instance, created, **kwargs):
|
||||
po_number=instance.po_number,
|
||||
url=reverse(
|
||||
"purchase_order_detail",
|
||||
kwargs={"dealer_slug": dealer.slug,"entity_slug":instance.entity.slug, "pk": instance.pk},
|
||||
kwargs={
|
||||
"dealer_slug": dealer.slug,
|
||||
"entity_slug": instance.entity.slug,
|
||||
"pk": instance.pk,
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
@ -1086,7 +1095,10 @@ def sale_order_created_notification(sender, instance, created, **kwargs):
|
||||
estimate_number=instance.estimate.estimate_number,
|
||||
url=reverse(
|
||||
"estimate_detail",
|
||||
kwargs={"dealer_slug": instance.dealer.slug, "pk": instance.estimate.pk},
|
||||
kwargs={
|
||||
"dealer_slug": instance.dealer.slug,
|
||||
"pk": instance.estimate.pk,
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
@ -1131,7 +1143,7 @@ def estimate_in_review_notification(sender, instance, created, **kwargs):
|
||||
url=reverse(
|
||||
"estimate_detail",
|
||||
kwargs={"dealer_slug": dealer.slug, "pk": instance.pk},
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@ -1188,7 +1200,11 @@ def bill_model_in_approve_notification(sender, instance, created, **kwargs):
|
||||
bill_number=instance.bill_number,
|
||||
url=reverse(
|
||||
"bill-update",
|
||||
kwargs={"dealer_slug": dealer.slug, "entity_slug": dealer.entity.slug, "bill_pk": instance.pk},
|
||||
kwargs={
|
||||
"dealer_slug": dealer.slug,
|
||||
"entity_slug": dealer.entity.slug,
|
||||
"bill_pk": instance.pk,
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
@ -1224,7 +1240,6 @@ def bill_model_in_approve_notification(sender, instance, created, **kwargs):
|
||||
# )
|
||||
|
||||
|
||||
|
||||
@receiver(post_save, sender=models.Ticket)
|
||||
def send_ticket_notification(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
@ -1249,20 +1264,23 @@ def send_ticket_notification(sender, instance, created, **kwargs):
|
||||
)
|
||||
else:
|
||||
models.Notification.objects.create(
|
||||
user=instance.dealer.user,
|
||||
message=_(
|
||||
"""
|
||||
user=instance.dealer.user,
|
||||
message=_(
|
||||
"""
|
||||
Support Ticket #{ticket_number} has been updated.
|
||||
<a href="{url}" target="_blank">View</a>.
|
||||
"""
|
||||
).format(
|
||||
ticket_number=instance.pk,
|
||||
url=reverse(
|
||||
"ticket_detail",
|
||||
kwargs={"dealer_slug": instance.dealer.slug, "ticket_id": instance.pk},
|
||||
),
|
||||
).format(
|
||||
ticket_number=instance.pk,
|
||||
url=reverse(
|
||||
"ticket_detail",
|
||||
kwargs={
|
||||
"dealer_slug": instance.dealer.slug,
|
||||
"ticket_id": instance.pk,
|
||||
},
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_save, sender=models.CarColors)
|
||||
@ -1273,30 +1291,31 @@ def handle_car_image(sender, instance, created, **kwargs):
|
||||
try:
|
||||
# Create or get car image record
|
||||
car = instance.car
|
||||
car_image, created = models.CarImage.objects.get_or_create(car=car, defaults={'image_hash': car.get_hash})
|
||||
car_image, created = models.CarImage.objects.get_or_create(
|
||||
car=car, defaults={"image_hash": car.get_hash}
|
||||
)
|
||||
|
||||
# Check for existing image with same hash
|
||||
existing = models.CarImage.objects.filter(
|
||||
image_hash=car_image.image_hash,
|
||||
image__isnull=False
|
||||
).exclude(car=car).first()
|
||||
existing = (
|
||||
models.CarImage.objects.filter(
|
||||
image_hash=car_image.image_hash, image__isnull=False
|
||||
)
|
||||
.exclude(car=car)
|
||||
.first()
|
||||
)
|
||||
|
||||
if existing:
|
||||
# Copy existing image
|
||||
car_image.image.save(
|
||||
existing.image.name,
|
||||
existing.image.file,
|
||||
save=True
|
||||
)
|
||||
car_image.image.save(existing.image.name, existing.image.file, save=True)
|
||||
logger.info(f"Reused image for car {car.vin}")
|
||||
else:
|
||||
# Schedule async generation
|
||||
async_task(
|
||||
'inventory.tasks.generate_car_image_task',
|
||||
"inventory.tasks.generate_car_image_task",
|
||||
car_image.id,
|
||||
task_name=f"generate_car_image_{car.vin}"
|
||||
task_name=f"generate_car_image_{car.vin}",
|
||||
)
|
||||
logger.info(f"Scheduled image generation for car {car.vin}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling car image for {car.vin}: {e}")
|
||||
logger.error(f"Error handling car image for {car.vin}: {e}")
|
||||
|
||||
@ -17,11 +17,19 @@ from django.core.files.base import ContentFile
|
||||
from django.contrib.auth import get_user_model
|
||||
from allauth.account.models import EmailAddress
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from .utils import get_accounts_data,create_account
|
||||
from .utils import get_accounts_data, create_account
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.contrib.auth.models import User, Group, Permission
|
||||
from inventory.models import DealerSettings, Dealer,Schedule,Notification,CarReservation,CarStatusChoices,CarImage
|
||||
from inventory.models import (
|
||||
DealerSettings,
|
||||
Dealer,
|
||||
Schedule,
|
||||
Notification,
|
||||
CarReservation,
|
||||
CarStatusChoices,
|
||||
CarImage,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
@ -52,6 +60,7 @@ def create_settings(pk):
|
||||
.first(),
|
||||
)
|
||||
|
||||
|
||||
def create_coa_accounts(**kwargs):
|
||||
logger.info("creating all accounts are created")
|
||||
instance = kwargs.get("dealer")
|
||||
@ -62,9 +71,6 @@ def create_coa_accounts(**kwargs):
|
||||
create_account(entity, coa, account_data)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# def create_coa_accounts1(pk):
|
||||
# with transaction.atomic():
|
||||
# instance = Dealer.objects.select_for_update().get(pk=pk)
|
||||
@ -800,8 +806,6 @@ def create_user_dealer(email, password, name, arabic_name, phone, crn, vrn, addr
|
||||
# transaction.on_commit(run)
|
||||
|
||||
|
||||
|
||||
|
||||
def send_bilingual_reminder(user_id, plan_id, expiration_date, days_until_expire):
|
||||
"""Send bilingual email reminder using Django-Q"""
|
||||
try:
|
||||
@ -809,41 +813,49 @@ def send_bilingual_reminder(user_id, plan_id, expiration_date, days_until_expire
|
||||
plan = Plan.objects.get(id=plan_id)
|
||||
|
||||
# Determine user language preference
|
||||
user_language = getattr(user, 'language', settings.LANGUAGE_CODE)
|
||||
user_language = getattr(user, "language", settings.LANGUAGE_CODE)
|
||||
activate(user_language)
|
||||
|
||||
# Context data
|
||||
context = {
|
||||
'user': user,
|
||||
'plan': plan,
|
||||
'expiration_date': expiration_date,
|
||||
'days_until_expire': days_until_expire,
|
||||
'SITE_NAME': settings.SITE_NAME,
|
||||
'RENEWAL_URL': "url" ,#settings.RENEWAL_URL,
|
||||
'direction': 'rtl' if user_language.startswith('ar') else 'ltr'
|
||||
"user": user,
|
||||
"plan": plan,
|
||||
"expiration_date": expiration_date,
|
||||
"days_until_expire": days_until_expire,
|
||||
"SITE_NAME": settings.SITE_NAME,
|
||||
"RENEWAL_URL": "url", # settings.RENEWAL_URL,
|
||||
"direction": "rtl" if user_language.startswith("ar") else "ltr",
|
||||
}
|
||||
|
||||
# Subject with translation
|
||||
subject_en = f"Your {plan.name} subscription expires in {days_until_expire} days"
|
||||
subject_en = (
|
||||
f"Your {plan.name} subscription expires in {days_until_expire} days"
|
||||
)
|
||||
subject_ar = f"اشتراكك في {plan.name} ينتهي خلال {days_until_expire} أيام"
|
||||
|
||||
# Render templates
|
||||
text_content = render_to_string([
|
||||
f'emails/expiration_reminder_{user_language}.txt',
|
||||
'emails/expiration_reminder.txt'
|
||||
], context)
|
||||
text_content = render_to_string(
|
||||
[
|
||||
f"emails/expiration_reminder_{user_language}.txt",
|
||||
"emails/expiration_reminder.txt",
|
||||
],
|
||||
context,
|
||||
)
|
||||
|
||||
html_content = render_to_string([
|
||||
f'emails/expiration_reminder_{user_language}.html',
|
||||
'emails/expiration_reminder.html'
|
||||
], context)
|
||||
html_content = render_to_string(
|
||||
[
|
||||
f"emails/expiration_reminder_{user_language}.html",
|
||||
"emails/expiration_reminder.html",
|
||||
],
|
||||
context,
|
||||
)
|
||||
|
||||
# Create email
|
||||
email = EmailMultiAlternatives(
|
||||
subject=subject_ar if user_language.startswith('ar') else subject_en,
|
||||
subject=subject_ar if user_language.startswith("ar") else subject_en,
|
||||
body=text_content,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
to=[user.email]
|
||||
to=[user.email],
|
||||
)
|
||||
email.attach_alternative(html_content, "text/html")
|
||||
email.send()
|
||||
@ -853,6 +865,7 @@ def send_bilingual_reminder(user_id, plan_id, expiration_date, days_until_expire
|
||||
logger.error(f"Email failed: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
def handle_email_result(task):
|
||||
"""Callback for email results"""
|
||||
if task.success:
|
||||
@ -861,7 +874,6 @@ def handle_email_result(task):
|
||||
logger.error(f"Email task failed: {task.result}")
|
||||
|
||||
|
||||
|
||||
def send_schedule_reminder_email(schedule_id):
|
||||
"""
|
||||
Sends an email reminder for a specific schedule.
|
||||
@ -871,46 +883,67 @@ def send_schedule_reminder_email(schedule_id):
|
||||
schedule = Schedule.objects.get(pk=schedule_id)
|
||||
|
||||
# Ensure the user has an email and the schedule is not completed/canceled
|
||||
if not schedule.scheduled_by.email or schedule.status in ["completed", "canceled"]:
|
||||
logger.error(f"Skipping email for Schedule ID {schedule_id}: No email or schedule status is {schedule.status}.")
|
||||
if not schedule.scheduled_by.email or schedule.status in [
|
||||
"completed",
|
||||
"canceled",
|
||||
]:
|
||||
logger.error(
|
||||
f"Skipping email for Schedule ID {schedule_id}: No email or schedule status is {schedule.status}."
|
||||
)
|
||||
return
|
||||
|
||||
user_email = schedule.scheduled_by.email
|
||||
Notification.objects.create(
|
||||
user=schedule.scheduled_by,
|
||||
message=_(
|
||||
"""
|
||||
user=schedule.scheduled_by,
|
||||
message=_(
|
||||
"""
|
||||
Reminder: You have an appointment scheduled for {scheduled_type} After 15 minutes <a href="{url}" target="_blank">View</a>.
|
||||
"""
|
||||
).format(scheduled_type=schedule.scheduled_type, url=reverse("schedule_calendar", kwargs={"dealer_slug": schedule.dealer.slug})),)
|
||||
).format(
|
||||
scheduled_type=schedule.scheduled_type,
|
||||
url=reverse(
|
||||
"schedule_calendar", kwargs={"dealer_slug": schedule.dealer.slug}
|
||||
),
|
||||
),
|
||||
)
|
||||
# Prepare context for email templates
|
||||
context = {
|
||||
'schedule_purpose': schedule.purpose,
|
||||
'scheduled_at': schedule.scheduled_at.astimezone(timezone.get_current_timezone()).strftime('%Y-%m-%d %H:%M %Z'), # Format with timezone
|
||||
'schedule_type': schedule.scheduled_type,
|
||||
'customer_name': schedule.customer.customer_name if schedule.customer else 'N/A',
|
||||
'notes': schedule.notes,
|
||||
'user_name': schedule.scheduled_by.get_full_name() or schedule.scheduled_by.email,
|
||||
"schedule_purpose": schedule.purpose,
|
||||
"scheduled_at": schedule.scheduled_at.astimezone(
|
||||
timezone.get_current_timezone()
|
||||
).strftime("%Y-%m-%d %H:%M %Z"), # Format with timezone
|
||||
"schedule_type": schedule.scheduled_type,
|
||||
"customer_name": schedule.customer.customer_name
|
||||
if schedule.customer
|
||||
else "N/A",
|
||||
"notes": schedule.notes,
|
||||
"user_name": schedule.scheduled_by.get_full_name()
|
||||
or schedule.scheduled_by.email,
|
||||
}
|
||||
|
||||
# Render email content from templates
|
||||
html_message = render_to_string('emails/schedule_reminder.html', context)
|
||||
plain_message = render_to_string('emails/schedule_reminder.txt', context)
|
||||
html_message = render_to_string("emails/schedule_reminder.html", context)
|
||||
plain_message = render_to_string("emails/schedule_reminder.txt", context)
|
||||
|
||||
send_mail(
|
||||
f'Reminder: Your Upcoming Schedule - {schedule.purpose}',
|
||||
f"Reminder: Your Upcoming Schedule - {schedule.purpose}",
|
||||
plain_message,
|
||||
settings.DEFAULT_FROM_EMAIL,
|
||||
[user_email],
|
||||
html_message=html_message,
|
||||
)
|
||||
logger.info(f"Successfully sent reminder email for Schedule ID: {schedule_id} to {user_email}")
|
||||
logger.info(
|
||||
f"Successfully sent reminder email for Schedule ID: {schedule_id} to {user_email}"
|
||||
)
|
||||
|
||||
except Schedule.DoesNotExist:
|
||||
logger.info(f"Schedule with ID {schedule_id} does not exist. Cannot send reminder.")
|
||||
logger.info(
|
||||
f"Schedule with ID {schedule_id} does not exist. Cannot send reminder."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.info(f"Error sending reminder email for Schedule ID {schedule_id}: {e}")
|
||||
|
||||
|
||||
# Optional: A hook function to log the status of the email task (add to your_app/tasks.py)
|
||||
def log_email_status(task):
|
||||
"""
|
||||
@ -918,9 +951,14 @@ def log_email_status(task):
|
||||
It logs whether the task was successful or not.
|
||||
"""
|
||||
if task.success:
|
||||
logger.info(f"Email task for Schedule ID {task.args[0]} completed successfully. Result: {task.result}")
|
||||
logger.info(
|
||||
f"Email task for Schedule ID {task.args[0]} completed successfully. Result: {task.result}"
|
||||
)
|
||||
else:
|
||||
logger.error(f"Email task for Schedule ID {task.args[0]} failed. Error: {task.result}")
|
||||
logger.error(
|
||||
f"Email task for Schedule ID {task.args[0]} failed. Error: {task.result}"
|
||||
)
|
||||
|
||||
|
||||
def remove_reservation_by_id(reservation_id):
|
||||
try:
|
||||
@ -931,8 +969,9 @@ def remove_reservation_by_id(reservation_id):
|
||||
except Exception as e:
|
||||
logger.error(f"Error removing reservation with ID {reservation_id}: {e}")
|
||||
|
||||
|
||||
def test_task(**kwargs):
|
||||
print("TASK : ",kwargs.get("dealer"))
|
||||
print("TASK : ", kwargs.get("dealer"))
|
||||
|
||||
|
||||
def generate_car_image_task(car_image_id):
|
||||
@ -940,22 +979,25 @@ def generate_car_image_task(car_image_id):
|
||||
Simple async task to generate car image
|
||||
"""
|
||||
from inventory.utils import generate_car_image_simple
|
||||
|
||||
try:
|
||||
car_image = CarImage.objects.get(id=car_image_id)
|
||||
result = generate_car_image_simple(car_image)
|
||||
|
||||
return {
|
||||
'success': result.get('success', False),
|
||||
'car_image_id': car_image_id,
|
||||
'error': result.get('error'),
|
||||
'message': 'Image generated' if result.get('success') else 'Generation failed'
|
||||
"success": result.get("success", False),
|
||||
"car_image_id": car_image_id,
|
||||
"error": result.get("error"),
|
||||
"message": "Image generated"
|
||||
if result.get("success")
|
||||
else "Generation failed",
|
||||
}
|
||||
|
||||
except CarImage.DoesNotExist:
|
||||
error_msg = f"CarImage with id {car_image_id} not found"
|
||||
logger.error(error_msg)
|
||||
return {'success': False, 'error': error_msg}
|
||||
return {"success": False, "error": error_msg}
|
||||
except Exception as e:
|
||||
error_msg = f"Unexpected error: {e}"
|
||||
logger.error(error_msg)
|
||||
return {'success': False, 'error': error_msg}
|
||||
return {"success": False, "error": error_msg}
|
||||
|
||||
@ -13,9 +13,9 @@ from django.db.models import Case, Value, When, IntegerField
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter
|
||||
def get_percentage(value, total):
|
||||
|
||||
try:
|
||||
value = int(value)
|
||||
total = int(total)
|
||||
@ -25,6 +25,7 @@ def get_percentage(value, total):
|
||||
except (ValueError, TypeError):
|
||||
return 0
|
||||
|
||||
|
||||
@register.filter(name="percentage")
|
||||
def percentage(value):
|
||||
if value is not None:
|
||||
@ -686,11 +687,12 @@ def count_checked(permissions, group_permission_ids):
|
||||
# """Count how many permissions are checked from the allowed list"""
|
||||
# return sum(1 for perm in permissions if perm.id in group_permission_ids)
|
||||
|
||||
@register.inclusion_tag('sales/tags/invoice_item_formset.html', takes_context=True)
|
||||
|
||||
@register.inclusion_tag("sales/tags/invoice_item_formset.html", takes_context=True)
|
||||
def invoice_item_formset_table(context, itemtxs_formset):
|
||||
return {
|
||||
'entity_slug': context['view'].kwargs['entity_slug'],
|
||||
'invoice_model': context['invoice'],
|
||||
'total_amount__sum': context['total_amount__sum'],
|
||||
'itemtxs_formset': itemtxs_formset,
|
||||
"entity_slug": context["view"].kwargs["entity_slug"],
|
||||
"invoice_model": context["invoice"],
|
||||
"total_amount__sum": context["total_amount__sum"],
|
||||
"itemtxs_formset": itemtxs_formset,
|
||||
}
|
||||
|
||||
@ -40,13 +40,18 @@ urlpatterns = [
|
||||
views.assign_car_makes,
|
||||
name="assign_car_makes",
|
||||
),
|
||||
|
||||
|
||||
#dashboards for manager, dealer, inventory and accounatant
|
||||
path("dashboards/<slug:dealer_slug>/general/", views.general_dashboard,name="general_dashboard"),
|
||||
#dashboard for sales
|
||||
path("dashboards/<slug:dealer_slug>/sales/", views.sales_dashboard, name="sales_dashboard"),
|
||||
|
||||
# dashboards for manager, dealer, inventory and accounatant
|
||||
path(
|
||||
"dashboards/<slug:dealer_slug>/general/",
|
||||
views.general_dashboard,
|
||||
name="general_dashboard",
|
||||
),
|
||||
# dashboard for sales
|
||||
path(
|
||||
"dashboards/<slug:dealer_slug>/sales/",
|
||||
views.sales_dashboard,
|
||||
name="sales_dashboard",
|
||||
),
|
||||
path(
|
||||
"<slug:dealer_slug>/cars/aging-inventory/list",
|
||||
views.aging_inventory_list_view,
|
||||
@ -777,7 +782,11 @@ urlpatterns = [
|
||||
views.EstimateDetailView.as_view(),
|
||||
name="estimate_detail",
|
||||
),
|
||||
path('<slug:dealer_slug>/sales/estimates/print/<uuid:pk>/', views.EstimatePrintView.as_view(), name='estimate_print'),
|
||||
path(
|
||||
"<slug:dealer_slug>/sales/estimates/print/<uuid:pk>/",
|
||||
views.EstimatePrintView.as_view(),
|
||||
name="estimate_print",
|
||||
),
|
||||
path(
|
||||
"<slug:dealer_slug>/sales/estimates/create/",
|
||||
views.create_estimate,
|
||||
@ -934,7 +943,6 @@ urlpatterns = [
|
||||
views.ItemServiceUpdateView.as_view(),
|
||||
name="item_service_update",
|
||||
),
|
||||
|
||||
# Expanese
|
||||
path(
|
||||
"<slug:dealer_slug>/items/expeneses/",
|
||||
@ -1093,32 +1101,47 @@ urlpatterns = [
|
||||
name="entity-ic-date",
|
||||
),
|
||||
# Chart of Accounts...
|
||||
path('<slug:dealer_slug>/chart-of-accounts/<slug:entity_slug>/list/',
|
||||
path(
|
||||
"<slug:dealer_slug>/chart-of-accounts/<slug:entity_slug>/list/",
|
||||
views.ChartOfAccountModelListView.as_view(),
|
||||
name='coa-list'),
|
||||
path('<slug:dealer_slug>/chart-of-accounts/<slug:entity_slug>/list/inactive/',
|
||||
views.ChartOfAccountModelListView.as_view(inactive=True),
|
||||
name='coa-list-inactive'),
|
||||
path('<slug:dealer_slug>/<slug:entity_slug>/create/',
|
||||
views.ChartOfAccountModelCreateView.as_view(),
|
||||
name='coa-create'),
|
||||
path('<slug:dealer_slug>/<slug:entity_slug>/detail/<slug:coa_slug>/',
|
||||
views.ChartOfAccountModelListView.as_view(),
|
||||
name='coa-detail'),
|
||||
path('<slug:dealer_slug>/<slug:entity_slug>/update/<slug:coa_slug>/',
|
||||
views.ChartOfAccountModelUpdateView.as_view(),
|
||||
name='coa-update'),
|
||||
|
||||
name="coa-list",
|
||||
),
|
||||
path(
|
||||
"<slug:dealer_slug>/chart-of-accounts/<slug:entity_slug>/list/inactive/",
|
||||
views.ChartOfAccountModelListView.as_view(inactive=True),
|
||||
name="coa-list-inactive",
|
||||
),
|
||||
path(
|
||||
"<slug:dealer_slug>/<slug:entity_slug>/create/",
|
||||
views.ChartOfAccountModelCreateView.as_view(),
|
||||
name="coa-create",
|
||||
),
|
||||
path(
|
||||
"<slug:dealer_slug>/<slug:entity_slug>/detail/<slug:coa_slug>/",
|
||||
views.ChartOfAccountModelListView.as_view(),
|
||||
name="coa-detail",
|
||||
),
|
||||
path(
|
||||
"<slug:dealer_slug>/<slug:entity_slug>/update/<slug:coa_slug>/",
|
||||
views.ChartOfAccountModelUpdateView.as_view(),
|
||||
name="coa-update",
|
||||
),
|
||||
# ACTIONS....
|
||||
path('<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-default/',
|
||||
views.CharOfAccountModelActionView.as_view(action_name='mark_as_default'),
|
||||
name='coa-action-mark-as-default'),
|
||||
path('<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-active/',
|
||||
views.CharOfAccountModelActionView.as_view(action_name='mark_as_active'),
|
||||
name='coa-action-mark-as-active'),
|
||||
path('<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-inactive/',
|
||||
views.CharOfAccountModelActionView.as_view(action_name='mark_as_inactive'),
|
||||
name='coa-action-mark-as-inactive'),
|
||||
path(
|
||||
"<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-default/",
|
||||
views.CharOfAccountModelActionView.as_view(action_name="mark_as_default"),
|
||||
name="coa-action-mark-as-default",
|
||||
),
|
||||
path(
|
||||
"<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-active/",
|
||||
views.CharOfAccountModelActionView.as_view(action_name="mark_as_active"),
|
||||
name="coa-action-mark-as-active",
|
||||
),
|
||||
path(
|
||||
"<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-inactive/",
|
||||
views.CharOfAccountModelActionView.as_view(action_name="mark_as_inactive"),
|
||||
name="coa-action-mark-as-inactive",
|
||||
),
|
||||
# CASH FLOW STATEMENTS...
|
||||
# Entities...
|
||||
path(
|
||||
@ -1294,40 +1317,74 @@ urlpatterns = [
|
||||
views.PurchaseOrderMarkAsVoidView.as_view(),
|
||||
name="po-action-mark-as-void",
|
||||
),
|
||||
|
||||
# reports
|
||||
path(
|
||||
path(
|
||||
"<slug:dealer_slug>/purchase-report/",
|
||||
views.purchase_report_view,
|
||||
name="po-report",
|
||||
),
|
||||
path('purchase-report/<slug:dealer_slug>/csv/', views.purchase_report_csv_export, name='purchase-report-csv-export'),
|
||||
|
||||
path(
|
||||
path(
|
||||
"purchase-report/<slug:dealer_slug>/csv/",
|
||||
views.purchase_report_csv_export,
|
||||
name="purchase-report-csv-export",
|
||||
),
|
||||
path(
|
||||
"<slug:dealer_slug>/car-sale-report/",
|
||||
views.car_sale_report_view,
|
||||
name="car-sale-report",
|
||||
),
|
||||
path('car-sale-report/<slug:dealer_slug>/csv/', views.car_sale_report_csv_export, name='car-sale-report-csv-export'),
|
||||
|
||||
path('feature/recall/', views.RecallListView.as_view(), name='recall_list'),
|
||||
path('feature/recall/filter/', views.RecallFilterView, name='recall_filter'),
|
||||
path('feature/recall/<int:pk>/view/', views.RecallDetailView.as_view(), name='recall_detail'),
|
||||
path('feature/recall/create/', views.RecallCreateView.as_view(), name='recall_create'),
|
||||
path('feature/recall/success/', views.RecallSuccessView.as_view(), name='recall_success'),
|
||||
|
||||
path('<slug:dealer_slug>/schedules/calendar/', views.schedule_calendar, name='schedule_calendar'),
|
||||
|
||||
path(
|
||||
"car-sale-report/<slug:dealer_slug>/csv/",
|
||||
views.car_sale_report_csv_export,
|
||||
name="car-sale-report-csv-export",
|
||||
),
|
||||
path("feature/recall/", views.RecallListView.as_view(), name="recall_list"),
|
||||
path("feature/recall/filter/", views.RecallFilterView, name="recall_filter"),
|
||||
path(
|
||||
"feature/recall/<int:pk>/view/",
|
||||
views.RecallDetailView.as_view(),
|
||||
name="recall_detail",
|
||||
),
|
||||
path(
|
||||
"feature/recall/create/", views.RecallCreateView.as_view(), name="recall_create"
|
||||
),
|
||||
path(
|
||||
"feature/recall/success/",
|
||||
views.RecallSuccessView.as_view(),
|
||||
name="recall_success",
|
||||
),
|
||||
path(
|
||||
"<slug:dealer_slug>/schedules/calendar/",
|
||||
views.schedule_calendar,
|
||||
name="schedule_calendar",
|
||||
),
|
||||
# staff profile
|
||||
path('<slug:dealer_slug>/staff/<slug:slug>detail/', views.StaffDetailView.as_view(), name='staff_detail'),
|
||||
path(
|
||||
"<slug:dealer_slug>/staff/<slug:slug>detail/",
|
||||
views.StaffDetailView.as_view(),
|
||||
name="staff_detail",
|
||||
),
|
||||
# tickets
|
||||
path('help_center/view/', views.help_center, name='help_center'),
|
||||
path('<slug:dealer_slug>/help_center/tickets/', views.ticket_list, name='ticket_list'),
|
||||
path('help_center/tickets/<slug:dealer_slug>/create/', views.create_ticket, name='create_ticket'),
|
||||
path('<slug:dealer_slug>/help_center/tickets/<int:ticket_id>/', views.ticket_detail, name='ticket_detail'),
|
||||
path('help_center/tickets/<int:ticket_id>/update/', views.ticket_update, name='ticket_update'),
|
||||
path("help_center/view/", views.help_center, name="help_center"),
|
||||
path(
|
||||
"<slug:dealer_slug>/help_center/tickets/", views.ticket_list, name="ticket_list"
|
||||
),
|
||||
path(
|
||||
"help_center/tickets/<slug:dealer_slug>/create/",
|
||||
views.create_ticket,
|
||||
name="create_ticket",
|
||||
),
|
||||
path(
|
||||
"<slug:dealer_slug>/help_center/tickets/<int:ticket_id>/",
|
||||
views.ticket_detail,
|
||||
name="ticket_detail",
|
||||
),
|
||||
path(
|
||||
"help_center/tickets/<int:ticket_id>/update/",
|
||||
views.ticket_update,
|
||||
name="ticket_update",
|
||||
),
|
||||
# path('help_center/tickets/<int:ticket_id>/ticket_mark_resolved/', views.ticket_mark_resolved, name='ticket_mark_resolved'),
|
||||
|
||||
]
|
||||
|
||||
handler404 = "inventory.views.custom_page_not_found_view"
|
||||
|
||||
@ -73,15 +73,12 @@ def get_jwt_token():
|
||||
try:
|
||||
response = requests.post(url, headers=headers, json=data)
|
||||
response.raise_for_status()
|
||||
#logging for success
|
||||
# logging for success
|
||||
logger.info("Successfully fetched JWT token.")
|
||||
return response.text
|
||||
except requests.exceptions.RequestException as e:
|
||||
#logging for error
|
||||
logger.error(
|
||||
f"HTTP error fetching JWT token from {url}: ",
|
||||
exc_info=True
|
||||
)
|
||||
# logging for error
|
||||
logger.error(f"HTTP error fetching JWT token from {url}: ", exc_info=True)
|
||||
print(f"Error obtaining JWT token: {e}")
|
||||
return None
|
||||
|
||||
@ -169,7 +166,7 @@ def send_email(from_, to_, subject, message):
|
||||
message = message
|
||||
from_email = from_
|
||||
recipient_list = [to_]
|
||||
async_task(send_mail,subject, message, from_email, recipient_list)
|
||||
async_task(send_mail, subject, message, from_email, recipient_list)
|
||||
|
||||
|
||||
def get_user_type(request):
|
||||
@ -236,10 +233,10 @@ def reserve_car(car, request):
|
||||
)
|
||||
car.status = models.CarStatusChoices.RESERVED
|
||||
car.save()
|
||||
# --- Logging for Success ---
|
||||
# --- Logging for Success ---
|
||||
DjangoQSchedule.objects.create(
|
||||
name=f"remove_reservation_for_car_with_vin_{car.vin}",
|
||||
func='inventory.tasks.remove_reservation_by_id',
|
||||
func="inventory.tasks.remove_reservation_by_id",
|
||||
args=reservation.pk,
|
||||
schedule_type=DjangoQSchedule.ONCE,
|
||||
next_run=reserved_until,
|
||||
@ -257,7 +254,7 @@ def reserve_car(car, request):
|
||||
f"Error reserving car {car.pk} ('{car.id_car_make} {car.id_car_model}') "
|
||||
f"for user {request.user} . "
|
||||
f"Error: {e}",
|
||||
exc_info=True
|
||||
exc_info=True,
|
||||
)
|
||||
messages.error(request, f"Error reserving car: {e}")
|
||||
|
||||
@ -1038,22 +1035,25 @@ class CarFinanceCalculator1:
|
||||
self.item_transactions = self._get_item_transactions()
|
||||
# self.additional_services = self._get_additional_services()
|
||||
|
||||
|
||||
def _get_vat_rate(self):
|
||||
vat = models.VatRate.objects.filter(dealer=self.dealer,is_active=True).first()
|
||||
vat = models.VatRate.objects.filter(dealer=self.dealer, is_active=True).first()
|
||||
if not vat:
|
||||
raise ObjectDoesNotExist("No active VAT rate found")
|
||||
return vat.rate
|
||||
|
||||
def _get_additional_services(self):
|
||||
return [x for item in self.item_transactions
|
||||
for x in item.item_model.car.additional_services
|
||||
]
|
||||
return [
|
||||
x
|
||||
for item in self.item_transactions
|
||||
for x in item.item_model.car.additional_services
|
||||
]
|
||||
|
||||
def _get_item_transactions(self):
|
||||
return self.model.get_itemtxs_data()[0].all()
|
||||
|
||||
def get_items(self):
|
||||
return self._get_item_transactions()
|
||||
|
||||
@staticmethod
|
||||
def _get_quantity(item):
|
||||
return item.ce_quantity or item.quantity
|
||||
@ -1068,17 +1068,17 @@ class CarFinanceCalculator1:
|
||||
quantity = self._get_quantity(item)
|
||||
car = item.item_model.car
|
||||
unit_price = Decimal(car.marked_price)
|
||||
discount = self.extra_info.data.get("discount",0)
|
||||
discount = self.extra_info.data.get("discount", 0)
|
||||
sell_price = unit_price - Decimal(discount)
|
||||
return {
|
||||
"item_number": item.item_model.item_number,
|
||||
"vin": car.vin, #car_info.get("vin"),
|
||||
"make": car.id_car_make ,#car_info.get("make"),
|
||||
"model": car.id_car_model ,#car_info.get("model"),
|
||||
"year": car.year ,# car_info.get("year"),
|
||||
"logo": car.logo, # getattr(car.id_car_make, "logo", ""),
|
||||
"trim": car.id_car_trim ,# car_info.get("trim"),
|
||||
"mileage": car.mileage ,# car_info.get("mileage"),
|
||||
"vin": car.vin, # car_info.get("vin"),
|
||||
"make": car.id_car_make, # car_info.get("make"),
|
||||
"model": car.id_car_model, # car_info.get("model"),
|
||||
"year": car.year, # car_info.get("year"),
|
||||
"logo": car.logo, # getattr(car.id_car_make, "logo", ""),
|
||||
"trim": car.id_car_trim, # car_info.get("trim"),
|
||||
"mileage": car.mileage, # car_info.get("mileage"),
|
||||
"cost_price": car.cost_price,
|
||||
"selling_price": car.selling_price,
|
||||
"marked_price": car.marked_price,
|
||||
@ -1091,21 +1091,23 @@ class CarFinanceCalculator1:
|
||||
"total_discount": discount,
|
||||
"final_price": sell_price + (sell_price * self.vat_rate),
|
||||
"total_additionals": car.total_additional_services,
|
||||
"grand_total": sell_price + (sell_price * self.vat_rate) + car.total_additional_services,
|
||||
"additional_services": car.additional_services,# self._get_nested_value(
|
||||
#item, self.ADDITIONAL_SERVICES_KEY
|
||||
#),
|
||||
"grand_total": sell_price
|
||||
+ (sell_price * self.vat_rate)
|
||||
+ car.total_additional_services,
|
||||
"additional_services": car.additional_services, # self._get_nested_value(
|
||||
# item, self.ADDITIONAL_SERVICES_KEY
|
||||
# ),
|
||||
}
|
||||
|
||||
def calculate_totals(self):
|
||||
total_price = sum(
|
||||
Decimal(item.item_model.car.marked_price)
|
||||
for item in self.item_transactions
|
||||
Decimal(item.item_model.car.marked_price) for item in self.item_transactions
|
||||
)
|
||||
total_additionals = sum(
|
||||
Decimal(item.price_) for item in self._get_additional_services())
|
||||
Decimal(item.price_) for item in self._get_additional_services()
|
||||
)
|
||||
|
||||
total_discount = self.extra_info.data.get("discount",0)
|
||||
total_discount = self.extra_info.data.get("discount", 0)
|
||||
total_price_discounted = total_price
|
||||
if total_discount:
|
||||
total_price_discounted = total_price - Decimal(total_discount)
|
||||
@ -1113,13 +1115,15 @@ class CarFinanceCalculator1:
|
||||
total_vat_amount = total_price_discounted * self.vat_rate
|
||||
|
||||
return {
|
||||
"total_price_discounted":total_price_discounted,
|
||||
"total_price_before_discount":total_price,
|
||||
"total_price_discounted": total_price_discounted,
|
||||
"total_price_before_discount": total_price,
|
||||
"total_price": total_price_discounted,
|
||||
"total_vat_amount": total_vat_amount,
|
||||
"total_discount": Decimal(total_discount),
|
||||
"total_additionals": total_additionals,
|
||||
"grand_total":total_price_discounted + total_vat_amount + total_additionals,
|
||||
"grand_total": total_price_discounted
|
||||
+ total_vat_amount
|
||||
+ total_additionals,
|
||||
}
|
||||
|
||||
def get_finance_data(self):
|
||||
@ -1131,7 +1135,9 @@ class CarFinanceCalculator1:
|
||||
),
|
||||
"total_price": round(totals["total_price"], 2),
|
||||
"total_price_discounted": round(totals["total_price_discounted"], 2),
|
||||
"total_price_before_discount": round(totals["total_price_before_discount"], 2),
|
||||
"total_price_before_discount": round(
|
||||
totals["total_price_before_discount"], 2
|
||||
),
|
||||
"total_vat": round(totals["total_vat_amount"] + totals["total_price"], 2),
|
||||
"total_vat_amount": round(totals["total_vat_amount"], 2),
|
||||
"total_discount": round(totals["total_discount"], 2),
|
||||
@ -1140,6 +1146,8 @@ class CarFinanceCalculator1:
|
||||
"additionals": self._get_additional_services(),
|
||||
"vat": round(self.vat_rate, 2),
|
||||
}
|
||||
|
||||
|
||||
class CarFinanceCalculator:
|
||||
"""
|
||||
Class responsible for calculating car financing details.
|
||||
@ -1185,22 +1193,25 @@ class CarFinanceCalculator:
|
||||
self.item_transactions = self._get_item_transactions()
|
||||
# self.additional_services = self._get_additional_services()
|
||||
|
||||
|
||||
def _get_vat_rate(self):
|
||||
vat = models.VatRate.objects.filter(dealer=self.dealer,is_active=True).first()
|
||||
vat = models.VatRate.objects.filter(dealer=self.dealer, is_active=True).first()
|
||||
if not vat:
|
||||
raise ObjectDoesNotExist("No active VAT rate found")
|
||||
return vat.rate
|
||||
|
||||
def _get_additional_services(self):
|
||||
return [x for item in self.item_transactions
|
||||
for x in item.item_model.car.additional_services
|
||||
]
|
||||
return [
|
||||
x
|
||||
for item in self.item_transactions
|
||||
for x in item.item_model.car.additional_services
|
||||
]
|
||||
|
||||
def _get_item_transactions(self):
|
||||
return self.model.get_itemtxs_data()[0].all()
|
||||
|
||||
def get_items(self):
|
||||
return self._get_item_transactions()
|
||||
|
||||
@staticmethod
|
||||
def _get_quantity(item):
|
||||
return item.ce_quantity or item.quantity
|
||||
@ -1215,17 +1226,17 @@ class CarFinanceCalculator:
|
||||
quantity = self._get_quantity(item)
|
||||
car = item.item_model.car
|
||||
unit_price = Decimal(car.marked_price)
|
||||
discount = self.extra_info.data.get("discount",0)
|
||||
discount = self.extra_info.data.get("discount", 0)
|
||||
sell_price = unit_price - Decimal(discount)
|
||||
return {
|
||||
"item_number": item.item_model.item_number,
|
||||
"vin": car.vin, #car_info.get("vin"),
|
||||
"make": car.id_car_make ,#car_info.get("make"),
|
||||
"model": car.id_car_model ,#car_info.get("model"),
|
||||
"year": car.year ,# car_info.get("year"),
|
||||
"logo": car.logo, # getattr(car.id_car_make, "logo", ""),
|
||||
"trim": car.id_car_trim ,# car_info.get("trim"),
|
||||
"mileage": car.mileage ,# car_info.get("mileage"),
|
||||
"vin": car.vin, # car_info.get("vin"),
|
||||
"make": car.id_car_make, # car_info.get("make"),
|
||||
"model": car.id_car_model, # car_info.get("model"),
|
||||
"year": car.year, # car_info.get("year"),
|
||||
"logo": car.logo, # getattr(car.id_car_make, "logo", ""),
|
||||
"trim": car.id_car_trim, # car_info.get("trim"),
|
||||
"mileage": car.mileage, # car_info.get("mileage"),
|
||||
"cost_price": car.cost_price,
|
||||
"selling_price": car.selling_price,
|
||||
"marked_price": car.marked_price,
|
||||
@ -1238,21 +1249,23 @@ class CarFinanceCalculator:
|
||||
"total_discount": discount,
|
||||
"final_price": sell_price + (sell_price * self.vat_rate),
|
||||
"total_additionals": car.total_additional_services,
|
||||
"grand_total": sell_price + (sell_price * self.vat_rate) + car.total_additional_services,
|
||||
"additional_services": car.additional_services,# self._get_nested_value(
|
||||
#item, self.ADDITIONAL_SERVICES_KEY
|
||||
#),
|
||||
"grand_total": sell_price
|
||||
+ (sell_price * self.vat_rate)
|
||||
+ car.total_additional_services,
|
||||
"additional_services": car.additional_services, # self._get_nested_value(
|
||||
# item, self.ADDITIONAL_SERVICES_KEY
|
||||
# ),
|
||||
}
|
||||
|
||||
def calculate_totals(self):
|
||||
total_price = sum(
|
||||
Decimal(item.item_model.car.marked_price)
|
||||
for item in self.item_transactions
|
||||
Decimal(item.item_model.car.marked_price) for item in self.item_transactions
|
||||
)
|
||||
total_additionals = sum(
|
||||
Decimal(item.price_) for item in self._get_additional_services())
|
||||
Decimal(item.price_) for item in self._get_additional_services()
|
||||
)
|
||||
|
||||
total_discount = self.extra_info.data.get("discount",0)
|
||||
total_discount = self.extra_info.data.get("discount", 0)
|
||||
total_price_discounted = total_price
|
||||
if total_discount:
|
||||
total_price_discounted = total_price - Decimal(total_discount)
|
||||
@ -1260,13 +1273,15 @@ class CarFinanceCalculator:
|
||||
total_vat_amount = total_price_discounted * self.vat_rate
|
||||
|
||||
return {
|
||||
"total_price_discounted":total_price_discounted,
|
||||
"total_price_before_discount":total_price,
|
||||
"total_price_discounted": total_price_discounted,
|
||||
"total_price_before_discount": total_price,
|
||||
"total_price": total_price_discounted,
|
||||
"total_vat_amount": total_vat_amount,
|
||||
"total_discount": Decimal(total_discount),
|
||||
"total_additionals": total_additionals,
|
||||
"grand_total":total_price_discounted + total_vat_amount + total_additionals,
|
||||
"grand_total": total_price_discounted
|
||||
+ total_vat_amount
|
||||
+ total_additionals,
|
||||
}
|
||||
|
||||
def get_finance_data(self):
|
||||
@ -1278,7 +1293,9 @@ class CarFinanceCalculator:
|
||||
),
|
||||
"total_price": round(totals["total_price"], 2),
|
||||
"total_price_discounted": round(totals["total_price_discounted"], 2),
|
||||
"total_price_before_discount": round(totals["total_price_before_discount"], 2),
|
||||
"total_price_before_discount": round(
|
||||
totals["total_price_before_discount"], 2
|
||||
),
|
||||
"total_vat": round(totals["total_vat_amount"] + totals["total_price"], 2),
|
||||
"total_vat_amount": round(totals["total_vat_amount"], 2),
|
||||
"total_discount": round(totals["total_discount"], 2),
|
||||
@ -1288,58 +1305,60 @@ class CarFinanceCalculator:
|
||||
"vat": round(self.vat_rate, 2),
|
||||
}
|
||||
|
||||
def get_finance_data(estimate,dealer):
|
||||
vat = models.VatRate.objects.filter(dealer=dealer,is_active=True).first()
|
||||
item = estimate.get_itemtxs_data()[0].first()
|
||||
car = item.item_model.car
|
||||
if isinstance(estimate,InvoiceModel) and hasattr(estimate, "ce_model"):
|
||||
estimate = estimate.ce_model
|
||||
|
||||
extra_info = models.ExtraInfo.objects.get(
|
||||
dealer=dealer,
|
||||
content_type=ContentType.objects.get_for_model(EstimateModel),
|
||||
object_id=estimate.pk,
|
||||
)
|
||||
discount = extra_info.data.get("discount", 0)
|
||||
discount = Decimal(discount)
|
||||
def get_finance_data(estimate, dealer):
|
||||
vat = models.VatRate.objects.filter(dealer=dealer, is_active=True).first()
|
||||
item = estimate.get_itemtxs_data()[0].first()
|
||||
car = item.item_model.car
|
||||
if isinstance(estimate, InvoiceModel) and hasattr(estimate, "ce_model"):
|
||||
estimate = estimate.ce_model
|
||||
|
||||
additional_services = car.get_additional_services()
|
||||
discounted_price=(Decimal(car.marked_price) - discount)
|
||||
vat_amount = discounted_price * vat.rate
|
||||
total_services_vat=sum([x[1] for x in additional_services.get("services")])
|
||||
total_vat=vat_amount+total_services_vat
|
||||
return {
|
||||
"car": car,
|
||||
"discounted_price": discounted_price or 0,
|
||||
"price_before_discount": car.marked_price,
|
||||
"vat_amount": vat_amount,
|
||||
"vat_rate": vat.rate,
|
||||
"discount_amount": discount,
|
||||
"additional_services": additional_services,
|
||||
"final_price": discounted_price + vat_amount,
|
||||
"total_services_vat":total_services_vat,
|
||||
"total_vat":total_vat,
|
||||
"grand_total": discounted_price + total_vat + additional_services.get("total")
|
||||
}
|
||||
extra_info = models.ExtraInfo.objects.get(
|
||||
dealer=dealer,
|
||||
content_type=ContentType.objects.get_for_model(EstimateModel),
|
||||
object_id=estimate.pk,
|
||||
)
|
||||
discount = extra_info.data.get("discount", 0)
|
||||
discount = Decimal(discount)
|
||||
|
||||
additional_services = car.get_additional_services()
|
||||
discounted_price = Decimal(car.marked_price) - discount
|
||||
vat_amount = discounted_price * vat.rate
|
||||
total_services_vat = sum([x[1] for x in additional_services.get("services")])
|
||||
total_vat = vat_amount + total_services_vat
|
||||
return {
|
||||
"car": car,
|
||||
"discounted_price": discounted_price or 0,
|
||||
"price_before_discount": car.marked_price,
|
||||
"vat_amount": vat_amount,
|
||||
"vat_rate": vat.rate,
|
||||
"discount_amount": discount,
|
||||
"additional_services": additional_services,
|
||||
"final_price": discounted_price + vat_amount,
|
||||
"total_services_vat": total_services_vat,
|
||||
"total_vat": total_vat,
|
||||
"grand_total": discounted_price + total_vat + additional_services.get("total"),
|
||||
}
|
||||
|
||||
# totals = self.calculate_totals()
|
||||
# return {
|
||||
# "car": [self._get_car_data(item) for item in self.item_transactions],
|
||||
# "quantity": sum(
|
||||
# self._get_quantity(item) for item in self.item_transactions
|
||||
# ),
|
||||
# "total_price": round(totals["total_price"], 2),
|
||||
# "total_price_discounted": round(totals["total_price_discounted"], 2),
|
||||
# "total_price_before_discount": round(totals["total_price_before_discount"], 2),
|
||||
# "total_vat": round(totals["total_vat_amount"] + totals["total_price"], 2),
|
||||
# "total_vat_amount": round(totals["total_vat_amount"], 2),
|
||||
# "total_discount": round(totals["total_discount"], 2),
|
||||
# "total_additionals": round(totals["total_additionals"], 2),
|
||||
# "grand_total": round(totals["grand_total"], 2),
|
||||
# "additionals": self._get_additional_services(),
|
||||
# "vat": round(self.vat_rate, 2),
|
||||
# }
|
||||
|
||||
|
||||
# totals = self.calculate_totals()
|
||||
# return {
|
||||
# "car": [self._get_car_data(item) for item in self.item_transactions],
|
||||
# "quantity": sum(
|
||||
# self._get_quantity(item) for item in self.item_transactions
|
||||
# ),
|
||||
# "total_price": round(totals["total_price"], 2),
|
||||
# "total_price_discounted": round(totals["total_price_discounted"], 2),
|
||||
# "total_price_before_discount": round(totals["total_price_before_discount"], 2),
|
||||
# "total_vat": round(totals["total_vat_amount"] + totals["total_price"], 2),
|
||||
# "total_vat_amount": round(totals["total_vat_amount"], 2),
|
||||
# "total_discount": round(totals["total_discount"], 2),
|
||||
# "total_additionals": round(totals["total_additionals"], 2),
|
||||
# "grand_total": round(totals["grand_total"], 2),
|
||||
# "additionals": self._get_additional_services(),
|
||||
# "vat": round(self.vat_rate, 2),
|
||||
# }
|
||||
# class CarFinanceCalculator:
|
||||
# """
|
||||
# Class responsible for calculating car financing details.
|
||||
@ -1554,7 +1573,6 @@ def get_local_name(self):
|
||||
return getattr(self, "name", None)
|
||||
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def set_invoice_payment(dealer, entity, invoice, amount, payment_method):
|
||||
"""
|
||||
@ -1566,6 +1584,7 @@ def set_invoice_payment(dealer, entity, invoice, amount, payment_method):
|
||||
|
||||
_post_sale_and_cogs(invoice, dealer)
|
||||
|
||||
|
||||
def _post_sale_and_cogs(invoice, dealer):
|
||||
"""
|
||||
For every car line on the invoice:
|
||||
@ -1574,15 +1593,39 @@ def _post_sale_and_cogs(invoice, dealer):
|
||||
"""
|
||||
entity = invoice.ledger.entity
|
||||
# calc = CarFinanceCalculator(invoice)
|
||||
data = get_finance_data(invoice,dealer)
|
||||
data = get_finance_data(invoice, dealer)
|
||||
car = data.get("car")
|
||||
cash_acc = entity.get_default_coa_accounts().filter(role_default=True, role=roles.ASSET_CA_CASH).first()
|
||||
ar_acc = entity.get_default_coa_accounts().filter(role_default=True, role=roles.ASSET_CA_RECEIVABLES).first()
|
||||
vat_acc = entity.get_default_coa_accounts().filter(role_default=True, role=roles.LIABILITY_CL_TAXES_PAYABLE).first()
|
||||
car_rev = entity.get_default_coa_accounts().filter(role_default=True, role=roles.INCOME_OPERATIONAL).first()
|
||||
add_rev = entity.get_default_coa_accounts().filter(code="4020").first()
|
||||
cogs_acc = entity.get_default_coa_accounts().filter(role_default=True, role=roles.COGS).first()
|
||||
inv_acc = entity.get_default_coa_accounts().filter(role_default=True, role=roles.ASSET_CA_INVENTORY).first()
|
||||
cash_acc = (
|
||||
entity.get_default_coa_accounts()
|
||||
.filter(role_default=True, role=roles.ASSET_CA_CASH)
|
||||
.first()
|
||||
)
|
||||
ar_acc = (
|
||||
entity.get_default_coa_accounts()
|
||||
.filter(role_default=True, role=roles.ASSET_CA_RECEIVABLES)
|
||||
.first()
|
||||
)
|
||||
vat_acc = (
|
||||
entity.get_default_coa_accounts()
|
||||
.filter(role_default=True, role=roles.LIABILITY_CL_TAXES_PAYABLE)
|
||||
.first()
|
||||
)
|
||||
car_rev = (
|
||||
entity.get_default_coa_accounts()
|
||||
.filter(role_default=True, role=roles.INCOME_OPERATIONAL)
|
||||
.first()
|
||||
)
|
||||
add_rev = entity.get_default_coa_accounts().filter(code="4020").first()
|
||||
cogs_acc = (
|
||||
entity.get_default_coa_accounts()
|
||||
.filter(role_default=True, role=roles.COGS)
|
||||
.first()
|
||||
)
|
||||
inv_acc = (
|
||||
entity.get_default_coa_accounts()
|
||||
.filter(role_default=True, role=roles.ASSET_CA_INVENTORY)
|
||||
.first()
|
||||
)
|
||||
|
||||
# for car_data in data['cars']:
|
||||
# car = invoice.get_itemtxs_data()[0].filter(
|
||||
@ -1590,12 +1633,12 @@ def _post_sale_and_cogs(invoice, dealer):
|
||||
# ).first().item_model.car
|
||||
# qty = Decimal(car_data['quantity'])
|
||||
|
||||
net_car_price = Decimal(data['discounted_price'])
|
||||
net_additionals_price = Decimal(data['additional_services']['total'])
|
||||
vat_amount = Decimal(data['vat_amount'])
|
||||
grand_total = net_car_price + car.get_additional_services_amount_ + vat_amount
|
||||
cost_total = Decimal(car.cost_price)
|
||||
discount_amount =Decimal(data['discount_amount'])
|
||||
net_car_price = Decimal(data["discounted_price"])
|
||||
net_additionals_price = Decimal(data["additional_services"]["total"])
|
||||
vat_amount = Decimal(data["vat_amount"])
|
||||
grand_total = net_car_price + car.get_additional_services_amount_ + vat_amount
|
||||
cost_total = Decimal(car.cost_price)
|
||||
discount_amount = Decimal(data["discount_amount"])
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 2A. Journal: Cash / A-R / VAT / Sales
|
||||
@ -1606,15 +1649,15 @@ def _post_sale_and_cogs(invoice, dealer):
|
||||
description=f"Sale {car.vin}",
|
||||
origin=f"Invoice {invoice.invoice_number}",
|
||||
locked=False,
|
||||
posted=False
|
||||
posted=False,
|
||||
)
|
||||
# Dr Cash (what the customer paid)
|
||||
TransactionModel.objects.create(
|
||||
journal_entry=je_sale,
|
||||
account=cash_acc,
|
||||
amount=grand_total,
|
||||
tx_type='debit',
|
||||
description='Debit to Cash on Hand'
|
||||
tx_type="debit",
|
||||
description="Debit to Cash on Hand",
|
||||
)
|
||||
|
||||
# # Cr A/R (clear the receivable)
|
||||
@ -1630,8 +1673,8 @@ def _post_sale_and_cogs(invoice, dealer):
|
||||
journal_entry=je_sale,
|
||||
account=vat_acc,
|
||||
amount=vat_amount,
|
||||
tx_type='credit',
|
||||
description="Credit to Tax Payable"
|
||||
tx_type="credit",
|
||||
description="Credit to Tax Payable",
|
||||
)
|
||||
|
||||
# Cr Sales – Car
|
||||
@ -1639,8 +1682,8 @@ def _post_sale_and_cogs(invoice, dealer):
|
||||
journal_entry=je_sale,
|
||||
account=car_rev,
|
||||
amount=net_car_price,
|
||||
tx_type='credit',
|
||||
description=" Credit to Car Sales"
|
||||
tx_type="credit",
|
||||
description=" Credit to Car Sales",
|
||||
)
|
||||
|
||||
if car.get_additional_services_amount > 0:
|
||||
@ -1649,16 +1692,15 @@ def _post_sale_and_cogs(invoice, dealer):
|
||||
journal_entry=je_sale,
|
||||
account=add_rev,
|
||||
amount=car.get_additional_services_amount,
|
||||
tx_type='credit',
|
||||
description="Credit to After-Sales Services"
|
||||
tx_type="credit",
|
||||
description="Credit to After-Sales Services",
|
||||
)
|
||||
TransactionModel.objects.create(
|
||||
journal_entry=je_sale,
|
||||
|
||||
account=vat_acc,
|
||||
amount=car.get_additional_services_vat,
|
||||
tx_type='credit',
|
||||
description="Credit to Tax Payable (Additional Services)"
|
||||
tx_type="credit",
|
||||
description="Credit to Tax Payable (Additional Services)",
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@ -1669,7 +1711,7 @@ def _post_sale_and_cogs(invoice, dealer):
|
||||
description=f"COGS {car.vin}",
|
||||
origin=f"Invoice {invoice.invoice_number}",
|
||||
locked=False,
|
||||
posted=False
|
||||
posted=False,
|
||||
)
|
||||
|
||||
# Dr COGS
|
||||
@ -1677,15 +1719,12 @@ def _post_sale_and_cogs(invoice, dealer):
|
||||
journal_entry=je_cogs,
|
||||
account=cogs_acc,
|
||||
amount=cost_total,
|
||||
tx_type='debit',
|
||||
tx_type="debit",
|
||||
)
|
||||
|
||||
# Cr Inventory
|
||||
TransactionModel.objects.create(
|
||||
journal_entry=je_cogs,
|
||||
account=inv_acc,
|
||||
amount=cost_total,
|
||||
tx_type='credit'
|
||||
journal_entry=je_cogs, account=inv_acc, amount=cost_total, tx_type="credit"
|
||||
)
|
||||
# ------------------------------------------------------------------
|
||||
# 2C. Update car state flags inside the same transaction
|
||||
@ -1693,10 +1732,12 @@ def _post_sale_and_cogs(invoice, dealer):
|
||||
entity.get_items_inventory().filter(name=car.vin).update(for_inventory=False)
|
||||
# car.item_model.for_inventory = False
|
||||
# car.item_model.save(update_fields=['for_inventory'])
|
||||
car.discount_amount=discount_amount
|
||||
car.discount_amount = discount_amount
|
||||
car.selling_price = grand_total
|
||||
# car.is_sold = True
|
||||
car.save()
|
||||
|
||||
|
||||
# def handle_account_process(invoice, amount, finance_data):
|
||||
# """
|
||||
# Processes accounting transactions based on an invoice, financial data,
|
||||
@ -1787,29 +1828,29 @@ def _post_sale_and_cogs(invoice, dealer):
|
||||
# car.finances.save()
|
||||
# car.item_model.save()
|
||||
|
||||
# TransactionModel.objects.create(
|
||||
# journal_entry=journal,
|
||||
# account=additional_services_account, # Debit Additional Services
|
||||
# amount=Decimal(car.finances.total_additionals),
|
||||
# tx_type="debit",
|
||||
# description="Additional Services",
|
||||
# )
|
||||
# TransactionModel.objects.create(
|
||||
# journal_entry=journal,
|
||||
# account=additional_services_account, # Debit Additional Services
|
||||
# amount=Decimal(car.finances.total_additionals),
|
||||
# tx_type="debit",
|
||||
# description="Additional Services",
|
||||
# )
|
||||
|
||||
# TransactionModel.objects.create(
|
||||
# journal_entry=journal,
|
||||
# account=inventory_account, # Credit Inventory account
|
||||
# amount=Decimal(finance_data.get("grand_total")),
|
||||
# tx_type="credit",
|
||||
# description="Account Adjustment",
|
||||
# )
|
||||
# TransactionModel.objects.create(
|
||||
# journal_entry=journal,
|
||||
# account=inventory_account, # Credit Inventory account
|
||||
# amount=Decimal(finance_data.get("grand_total")),
|
||||
# tx_type="credit",
|
||||
# description="Account Adjustment",
|
||||
# )
|
||||
|
||||
# TransactionModel.objects.create(
|
||||
# journal_entry=journal,
|
||||
# account=vat_payable_account, # Credit VAT Payable
|
||||
# amount=finance_data.get("total_vat_amount"),
|
||||
# tx_type="credit",
|
||||
# description="VAT Payable on Invoice",
|
||||
# )
|
||||
# TransactionModel.objects.create(
|
||||
# journal_entry=journal,
|
||||
# account=vat_payable_account, # Credit VAT Payable
|
||||
# amount=finance_data.get("total_vat_amount"),
|
||||
# tx_type="credit",
|
||||
# description="VAT Payable on Invoice",
|
||||
# )
|
||||
|
||||
|
||||
def create_make_accounts(dealer):
|
||||
@ -1857,6 +1898,7 @@ def create_make_accounts(dealer):
|
||||
active=True,
|
||||
)
|
||||
|
||||
|
||||
def handle_payment(request, order):
|
||||
url = "https://api.moyasar.com/v1/payments"
|
||||
callback_url = request.build_absolute_uri(
|
||||
@ -1943,7 +1985,6 @@ def handle_payment(request, order):
|
||||
# return user.dealer.quota
|
||||
|
||||
|
||||
|
||||
def get_accounts_data():
|
||||
return [
|
||||
# Current Assets (must start with 1)
|
||||
@ -2339,6 +2380,7 @@ def get_accounts_data():
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def create_account(entity, coa, account_data):
|
||||
try:
|
||||
account = entity.create_account(
|
||||
@ -2371,17 +2413,16 @@ def get_or_generate_car_image(car):
|
||||
return car_image.image.url
|
||||
|
||||
# Check for existing image with same hash
|
||||
existing = models.CarImage.objects.filter(
|
||||
image_hash=car_image.image_hash,
|
||||
image__isnull=False
|
||||
).exclude(car=car).first()
|
||||
existing = (
|
||||
models.CarImage.objects.filter(
|
||||
image_hash=car_image.image_hash, image__isnull=False
|
||||
)
|
||||
.exclude(car=car)
|
||||
.first()
|
||||
)
|
||||
|
||||
if existing:
|
||||
car_image.image.save(
|
||||
existing.image.name,
|
||||
existing.image.file,
|
||||
save=True
|
||||
)
|
||||
car_image.image.save(existing.image.name, existing.image.file, save=True)
|
||||
return car_image.image.url
|
||||
|
||||
# If no image exists and not already generating, schedule generation
|
||||
@ -2394,6 +2435,7 @@ def get_or_generate_car_image(car):
|
||||
logger.error(f"Error getting/generating car image: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def force_regenerate_car_image(car):
|
||||
"""
|
||||
Force regeneration of car image (useful for admin actions)
|
||||
@ -2414,6 +2456,7 @@ def force_regenerate_car_image(car):
|
||||
logger.error(f"Error forcing image regeneration: {e}")
|
||||
return False
|
||||
|
||||
|
||||
class CarImageAPIClient:
|
||||
"""Simple client to handle authenticated requests to the car image API"""
|
||||
|
||||
@ -2436,7 +2479,7 @@ class CarImageAPIClient:
|
||||
response.raise_for_status()
|
||||
|
||||
# Get CSRF token from cookies
|
||||
self.csrf_token = self.session.cookies.get('csrftoken')
|
||||
self.csrf_token = self.session.cookies.get("csrftoken")
|
||||
if not self.csrf_token:
|
||||
raise Exception("CSRF token not found in cookies")
|
||||
|
||||
@ -2444,16 +2487,17 @@ class CarImageAPIClient:
|
||||
login_data = {
|
||||
"username": self.USERNAME,
|
||||
"password": self.PASSWORD,
|
||||
"csrfmiddlewaretoken": self.csrf_token
|
||||
"csrfmiddlewaretoken": self.csrf_token,
|
||||
}
|
||||
|
||||
login_response = self.session.post(
|
||||
f"{self.BASE_URL}/login",
|
||||
data=login_data
|
||||
f"{self.BASE_URL}/login", data=login_data
|
||||
)
|
||||
|
||||
if login_response.status_code != 200:
|
||||
raise Exception(f"Login failed with status {login_response.status_code}")
|
||||
raise Exception(
|
||||
f"Login failed with status {login_response.status_code}"
|
||||
)
|
||||
|
||||
logger.info("Successfully logged in to car image API")
|
||||
return True
|
||||
@ -2472,39 +2516,38 @@ class CarImageAPIClient:
|
||||
|
||||
try:
|
||||
headers = {
|
||||
'X-CSRFToken': self.csrf_token,
|
||||
'Referer': self.BASE_URL,
|
||||
"X-CSRFToken": self.csrf_token,
|
||||
"Referer": self.BASE_URL,
|
||||
}
|
||||
print(payload)
|
||||
generate_data = {
|
||||
"year": payload['year'],
|
||||
"make": payload['make'],
|
||||
"model": payload['model'],
|
||||
"exterior_color": payload['color'],
|
||||
"year": payload["year"],
|
||||
"make": payload["make"],
|
||||
"model": payload["model"],
|
||||
"exterior_color": payload["color"],
|
||||
"angle": "3/4 rear",
|
||||
"reference_image": ""
|
||||
"reference_image": "",
|
||||
}
|
||||
|
||||
response = self.session.post(
|
||||
f"{self.BASE_URL}/generate",
|
||||
json=generate_data,
|
||||
headers=headers,
|
||||
timeout=160
|
||||
timeout=160,
|
||||
)
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
# Parse response
|
||||
result = response.json()
|
||||
image_url = result.get('url')
|
||||
image_url = result.get("url")
|
||||
|
||||
if not image_url:
|
||||
raise Exception("No image URL in response")
|
||||
|
||||
# Download the actual image
|
||||
image_response = self.session.get(
|
||||
f"{self.BASE_URL}{image_url}",
|
||||
timeout=160
|
||||
f"{self.BASE_URL}{image_url}", timeout=160
|
||||
)
|
||||
|
||||
image_response.raise_for_status()
|
||||
@ -2520,9 +2563,11 @@ class CarImageAPIClient:
|
||||
logger.error(error_msg)
|
||||
return None, error_msg
|
||||
|
||||
|
||||
# Global client instance
|
||||
api_client = CarImageAPIClient()
|
||||
|
||||
|
||||
def resize_image(image_data, max_size=(800, 600)):
|
||||
"""
|
||||
Resize image to make it smaller while maintaining aspect ratio
|
||||
@ -2539,29 +2584,31 @@ def resize_image(image_data, max_size=(800, 600)):
|
||||
# Save back to bytes in original format
|
||||
output_buffer = BytesIO()
|
||||
|
||||
if original_format and original_format.upper() in ['JPEG', 'JPG']:
|
||||
img.save(output_buffer, format='JPEG', quality=95, optimize=True)
|
||||
elif original_format and original_format.upper() == 'PNG':
|
||||
if original_format and original_format.upper() in ["JPEG", "JPG"]:
|
||||
img.save(output_buffer, format="JPEG", quality=95, optimize=True)
|
||||
elif original_format and original_format.upper() == "PNG":
|
||||
# Preserve transparency for PNG
|
||||
if original_mode == 'RGBA':
|
||||
img.save(output_buffer, format='PNG', optimize=True)
|
||||
if original_mode == "RGBA":
|
||||
img.save(output_buffer, format="PNG", optimize=True)
|
||||
else:
|
||||
img.save(output_buffer, format='PNG', optimize=True)
|
||||
img.save(output_buffer, format="PNG", optimize=True)
|
||||
else:
|
||||
# Default to JPEG for other formats
|
||||
if img.mode in ('RGBA', 'LA', 'P'):
|
||||
if img.mode in ("RGBA", "LA", "P"):
|
||||
# Convert to RGB if image has transparency
|
||||
background = Image.new('RGB', img.size, (255, 255, 255))
|
||||
if img.mode == 'RGBA':
|
||||
background = Image.new("RGB", img.size, (255, 255, 255))
|
||||
if img.mode == "RGBA":
|
||||
background.paste(img, mask=img.split()[3])
|
||||
else:
|
||||
background.paste(img, (0, 0))
|
||||
img = background
|
||||
img.save(output_buffer, format='JPEG', quality=95, optimize=True)
|
||||
img.save(output_buffer, format="JPEG", quality=95, optimize=True)
|
||||
|
||||
resized_data = output_buffer.getvalue()
|
||||
|
||||
logger.info(f"Resized image from {len(image_data)} to {len(resized_data)} bytes")
|
||||
logger.info(
|
||||
f"Resized image from {len(image_data)} to {len(resized_data)} bytes"
|
||||
)
|
||||
return resized_data, None
|
||||
|
||||
except Exception as e:
|
||||
@ -2569,6 +2616,7 @@ def resize_image(image_data, max_size=(800, 600)):
|
||||
logger.error(error_msg)
|
||||
return None, error_msg
|
||||
|
||||
|
||||
def generate_car_image_simple(car_image):
|
||||
"""
|
||||
Simple function to generate car image with authentication and resizing
|
||||
@ -2577,10 +2625,10 @@ def generate_car_image_simple(car_image):
|
||||
|
||||
# Prepare payload
|
||||
payload = {
|
||||
'make': car.id_car_make.name if car.id_car_make else '',
|
||||
'model': car.id_car_model.name if car.id_car_model else '',
|
||||
'year': car.year,
|
||||
'color': car.colors.exterior.name
|
||||
"make": car.id_car_make.name if car.id_car_make else "",
|
||||
"model": car.id_car_model.name if car.id_car_model else "",
|
||||
"year": car.year,
|
||||
"color": car.colors.exterior.name,
|
||||
}
|
||||
|
||||
logger.info(f"Generating image for car {car.vin}")
|
||||
@ -2589,10 +2637,10 @@ def generate_car_image_simple(car_image):
|
||||
image_data, error = api_client.generate_image(payload)
|
||||
|
||||
if error:
|
||||
return {'success': False, 'error': error}
|
||||
return {"success": False, "error": error}
|
||||
|
||||
if not image_data:
|
||||
return {'success': False, 'error': 'No image data received'}
|
||||
return {"success": False, "error": "No image data received"}
|
||||
|
||||
try:
|
||||
# Resize the image to make it smaller
|
||||
@ -2606,21 +2654,21 @@ def generate_car_image_simple(car_image):
|
||||
# Determine file extension based on content
|
||||
try:
|
||||
img = Image.open(BytesIO(resized_data))
|
||||
file_extension = img.format.lower() if img.format else 'jpg'
|
||||
file_extension = img.format.lower() if img.format else "jpg"
|
||||
except:
|
||||
file_extension = 'jpg'
|
||||
file_extension = "jpg"
|
||||
|
||||
# Save the resized image
|
||||
car_image.image.save(
|
||||
f"{car_image.image_hash}.{file_extension}",
|
||||
ContentFile(resized_data),
|
||||
save=False
|
||||
save=False,
|
||||
)
|
||||
|
||||
logger.info(f"Successfully generated and resized image for car {car.vin}")
|
||||
return {'success': True}
|
||||
return {"success": True}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Image processing failed: {e}"
|
||||
logger.error(error_msg)
|
||||
return {'success': False, 'error': error_msg}
|
||||
return {"success": False, "error": error_msg}
|
||||
|
||||
@ -2,13 +2,15 @@ from django.core.validators import RegexValidator
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import re
|
||||
|
||||
|
||||
class SaudiPhoneNumberValidator(RegexValidator):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(
|
||||
regex=r"^(\+9665|05)[0-9]{8}$",
|
||||
message=_("Enter a valid Saudi phone number (05XXXXXXXX or +9665XXXXXXXX)"),
|
||||
)
|
||||
|
||||
def __call__(self, value):
|
||||
# Remove any whitespace, dashes, or other separators
|
||||
cleaned_value = re.sub(r'[\s\-\(\)\.]', '', str(value))
|
||||
super().__call__(cleaned_value)
|
||||
cleaned_value = re.sub(r"[\s\-\(\)\.]", "", str(value))
|
||||
super().__call__(cleaned_value)
|
||||
|
||||
1622
inventory/views.py
1622
inventory/views.py
File diff suppressed because it is too large
Load Diff
@ -46,7 +46,7 @@ def run():
|
||||
# arabic_name=item.get("arabic_name", ""),
|
||||
# logo=item.get("Logo", ""),
|
||||
# is_sa_import=item.get("is_sa_import", False),
|
||||
slug=unique_slug
|
||||
slug=unique_slug,
|
||||
)
|
||||
|
||||
# Step 2: Insert CarModel
|
||||
@ -60,7 +60,7 @@ def run():
|
||||
id_car_make_id=item["id_car_make"],
|
||||
name=item["name"],
|
||||
# arabic_name=item.get("arabic_name", ""),
|
||||
slug=unique_slug
|
||||
slug=unique_slug,
|
||||
)
|
||||
|
||||
# Step 3: Insert CarSerie
|
||||
@ -77,7 +77,7 @@ def run():
|
||||
year_begin=item.get("year_begin"),
|
||||
year_end=item.get("year_end"),
|
||||
generation_name=item.get("generation_name", ""),
|
||||
slug=unique_slug
|
||||
slug=unique_slug,
|
||||
)
|
||||
|
||||
# Step 4: Insert CarTrim
|
||||
@ -98,9 +98,10 @@ def run():
|
||||
|
||||
# Step 5: Insert CarEquipment
|
||||
|
||||
|
||||
for item in tqdm(data["car_equipment"], desc="Inserting CarEquipment"):
|
||||
if not CarEquipment.objects.filter(id_car_equipment=item["id_car_equipment"]).exists():
|
||||
if not CarEquipment.objects.filter(
|
||||
id_car_equipment=item["id_car_equipment"]
|
||||
).exists():
|
||||
if CarTrim.objects.filter(id_car_trim=item["id_car_trim"]).exists():
|
||||
unique_slug = generate_unique_slug(CarEquipment, item["name"])
|
||||
CarEquipment.objects.create(
|
||||
@ -108,7 +109,7 @@ def run():
|
||||
id_car_trim_id=item["id_car_trim"],
|
||||
name=item["name"],
|
||||
year_begin=item.get("year"),
|
||||
slug=unique_slug
|
||||
slug=unique_slug,
|
||||
)
|
||||
|
||||
# Step 6: Insert CarSpecification (Parent specifications first)
|
||||
|
||||
@ -34,7 +34,7 @@ def run():
|
||||
is_boolean=True,
|
||||
url="pricing",
|
||||
)
|
||||
# Create the plans
|
||||
# Create the plans
|
||||
free_plan = Plan.objects.create(
|
||||
name="Free",
|
||||
description="Free plan with limited features",
|
||||
@ -46,7 +46,7 @@ def run():
|
||||
order=1,
|
||||
)
|
||||
free_plan.quotas.add(free_quota)
|
||||
|
||||
|
||||
# Create the plans
|
||||
basic_plan = Plan.objects.create(
|
||||
name="Basic",
|
||||
@ -58,7 +58,7 @@ def run():
|
||||
visible=True,
|
||||
order=1,
|
||||
)
|
||||
basic_plan.quotas.add(basic_quota,free_quota)
|
||||
basic_plan.quotas.add(basic_quota, free_quota)
|
||||
|
||||
pro_plan = Plan.objects.create(
|
||||
name="Professional",
|
||||
@ -69,7 +69,7 @@ def run():
|
||||
visible=True,
|
||||
# order=2
|
||||
)
|
||||
pro_plan.quotas.add(free_quota,basic_quota, pro_quota)
|
||||
pro_plan.quotas.add(free_quota, basic_quota, pro_quota)
|
||||
|
||||
premium_plan = Plan.objects.create(
|
||||
name="Premium",
|
||||
@ -80,4 +80,4 @@ def run():
|
||||
visible=True,
|
||||
order=3,
|
||||
)
|
||||
premium_plan.quotas.add(free_quota,basic_quota, pro_quota, premium_quota)
|
||||
premium_plan.quotas.add(free_quota, basic_quota, pro_quota, premium_quota)
|
||||
|
||||
@ -1,177 +1,175 @@
|
||||
{% load i18n %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>403 - Access Forbidden</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--dark-bg: #121212;
|
||||
--main-color: #ff3864;
|
||||
--secondary-color: #e6e6e6;
|
||||
}
|
||||
body, html {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: var(--dark-bg);
|
||||
color: var(--secondary-color);
|
||||
overflow: hidden; /* Hide overflow for the particles */
|
||||
}
|
||||
.center-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
z-index: 2; /* Ensure content is on top of particles */
|
||||
position: relative;
|
||||
}
|
||||
.glitch {
|
||||
font-size: 10rem;
|
||||
font-weight: bold;
|
||||
color: var(--main-color);
|
||||
position: relative;
|
||||
animation: glitch-animation 2.5s infinite;
|
||||
}
|
||||
@keyframes glitch-animation {
|
||||
0% { text-shadow: 2px 2px var(--secondary-color); }
|
||||
20% { text-shadow: -2px -2px var(--secondary-color); }
|
||||
40% { text-shadow: 4px 4px var(--main-color); }
|
||||
60% { text-shadow: -4px -4px var(--main-color); }
|
||||
80% { text-shadow: 6px 6px var(--secondary-color); }
|
||||
100% { text-shadow: -6px -6px var(--secondary-color); }
|
||||
}
|
||||
.main-message {
|
||||
font-size: 2.5rem;
|
||||
margin-top: -20px;
|
||||
letter-spacing: 5px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.sub-message {
|
||||
font-size: 1.2rem;
|
||||
color: #8c8c8c;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.home-button {
|
||||
margin-top: 30px;
|
||||
padding: 12px 30px;
|
||||
background-color: var(--main-color);
|
||||
border: none;
|
||||
color: #fff;
|
||||
border-radius: 5px;
|
||||
font-size: 1rem;
|
||||
text-decoration: none;
|
||||
transition: background-color 0.3s, transform 0.3s;
|
||||
}
|
||||
.home-button:hover {
|
||||
background-color: #d12e52;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>403 - Access Forbidden</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--dark-bg: #121212;
|
||||
--main-color: #ff3864;
|
||||
--secondary-color: #e6e6e6;
|
||||
}
|
||||
body, html {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: var(--dark-bg);
|
||||
color: var(--secondary-color);
|
||||
overflow: hidden; /* Hide overflow for the particles */
|
||||
}
|
||||
.center-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
z-index: 2; /* Ensure content is on top of particles */
|
||||
position: relative;
|
||||
}
|
||||
.glitch {
|
||||
font-size: 10rem;
|
||||
font-weight: bold;
|
||||
color: var(--main-color);
|
||||
position: relative;
|
||||
animation: glitch-animation 2.5s infinite;
|
||||
}
|
||||
@keyframes glitch-animation {
|
||||
0% { text-shadow: 2px 2px var(--secondary-color); }
|
||||
20% { text-shadow: -2px -2px var(--secondary-color); }
|
||||
40% { text-shadow: 4px 4px var(--main-color); }
|
||||
60% { text-shadow: -4px -4px var(--main-color); }
|
||||
80% { text-shadow: 6px 6px var(--secondary-color); }
|
||||
100% { text-shadow: -6px -6px var(--secondary-color); }
|
||||
}
|
||||
.main-message {
|
||||
font-size: 2.5rem;
|
||||
margin-top: -20px;
|
||||
letter-spacing: 5px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.sub-message {
|
||||
font-size: 1.2rem;
|
||||
color: #8c8c8c;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.home-button {
|
||||
margin-top: 30px;
|
||||
padding: 12px 30px;
|
||||
background-color: var(--main-color);
|
||||
border: none;
|
||||
color: #fff;
|
||||
border-radius: 5px;
|
||||
font-size: 1rem;
|
||||
text-decoration: none;
|
||||
transition: background-color 0.3s, transform 0.3s;
|
||||
}
|
||||
.home-button:hover {
|
||||
background-color: #d12e52;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Particle Background styles */
|
||||
#particles-js {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="particles-js"></div>
|
||||
|
||||
<div class="center-content container-fluid">
|
||||
<h1 class="glitch">403</h1>
|
||||
<h2 class="main-message">{% trans "Access Forbidden" %}</h2>
|
||||
<p class="sub-message">{% trans "You do not have permission to view this page."%}</p>
|
||||
<p class="sub-message fs-2">{% trans "Powered By Tenhal, Riyadh Saudi Arabia"%}</p>
|
||||
<a href="{% url 'home' %}" class="home-button">{% trans "Go Home" %}</a>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/particles.js@2.0.0/particles.min.js"></script>
|
||||
<script>
|
||||
#particles-js {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="particles-js"></div>
|
||||
<div class="center-content container-fluid">
|
||||
<h1 class="glitch">403</h1>
|
||||
<h2 class="main-message">{% trans "Access Forbidden" %}</h2>
|
||||
<p class="sub-message">{% trans "You do not have permission to view this page." %}</p>
|
||||
<p class="sub-message fs-2">{% trans "Powered By Tenhal, Riyadh Saudi Arabia" %}</p>
|
||||
<a href="{% url 'home' %}" class="home-button">{% trans "Go Home" %}</a>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/particles.js@2.0.0/particles.min.js"></script>
|
||||
<script>
|
||||
/* Particles.js Configuration */
|
||||
particlesJS("particles-js", {
|
||||
"particles": {
|
||||
"number": {
|
||||
"value": 80,
|
||||
"density": {
|
||||
"enable": true,
|
||||
"value_area": 800
|
||||
}
|
||||
},
|
||||
"color": {
|
||||
"value": "#ff3864"
|
||||
},
|
||||
"shape": {
|
||||
"type": "circle",
|
||||
},
|
||||
"opacity": {
|
||||
"value": 0.5,
|
||||
"random": false,
|
||||
"anim": {
|
||||
"enable": false
|
||||
}
|
||||
},
|
||||
"size": {
|
||||
"value": 3,
|
||||
"random": true,
|
||||
},
|
||||
"line_linked": {
|
||||
"enable": true,
|
||||
"distance": 150,
|
||||
"color": "#e6e6e6",
|
||||
"opacity": 0.4,
|
||||
"width": 1
|
||||
},
|
||||
"move": {
|
||||
"enable": true,
|
||||
"speed": 6,
|
||||
"direction": "none",
|
||||
"random": false,
|
||||
"straight": false,
|
||||
"out_mode": "out",
|
||||
"bounce": false,
|
||||
"attract": {
|
||||
"enable": false,
|
||||
}
|
||||
}
|
||||
},
|
||||
"interactivity": {
|
||||
"detect_on": "canvas",
|
||||
"events": {
|
||||
"onhover": {
|
||||
"enable": true,
|
||||
"mode": "grab"
|
||||
},
|
||||
"onclick": {
|
||||
"enable": true,
|
||||
"mode": "push"
|
||||
},
|
||||
"resize": true
|
||||
},
|
||||
"modes": {
|
||||
"grab": {
|
||||
"distance": 140,
|
||||
"line_linked": {
|
||||
"opacity": 1
|
||||
particlesJS("particles-js", {
|
||||
"particles": {
|
||||
"number": {
|
||||
"value": 80,
|
||||
"density": {
|
||||
"enable": true,
|
||||
"value_area": 800
|
||||
}
|
||||
},
|
||||
"push": {
|
||||
"particles_nb": 4
|
||||
"color": {
|
||||
"value": "#ff3864"
|
||||
},
|
||||
"shape": {
|
||||
"type": "circle",
|
||||
},
|
||||
"opacity": {
|
||||
"value": 0.5,
|
||||
"random": false,
|
||||
"anim": {
|
||||
"enable": false
|
||||
}
|
||||
},
|
||||
"size": {
|
||||
"value": 3,
|
||||
"random": true,
|
||||
},
|
||||
"line_linked": {
|
||||
"enable": true,
|
||||
"distance": 150,
|
||||
"color": "#e6e6e6",
|
||||
"opacity": 0.4,
|
||||
"width": 1
|
||||
},
|
||||
"move": {
|
||||
"enable": true,
|
||||
"speed": 6,
|
||||
"direction": "none",
|
||||
"random": false,
|
||||
"straight": false,
|
||||
"out_mode": "out",
|
||||
"bounce": false,
|
||||
"attract": {
|
||||
"enable": false,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"retina_detect": true
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
},
|
||||
"interactivity": {
|
||||
"detect_on": "canvas",
|
||||
"events": {
|
||||
"onhover": {
|
||||
"enable": true,
|
||||
"mode": "grab"
|
||||
},
|
||||
"onclick": {
|
||||
"enable": true,
|
||||
"mode": "push"
|
||||
},
|
||||
"resize": true
|
||||
},
|
||||
"modes": {
|
||||
"grab": {
|
||||
"distance": 140,
|
||||
"line_linked": {
|
||||
"opacity": 1
|
||||
}
|
||||
},
|
||||
"push": {
|
||||
"particles_nb": 4
|
||||
}
|
||||
}
|
||||
},
|
||||
"retina_detect": true
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -55,27 +55,27 @@
|
||||
rel="stylesheet"
|
||||
id="user-style-default" />
|
||||
<style>
|
||||
body, html {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
.main {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
}
|
||||
.flex-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
body, html {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
.main {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
}
|
||||
.flex-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@ -55,27 +55,27 @@
|
||||
rel="stylesheet"
|
||||
id="user-style-default" />
|
||||
<style>
|
||||
body, html {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
.main {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
}
|
||||
.flex-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
body, html {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
.main {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
}
|
||||
.flex-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@ -61,19 +61,19 @@
|
||||
rel="stylesheet"
|
||||
id="user-style-default">
|
||||
<script>
|
||||
var phoenixIsRTL = window.config.config.phoenixIsRTL;
|
||||
if (phoenixIsRTL) {
|
||||
var linkDefault = document.getElementById('style-default');
|
||||
var userLinkDefault = document.getElementById('user-style-default');
|
||||
linkDefault.setAttribute('disabled', true);
|
||||
userLinkDefault.setAttribute('disabled', true);
|
||||
document.querySelector('html').setAttribute('dir', 'rtl');
|
||||
} else {
|
||||
var linkRTL = document.getElementById('style-rtl');
|
||||
var userLinkRTL = document.getElementById('user-style-rtl');
|
||||
linkRTL.setAttribute('disabled', true);
|
||||
userLinkRTL.setAttribute('disabled', true);
|
||||
}
|
||||
var phoenixIsRTL = window.config.config.phoenixIsRTL;
|
||||
if (phoenixIsRTL) {
|
||||
var linkDefault = document.getElementById('style-default');
|
||||
var userLinkDefault = document.getElementById('user-style-default');
|
||||
linkDefault.setAttribute('disabled', true);
|
||||
userLinkDefault.setAttribute('disabled', true);
|
||||
document.querySelector('html').setAttribute('dir', 'rtl');
|
||||
} else {
|
||||
var linkRTL = document.getElementById('style-rtl');
|
||||
var userLinkRTL = document.getElementById('user-style-rtl');
|
||||
linkRTL.setAttribute('disabled', true);
|
||||
userLinkRTL.setAttribute('disabled', true);
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
@ -109,17 +109,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
var navbarTopStyle = window.config.config.phoenixNavbarTopStyle;
|
||||
var navbarTop = document.querySelector('.navbar-top');
|
||||
if (navbarTopStyle === 'darker') {
|
||||
navbarTop.setAttribute('data-navbar-appearance', 'darker');
|
||||
}
|
||||
var navbarTopStyle = window.config.config.phoenixNavbarTopStyle;
|
||||
var navbarTop = document.querySelector('.navbar-top');
|
||||
if (navbarTopStyle === 'darker') {
|
||||
navbarTop.setAttribute('data-navbar-appearance', 'darker');
|
||||
}
|
||||
|
||||
var navbarVerticalStyle = window.config.config.phoenixNavbarVerticalStyle;
|
||||
var navbarVertical = document.querySelector('.navbar-vertical');
|
||||
if (navbarVertical && navbarVerticalStyle === 'darker') {
|
||||
navbarVertical.setAttribute('data-navbar-appearance', 'darker');
|
||||
}
|
||||
var navbarVerticalStyle = window.config.config.phoenixNavbarVerticalStyle;
|
||||
var navbarVertical = document.querySelector('.navbar-vertical');
|
||||
if (navbarVertical && navbarVerticalStyle === 'darker') {
|
||||
navbarVertical.setAttribute('data-navbar-appearance', 'darker');
|
||||
}
|
||||
</script>
|
||||
<div class="support-chat-row">
|
||||
<div class="row-fluid support-chat">
|
||||
|
||||
@ -6,7 +6,6 @@
|
||||
{% trans "Sign In" %}
|
||||
{% endblock head_title %}
|
||||
{% block content %}
|
||||
|
||||
<section class="main my-2">
|
||||
<div class="container" style="max-width:40rem;">
|
||||
<div class="class="row form-container" id="form-container"">
|
||||
@ -78,9 +77,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% include 'footer.html' %}
|
||||
|
||||
{% include 'footer.html' %}
|
||||
{% if LOGIN_BY_CODE_ENABLED or PASSKEY_LOGIN_ENABLED %}
|
||||
<hr>
|
||||
{% element button_group vertical=True %}
|
||||
|
||||
@ -282,9 +282,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% include 'footer.html' %}
|
||||
|
||||
{% include 'footer.html' %}
|
||||
<script src="{% static 'js/phoenix.js' %}"></script>
|
||||
{% endblock content %}
|
||||
{% block customJS %}
|
||||
@ -293,97 +291,97 @@
|
||||
<script type="module"
|
||||
src="https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.0-beta.11/bundles/datastar.js"></script>
|
||||
<script>
|
||||
function validatePassword(password, confirmPassword) {
|
||||
return password === confirmPassword && password.length > 7 && password !== '';
|
||||
}
|
||||
function validateEmail(email) {
|
||||
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
return emailRegex.test(email) && email !== '';
|
||||
}
|
||||
function validateform2(name,arabic_name,phone_number) {
|
||||
if (name === '' || arabic_name === '' || phone_number === '' || phone_number.length < 10 || !phone_number.startsWith('056')) {
|
||||
return false;
|
||||
}
|
||||
return true
|
||||
}
|
||||
function validate_sa_phone_number(phone_number) {
|
||||
const phone_numberRegex = /^056[0-9]{7}$/;
|
||||
return phone_numberRegex.test(phone_number) && phone_numberRegex !== '';
|
||||
}
|
||||
function getAllFormData() {
|
||||
const forms = document.querySelectorAll('.needs-validation');
|
||||
const formData = {};
|
||||
forms.forEach(form => {
|
||||
const fields = form.querySelectorAll('input,textarea,select');
|
||||
fields.forEach(field => {
|
||||
formData[field.name] = field.value;
|
||||
});
|
||||
});
|
||||
return formData;
|
||||
}
|
||||
|
||||
function showLoading() {
|
||||
Swal.fire({
|
||||
title: "{% trans 'Please Wait' %}",
|
||||
text: "{% trans 'Loading' %}...",
|
||||
allowOutsideClick: false,
|
||||
didOpen: () => {
|
||||
Swal.showLoading();
|
||||
function validatePassword(password, confirmPassword) {
|
||||
return password === confirmPassword && password.length > 7 && password !== '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
Swal.close();
|
||||
}
|
||||
|
||||
function notify(tag,msg){
|
||||
Swal.fire({
|
||||
icon: tag,
|
||||
titleText: msg
|
||||
});
|
||||
}
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== "") {
|
||||
const cookies = document.cookie.split(";");
|
||||
for (let cookie of cookies) {
|
||||
cookie = cookie.trim();
|
||||
if (cookie.substring(0, name.length + 1) === name + "=") {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
function validateEmail(email) {
|
||||
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
return emailRegex.test(email) && email !== '';
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
async function sendFormData() {
|
||||
const formData = getAllFormData();
|
||||
const url = "{% url 'account_signup' %}";
|
||||
const csrftoken = getCookie('csrftoken');
|
||||
try {
|
||||
showLoading();
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': '{{csrf_token}}',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
hideLoading();
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
notify("success","Account created successfully");
|
||||
setTimeout(() => {
|
||||
window.location.href = "{% url 'account_login' %}";
|
||||
}, 1000);
|
||||
} else {
|
||||
notify("error",data.error);
|
||||
function validateform2(name,arabic_name,phone_number) {
|
||||
if (name === '' || arabic_name === '' || phone_number === '' || phone_number.length < 10 || !phone_number.startsWith('056')) {
|
||||
return false;
|
||||
}
|
||||
return true
|
||||
}
|
||||
function validate_sa_phone_number(phone_number) {
|
||||
const phone_numberRegex = /^056[0-9]{7}$/;
|
||||
return phone_numberRegex.test(phone_number) && phone_numberRegex !== '';
|
||||
}
|
||||
function getAllFormData() {
|
||||
const forms = document.querySelectorAll('.needs-validation');
|
||||
const formData = {};
|
||||
forms.forEach(form => {
|
||||
const fields = form.querySelectorAll('input,textarea,select');
|
||||
fields.forEach(field => {
|
||||
formData[field.name] = field.value;
|
||||
});
|
||||
});
|
||||
return formData;
|
||||
}
|
||||
|
||||
function showLoading() {
|
||||
Swal.fire({
|
||||
title: "{% trans 'Please Wait' %}",
|
||||
text: "{% trans 'Loading' %}...",
|
||||
allowOutsideClick: false,
|
||||
didOpen: () => {
|
||||
Swal.showLoading();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
Swal.close();
|
||||
}
|
||||
|
||||
function notify(tag,msg){
|
||||
Swal.fire({
|
||||
icon: tag,
|
||||
titleText: msg
|
||||
});
|
||||
}
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== "") {
|
||||
const cookies = document.cookie.split(";");
|
||||
for (let cookie of cookies) {
|
||||
cookie = cookie.trim();
|
||||
if (cookie.substring(0, name.length + 1) === name + "=") {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
async function sendFormData() {
|
||||
const formData = getAllFormData();
|
||||
const url = "{% url 'account_signup' %}";
|
||||
const csrftoken = getCookie('csrftoken');
|
||||
try {
|
||||
showLoading();
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': '{{csrf_token}}',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
hideLoading();
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
notify("success","Account created successfully");
|
||||
setTimeout(() => {
|
||||
window.location.href = "{% url 'account_login' %}";
|
||||
}, 1000);
|
||||
} else {
|
||||
notify("error",data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
notify("error",error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
notify("error",error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock customJS %}
|
||||
|
||||
@ -6,42 +6,42 @@
|
||||
{% trans 'Dealer Settings' %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<main class="d-flex align-items-center justify-content-center min-vh-80 py-5">
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
|
||||
<div class="card-header bg-gradient py-4 border-0 rounded-top-4">
|
||||
<h3 class="mb-0 fs-4 fw-bold text-center">
|
||||
{% trans "Dealer Settings" %}
|
||||
<i class="fas fa-solid fa-gear ms-2"></i>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<form action="" method="post" class="needs-validation" novalidate>
|
||||
{% csrf_token %}
|
||||
<div class="row g-1">
|
||||
<div class="col-12">
|
||||
<h4 class="mb-4 text-center">{% trans 'Default Invoice Accounts' %}</h4>
|
||||
{{ form.invoice_cash_account|as_crispy_field }}
|
||||
{{ form.invoice_prepaid_account|as_crispy_field }}
|
||||
{{ form.invoice_unearned_account|as_crispy_field }}
|
||||
<main class="d-flex align-items-center justify-content-center min-vh-80 py-5">
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
|
||||
<div class="card-header bg-gradient py-4 border-0 rounded-top-4">
|
||||
<h3 class="mb-0 fs-4 fw-bold text-center">
|
||||
{% trans "Dealer Settings" %}
|
||||
<i class="fas fa-solid fa-gear ms-2"></i>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<form action="" method="post" class="needs-validation" novalidate>
|
||||
{% csrf_token %}
|
||||
<div class="row g-1">
|
||||
<div class="col-12">
|
||||
<h4 class="mb-4 text-center">{% trans 'Default Invoice Accounts' %}</h4>
|
||||
{{ form.invoice_cash_account|as_crispy_field }}
|
||||
{{ form.invoice_prepaid_account|as_crispy_field }}
|
||||
{{ form.invoice_unearned_account|as_crispy_field }}
|
||||
</div>
|
||||
<div class="col-12 mt-4">
|
||||
<h4 class="mb-4 text-center">{% trans 'Default Bill Accounts' %}</h4>
|
||||
{{ form.bill_cash_account|as_crispy_field }}
|
||||
{{ form.bill_prepaid_account|as_crispy_field }}
|
||||
{{ form.bill_unearned_account|as_crispy_field }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 mt-4">
|
||||
<h4 class="mb-4 text-center">{% trans 'Default Bill Accounts' %}</h4>
|
||||
{{ form.bill_cash_account|as_crispy_field }}
|
||||
{{ form.bill_prepaid_account|as_crispy_field }}
|
||||
{{ form.bill_unearned_account|as_crispy_field }}
|
||||
<hr class="my-4">
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
|
||||
<button class="btn btn-phoenix-primary btn-lg" type="submit">
|
||||
<i class="fa-solid fa-pen-to-square me-1"></i>
|
||||
{% trans 'Update' %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="my-4">
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
|
||||
<button class="btn btn-phoenix-primary btn-lg" type="submit">
|
||||
<i class="fa-solid fa-pen-to-square me-1"></i>
|
||||
{% trans 'Update' %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
<h3 class="mb-0 fs-4 text-center text-white">{% trans 'Activate Account' %}</h3>
|
||||
</div>
|
||||
<div class="card-body bg-light-subtle">
|
||||
<p class="text-center">{{ _("Are you sure you want to activate this account")}} "{{ obj.email }}"?</p>
|
||||
<p class="text-center">{{ _("Are you sure you want to activate this account") }} "{{ obj.email }}"?</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<hr class="my-2">
|
||||
|
||||
@ -1,29 +1,33 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}
|
||||
{% trans 'Admin Management' %} {% endblock %}
|
||||
{% block content %}
|
||||
<h3 class="my-4">{% trans "Admin Management" %}<li class="fa fa-user-cog ms-2 text-primary"></li></h3>
|
||||
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-4 g-4 mt-10">
|
||||
<div class="col">
|
||||
<a href="{% url 'user_management' request.dealer.slug %}">
|
||||
<div class="card h-100">
|
||||
<div class="card-header text-center">
|
||||
<h5 class="card-title">{{ _("User Management") }}</h5>
|
||||
<span class="me-2"><i class="fas fa-user fa-2x"></i></span>
|
||||
</div>
|
||||
{% trans 'Admin Management' %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<h3 class="my-4">
|
||||
{% trans "Admin Management" %}
|
||||
<li class="fa fa-user-cog ms-2 text-primary"></li>
|
||||
</h3>
|
||||
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-4 g-4 mt-10">
|
||||
<div class="col">
|
||||
<a href="{% url 'user_management' request.dealer.slug %}">
|
||||
<div class="card h-100">
|
||||
<div class="card-header text-center">
|
||||
<h5 class="card-title">{{ _("User Management") }}</h5>
|
||||
<span class="me-2"><i class="fas fa-user fa-2x"></i></span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col">
|
||||
<a href="{% url 'audit_log_dashboard' request.dealer.slug %}">
|
||||
<div class="card h-100">
|
||||
<div class="card-header text-center">
|
||||
<h5 class="card-title">{{ _("Audit Log Dashboard") }}</h5>
|
||||
<span class="me-2"><i class="fas fa-user fa-2x"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
<div class="col">
|
||||
<a href="{% url 'audit_log_dashboard' request.dealer.slug %}">
|
||||
<div class="card h-100">
|
||||
<div class="card-header text-center">
|
||||
<h5 class="card-title">{{ _("Audit Log Dashboard") }}</h5>
|
||||
<span class="me-2"><i class="fas fa-user fa-2x"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
@ -32,15 +32,7 @@
|
||||
<div class="messages" style="margin: 20px 0">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-dismissible
|
||||
{% if message.tags %}
|
||||
alert-
|
||||
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
|
||||
danger
|
||||
{% else %}
|
||||
{{ message.tags }}
|
||||
{% endif %}
|
||||
{% endif %}"
|
||||
<div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
|
||||
role="alert">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
@ -78,15 +78,7 @@
|
||||
<div class="messages" style="margin: 20px 0">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-dismissible
|
||||
{% if message.tags %}
|
||||
alert-
|
||||
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
|
||||
danger
|
||||
{% else %}
|
||||
{{ message.tags }}
|
||||
{% endif %}
|
||||
{% endif %}"
|
||||
<div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
|
||||
role="alert">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
@ -65,15 +65,7 @@
|
||||
<div class="messages" style="margin: 20px 0">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-dismissible
|
||||
{% if message.tags %}
|
||||
alert-
|
||||
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
|
||||
danger
|
||||
{% else %}
|
||||
{{ message.tags }}
|
||||
{% endif %}
|
||||
{% endif %}"
|
||||
<div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
|
||||
role="alert">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
@ -25,15 +25,7 @@
|
||||
<div class="messages" style="margin: 20px 0">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-dismissible
|
||||
{% if message.tags %}
|
||||
alert-
|
||||
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
|
||||
danger
|
||||
{% else %}
|
||||
{{ message.tags }}
|
||||
{% endif %}
|
||||
{% endif %}"
|
||||
<div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
|
||||
role="alert">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
@ -11,22 +11,9 @@
|
||||
method="post"
|
||||
action=""
|
||||
id="workingHoursForm"
|
||||
data-action="{% if working_hours_instance %}
|
||||
update
|
||||
{% else %}
|
||||
create
|
||||
{% endif %}"
|
||||
data-working-hours-id="
|
||||
{% if working_hours_instance %}
|
||||
{{ working_hours_instance.id }}
|
||||
{% else %}
|
||||
0
|
||||
{% endif %}"
|
||||
data-staff-user-id="{% if staff_user_id %}
|
||||
{{ staff_user_id }}
|
||||
{% else %}
|
||||
0
|
||||
{% endif %}">
|
||||
data-action="{% if working_hours_instance %} update {% else %} create {% endif %}"
|
||||
data-working-hours-id=" {% if working_hours_instance %} {{ working_hours_instance.id }} {% else %} 0 {% endif %}"
|
||||
data-staff-user-id="{% if staff_user_id %} {{ staff_user_id }} {% else %} 0 {% endif %}">
|
||||
{% csrf_token %}
|
||||
{% if working_hours_form.staff_member %}
|
||||
<div class="form-group mb-3">
|
||||
@ -94,15 +81,7 @@
|
||||
<div class="messages" style="margin: 20px 0">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-dismissible
|
||||
{% if message.tags %}
|
||||
alert-
|
||||
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
|
||||
danger
|
||||
{% else %}
|
||||
{{ message.tags }}
|
||||
{% endif %}
|
||||
{% endif %}"
|
||||
<div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
|
||||
role="alert">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
@ -23,15 +23,7 @@
|
||||
<div class="messages" style="margin: 20px 0">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-dismissible
|
||||
{% if message.tags %}
|
||||
alert-
|
||||
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
|
||||
danger
|
||||
{% else %}
|
||||
{{ message.tags }}
|
||||
{% endif %}
|
||||
{% endif %}"
|
||||
<div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
|
||||
role="alert">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
@ -68,7 +68,9 @@
|
||||
{% endif %}
|
||||
{% for sf in all_staff_members %}
|
||||
<option value="{{ sf.id }}"
|
||||
{% if staff_member and staff_member.id == sf.id %}selected{% endif %}>{{ sf.get_staff_member_name }}</option>
|
||||
{% if staff_member and staff_member.id == sf.id %}selected{% endif %}>
|
||||
{{ sf.get_staff_member_name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
@ -88,15 +90,7 @@
|
||||
</div>
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-dismissible
|
||||
{% if message.tags %}
|
||||
alert-
|
||||
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
|
||||
danger
|
||||
{% else %}
|
||||
{{ message.tags }}
|
||||
{% endif %}
|
||||
{% endif %}"
|
||||
<div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
|
||||
role="alert">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
@ -24,15 +24,7 @@
|
||||
</ul>
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-dismissible
|
||||
{% if message.tags %}
|
||||
alert-
|
||||
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
|
||||
danger
|
||||
{% else %}
|
||||
{{ message.tags }}
|
||||
{% endif %}
|
||||
{% endif %}"
|
||||
<div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
|
||||
role="alert">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
@ -29,8 +29,7 @@
|
||||
</form>
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="vcode-alert vcode-alert-
|
||||
{% if message.tags %}{{ message.tags }}{% endif %}">{{ message }}</div>
|
||||
<div class="vcode-alert vcode-alert- {% if message.tags %}{{ message.tags }}{% endif %}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -69,15 +69,7 @@
|
||||
</div>
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-dismissible
|
||||
{% if message.tags %}
|
||||
alert-
|
||||
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
|
||||
danger
|
||||
{% else %}
|
||||
{{ message.tags }}
|
||||
{% endif %}
|
||||
{% endif %}"
|
||||
<div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
|
||||
role="alert">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
@ -40,15 +40,7 @@
|
||||
<p class="message">{{ page_message }}</p>
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-dismissible
|
||||
{% if message.tags %}
|
||||
alert-
|
||||
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
|
||||
danger
|
||||
{% else %}
|
||||
{{ message.tags }}
|
||||
{% endif %}
|
||||
{% endif %}"
|
||||
<div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
|
||||
role="alert">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
@ -3,11 +3,7 @@
|
||||
<!DOCTYPE html>
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
<html lang="{{ LANGUAGE_CODE }}"
|
||||
dir="{% if LANGUAGE_CODE == 'ar' %}
|
||||
rtl
|
||||
{% else %}
|
||||
ltr
|
||||
{% endif %}"
|
||||
dir="{% if LANGUAGE_CODE == 'ar' %} rtl {% else %} ltr {% endif %}"
|
||||
data-bs-theme=""
|
||||
data-navigation-type="default"
|
||||
data-navbar-horizontal-shape="default">
|
||||
|
||||
@ -2,11 +2,7 @@
|
||||
<!DOCTYPE html>
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
<html lang="{{ LANGUAGE_CODE }}"
|
||||
{% if LANGUAGE_CODE == 'ar' %}
|
||||
dir="rtl"
|
||||
{% else %}
|
||||
dir="ltr"
|
||||
{% endif %}
|
||||
{% if LANGUAGE_CODE == 'ar' %} dir="rtl" {% else %} dir="ltr" {% endif %}
|
||||
data-bs-theme=""
|
||||
data-navigation-type="default"
|
||||
data-navbar-horizontal-shape="default">
|
||||
@ -54,8 +50,14 @@
|
||||
<link href="{% static 'css/custom.css' %}" rel="stylesheet">
|
||||
{% comment %} <link rel="stylesheet" href="https://unicons.iconscout.com/release/v4.0.8/css/line.css"> {% endcomment %}
|
||||
{% if LANGUAGE_CODE == 'ar' %}
|
||||
<link href="{% static 'css/theme-rtl.min.css' %}" type="text/css" rel="stylesheet" id="style-rtl">
|
||||
<link href="{% static 'css/user-rtl.min.css' %}" type="text/css" rel="stylesheet" id="user-style-rtl">
|
||||
<link href="{% static 'css/theme-rtl.min.css' %}"
|
||||
type="text/css"
|
||||
rel="stylesheet"
|
||||
id="style-rtl">
|
||||
<link href="{% static 'css/user-rtl.min.css' %}"
|
||||
type="text/css"
|
||||
rel="stylesheet"
|
||||
id="user-style-rtl">
|
||||
{% else %}
|
||||
<link href="{% static 'css/theme.min.css' %}"
|
||||
type="text/css"
|
||||
@ -66,11 +68,8 @@
|
||||
rel="stylesheet"
|
||||
id="user-style-default">
|
||||
{% endif %}
|
||||
|
||||
|
||||
<script src="{% static 'js/main.js' %}"></script>
|
||||
<script src="{% static 'js/jquery.min.js' %}"></script>
|
||||
|
||||
{% comment %} <script src="{% static 'js/echarts.js' %}"></script> {% endcomment %}
|
||||
{% comment %} {% block customCSS %}{% endblock %} {% endcomment %}
|
||||
</head>
|
||||
@ -85,19 +84,25 @@
|
||||
{% block period_navigation %}
|
||||
{% endblock period_navigation %}
|
||||
<div id="spinner" class="htmx-indicator spinner-bg">
|
||||
<img src="{% static 'spinner.svg' %}" width="100" height="100" alt="">
|
||||
</div>
|
||||
<div id="main_content" class="fade-me-in" hx-boost="false" hx-target="#main_content" hx-select="#main_content" hx-swap="outerHTML transition:true" hx-select-oob="#toast-container" hx-history-elt>
|
||||
<img src="{% static 'spinner.svg' %}" width="100" height="100" alt="">
|
||||
</div>
|
||||
<div id="main_content"
|
||||
class="fade-me-in"
|
||||
hx-boost="false"
|
||||
hx-target="#main_content"
|
||||
hx-select="#main_content"
|
||||
hx-swap="outerHTML transition:true"
|
||||
hx-select-oob="#toast-container"
|
||||
hx-history-elt>
|
||||
{% block customCSS %}{% endblock %}
|
||||
{% block content %}{% endblock content %}
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
{% block customJS %}{% endblock %}
|
||||
|
||||
{% comment %} <script src="{% static 'vendors/feather-icons/feather.min.js' %}"></script>
|
||||
<script src="{% static 'vendors/fontawesome/all.min.js' %}"></script>
|
||||
<script src="{% static 'vendors/popper/popper.min.js' %}"></script>
|
||||
<script src="{% static 'vendors/fontawesome/all.min.js' %}"></script>
|
||||
<script src="{% static 'vendors/popper/popper.min.js' %}"></script>
|
||||
<script src="{% static 'vendors/bootstrap/bootstrap.min.js' %}"></script> {% endcomment %}
|
||||
<script src="{% static 'js/phoenix.js' %}"></script>
|
||||
|
||||
</div>
|
||||
{% block body %}
|
||||
{% endblock body %}
|
||||
@ -126,7 +131,7 @@
|
||||
{% comment %} <script src="{% static 'vendors/echarts/echarts.min.js' %}"></script> {% endcomment %}
|
||||
{% comment %} <script src="{% static 'js/crm-analytics.js' %}"></script> {% endcomment %}
|
||||
{% comment %} <script src="{% static 'js/travel-agency-dashboard.js' %}"></script>
|
||||
<script src="{% static 'js/crm-dashboard.js' %}"></script>
|
||||
<script src="{% static 'js/crm-dashboard.js' %}"></script>
|
||||
<script src="{% static 'js/projectmanagement-dashboard.js' %}"></script> {% endcomment %}
|
||||
{% comment %} <script src="{% static 'vendors/mapbox-gl/mapbox-gl.js' %}"></script> {% endcomment %}
|
||||
{% comment %} <script src="{% static 'vendors/turf.min.js' %}"></script> {% endcomment %}
|
||||
@ -156,57 +161,57 @@
|
||||
document.getElementById('global-indicator')
|
||||
];
|
||||
});*/
|
||||
let Toast = Swal.mixin({
|
||||
toast: true,
|
||||
position: "top-end",
|
||||
showConfirmButton: false,
|
||||
timer: 3000,
|
||||
timerProgressBar: true,
|
||||
didOpen: (toast) => {
|
||||
toast.onmouseenter = Swal.stopTimer;
|
||||
toast.onmouseleave = Swal.resumeTimer;
|
||||
}
|
||||
});
|
||||
function notify(tag, msg) {
|
||||
Toast.fire({
|
||||
icon: tag,
|
||||
titleText: msg
|
||||
});
|
||||
}
|
||||
document.addEventListener('htmx:afterRequest', function(evt) {
|
||||
if(evt.detail.xhr.status == 403){
|
||||
/* Notify the user of a 404 Not Found response */
|
||||
notify("error", "You do not have permission to view this page");
|
||||
}
|
||||
if(evt.detail.xhr.status == 404){
|
||||
/* Notify the user of a 404 Not Found response */
|
||||
return alert("Error: Could Not Find Resource");
|
||||
}
|
||||
if (evt.detail.successful != true) {
|
||||
console.log(evt.detail.xhr.statusText)
|
||||
/* Notify of an unexpected error, & print error to console */
|
||||
notify("error", `Unexpected Error ,${evt.detail.xhr.statusText}`);
|
||||
}
|
||||
});
|
||||
document.body.addEventListener('htmx:beforeSwap', function(evt) {
|
||||
if (evt.detail.target.id === 'main_content') {
|
||||
var backdrops = document.querySelectorAll('.modal-backdrop');
|
||||
backdrops.forEach(function(backdrop) {
|
||||
backdrop.remove();
|
||||
});
|
||||
}
|
||||
});
|
||||
// Close modal after successful form submission
|
||||
document.body.addEventListener('htmx:afterSwap', function(evt) {
|
||||
if (evt.detail.target.id === 'main_content') {
|
||||
document.querySelectorAll('.modal').forEach(function(m) {
|
||||
var modal = bootstrap.Modal.getInstance(m);
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
let Toast = Swal.mixin({
|
||||
toast: true,
|
||||
position: "top-end",
|
||||
showConfirmButton: false,
|
||||
timer: 3000,
|
||||
timerProgressBar: true,
|
||||
didOpen: (toast) => {
|
||||
toast.onmouseenter = Swal.stopTimer;
|
||||
toast.onmouseleave = Swal.resumeTimer;
|
||||
}
|
||||
});
|
||||
function notify(tag, msg) {
|
||||
Toast.fire({
|
||||
icon: tag,
|
||||
titleText: msg
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
document.addEventListener('htmx:afterRequest', function(evt) {
|
||||
if(evt.detail.xhr.status == 403){
|
||||
/* Notify the user of a 404 Not Found response */
|
||||
notify("error", "You do not have permission to view this page");
|
||||
}
|
||||
if(evt.detail.xhr.status == 404){
|
||||
/* Notify the user of a 404 Not Found response */
|
||||
return alert("Error: Could Not Find Resource");
|
||||
}
|
||||
if (evt.detail.successful != true) {
|
||||
console.log(evt.detail.xhr.statusText)
|
||||
/* Notify of an unexpected error, & print error to console */
|
||||
notify("error", `Unexpected Error ,${evt.detail.xhr.statusText}`);
|
||||
}
|
||||
});
|
||||
document.body.addEventListener('htmx:beforeSwap', function(evt) {
|
||||
if (evt.detail.target.id === 'main_content') {
|
||||
var backdrops = document.querySelectorAll('.modal-backdrop');
|
||||
backdrops.forEach(function(backdrop) {
|
||||
backdrop.remove();
|
||||
});
|
||||
}
|
||||
});
|
||||
// Close modal after successful form submission
|
||||
document.body.addEventListener('htmx:afterSwap', function(evt) {
|
||||
if (evt.detail.target.id === 'main_content') {
|
||||
document.querySelectorAll('.modal').forEach(function(m) {
|
||||
var modal = bootstrap.Modal.getInstance(m);
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% comment %} {% block customJS %}{% endblock %} {% endcomment %}
|
||||
</body>
|
||||
|
||||
@ -6,48 +6,48 @@
|
||||
{% block title %}
|
||||
{{ _("Create Bill") |capfirst }}
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<main class="d-flex align-items-center justify-content-center min-vh-80 py-5 ">
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
|
||||
<div class="card-header bg-gradient py-4 border-0 rounded-top-4">
|
||||
<h3 class="mb-0 fs-4 fw-bold text-center">
|
||||
{% trans 'Create Bill' %}
|
||||
<i class="fas fa-money-bills ms-2"></i>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<form action="{{ form_action_url }}" method="post" id="djl-bill-model-create-form-id" class="needs-validation" novalidate>
|
||||
{% csrf_token %}
|
||||
{% if po_model %}
|
||||
<div class="text-center mb-4">
|
||||
<h3 class="h5">{% trans 'Bill for' %} {{ po_model.po_number }}</h3>
|
||||
<p class="text-muted mb-3">{% trans 'Bill for' %} {{ po_model.po_title }}</p>
|
||||
<div class="d-flex flex-column gap-2">
|
||||
{% for itemtxs in po_itemtxs_qs %}
|
||||
<span class="badge bg-secondary">{{ itemtxs }}</span>
|
||||
{% endfor %}
|
||||
<main class="d-flex align-items-center justify-content-center min-vh-80 py-5 ">
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
|
||||
<div class="card-header bg-gradient py-4 border-0 rounded-top-4">
|
||||
<h3 class="mb-0 fs-4 fw-bold text-center">
|
||||
{% trans 'Create Bill' %}
|
||||
<i class="fas fa-money-bills ms-2"></i>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<form action="{{ form_action_url }}"
|
||||
method="post"
|
||||
id="djl-bill-model-create-form-id"
|
||||
class="needs-validation"
|
||||
novalidate>
|
||||
{% csrf_token %}
|
||||
{% if po_model %}
|
||||
<div class="text-center mb-4">
|
||||
<h3 class="h5">{% trans 'Bill for' %} {{ po_model.po_number }}</h3>
|
||||
<p class="text-muted mb-3">{% trans 'Bill for' %} {{ po_model.po_title }}</p>
|
||||
<div class="d-flex flex-column gap-2">
|
||||
{% for itemtxs in po_itemtxs_qs %}<span class="badge bg-secondary">{{ itemtxs }}</span>{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="mb-4">{{ form|crispy }}</div>
|
||||
<hr class="my-4">
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
|
||||
<button class="btn btn-phoenix-primary btn-lg me-md-2" type="submit">
|
||||
<i class="fa-solid fa-floppy-disk me-1"></i>
|
||||
{{ _("Save") }}
|
||||
</button>
|
||||
<a href="{% url 'bill_list' request.dealer.slug %}"
|
||||
class="btn btn-phoenix-secondary btn-lg">
|
||||
<i class="fa-solid fa-ban me-1"></i>
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="mb-4">
|
||||
{{ form|crispy }}
|
||||
</div>
|
||||
<hr class="my-4">
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
|
||||
<button class="btn btn-phoenix-primary btn-lg me-md-2" type="submit">
|
||||
<i class="fa-solid fa-floppy-disk me-1"></i>
|
||||
{{ _("Save") }}
|
||||
</button>
|
||||
<a href="{% url 'bill_list' request.dealer.slug%}" class="btn btn-phoenix-secondary btn-lg">
|
||||
<i class="fa-solid fa-ban me-1"></i>
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@ -4,180 +4,160 @@
|
||||
{% load django_ledger %}
|
||||
{% load custom_filters %}
|
||||
{% block title %}Bill Details{% endblock %}
|
||||
|
||||
{% block content%}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mt-4">
|
||||
<div class="col-12 mb-3">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
{% include 'bill/includes/card_bill.html' with dealer_slug=request.dealer.slug bill=bill entity_slug=view.kwargs.entity_slug style='bill-detail' %}
|
||||
</div>
|
||||
{% if bill.is_configured %}
|
||||
|
||||
<div class="row text-center g-3 mb-3">
|
||||
<div class="col-12 col-md-3">
|
||||
|
||||
<h6 class="text-uppercase text-xs text-muted mb-2">
|
||||
{% trans 'Cash Account' %}:
|
||||
{% if bill.cash_account %}
|
||||
<a href="{% url 'account_detail' request.dealer.slug bill.cash_account.coa_model.pk bill.cash_account.uuid %}"
|
||||
class="text-decoration-none ms-1">{{ bill.cash_account.code }}</a>
|
||||
{% else %}
|
||||
{{ bill.cash_account.code }}
|
||||
{% endif %}
|
||||
</h6>
|
||||
<h4 class="mb-0" id="djl-bill-detail-amount-paid">
|
||||
{% currency_symbol %}{{ bill.get_amount_cash | absolute | currency_format }}
|
||||
</h4>
|
||||
|
||||
</div>
|
||||
{% if bill.accrue %}
|
||||
<div class="col-12 col-md-3">
|
||||
|
||||
<h6 class="text-uppercase text-xs text-muted mb-2">
|
||||
{% trans 'Prepaid Account' %}:
|
||||
{% if bill.prepaid_account %}
|
||||
<a href="{% url 'account_detail' request.dealer.slug bill.prepaid_account.coa_model.pk bill.prepaid_account.uuid %}"
|
||||
class="text-decoration-none ms-1">
|
||||
{{ bill.prepaid_account.code }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ bill.prepaid_account.code }}
|
||||
{% endif %}
|
||||
</h6>
|
||||
<h4 class="text-success mb-0" id="djl-bill-detail-amount-prepaid">
|
||||
{% currency_symbol %}{{ bill.get_amount_prepaid | currency_format }}
|
||||
</h4>
|
||||
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
|
||||
<h6 class="text-uppercase text-xs text-muted mb-2">
|
||||
{% trans 'Accounts Payable' %}:
|
||||
{% if bill.unearned_account %}
|
||||
<a href="{% url 'account_detail' request.dealer.slug bill.unearned_account.coa_model.pk bill.unearned_account.uuid %}"
|
||||
class="text-decoration-none ms-1">
|
||||
{{ bill.unearned_account.code }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ bill.unearned_account.code }}
|
||||
{% endif %}
|
||||
</h6>
|
||||
<h4 class="text-danger mb-0" id="djl-bill-detail-amount-unearned">
|
||||
{% currency_symbol %}{{ bill.get_amount_unearned | currency_format }}
|
||||
</h4>
|
||||
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
|
||||
<h6 class="text-uppercase text-xs text-muted mb-2">{% trans 'Accrued' %} {{ bill.get_progress | percentage }}</h6>
|
||||
<h4 class="mb-0">{% currency_symbol %}{{ bill.get_amount_earned | currency_format }}</h4>
|
||||
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-12 col-md-3 offset-md-6">
|
||||
|
||||
<h6 class="text-uppercase text-xs text-muted mb-2">{% trans 'You Still Owe' %}</h6>
|
||||
<h4 class="text-danger mb-0" id="djl-bill-detail-amount-owed">
|
||||
{% currency_symbol %}{{ bill.get_amount_open | currency_format }}
|
||||
</h4>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
{% include 'bill/includes/card_bill.html' with dealer_slug=request.dealer.slug bill=bill entity_slug=view.kwargs.entity_slug style='bill-detail' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card mb-3 shadow-sm">
|
||||
<div class="card-header pb-0">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="fas fa-receipt me-3 text-primary"></i>
|
||||
<h5 class="mb-0">{% trans 'Bill Items' %}</h5>
|
||||
</div>
|
||||
{% if bill.is_configured %}
|
||||
<div class="row text-center g-3 mb-3">
|
||||
<div class="col-12 col-md-3">
|
||||
<h6 class="text-uppercase text-xs text-muted mb-2">
|
||||
{% trans 'Cash Account' %}:
|
||||
{% if bill.cash_account %}
|
||||
<a href="{% url 'account_detail' request.dealer.slug bill.cash_account.coa_model.pk bill.cash_account.uuid %}"
|
||||
class="text-decoration-none ms-1">{{ bill.cash_account.code }}</a>
|
||||
{% else %}
|
||||
{{ bill.cash_account.code }}
|
||||
{% endif %}
|
||||
</h6>
|
||||
<h4 class="mb-0" id="djl-bill-detail-amount-paid">
|
||||
{% currency_symbol %}{{ bill.get_amount_cash | absolute | currency_format }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body px-0 pt-0 pb-2">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr class="bg-body-highlight">
|
||||
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Item' %}</th>
|
||||
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Entity Unit' %}</th>
|
||||
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Unit Cost' %}</th>
|
||||
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Quantity' %}</th>
|
||||
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Total' %}</th>
|
||||
<th class="sort white-space-nowrap align-middle " scope="col">{% trans 'PO' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="list fs-9" id="project-list-table-body">
|
||||
{% for bill_item in itemtxs_qs %}
|
||||
<tr>
|
||||
<td class="align-middle white-space-nowrap">
|
||||
<div class="d-flex px-2 py-1">
|
||||
<div class="d-flex flex-column justify-content-center">
|
||||
<h6 class="mb-0 text-sm">{{ bill_item.item_model }}</h6>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap">
|
||||
<span class="text-xs font-weight-bold">
|
||||
{% if bill_item.entity_unit %}{{ bill_item.entity_unit }}{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap">
|
||||
<span class="text-xs font-weight-bold">{{ bill_item.unit_cost | currency_format }}</span>
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap">
|
||||
<span class="text-xs font-weight-bold">{{ bill_item.quantity }}</span>
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap">
|
||||
<span class="text-xs font-weight-bold">{{ bill_item.total_amount | currency_format }}</span>
|
||||
</td>
|
||||
<td class="align-items-start white-space-nowrap pe-2">
|
||||
{% if bill_item.po_model_id %}
|
||||
{% if perms.django_ledger.view_purchaseordermodel %}
|
||||
<a class="btn btn-sm btn-phoenix-primary"
|
||||
href="{% url 'purchase_order_detail' request.dealer.slug request.dealer.entity.slug bill_item.po_model_id %}">
|
||||
{% trans 'View PO' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="3"></td>
|
||||
<td class="text-end">
|
||||
<strong>{% trans 'Total' %}</strong>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<strong>{% currency_symbol %}{{ total_amount__sum | currency_format }}</strong>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
{% if bill.accrue %}
|
||||
<div class="col-12 col-md-3">
|
||||
<h6 class="text-uppercase text-xs text-muted mb-2">
|
||||
{% trans 'Prepaid Account' %}:
|
||||
{% if bill.prepaid_account %}
|
||||
<a href="{% url 'account_detail' request.dealer.slug bill.prepaid_account.coa_model.pk bill.prepaid_account.uuid %}"
|
||||
class="text-decoration-none ms-1">{{ bill.prepaid_account.code }}</a>
|
||||
{% else %}
|
||||
{{ bill.prepaid_account.code }}
|
||||
{% endif %}
|
||||
</h6>
|
||||
<h4 class="text-success mb-0" id="djl-bill-detail-amount-prepaid">
|
||||
{% currency_symbol %}{{ bill.get_amount_prepaid | currency_format }}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<h6 class="text-uppercase text-xs text-muted mb-2">
|
||||
{% trans 'Accounts Payable' %}:
|
||||
{% if bill.unearned_account %}
|
||||
<a href="{% url 'account_detail' request.dealer.slug bill.unearned_account.coa_model.pk bill.unearned_account.uuid %}"
|
||||
class="text-decoration-none ms-1">{{ bill.unearned_account.code }}</a>
|
||||
{% else %}
|
||||
{{ bill.unearned_account.code }}
|
||||
{% endif %}
|
||||
</h6>
|
||||
<h4 class="text-danger mb-0" id="djl-bill-detail-amount-unearned">
|
||||
{% currency_symbol %}{{ bill.get_amount_unearned | currency_format }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<h6 class="text-uppercase text-xs text-muted mb-2">{% trans 'Accrued' %} {{ bill.get_progress | percentage }}</h6>
|
||||
<h4 class="mb-0">{% currency_symbol %}{{ bill.get_amount_earned | currency_format }}</h4>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-12 col-md-3 offset-md-6">
|
||||
<h6 class="text-uppercase text-xs text-muted mb-2">{% trans 'You Still Owe' %}</h6>
|
||||
<h4 class="text-danger mb-0" id="djl-bill-detail-amount-owed">
|
||||
{% currency_symbol %}{{ bill.get_amount_open | currency_format }}
|
||||
</h4>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card mb-3 shadow-sm">
|
||||
<div class="card-header pb-0">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="fas fa-exchange-alt me-3 text-primary"></i>
|
||||
<h5 class="mb-0">{% trans 'Bill Transactions' %}</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body px-0 pt-0 pb-2 table-responsive">{% transactions_table bill %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{% include "bill/includes/mark_as.html" %}
|
||||
{% endblock %}
|
||||
<div class="col-12">
|
||||
<div class="card mb-3 shadow-sm">
|
||||
<div class="card-header pb-0">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="fas fa-receipt me-3 text-primary"></i>
|
||||
<h5 class="mb-0">{% trans 'Bill Items' %}</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body px-0 pt-0 pb-2">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr class="bg-body-highlight">
|
||||
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Item' %}</th>
|
||||
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Entity Unit' %}</th>
|
||||
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Unit Cost' %}</th>
|
||||
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Quantity' %}</th>
|
||||
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Total' %}</th>
|
||||
<th class="sort white-space-nowrap align-middle " scope="col">{% trans 'PO' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="list fs-9" id="project-list-table-body">
|
||||
{% for bill_item in itemtxs_qs %}
|
||||
<tr>
|
||||
<td class="align-middle white-space-nowrap">
|
||||
<div class="d-flex px-2 py-1">
|
||||
<div class="d-flex flex-column justify-content-center">
|
||||
<h6 class="mb-0 text-sm">{{ bill_item.item_model }}</h6>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap">
|
||||
<span class="text-xs font-weight-bold">
|
||||
{% if bill_item.entity_unit %}{{ bill_item.entity_unit }}{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap">
|
||||
<span class="text-xs font-weight-bold">{{ bill_item.unit_cost | currency_format }}</span>
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap">
|
||||
<span class="text-xs font-weight-bold">{{ bill_item.quantity }}</span>
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap">
|
||||
<span class="text-xs font-weight-bold">{{ bill_item.total_amount | currency_format }}</span>
|
||||
</td>
|
||||
<td class="align-items-start white-space-nowrap pe-2">
|
||||
{% if bill_item.po_model_id %}
|
||||
{% if perms.django_ledger.view_purchaseordermodel %}
|
||||
<a class="btn btn-sm btn-phoenix-primary"
|
||||
href="{% url 'purchase_order_detail' request.dealer.slug request.dealer.entity.slug bill_item.po_model_id %}">
|
||||
{% trans 'View PO' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="3"></td>
|
||||
<td class="text-end">
|
||||
<strong>{% trans 'Total' %}</strong>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<strong>{% currency_symbol %}{{ total_amount__sum | currency_format }}</strong>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="card mb-3 shadow-sm">
|
||||
<div class="card-header pb-0">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="fas fa-exchange-alt me-3 text-primary"></i>
|
||||
<h5 class="mb-0">{% trans 'Bill Transactions' %}</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body px-0 pt-0 pb-2 table-responsive">{% transactions_table bill %}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{% include "bill/includes/mark_as.html" %}
|
||||
{% endblock %}
|
||||
|
||||
@ -14,7 +14,8 @@
|
||||
<div class="card mb-2">
|
||||
<div class="card-body">
|
||||
{% include 'bill/includes/card_bill.html' with dealer_slug=request.dealer.slug bill=bill_model style='bill-detail' entity_slug=view.kwargs.entity_slug %}
|
||||
<form action="{% url 'bill-update' dealer_slug=request.dealer.slug entity_slug=view.kwargs.entity_slug bill_pk=bill_model.uuid %}" method="post">
|
||||
<form action="{% url 'bill-update' dealer_slug=request.dealer.slug entity_slug=view.kwargs.entity_slug bill_pk=bill_model.uuid %}"
|
||||
method="post">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">{{ form|crispy }}</div>
|
||||
<button type="submit" class="btn btn-phoenix-primary mb-2 me-2">
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
{% if not create_bill %}
|
||||
{% if style == 'dashboard' %}
|
||||
<!-- Dashboard Style Card -->
|
||||
|
||||
<div class="">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 text-primary">
|
||||
@ -51,15 +50,16 @@
|
||||
</div>
|
||||
<!-- Modal Action -->
|
||||
{% modal_action bill 'get' entity_slug %}
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<a href="{% url 'django_ledger:bill-detail' entity_slug=entity_slug bill_pk=bill.uuid %}"
|
||||
class="btn btn-sm btn-phoenix-primary me-md-2">{% trans 'View' %}</a>
|
||||
{% if perms.django_ledger.change_billmodel %}
|
||||
<a hx-boost="true" href="{% url 'django_ledger:bill-update' entity_slug=entity_slug bill_pk=bill.uuid %}"
|
||||
<a hx-boost="true"
|
||||
href="{% url 'django_ledger:bill-update' entity_slug=entity_slug bill_pk=bill.uuid %}"
|
||||
class="btn btn-sm btn-phoenix-warning me-md-2">{% trans 'Update' %}</a>
|
||||
{% if bill.can_pay %}
|
||||
<button onclick="djLedger.toggleModal('{{ bill.get_html_id }}')" class="btn btn-sm btn-phoenix-info">{% trans 'Mark as Paid' %}</button>
|
||||
<button onclick="djLedger.toggleModal('{{ bill.get_html_id }}')"
|
||||
class="btn btn-sm btn-phoenix-info">{% trans 'Mark as Paid' %}</button>
|
||||
{% endif %}
|
||||
{% if bill.can_cancel %}
|
||||
<button onclick="djLedger.toggleModal('{{ bill.get_html_id }}')"
|
||||
@ -202,10 +202,11 @@
|
||||
<div class="d-flex flex-wrap gap-2 mt-2">
|
||||
<!-- Update Button -->
|
||||
{% if perms.django_ledger.change_billmodel %}
|
||||
{% if "update" not in request.path %}
|
||||
<a hx-boost="true" href="{% url 'bill-update' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill.uuid %}">
|
||||
<button class="btn btn-phoenix-primary">
|
||||
<i class="fas fa-edit me-2"></i>{% trans 'Update' %}
|
||||
{% if "update" not in request.path %}
|
||||
<a hx-boost="true"
|
||||
href="{% url 'bill-update' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill.uuid %}">
|
||||
<button class="btn btn-phoenix-primary">
|
||||
<i class="fas fa-edit me-2"></i>{% trans 'Update' %}
|
||||
</button>
|
||||
</a>
|
||||
{% endif %}
|
||||
@ -219,7 +220,6 @@
|
||||
{% endif %}
|
||||
<!-- Mark as Review -->
|
||||
{% if bill.can_review %}
|
||||
|
||||
<button class="btn btn-phoenix-warning"
|
||||
onclick="showPOModal('Mark as Review', '{% url 'bill-action-mark-as-review' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Review')">
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Review' %}
|
||||
@ -227,41 +227,40 @@
|
||||
{% endif %}
|
||||
<!-- Mark as Approved -->
|
||||
{% endif %}
|
||||
{% if bill.can_approve and request.is_accountant %}
|
||||
<button class="btn btn-phoenix-warning" disabled>
|
||||
<i class="fas fa-hourglass-start me-2"></i><span class="text-warning">{% trans 'Waiting for Manager Approval' %}</span>
|
||||
</button>
|
||||
{% else %}
|
||||
{% if bill.can_approve and perms.django_ledger.can_approve_billmodel %}
|
||||
<button class="btn btn-phoenix-success"
|
||||
onclick="showPOModal('Mark as Approved', '{% url 'bill-action-mark-as-approved' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Approved')">
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Approved' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<!-- Mark as Paid -->
|
||||
{% if bill.can_pay %}
|
||||
{% if bill.can_approve and request.is_accountant %}
|
||||
<button class="btn btn-phoenix-warning" disabled>
|
||||
<i class="fas fa-hourglass-start me-2"></i><span class="text-warning">{% trans 'Waiting for Manager Approval' %}</span>
|
||||
</button>
|
||||
{% else %}
|
||||
{% if bill.can_approve and perms.django_ledger.can_approve_billmodel %}
|
||||
<button class="btn btn-phoenix-success"
|
||||
onclick="showPOModal('Mark as Paid', '{% url 'bill-action-mark-as-paid' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Paid')">
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Paid' %}
|
||||
onclick="showPOModal('Mark as Approved', '{% url 'bill-action-mark-as-approved' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Approved')">
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Approved' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<!-- Void Button -->
|
||||
{% if bill.can_void %}
|
||||
<button class="btn btn-phoenix-danger"
|
||||
onclick="showPOModal('Mark as Void', '{% url 'bill-action-mark-as-void' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Void')">
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Void' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<!-- Cancel Button -->
|
||||
{% if bill.can_cancel %}
|
||||
<button class="btn btn-phoenix-danger"
|
||||
onclick="showPOModal('Mark as Canceled', '{% url 'bill-action-mark-as-canceled' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Canceled')">
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Canceled' %}
|
||||
</button>
|
||||
{% modal_action_v2 bill bill.get_mark_as_canceled_url bill.get_mark_as_canceled_message bill.get_mark_as_canceled_html_id %}
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
<!-- Mark as Paid -->
|
||||
{% if bill.can_pay %}
|
||||
<button class="btn btn-phoenix-success"
|
||||
onclick="showPOModal('Mark as Paid', '{% url 'bill-action-mark-as-paid' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Paid')">
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Paid' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<!-- Void Button -->
|
||||
{% if bill.can_void %}
|
||||
<button class="btn btn-phoenix-danger"
|
||||
onclick="showPOModal('Mark as Void', '{% url 'bill-action-mark-as-void' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Void')">
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Void' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<!-- Cancel Button -->
|
||||
{% if bill.can_cancel %}
|
||||
<button class="btn btn-phoenix-danger"
|
||||
onclick="showPOModal('Mark as Canceled', '{% url 'bill-action-mark-as-canceled' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Canceled')">
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Canceled' %}
|
||||
</button>
|
||||
{% modal_action_v2 bill bill.get_mark_as_canceled_url bill.get_mark_as_canceled_message bill.get_mark_as_canceled_html_id %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,137 +2,142 @@
|
||||
{% load static %}
|
||||
{% load django_ledger %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% if bill.get_itemtxs_data.1.total_amount__sum > 0 %}
|
||||
<form id="bill-update-form" action="{% url 'bill-update-items' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill_pk %}"
|
||||
method="post">
|
||||
{% else %}
|
||||
<form id="bill-update-form" hx-trigger="load delay:300ms" hx-swap="outerHTML" hx-target="#bill-update-form" hx-select="#bill-update-form" hx-post="{% url 'bill-update-items' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill_pk %}"
|
||||
method="post">
|
||||
{% endif %}
|
||||
<div class="container-fluid py-4">
|
||||
<!-- Page Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h2 class="text-primary mb-0 d-flex align-items-center">
|
||||
<i class="fas fa-receipt me-2"></i>
|
||||
{% trans 'Bill Items' %}
|
||||
</h2>
|
||||
<hr class="my-3">
|
||||
<form id="bill-update-form"
|
||||
action="{% url 'bill-update-items' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill_pk %}"
|
||||
method="post">
|
||||
{% else %}
|
||||
<form id="bill-update-form"
|
||||
hx-trigger="load delay:300ms"
|
||||
hx-swap="outerHTML"
|
||||
hx-target="#bill-update-form"
|
||||
hx-select="#bill-update-form"
|
||||
hx-post="{% url 'bill-update-items' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill_pk %}"
|
||||
method="post">
|
||||
{% endif %}
|
||||
<div class="container-fluid py-4">
|
||||
<!-- Page Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h2 class="text-primary mb-0 d-flex align-items-center">
|
||||
<i class="fas fa-receipt me-2"></i>
|
||||
{% trans 'Bill Items' %}
|
||||
</h2>
|
||||
<hr class="my-3">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Form Content -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
{% csrf_token %}
|
||||
{{ item_formset.non_form_errors }}
|
||||
{{ item_formset.management_form }}
|
||||
<!-- Card Container -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
<!-- Responsive Table -->
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="text-uppercase text-xxs font-weight-bolder">{% trans 'Item' %}</th>
|
||||
<th class="text-uppercase text-xxs font-weight-bolder text-center">{% trans 'PO Qty' %}</th>
|
||||
<th class="text-uppercase text-xxs font-weight-bolder text-center">{% trans 'PO Amount' %}</th>
|
||||
<th class="text-uppercase text-xxs font-weight-bolder text-center">{% trans 'Quantity' %}</th>
|
||||
<th class="text-uppercase text-xxs font-weight-bolder text-center">{% trans 'Unit Cost' %}</th>
|
||||
<th class="text-uppercase text-xxs font-weight-bolder text-center">{% trans 'Unit' %}</th>
|
||||
<th class="text-uppercase text-xxs font-weight-bolder text-end">{% trans 'Total' %}</th>
|
||||
<th class="text-uppercase text-xxs font-weight-bolder text-center">{% trans 'Delete' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for f in item_formset %}
|
||||
<tr class="align-middle">
|
||||
<!-- Item Column -->
|
||||
<td>
|
||||
<div class="d-flex flex-column ms-2">
|
||||
{% for hidden_field in f.hidden_fields %}{{ hidden_field }}{% endfor %}
|
||||
{{ f.item_model|add_class:"form-control" }}
|
||||
{% if f.errors %}<span class="text-danger text-xs">{{ f.errors }}</span>{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<!-- PO Quantity -->
|
||||
<td class="text-center">
|
||||
<span class="text-muted text-xs">
|
||||
{% if f.instance.po_quantity %}{{ f.instance.po_quantity }}{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<!-- PO Amount -->
|
||||
<td class="text-center">
|
||||
{% if f.instance.po_total_amount %}
|
||||
<div class="d-flex flex-column">
|
||||
<span class="text-xs font-weight-bold">{% currency_symbol %}{{ f.instance.po_total_amount | currency_format }}</span>
|
||||
<a class="btn btn-sm btn-phoenix-info mt-1"
|
||||
href="{% url 'purchase_order_detail' dealer_slug entity_slug f.instance.po_model_id %}">
|
||||
{% trans 'View PO' %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<!-- Quantity -->
|
||||
<td class="text-center">
|
||||
<div class="input-group input-group-sm w-100">{{ f.quantity|add_class:"form-control" }}</div>
|
||||
</td>
|
||||
<!-- Unit Cost -->
|
||||
<td class="text-center">
|
||||
<div class="input-group input-group-sm w-100">{{ f.unit_cost|add_class:"form-control" }}</div>
|
||||
</td>
|
||||
<!-- Entity Unit -->
|
||||
<td class="text-center">{{ f.entity_unit|add_class:"form-control" }}</td>
|
||||
<!-- Total Amount -->
|
||||
<td class="text-end">
|
||||
<span class="text-xs font-weight-bold">
|
||||
<span>{% currency_symbol %}</span>{{ f.instance.total_amount | currency_format }}
|
||||
</span>
|
||||
</td>
|
||||
<!-- Delete Checkbox -->
|
||||
<td class="text-center">
|
||||
{% if item_formset.can_delete %}<div class="form-check d-flex justify-content-center">{{ f.DELETE }}</div>{% endif %}
|
||||
</td>
|
||||
<!-- Form Content -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
{% csrf_token %}
|
||||
{{ item_formset.non_form_errors }}
|
||||
{{ item_formset.management_form }}
|
||||
<!-- Card Container -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
<!-- Responsive Table -->
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="text-uppercase text-xxs font-weight-bolder">{% trans 'Item' %}</th>
|
||||
<th class="text-uppercase text-xxs font-weight-bolder text-center">{% trans 'PO Qty' %}</th>
|
||||
<th class="text-uppercase text-xxs font-weight-bolder text-center">{% trans 'PO Amount' %}</th>
|
||||
<th class="text-uppercase text-xxs font-weight-bolder text-center">{% trans 'Quantity' %}</th>
|
||||
<th class="text-uppercase text-xxs font-weight-bolder text-center">{% trans 'Unit Cost' %}</th>
|
||||
<th class="text-uppercase text-xxs font-weight-bolder text-center">{% trans 'Unit' %}</th>
|
||||
<th class="text-uppercase text-xxs font-weight-bolder text-end">{% trans 'Total' %}</th>
|
||||
<th class="text-uppercase text-xxs font-weight-bolder text-center">{% trans 'Delete' %}</th>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<!-- Footer Total -->
|
||||
<tfoot class="total-row">
|
||||
<tr>
|
||||
<td colspan="5"></td>
|
||||
<td class="text-end">
|
||||
<strong>{% trans 'Total' %}</strong>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<strong>{% currency_symbol %}{{ total_amount__sum | currency_format }}</strong>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for f in item_formset %}
|
||||
<tr class="align-middle">
|
||||
<!-- Item Column -->
|
||||
<td>
|
||||
<div class="d-flex flex-column ms-2">
|
||||
{% for hidden_field in f.hidden_fields %}{{ hidden_field }}{% endfor %}
|
||||
{{ f.item_model|add_class:"form-control" }}
|
||||
{% if f.errors %}<span class="text-danger text-xs">{{ f.errors }}</span>{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<!-- PO Quantity -->
|
||||
<td class="text-center">
|
||||
<span class="text-muted text-xs">
|
||||
{% if f.instance.po_quantity %}{{ f.instance.po_quantity }}{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<!-- PO Amount -->
|
||||
<td class="text-center">
|
||||
{% if f.instance.po_total_amount %}
|
||||
<div class="d-flex flex-column">
|
||||
<span class="text-xs font-weight-bold">{% currency_symbol %}{{ f.instance.po_total_amount | currency_format }}</span>
|
||||
<a class="btn btn-sm btn-phoenix-info mt-1"
|
||||
href="{% url 'purchase_order_detail' dealer_slug entity_slug f.instance.po_model_id %}">
|
||||
{% trans 'View PO' %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<!-- Quantity -->
|
||||
<td class="text-center">
|
||||
<div class="input-group input-group-sm w-100">{{ f.quantity|add_class:"form-control" }}</div>
|
||||
</td>
|
||||
<!-- Unit Cost -->
|
||||
<td class="text-center">
|
||||
<div class="input-group input-group-sm w-100">{{ f.unit_cost|add_class:"form-control" }}</div>
|
||||
</td>
|
||||
<!-- Entity Unit -->
|
||||
<td class="text-center">{{ f.entity_unit|add_class:"form-control" }}</td>
|
||||
<!-- Total Amount -->
|
||||
<td class="text-end">
|
||||
<span class="text-xs font-weight-bold">
|
||||
<span>{% currency_symbol %}</span>{{ f.instance.total_amount | currency_format }}
|
||||
</span>
|
||||
</td>
|
||||
<!-- Delete Checkbox -->
|
||||
<td class="text-center">
|
||||
{% if item_formset.can_delete %}<div class="form-check d-flex justify-content-center">{{ f.DELETE }}</div>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<!-- Footer Total -->
|
||||
<tfoot class="total-row">
|
||||
<tr>
|
||||
<td colspan="5"></td>
|
||||
<td class="text-end">
|
||||
<strong>{% trans 'Total' %}</strong>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<strong>{% currency_symbol %}{{ total_amount__sum | currency_format }}</strong>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Action Buttons -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-start gap-2">
|
||||
{% if not item_formset.has_po %}
|
||||
<a href="{% url 'django_ledger:product-create' entity_slug=entity_slug %}"
|
||||
class="btn btn-phoenix-primary">
|
||||
<i class="fas fa-plus me-1"></i>
|
||||
{% trans 'New Item' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<button type="submit" class="btn btn-phoenix-primary">
|
||||
<i class="fas fa-save me-1"></i>
|
||||
{% trans 'Save Changes' %}
|
||||
</button>
|
||||
<!-- Action Buttons -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-start gap-2">
|
||||
{% if not item_formset.has_po %}
|
||||
<a href="{% url 'django_ledger:product-create' entity_slug=entity_slug %}"
|
||||
class="btn btn-phoenix-primary">
|
||||
<i class="fas fa-plus me-1"></i>
|
||||
{% trans 'New Item' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<button type="submit" class="btn btn-phoenix-primary">
|
||||
<i class="fas fa-save me-1"></i>
|
||||
{% trans 'Save Changes' %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
|
||||
@ -2,58 +2,52 @@
|
||||
{% load i18n static %}
|
||||
{% load django_ledger %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block content %}
|
||||
<main class="d-flex align-items-center justify-content-center min-vh-80 py-5">
|
||||
<div class="col-12 col-sm-10 col-md-8 col-lg-6 col-xl-5">
|
||||
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
|
||||
<div class="card-header bg-gradient py-4 border-0 rounded-top-4">
|
||||
<h3 class="mb-0 fs-4 fw-bold text-center">
|
||||
{% trans 'Create Chart of Accounts' %}
|
||||
<i class="fa-solid fa-chart-pie ms-2"></i>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<form method="post" id="{{ form.get_form_id }}" class="needs-validation" novalidate>
|
||||
{% csrf_token %}
|
||||
|
||||
{# Bootstrap form rendering #}
|
||||
<div class="mb-3">
|
||||
{{ form.name.label_tag }}
|
||||
{{ form.name|add_class:"form-control" }}
|
||||
{% if form.name.help_text %}
|
||||
<small class="form-text text-muted">{{ form.name.help_text }}</small>
|
||||
{% endif %}
|
||||
{% for error in form.name.errors %}
|
||||
<div class="invalid-feedback d-block">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
{{ form.description.label_tag }}
|
||||
{{ form.description|add_class:"form-control" }}
|
||||
{% if form.description.help_text %}
|
||||
<small class="form-text text-muted">{{ form.description.help_text }}</small>
|
||||
{% endif %}
|
||||
{% for error in form.description.errors %}
|
||||
<div class="invalid-feedback d-block">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
|
||||
<button type="submit" class="btn btn-phoenix-primary btn-lg me-md-2">
|
||||
<i class="fa-solid fa-plus me-1"></i>
|
||||
{% trans 'Create' %}
|
||||
</button>
|
||||
<a href="{% url 'coa-list' request.dealer.slug request.entity.slug %}"
|
||||
class="btn btn-phoenix-secondary btn-lg">
|
||||
<i class="fa-solid fa-ban me-1"></i>
|
||||
{% trans 'Cancel' %}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
<main class="d-flex align-items-center justify-content-center min-vh-80 py-5">
|
||||
<div class="col-12 col-sm-10 col-md-8 col-lg-6 col-xl-5">
|
||||
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
|
||||
<div class="card-header bg-gradient py-4 border-0 rounded-top-4">
|
||||
<h3 class="mb-0 fs-4 fw-bold text-center">
|
||||
{% trans 'Create Chart of Accounts' %}
|
||||
<i class="fa-solid fa-chart-pie ms-2"></i>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<form method="post"
|
||||
id="{{ form.get_form_id }}"
|
||||
class="needs-validation"
|
||||
novalidate>
|
||||
{% csrf_token %}
|
||||
{# Bootstrap form rendering #}
|
||||
<div class="mb-3">
|
||||
{{ form.name.label_tag }}
|
||||
{{ form.name|add_class:"form-control" }}
|
||||
{% if form.name.help_text %}<small class="form-text text-muted">{{ form.name.help_text }}</small>{% endif %}
|
||||
{% for error in form.name.errors %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
{{ form.description.label_tag }}
|
||||
{{ form.description|add_class:"form-control" }}
|
||||
{% if form.description.help_text %}
|
||||
<small class="form-text text-muted">{{ form.description.help_text }}</small>
|
||||
{% endif %}
|
||||
{% for error in form.description.errors %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %}
|
||||
</div>
|
||||
<hr class="my-4">
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
|
||||
<button type="submit" class="btn btn-phoenix-primary btn-lg me-md-2">
|
||||
<i class="fa-solid fa-plus me-1"></i>
|
||||
{% trans 'Create' %}
|
||||
</button>
|
||||
<a href="{% url 'coa-list' request.dealer.slug request.entity.slug %}"
|
||||
class="btn btn-phoenix-secondary btn-lg">
|
||||
<i class="fa-solid fa-ban me-1"></i>
|
||||
{% trans 'Cancel' %}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load icon from django_ledger %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
@ -10,17 +9,19 @@
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="display-4 mb-0">{% trans "Chart of Accounts" %}</h1>
|
||||
<a href="{% url 'coa-create' request.dealer.slug request.entity.slug %}" class="btn btn-phoenix-primary">
|
||||
<a href="{% url 'coa-create' request.dealer.slug request.entity.slug %}"
|
||||
class="btn btn-phoenix-primary">
|
||||
<i class="fa-solid fa-plus"></i> {% trans "Add New" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if not inactive %}
|
||||
<a class="btn btn-phoenix-warning mb-4" href="{% url 'coa-list-inactive' request.dealer.slug request.entity.slug %}">
|
||||
<a class="btn btn-phoenix-warning mb-4"
|
||||
href="{% url 'coa-list-inactive' request.dealer.slug request.entity.slug %}">
|
||||
{% trans 'Show Inactive' %}
|
||||
</a>
|
||||
{% else %}
|
||||
<a class="btn btn-phoenix-warning mb-4" href="{% url 'coa-list' request.dealer.slug request.entity.slug %}">
|
||||
<a class="btn btn-phoenix-warning mb-4"
|
||||
href="{% url 'coa-list' request.dealer.slug request.entity.slug %}">
|
||||
{% trans 'Show Active' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
@ -28,11 +29,9 @@
|
||||
</div>
|
||||
<div class="row row-cols-1 row-cols-md-2 g-4">
|
||||
{% for coa_model in coa_list %}
|
||||
<div class="col">
|
||||
{% include 'chart_of_accounts/includes/coa_card.html' with coa_model=coa_model %}
|
||||
</div>
|
||||
<div class="col">{% include 'chart_of_accounts/includes/coa_card.html' with coa_model=coa_model %}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@ -2,23 +2,20 @@
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-6 col-md-8">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<form action="{% url 'coa-update' request.dealer.slug request.entity.slug coa_model.slug %}" id="{{ form.form_id }}" method="post">
|
||||
<form action="{% url 'coa-update' request.dealer.slug request.entity.slug coa_model.slug %}"
|
||||
id="{{ form.form_id }}"
|
||||
method="post">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
{{ form.name.label_tag }}
|
||||
{{ form.name|add_class:"form-control" }}
|
||||
{% if form.name.help_text %}
|
||||
<small class="form-text text-muted">{{ form.name.help_text }}</small>
|
||||
{% endif %}
|
||||
{% for error in form.name.errors %}
|
||||
<div class="invalid-feedback d-block">{{ error }}</div>
|
||||
{% endfor %}
|
||||
{% if form.name.help_text %}<small class="form-text text-muted">{{ form.name.help_text }}</small>{% endif %}
|
||||
{% for error in form.name.errors %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
{{ form.description.label_tag }}
|
||||
@ -26,19 +23,13 @@
|
||||
{% if form.description.help_text %}
|
||||
<small class="form-text text-muted">{{ form.description.help_text }}</small>
|
||||
{% endif %}
|
||||
{% for error in form.description.errors %}
|
||||
<div class="invalid-feedback d-block">{{ error }}</div>
|
||||
{% endfor %}
|
||||
{% for error in form.description.errors %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="d-flex justify-content-center gap-2 mt-4">
|
||||
<button class="btn btn-phoenix-primary" type="submit">
|
||||
{% trans 'Update'%}
|
||||
</button>
|
||||
<button class="btn btn-phoenix-primary" type="submit">{% trans 'Update' %}</button>
|
||||
<a class="btn btn-phoenix-secondary"
|
||||
href="{% url 'coa-list' request.dealer.slug request.entity.slug %}">
|
||||
{% trans 'Back'%}
|
||||
{% trans 'Back' %}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
@ -46,4 +37,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
{% load django_ledger %}
|
||||
{% load i18n %}
|
||||
{% now "Y" as current_year %}
|
||||
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header {% if coa_model.is_default %}bg-gray-100{% elif not coa_model.is_active %}bg-danger text-white{% endif %} py-3 d-flex align-items-center">
|
||||
<div class="me-3">
|
||||
@ -19,17 +18,16 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="ms-auto d-flex flex-column align-items-end">
|
||||
{% if coa_model.is_active %}
|
||||
{% if coa_model.is_active %}
|
||||
<span class="badge bg-success"><i class="fas fa-check-circle"></i> {% trans 'Active' %}</span>
|
||||
{% else %}
|
||||
{% else %}
|
||||
<span class="badge bg-danger"><i class="fas fa-times-circle"></i> {% trans 'Inactive' %}</span>
|
||||
{% endif %}
|
||||
{% if coa_model.is_default %}
|
||||
<span class="badge bg-primary-subtle text-primary mt-1">{% trans 'Entity Default' %}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if coa_model.is_default %}
|
||||
<span class="badge bg-primary-subtle text-primary mt-1">{% trans 'Entity Default' %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-sm-6">
|
||||
@ -46,27 +44,22 @@
|
||||
<span class="text-muted ms-2">{{ coa_model.slug }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<div class="mb-2">
|
||||
<span class="fw-bold"><i class="fas fa-list-alt me-1"></i> {% trans 'Total Accounts' %}:</span>
|
||||
<span class="ms-2">{{ coa_model.accountmodel_total__count }}</span>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<span class="fw-bold text-info"><i class="fas fa-check-circle me-1"></i> {% trans 'Active Accounts' %}:</span>
|
||||
<span class="ms-2">{{ coa_model.accountmodel_active__count }}</span>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<span class="fw-bold text-danger"><i class="fas fa-lock me-1"></i> {% trans 'Locked Accounts' %}:</span>
|
||||
<span class="ms-2">{{ coa_model.accountmodel_locked__count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-3">
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="col-sm-6">
|
||||
<small class="text-muted d-block">
|
||||
@ -84,36 +77,37 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer bg-transparent border-top-0 pt-0 pt-sm-3">
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a href="{% url 'coa-update' request.dealer.slug request.entity.slug coa_model.slug %}" class="btn btn-sm btn-phoenix-warning fw-bold flex-grow-1 flex-sm-grow-0">
|
||||
<a href="{% url 'coa-update' request.dealer.slug request.entity.slug coa_model.slug %}"
|
||||
class="btn btn-sm btn-phoenix-warning fw-bold flex-grow-1 flex-sm-grow-0">
|
||||
<i class="fas fa-edit me-1"></i> {% trans 'Update' %}
|
||||
</a>
|
||||
|
||||
<a href="{% url 'account_list' request.dealer.slug coa_model.pk %}" class="btn btn-sm btn-phoenix-success fw-bold flex-grow-1 flex-sm-grow-0">
|
||||
<a href="{% url 'account_list' request.dealer.slug coa_model.pk %}"
|
||||
class="btn btn-sm btn-phoenix-success fw-bold flex-grow-1 flex-sm-grow-0">
|
||||
<i class="fas fa-book me-1"></i> {% trans 'Accounts' %}
|
||||
</a>
|
||||
|
||||
<a href="{% url 'account_create' request.dealer.slug coa_model.pk %}" class="btn btn-sm btn-phoenix-info fw-bold flex-grow-1 flex-sm-grow-0">
|
||||
<a href="{% url 'account_create' request.dealer.slug coa_model.pk %}"
|
||||
class="btn btn-sm btn-phoenix-info fw-bold flex-grow-1 flex-sm-grow-0">
|
||||
<i class="fas fa-plus-circle me-1"></i> {% trans 'Add Account' %}
|
||||
</a>
|
||||
|
||||
{% if coa_model.can_mark_as_default %}
|
||||
<a href="{% url 'coa-action-mark-as-default' request.dealer.slug request.entity.slug coa_model.slug %}" class="btn btn-sm btn-phoenix-danger fw-bold flex-grow-1 flex-sm-grow-0">
|
||||
<a href="{% url 'coa-action-mark-as-default' request.dealer.slug request.entity.slug coa_model.slug %}"
|
||||
class="btn btn-sm btn-phoenix-danger fw-bold flex-grow-1 flex-sm-grow-0">
|
||||
<i class="fas fa-star me-1"></i> {% trans 'Mark as Default' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if coa_model.can_deactivate %}
|
||||
<a href="{% url 'coa-action-mark-as-inactive' request.dealer.slug request.entity.slug coa_model.slug %}" class="btn btn-sm btn-phoenix-warning fw-bold flex-grow-1 flex-sm-grow-0">
|
||||
<a href="{% url 'coa-action-mark-as-inactive' request.dealer.slug request.entity.slug coa_model.slug %}"
|
||||
class="btn btn-sm btn-phoenix-warning fw-bold flex-grow-1 flex-sm-grow-0">
|
||||
<i class="fas fa-toggle-off me-1"></i> {% trans 'Mark as Inactive' %}
|
||||
</a>
|
||||
{% elif coa_model.can_activate %}
|
||||
<a href="{% url 'coa-action-mark-as-active' request.dealer.slug request.entity.slug coa_model.slug %}" class="btn btn-sm btn-phoenix-success fw-bold flex-grow-1 flex-sm-grow-0">
|
||||
<a href="{% url 'coa-action-mark-as-active' request.dealer.slug request.entity.slug coa_model.slug %}"
|
||||
class="btn btn-sm btn-phoenix-success fw-bold flex-grow-1 flex-sm-grow-0">
|
||||
<i class="fas fa-toggle-on me-1"></i> {% trans 'Mark as Active' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,16 +1,22 @@
|
||||
{% load i18n crispy_forms_tags %}
|
||||
<div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true">
|
||||
<div class="modal fade"
|
||||
id="emailModal"
|
||||
tabindex="-1"
|
||||
aria-labelledby="emailModalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header justify-content-between align-items-start gap-5 px-4 pt-4 pb-3 border-0">
|
||||
<h4 class="modal-title" id="emailModalLabel">{% trans 'Send Email' %}</h4>
|
||||
<button class="btn p-0 text-body-quaternary fs-6" data-bs-dismiss="modal" aria-label="Close">
|
||||
<span class="fas fa-times"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="emailModalBody" class="modal-body">
|
||||
<h1>hi</h1>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header justify-content-between align-items-start gap-5 px-4 pt-4 pb-3 border-0">
|
||||
<h4 class="modal-title" id="emailModalLabel">{% trans 'Send Email' %}</h4>
|
||||
<button class="btn p-0 text-body-quaternary fs-6"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close">
|
||||
<span class="fas fa-times"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="emailModalBody" class="modal-body">
|
||||
<h1>hi</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -15,13 +15,11 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="noteForm" action="{% url 'add_note' request.dealer.slug content_type slug %}"
|
||||
<form id="noteForm"
|
||||
action="{% url 'add_note' request.dealer.slug content_type slug %}"
|
||||
hx-select="#notesTable"
|
||||
hx-target="#notesTable"
|
||||
hx-on::after-request="{
|
||||
resetSubmitButton(document.querySelector('.add_note_form button[type=submit]'));
|
||||
$('#noteModal').modal('hide');
|
||||
}"
|
||||
hx-on::after-request="{ resetSubmitButton(document.querySelector('.add_note_form button[type=submit]')); $('#noteModal').modal('hide'); }"
|
||||
hx-swap="outerHTML"
|
||||
method="post"
|
||||
class="add_note_form">
|
||||
@ -34,7 +32,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function updateNote(e) {
|
||||
function updateNote(e) {
|
||||
let url = e.getAttribute('data-url');
|
||||
let note = e.getAttribute('data-note');
|
||||
|
||||
|
||||
@ -15,13 +15,11 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="scheduleForm" action="{% url 'schedule_event' request.dealer.slug content_type slug %}"
|
||||
<form id="scheduleForm"
|
||||
action="{% url 'schedule_event' request.dealer.slug content_type slug %}"
|
||||
hx-select=".taskTable"
|
||||
hx-target=".taskTable"
|
||||
hx-on::after-request="{
|
||||
resetSubmitButton(document.querySelector('.add_schedule_form button[type=submit]'));
|
||||
$('#scheduleModal').modal('hide');
|
||||
}"
|
||||
hx-on::after-request="{ resetSubmitButton(document.querySelector('.add_schedule_form button[type=submit]')); $('#scheduleModal').modal('hide'); }"
|
||||
hx-swap="outerHTML"
|
||||
method="post"
|
||||
class="add_schedule_form">
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
{% load static i18n crispy_forms_tags %}
|
||||
<!-- task Modal -->
|
||||
<style>
|
||||
.completed-task {
|
||||
text-decoration: line-through;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.completed-task {
|
||||
text-decoration: line-through;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
<div class="modal fade"
|
||||
id="taskModal"
|
||||
@ -22,13 +22,14 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="taskForm" action="{% url 'add_task' request.dealer.slug content_type slug %}"
|
||||
method="post"
|
||||
class="add_task_form"
|
||||
hx-post="{% url 'add_task' request.dealer.slug content_type slug %}"
|
||||
hx-target="#your-content-container"
|
||||
hx-swap="innerHTML"
|
||||
hx-boost="true">
|
||||
<form id="taskForm"
|
||||
action="{% url 'add_task' request.dealer.slug content_type slug %}"
|
||||
method="post"
|
||||
class="add_task_form"
|
||||
hx-post="{% url 'add_task' request.dealer.slug content_type slug %}"
|
||||
hx-target="#your-content-container"
|
||||
hx-swap="innerHTML"
|
||||
hx-boost="true">
|
||||
{% csrf_token %}
|
||||
{{ staff_task_form|crispy }}
|
||||
<button type="submit" class="btn btn-phoenix-success w-100">{% trans 'Save' %}</button>
|
||||
|
||||
@ -3,31 +3,31 @@
|
||||
{% load crispy_forms_tags %}
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
.main-tab li:last-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
.kanban-header {
|
||||
position: relative;
|
||||
background-color:rgb(237, 241, 245);
|
||||
font-weight: 600;
|
||||
padding: 0.5rem 1rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #333;
|
||||
clip-path: polygon(0 0, calc(100% - 15px) 0, 100% 50%, calc(100% - 15px) 100%, 0 100%);
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
||||
}
|
||||
.main-tab li:last-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
.kanban-header {
|
||||
position: relative;
|
||||
background-color:rgb(237, 241, 245);
|
||||
font-weight: 600;
|
||||
padding: 0.5rem 1rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #333;
|
||||
clip-path: polygon(0 0, calc(100% - 15px) 0, 100% 50%, calc(100% - 15px) 100%, 0 100%);
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.kanban-header::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: -20px;
|
||||
top: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 28px solid transparent;
|
||||
border-bottom: 28px solid transparent;
|
||||
border-left: 20px solid #dee2e6;
|
||||
}
|
||||
.kanban-header::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: -20px;
|
||||
top: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 28px solid transparent;
|
||||
border-bottom: 28px solid transparent;
|
||||
border-left: 20px solid #dee2e6;
|
||||
}
|
||||
|
||||
</style>
|
||||
{% endblock customCSS %}
|
||||
@ -39,7 +39,6 @@
|
||||
<div class="col-12 col-md-auto">
|
||||
<h3 class="mb-0">{{ _("Lead Details") }}</h3>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -47,7 +46,6 @@
|
||||
<div class="col-md-5 col-lg-5 col-xl-4">
|
||||
<div class="sticky-leads-sidebar">
|
||||
<div class="lead-details" data-breakpoint="md">
|
||||
|
||||
<div class="card mb-2">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center g-3 text-center text-xxl-start">
|
||||
@ -80,11 +78,11 @@
|
||||
<div class="col-6 col-sm-auto d-flex flex-column align-items-center text-center">
|
||||
<h5 class="fw-bolder mb-2 text-body-highlight">{{ _("Car Requested") }}</h5>
|
||||
{% if lead.id_car_make.logo %}
|
||||
<img src="{{ lead.id_car_make.logo.url }}"
|
||||
alt="Car Make Logo"
|
||||
class="img-fluid rounded mb-2"
|
||||
style="width: 60px;
|
||||
height: 60px">
|
||||
<img src="{{ lead.id_car_make.logo.url }}"
|
||||
alt="Car Make Logo"
|
||||
class="img-fluid rounded mb-2"
|
||||
style="width: 60px;
|
||||
height: 60px">
|
||||
{% endif %}
|
||||
<p class="mb-0">{{ lead.id_car_make.get_local_name }} - {{ lead.id_car_model.get_local_name }} {{ lead.year }}</p>
|
||||
</div>
|
||||
@ -93,16 +91,17 @@
|
||||
</div>
|
||||
<div class="card mb-2">
|
||||
<div class="card-body">
|
||||
<div id="assignedTo" class="row align-items-center g-3 text-center text-xxl-start">
|
||||
<div id="assignedTo"
|
||||
class="row align-items-center g-3 text-center text-xxl-start">
|
||||
<div class="col-6 col-sm-auto d-flex flex-column align-items-center text-center">
|
||||
<h5 class="fw-bolder mb-2 text-body-highlight">{{ _("Assigned To") }}</h5>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="avatar avatar-tiny me-2">
|
||||
{% if lead.staff.logo %}
|
||||
<img class="avatar-img rounded-circle"
|
||||
src="{{ lead.staff.thumbnail.url }}"
|
||||
onerror="this.src='/static/img/brand/brand-logo.png'"
|
||||
alt="Logo">
|
||||
src="{{ lead.staff.thumbnail.url }}"
|
||||
onerror="this.src='/static/img/brand/brand-logo.png'"
|
||||
alt="Logo">
|
||||
{% endif %}
|
||||
</div>
|
||||
<small>
|
||||
@ -293,11 +292,9 @@
|
||||
<div class="modal-content">
|
||||
<form class="modal-content"
|
||||
action="{% url 'lead_transfer' request.dealer.slug lead.slug %}"
|
||||
hx-select-oob="#assignedTo:outerHTML,#toast-container:outerHTML"
|
||||
hx-swap="none"
|
||||
hx-on::after-request="{
|
||||
resetSubmitButton(document.querySelector('#exampleModal button[type=submit]'));
|
||||
$('#exampleModal').modal('hide');}"
|
||||
hx-select-oob="#assignedTo:outerHTML,#toast-container:outerHTML"
|
||||
hx-swap="none"
|
||||
hx-on::after-request="{ resetSubmitButton(document.querySelector('#exampleModal button[type=submit]')); $('#exampleModal').modal('hide');}"
|
||||
method="post">
|
||||
{% csrf_token %}
|
||||
<div class="modal-header">
|
||||
@ -488,7 +485,7 @@
|
||||
data-url="{% url 'update_note' request.dealer.slug note.pk %}"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#noteModal"
|
||||
data-note-title="{{ _('Update') }}">
|
||||
data-note-title="{{ _("Update") }}">
|
||||
<i class='fas fa-pen-square text-primary ms-2'></i>
|
||||
{{ _("Update") }}
|
||||
</a>
|
||||
@ -528,8 +525,7 @@
|
||||
hx-get="{% url 'send_lead_email' request.dealer.slug lead.slug %}"
|
||||
hx-target="#emailModalBody"
|
||||
hx-select=".email-form"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
hx-swap="innerHTML">
|
||||
<span class="fas fa-plus me-1"></span>{{ _("Send Email") }}
|
||||
</button>
|
||||
{% endif %}
|
||||
@ -807,7 +803,6 @@
|
||||
href="{% url 'appointment:get_user_appointments' %}"> <span class="me-2 text-body align-bottom" data-feather="calendar"></span>{{ _("View in Calendar") }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -821,62 +816,62 @@
|
||||
{% include "components/note_modal.html" with content_type="lead" slug=lead.slug %}
|
||||
<!-- schedule Modal -->
|
||||
{% include "components/schedule_modal.html" with content_type="lead" slug=lead.slug %}
|
||||
{% endblock content %}
|
||||
{% block customJS %}
|
||||
<script>
|
||||
function reset_form() {
|
||||
document.querySelector('#id_note').value = ""
|
||||
let form = document.querySelector('.add_note_form')
|
||||
form.action = "{% url 'add_note' request.dealer.slug 'lead' lead.slug %}"
|
||||
}
|
||||
{% endblock content %}
|
||||
{% block customJS %}
|
||||
<script>
|
||||
function reset_form() {
|
||||
document.querySelector('#id_note').value = ""
|
||||
let form = document.querySelector('.add_note_form')
|
||||
form.action = "{% url 'add_note' request.dealer.slug 'lead' lead.slug %}"
|
||||
}
|
||||
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
Toast.fire({
|
||||
icon: "{{ message.tags }}",
|
||||
titleText: "{{ message|safe }}"
|
||||
});
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
Toast.fire({
|
||||
icon: "{{ message.tags }}",
|
||||
titleText: "{{ message|safe }}"
|
||||
});
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
function openActionModal(leadId, currentAction, nextAction, nextActionDate) {
|
||||
function openActionModal(leadId, currentAction, nextAction, nextActionDate) {
|
||||
|
||||
const modal = new bootstrap.Modal(document.getElementById('actionTrackingModal'));
|
||||
document.getElementById('actionTrackingForm').setAttribute('hx-boost', 'false');
|
||||
document.getElementById('leadId').value = leadId;
|
||||
document.getElementById('currentAction').value = currentAction;
|
||||
document.getElementById('nextAction').value = nextAction;
|
||||
document.getElementById('nextActionDate').value = nextActionDate;
|
||||
modal.show();
|
||||
}
|
||||
const modal = new bootstrap.Modal(document.getElementById('actionTrackingModal'));
|
||||
document.getElementById('actionTrackingForm').setAttribute('hx-boost', 'false');
|
||||
document.getElementById('leadId').value = leadId;
|
||||
document.getElementById('currentAction').value = currentAction;
|
||||
document.getElementById('nextAction').value = nextAction;
|
||||
document.getElementById('nextActionDate').value = nextActionDate;
|
||||
modal.show();
|
||||
}
|
||||
|
||||
|
||||
function notify(tag, msg) {
|
||||
Toast.fire({
|
||||
icon: tag,
|
||||
titleText: msg
|
||||
});
|
||||
}
|
||||
function notify(tag, msg) {
|
||||
Toast.fire({
|
||||
icon: tag,
|
||||
titleText: msg
|
||||
});
|
||||
}
|
||||
|
||||
// Close modal after successful form submission
|
||||
document.body.addEventListener('htmx:afterSwap', function(evt) {
|
||||
if (evt.detail.target.id === 'main_content') {
|
||||
var modal = bootstrap.Modal.getInstance(document.getElementById('exampleModal'));
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
document.body.addEventListener('htmx:afterSwap', function(evt) {
|
||||
if (evt.detail.target.id === 'main_content') {
|
||||
var modal = bootstrap.Modal.getInstance(document.getElementById('exampleModal'));
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup modal backdrop if needed
|
||||
document.body.addEventListener('htmx:beforeSwap', function(evt) {
|
||||
if (evt.detail.target.id === 'main_content') {
|
||||
var backdrops = document.querySelectorAll('.modal-backdrop');
|
||||
backdrops.forEach(function(backdrop) {
|
||||
backdrop.remove();
|
||||
});
|
||||
}
|
||||
});
|
||||
document.body.addEventListener('htmx:beforeSwap', function(evt) {
|
||||
if (evt.detail.target.id === 'main_content') {
|
||||
var backdrops = document.querySelectorAll('.modal-backdrop');
|
||||
backdrops.forEach(function(backdrop) {
|
||||
backdrop.remove();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock customJS %}
|
||||
</script>
|
||||
{% endblock customJS %}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n static crispy_forms_filters %}
|
||||
|
||||
{% block title %}
|
||||
{% if object %}
|
||||
{% trans 'Update Lead' %}
|
||||
@ -8,7 +7,6 @@
|
||||
{% trans 'Add New Lead' %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block customcss %}
|
||||
<style>
|
||||
.htmx-indicator{
|
||||
@ -30,41 +28,40 @@
|
||||
}
|
||||
</style>
|
||||
{% endblock customcss %}
|
||||
|
||||
{% block content %}
|
||||
<main class="d-flex align-items-center justify-content-center min-vh-100 py-5">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
|
||||
<div class="card-header bg-gradient py-4 border-0 rounded-top-4">
|
||||
<h3 class="mb-0 fs-4 fw-bold text-center">
|
||||
{% if object %}
|
||||
{% trans "Update Lead" %}
|
||||
<i class="fa-solid fa-edit ms-2"></i>
|
||||
{% else %}
|
||||
{% trans "Create New Lead" %}
|
||||
<i class="fa-solid fa-bullhorn ms-2"></i>
|
||||
{% endif %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<form class="form" method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
|
||||
<hr class="my-4">
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
|
||||
<button class="btn btn-phoenix-primary btn-lg me-md-2" type="submit">
|
||||
<i class="fa-solid fa-floppy-disk me-1"></i>
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
<a href="{% url 'lead_list' request.dealer.slug %}" class="btn btn-phoenix-secondary btn-lg">
|
||||
<i class="fa-solid fa-ban me-1"></i>
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
<main class="d-flex align-items-center justify-content-center min-vh-100 py-5">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
|
||||
<div class="card-header bg-gradient py-4 border-0 rounded-top-4">
|
||||
<h3 class="mb-0 fs-4 fw-bold text-center">
|
||||
{% if object %}
|
||||
{% trans "Update Lead" %}
|
||||
<i class="fa-solid fa-edit ms-2"></i>
|
||||
{% else %}
|
||||
{% trans "Create New Lead" %}
|
||||
<i class="fa-solid fa-bullhorn ms-2"></i>
|
||||
{% endif %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<form class="form" method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<hr class="my-4">
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
|
||||
<button class="btn btn-phoenix-primary btn-lg me-md-2" type="submit">
|
||||
<i class="fa-solid fa-floppy-disk me-1"></i>
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
<a href="{% url 'lead_list' request.dealer.slug %}"
|
||||
class="btn btn-phoenix-secondary btn-lg">
|
||||
<i class="fa-solid fa-ban me-1"></i>
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@ -1,270 +1,263 @@
|
||||
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n static humanize %}
|
||||
{% block title %}
|
||||
{{ _("Leads") |capfirst }}
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
|
||||
{% if page_obj.object_list or request.GET.q%}
|
||||
<div class="row g-3 mt-4 mb-4">
|
||||
<h2 class="mb-2">
|
||||
{{ _("Leads") |capfirst }}
|
||||
<li class="fas fa-bullhorn text-primary ms-2"></li>
|
||||
</h2>
|
||||
<!-- Action Tracking Modal -->
|
||||
{% comment %} {% include "crm/leads/partials/update_action.html" %} {% endcomment %}
|
||||
|
||||
<div class="row g-3 justify-content-between mb-4">
|
||||
<div class="col-auto">
|
||||
<div class="d-md-flex justify-content-between">
|
||||
{% if perms.inventory.add_lead %}
|
||||
<div>
|
||||
<a href="{% url 'lead_create' request.dealer.slug %}"
|
||||
class="btn btn-sm btn-phoenix-primary"><span class="fas fa-plus me-2"></span>{{ _("Add Lead") }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if page_obj.object_list or request.GET.q %}
|
||||
<div class="row g-3 mt-4 mb-4">
|
||||
<h2 class="mb-2">
|
||||
{{ _("Leads") |capfirst }}
|
||||
<li class="fas fa-bullhorn text-primary ms-2"></li>
|
||||
</h2>
|
||||
<!-- Action Tracking Modal -->
|
||||
{% comment %} {% include "crm/leads/partials/update_action.html" %} {% endcomment %}
|
||||
<div class="row g-3 justify-content-between mb-4">
|
||||
<div class="col-auto">
|
||||
<div class="d-md-flex justify-content-between">
|
||||
{% if perms.inventory.add_lead %}
|
||||
<div>
|
||||
<a href="{% url 'lead_create' request.dealer.slug %}"
|
||||
class="btn btn-sm btn-phoenix-primary"><span class="fas fa-plus me-2"></span>{{ _("Add Lead") }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="d-flex">{% include 'partials/search_box.html' %}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="d-flex">{% include 'partials/search_box.html' %}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
{% if page_obj.object_list or request.GET.q%}
|
||||
<div class="table-responsive scrollbar mx-n1 px-1">
|
||||
<table class="table align-items-center table-flush table-hover">
|
||||
<thead>
|
||||
<tr class="bg-body-highlight">
|
||||
<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-success-subtle rounded me-2">
|
||||
<i class="text-success-dark fas fa-car"></i>
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
{% if page_obj.object_list or request.GET.q %}
|
||||
<div class="table-responsive scrollbar mx-n1 px-1">
|
||||
<table class="table align-items-center table-flush table-hover">
|
||||
<thead>
|
||||
<tr class="bg-body-highlight">
|
||||
<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-success-subtle rounded me-2">
|
||||
<i class="text-success-dark fas fa-car"></i>
|
||||
</div>
|
||||
<span>{{ _("Car") |capfirst }}</span>
|
||||
</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">
|
||||
<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>
|
||||
</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-success-subtle rounded me-2">
|
||||
<span class="text-success-dark" data-feather="mail"></span>
|
||||
</div>
|
||||
<span>{{ _("email") |capfirst }}</span>
|
||||
</div>
|
||||
<span>{{ _("email") |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">
|
||||
<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>
|
||||
</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>
|
||||
<div class="" dir="ltr">{{ _("Phone Number") }}</div>
|
||||
</div>
|
||||
<div class="" dir="ltr">{{ _("Phone Number") }}</div>
|
||||
</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-warning-subtle rounded me-2">
|
||||
<span class="text-warning-dark" data-feather="zap"></span>
|
||||
</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="zap"></span>
|
||||
</div>
|
||||
<span>{{ _("Next Action") |capfirst }}</span>
|
||||
</div>
|
||||
<span>{{ _("Next Action") |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">
|
||||
<div class="d-flex align-items-center px-1 py-1 bg-primary-subtle rounded me-2">
|
||||
<span class="far fa-calendar-alt"></span>
|
||||
</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="far fa-calendar-alt"></span>
|
||||
</div>
|
||||
<span>{{ _("Scheduled at") }}</span>
|
||||
</div>
|
||||
<span>{{ _("Scheduled at") }}</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-success-subtle rounded me-2">
|
||||
<span class="text-success-dark" data-feather="user-check"></span>
|
||||
</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-success-subtle rounded me-2">
|
||||
<span class="text-success-dark" data-feather="user-check"></span>
|
||||
</div>
|
||||
<span>{{ _("Assigned To") |capfirst }}</span>
|
||||
</div>
|
||||
<span>{{ _("Assigned To") |capfirst }}</span>
|
||||
</div>
|
||||
</th>
|
||||
{% comment %} <th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 10%;">
|
||||
</th>
|
||||
{% comment %} <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>
|
||||
<span>{{ _("Opportunity")|capfirst }}</span>
|
||||
</div>
|
||||
</th> {% endcomment %}
|
||||
<th class="align-middle white-space-nowrap text-uppercase"
|
||||
scope="col"
|
||||
style="width: 15%">{{ _("Action") }}</th>
|
||||
<th class="text-end white-space-nowrap align-middle" scope="col"></th>
|
||||
</tr>
|
||||
{% for lead in leads %}
|
||||
<!-- 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 justify-content-between align-items-start gap-5 px-4 pt-4 pb-3 border-0">
|
||||
<h4 class="mb-0 me-2 text-danger">
|
||||
{{ _("Delete") }}<i class="fas fa-exclamation-circle text-danger ms-2"></i>
|
||||
</h4>
|
||||
<button class="btn p-0 text-body-quaternary fs-6"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close">
|
||||
<span class="fas fa-times"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body p-4">
|
||||
<p>{% trans "Are you sure you want to delete this lead?" %}</p>
|
||||
</div>
|
||||
<div class="modal-footer flex justify-content-center border-top-0">
|
||||
<a type="button"
|
||||
class="btn btn-sm btn-phoenix-danger w-100"
|
||||
href="{% url 'lead_delete' request.dealer.slug lead.slug %}">
|
||||
{% trans "Yes" %}
|
||||
</a>
|
||||
</th> {% endcomment %}
|
||||
<th class="align-middle white-space-nowrap text-uppercase"
|
||||
scope="col"
|
||||
style="width: 15%">{{ _("Action") }}</th>
|
||||
<th class="text-end white-space-nowrap align-middle" scope="col"></th>
|
||||
</tr>
|
||||
{% for lead in leads %}
|
||||
<!-- 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 justify-content-between align-items-start gap-5 px-4 pt-4 pb-3 border-0">
|
||||
<h4 class="mb-0 me-2 text-danger">
|
||||
{{ _("Delete") }}<i class="fas fa-exclamation-circle text-danger ms-2"></i>
|
||||
</h4>
|
||||
<button class="btn p-0 text-body-quaternary fs-6"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close">
|
||||
<span class="fas fa-times"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body p-4">
|
||||
<p>{% trans "Are you sure you want to delete this lead?" %}</p>
|
||||
</div>
|
||||
<div class="modal-footer flex justify-content-center border-top-0">
|
||||
<a type="button"
|
||||
class="btn btn-sm btn-phoenix-danger w-100"
|
||||
href="{% url 'lead_delete' request.dealer.slug lead.slug %}">
|
||||
{% trans "Yes" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<tbody>
|
||||
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
|
||||
<td class="name align-middle white-space-nowrap ps-1">
|
||||
<div class="d-flex align-items-center">
|
||||
<div>
|
||||
<a class="fs-8 fw-bold"
|
||||
href="{% url 'lead_detail' request.dealer.slug lead.slug %}">{{ lead.full_name|capfirst }}</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 == "contacted" %}
|
||||
<span class="badge badge-phoenix badge-phoenix-info"><span class="badge-label">{{ _("Contacted") }}</span><span class="fa fa-times 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 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 text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">
|
||||
{{ lead.next_action|upper }}
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap fw-semibold">{{ lead.next_action_date|upper }}</td>
|
||||
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="avatar avatar-tiny me-2">
|
||||
{% if lead.staff.logo %}
|
||||
<img class="avatar-img rounded-circle"
|
||||
src="{{ lead.staff.thumbnail.url }}"
|
||||
onerror="this.src='/static/img/brand/brand-logo.png'"
|
||||
alt="Logo">
|
||||
{% endif %}
|
||||
</div>
|
||||
<small>
|
||||
{% if lead.staff == request.staff %}
|
||||
{{ _("Me") }}
|
||||
{% elif LANGUAGE_CODE == "en" %}
|
||||
{{ lead.staff.fullname|capfirst }}
|
||||
{% else %}
|
||||
{{ lead.staff.arabic_name }}
|
||||
{% endif %}
|
||||
</small>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="align-middle white-space-nowrap text-end">
|
||||
{% if user == lead.staff.user or request.is_dealer %}
|
||||
<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">
|
||||
{% if perms.inventory.change_lead %}
|
||||
<a href="{% url 'lead_update' request.dealer.slug lead.slug %}"
|
||||
class="dropdown-item text-success-dark">{% trans "Edit" %}</a>
|
||||
{% endif %}
|
||||
{% if perms.inventory.change_lead %}
|
||||
{% endif %}
|
||||
{% if not lead.opportunity %}
|
||||
{% if perms.inventory.add_opportunity %}
|
||||
<a href="{% url 'lead_opportunity_create' request.dealer.slug lead.slug %}"
|
||||
class="dropdown-item text-success-dark">{% trans "Convert to Opportunity" %}</a>
|
||||
<tbody>
|
||||
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
|
||||
<td class="name align-middle white-space-nowrap ps-1">
|
||||
<div class="d-flex align-items-center">
|
||||
<div>
|
||||
<a class="fs-8 fw-bold"
|
||||
href="{% url 'lead_detail' request.dealer.slug lead.slug %}">{{ lead.full_name|capfirst }}</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 == "contacted" %}
|
||||
<span class="badge badge-phoenix badge-phoenix-info"><span class="badge-label">{{ _("Contacted") }}</span><span class="fa fa-times 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 %}
|
||||
{% endif %}
|
||||
{% if perms.inventory.delete_lead %}
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="dropdown-item text-danger"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#deleteModal">
|
||||
{% trans "Delete" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">{% trans "No Leads found." %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if page_obj.paginator.num_pages > 1 %}
|
||||
<div class="d-flex justify-content-end mt-3">
|
||||
<div class="d-flex">{% include 'partials/pagination.html' %}</div>
|
||||
</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 text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">
|
||||
{{ lead.next_action|upper }}
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap fw-semibold">{{ lead.next_action_date|upper }}</td>
|
||||
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="avatar avatar-tiny me-2">
|
||||
{% if lead.staff.logo %}
|
||||
<img class="avatar-img rounded-circle"
|
||||
src="{{ lead.staff.thumbnail.url }}"
|
||||
onerror="this.src='/static/img/brand/brand-logo.png'"
|
||||
alt="Logo">
|
||||
{% endif %}
|
||||
</div>
|
||||
<small>
|
||||
{% if lead.staff == request.staff %}
|
||||
{{ _("Me") }}
|
||||
{% elif LANGUAGE_CODE == "en" %}
|
||||
{{ lead.staff.fullname|capfirst }}
|
||||
{% else %}
|
||||
{{ lead.staff.arabic_name }}
|
||||
{% endif %}
|
||||
</small>
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap text-end">
|
||||
{% if user == lead.staff.user or request.is_dealer %}
|
||||
<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">
|
||||
{% if perms.inventory.change_lead %}
|
||||
<a href="{% url 'lead_update' request.dealer.slug lead.slug %}"
|
||||
class="dropdown-item text-success-dark">{% trans "Edit" %}</a>
|
||||
{% endif %}
|
||||
{% if perms.inventory.change_lead %}{% endif %}
|
||||
{% if not lead.opportunity %}
|
||||
{% if perms.inventory.add_opportunity %}
|
||||
<a href="{% url 'lead_opportunity_create' request.dealer.slug lead.slug %}"
|
||||
class="dropdown-item text-success-dark">{% trans "Convert to Opportunity" %}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if perms.inventory.delete_lead %}
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="dropdown-item text-danger"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#deleteModal">
|
||||
{% trans "Delete" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">{% trans "No Leads found." %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if page_obj.paginator.num_pages > 1 %}
|
||||
<div class="d-flex justify-content-end mt-3">
|
||||
<div class="d-flex">{% include 'partials/pagination.html' %}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{% url 'lead_create' request.dealer.slug as create_lead_url %}
|
||||
{% include "empty-illustration-page.html" with value="lead" url=create_lead_url %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% else %}
|
||||
{% url 'lead_create' request.dealer.slug as create_lead_url %}
|
||||
{% include "empty-illustration-page.html" with value="lead" url=create_lead_url %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@ -5,179 +5,176 @@
|
||||
{% endblock title %}
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
.kanban-column {
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
min-height: 500px;
|
||||
}
|
||||
.kanban-header {
|
||||
position: relative;
|
||||
font-weight: 600;
|
||||
padding: 0.5rem 1rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #333;
|
||||
--pointed-edge: {% if LANGUAGE_CODE == 'en' %} right {% else %} left {% endif %};
|
||||
clip-path: {% if LANGUAGE_CODE == 'en' %}
|
||||
polygon(0 0, calc(100% - 15px) 0, 100% 50%, calc(100% - 15px) 100%, 0 100%)
|
||||
{% else %}
|
||||
polygon(15px 0, 100% 0, 100% 100%, 15px 100%, 0 50%)
|
||||
{% endif %};
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
||||
}
|
||||
.kanban-column {
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
min-height: 500px;
|
||||
}
|
||||
.kanban-header {
|
||||
position: relative;
|
||||
font-weight: 600;
|
||||
padding: 0.5rem 1rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #333;
|
||||
--pointed-edge: {% if LANGUAGE_CODE == 'en' %} right {% else %} left {% endif %};
|
||||
clip-path: {% if LANGUAGE_CODE == 'en' %}
|
||||
polygon(0 0, calc(100% - 15px) 0, 100% 50%, calc(100% - 15px) 100%, 0 100%)
|
||||
{% else %}
|
||||
polygon(15px 0, 100% 0, 100% 100%, 15px 100%, 0 50%)
|
||||
{% endif %};
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
|
||||
.kanban-header::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: -20px;
|
||||
top: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 28px solid transparent;
|
||||
border-bottom: 28px solid transparent;
|
||||
border-left: 20px solid #dee2e6;
|
||||
}
|
||||
.lead-card {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.lead-card small {
|
||||
color: #6c757d;
|
||||
}
|
||||
.bg-success-soft {
|
||||
background-color: rgba(17, 240, 66, 0.1) !important;
|
||||
opacity: .8;
|
||||
}
|
||||
.bg-danger-soft {
|
||||
background-color: rgba(230, 50, 68, 0.1) !important;
|
||||
opacity: .8;
|
||||
}
|
||||
.bg-info-soft {
|
||||
background-color: rgba(41, 197, 245, 0.1) !important;
|
||||
opacity: .8;
|
||||
}
|
||||
.bg-negotiation-soft {
|
||||
background-color: rgba(113, 206, 206, 0.1) !important;
|
||||
opacity: .8;
|
||||
}
|
||||
.kanban-header::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: -20px;
|
||||
top: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 28px solid transparent;
|
||||
border-bottom: 28px solid transparent;
|
||||
border-left: 20px solid #dee2e6;
|
||||
}
|
||||
.lead-card {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.lead-card small {
|
||||
color: #6c757d;
|
||||
}
|
||||
.bg-success-soft {
|
||||
background-color: rgba(17, 240, 66, 0.1) !important;
|
||||
opacity: .8;
|
||||
}
|
||||
.bg-danger-soft {
|
||||
background-color: rgba(230, 50, 68, 0.1) !important;
|
||||
opacity: .8;
|
||||
}
|
||||
.bg-info-soft {
|
||||
background-color: rgba(41, 197, 245, 0.1) !important;
|
||||
opacity: .8;
|
||||
}
|
||||
.bg-negotiation-soft {
|
||||
background-color: rgba(113, 206, 206, 0.1) !important;
|
||||
opacity: .8;
|
||||
}
|
||||
</style>
|
||||
{% endblock customCSS %}
|
||||
{% block content %}
|
||||
|
||||
{% if leads %}
|
||||
<div class="container-fluid my-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col">
|
||||
<div class="d-flex justify-content-between mb-3">
|
||||
<h3>
|
||||
{{ _("Lead Tracking") }}
|
||||
<li class="fas fa-bullhorn text-primary ms-2"></li>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<!-- New Lead -->
|
||||
<div class="col-md">
|
||||
<div class="kanban-column bg-body">
|
||||
<div class="kanban-header opacity-75">
|
||||
<span class="text-body">{{ _("New Leads") }} ({{ new|length }})</span>
|
||||
{% if leads %}
|
||||
<div class="container-fluid my-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col">
|
||||
<div class="d-flex justify-content-between mb-3">
|
||||
<h3>
|
||||
{{ _("Lead Tracking") }}
|
||||
<li class="fas fa-bullhorn text-primary ms-2"></li>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<!-- New Lead -->
|
||||
<div class="col-md">
|
||||
<div class="kanban-column bg-body">
|
||||
<div class="kanban-header opacity-75">
|
||||
<span class="text-body">{{ _("New Leads") }} ({{ new|length }})</span>
|
||||
</div>
|
||||
{% for lead in new %}
|
||||
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
|
||||
<div class="lead-card">
|
||||
<strong>{{ lead.full_name|capfirst }}</strong>
|
||||
<br>
|
||||
<small>{{ lead.email }}</small>
|
||||
<br>
|
||||
<small>{{ lead.phone_number }}</small>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% for lead in new %}
|
||||
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
|
||||
<div class="lead-card">
|
||||
<strong>{{ lead.full_name|capfirst }}</strong>
|
||||
<br>
|
||||
<small>{{ lead.email }}</small>
|
||||
<br>
|
||||
<small>{{ lead.phone_number }}</small>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Follow Ups -->
|
||||
<div class="col-md">
|
||||
<div class="kanban-column bg-body">
|
||||
<div class="kanban-header opacity-75">
|
||||
<span class="text-body">{{ _("Follow Ups") }} ({{ follow_up|length }})</span>
|
||||
<!-- Follow Ups -->
|
||||
<div class="col-md">
|
||||
<div class="kanban-column bg-body">
|
||||
<div class="kanban-header opacity-75">
|
||||
<span class="text-body">{{ _("Follow Ups") }} ({{ follow_up|length }})</span>
|
||||
</div>
|
||||
{% for lead in follow_up %}
|
||||
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
|
||||
<div class="lead-card">
|
||||
<strong>{{ lead.full_name|capfirst }}</strong>
|
||||
<br>
|
||||
<small>{{ lead.email }}</small>
|
||||
<br>
|
||||
<small>{{ lead.phone_number }}</small>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% for lead in follow_up %}
|
||||
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
|
||||
<div class="lead-card">
|
||||
<strong>{{ lead.full_name|capfirst }}</strong>
|
||||
<br>
|
||||
<small>{{ lead.email }}</small>
|
||||
<br>
|
||||
<small>{{ lead.phone_number }}</small>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Negotiation -->
|
||||
<div class="col-md">
|
||||
<div class="kanban-column bg-body">
|
||||
<div class="kanban-header opacity-75">
|
||||
<span class="text-body">{{ _("Negotiation Ups") }} ({{ follow_up|length }})</span>
|
||||
<!-- Negotiation -->
|
||||
<div class="col-md">
|
||||
<div class="kanban-column bg-body">
|
||||
<div class="kanban-header opacity-75">
|
||||
<span class="text-body">{{ _("Negotiation Ups") }} ({{ follow_up|length }})</span>
|
||||
</div>
|
||||
{% for lead in negotiation %}
|
||||
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
|
||||
<div class="lead-card">
|
||||
<strong>{{ lead.full_name|capfirst }}</strong>
|
||||
<br>
|
||||
<small>{{ lead.email }}</small>
|
||||
<br>
|
||||
<small>{{ lead.phone_number }}</small>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% for lead in negotiation %}
|
||||
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
|
||||
<div class="lead-card">
|
||||
<strong>{{ lead.full_name|capfirst }}</strong>
|
||||
<br>
|
||||
<small>{{ lead.email }}</small>
|
||||
<br>
|
||||
<small>{{ lead.phone_number }}</small>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Won -->
|
||||
<div class="col-md">
|
||||
<div class="kanban-column bg-body">
|
||||
<div class="kanban-header bg-success-light opacity-75">
|
||||
<span class="text-body">{{ _("Won") }} ({{ won|length }}) ({{ follow_up|length }})</span>
|
||||
<!-- Won -->
|
||||
<div class="col-md">
|
||||
<div class="kanban-column bg-body">
|
||||
<div class="kanban-header bg-success-light opacity-75">
|
||||
<span class="text-body">{{ _("Won") }} ({{ won|length }}) ({{ follow_up|length }})</span>
|
||||
</div>
|
||||
{% for lead in won %}
|
||||
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
|
||||
<div class="lead-card">
|
||||
<strong>{{ lead.full_name|capfirst }}</strong>
|
||||
<br>
|
||||
<small>{{ lead.email }}</small>
|
||||
<br>
|
||||
<small>{{ lead.phone_number }}</small>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Lose -->
|
||||
<div class="col-md">
|
||||
<div class="kanban-column bg-body">
|
||||
<div class="kanban-header bg-danger-light opacity-75">{{ _("Lost") }} ({{ lose|length }})</div>
|
||||
{% for lead in lose %}
|
||||
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
|
||||
<div class="lead-card">
|
||||
<strong>{{ lead.full_name|capfirst }}</strong>
|
||||
<br>
|
||||
<small>{{ lead.email }}</small>
|
||||
<br>
|
||||
<small>{{ lead.phone_number }}</small>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% for lead in won %}
|
||||
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
|
||||
<div class="lead-card">
|
||||
<strong>{{ lead.full_name|capfirst }}</strong>
|
||||
<br>
|
||||
<small>{{ lead.email }}</small>
|
||||
<br>
|
||||
<small>{{ lead.phone_number }}</small>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Lose -->
|
||||
<div class="col-md">
|
||||
<div class="kanban-column bg-body">
|
||||
<div class="kanban-header bg-danger-light opacity-75">{{ _("Lost") }} ({{ lose|length }})</div>
|
||||
{% for lead in lose %}
|
||||
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
|
||||
<div class="lead-card">
|
||||
<strong>{{ lead.full_name|capfirst }}</strong>
|
||||
<br>
|
||||
<small>{{ lead.email }}</small>
|
||||
<br>
|
||||
<small>{{ lead.phone_number }}</small>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{% url 'lead_create' request.dealer.slug as create_lead_url %}
|
||||
{% include "empty-illustration-page.html" with value="lead" url=create_lead_url %}
|
||||
{% else %}
|
||||
{% url 'lead_create' request.dealer.slug as create_lead_url %}
|
||||
{% include "empty-illustration-page.html" with value="lead" url=create_lead_url %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@ -2,22 +2,22 @@
|
||||
{% load static i18n humanize %}
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
.card {
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
border: none;
|
||||
}
|
||||
.card-header {
|
||||
background-color: #f0f2f5;
|
||||
font-weight: 600;
|
||||
}
|
||||
.table thead {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 50px;
|
||||
color: #aaa;
|
||||
}
|
||||
.card {
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
border: none;
|
||||
}
|
||||
.card-header {
|
||||
background-color: #f0f2f5;
|
||||
font-weight: 600;
|
||||
}
|
||||
.table thead {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 50px;
|
||||
color: #aaa;
|
||||
}
|
||||
</style>
|
||||
{% endblock customCSS %}
|
||||
{% block content %}
|
||||
@ -27,7 +27,7 @@
|
||||
<h5 class="mb-0">مرحبًا</h5>
|
||||
<div>
|
||||
<button class="btn btn-phoenix-secondary dropdown-toggle"
|
||||
data-bs-toggle="dropdown">الصفحة الرئيسية لـ </button>
|
||||
data-bs-toggle="dropdown">الصفحة الرئيسية لـ</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Main Row -->
|
||||
|
||||
@ -13,14 +13,11 @@
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<form id="actionTrackingForm"
|
||||
action="{% url 'update_lead_actions' request.dealer.slug %}"
|
||||
hx-select-oob="#currentStage:outerHTML,#leadStatus:outerHTML,#toast-container:outerHTML"
|
||||
hx-swap="none"
|
||||
hx-on::after-request="{
|
||||
resetSubmitButton(document.querySelector('#actionTrackingForm button[type=submit]'));
|
||||
$('#actionTrackingModal').modal('hide');
|
||||
}"
|
||||
method="post">
|
||||
action="{% url 'update_lead_actions' request.dealer.slug %}"
|
||||
hx-select-oob="#currentStage:outerHTML,#leadStatus:outerHTML,#toast-container:outerHTML"
|
||||
hx-swap="none"
|
||||
hx-on::after-request="{ resetSubmitButton(document.querySelector('#actionTrackingForm button[type=submit]')); $('#actionTrackingModal').modal('hide'); }"
|
||||
method="post">
|
||||
<div class="modal-body">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" id="leadId" name="lead_id">
|
||||
|
||||
@ -3,7 +3,9 @@
|
||||
<div class="content">
|
||||
<h2 class="mb-5">{{ _("Notifications") }}</h2>
|
||||
<div class="d-flex justify-content-end mb-3">
|
||||
<a href="{% url 'mark_all_notifications_as_read' %}" hx-select-oob="#toast-container:outerHTML" class="btn btn-phoenix-primary"><i class="far fa-envelope fs-8 me-2"></i>{{ _("Mark all as read") }}</a>
|
||||
<a href="{% url 'mark_all_notifications_as_read' %}"
|
||||
hx-select-oob="#toast-container:outerHTML"
|
||||
class="btn btn-phoenix-primary"><i class="far fa-envelope fs-8 me-2"></i>{{ _("Mark all as read") }}</a>
|
||||
</div>
|
||||
{% if notifications %}
|
||||
<div class="mx-n4 mx-lg-n6 mb-5 border-bottom">
|
||||
|
||||
@ -40,7 +40,8 @@
|
||||
href="{% url 'update_opportunity' request.dealer.slug opportunity.slug %}">Update Opportunity</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" type="button"
|
||||
<a class="dropdown-item"
|
||||
type="button"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#updateStageModal">Update Stage</a>
|
||||
</li>
|
||||
@ -683,16 +684,16 @@
|
||||
<form action="{% url 'add_note_to_opportunity' request.dealer.slug opportunity.slug %}" method="post">
|
||||
{% csrf_token %}
|
||||
<textarea class="form-control mb-3" id="notes" rows="4" name="notes" required> </textarea>
|
||||
<button type="submit" class="btn btn-phoenix-primary mb-3">Add Note</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<div class="row gy-4 note-list">
|
||||
<div class="col-12 col-xl-auto flex-1">
|
||||
{% for note in opportunity.get_notes %}
|
||||
<div class="border-2 border-dashed mb-4 pb-4 border-bottom border-translucent">
|
||||
<p class="mb-1 text-body-highlight">{{ note.note }}</p>
|
||||
<div class="d-flex">
|
||||
<div class="fs-9 text-body-tertiary text-opacity-85"><span class="fa-solid fa-clock me-2"></span><span class="fw-semibold me-1">{{note.created|naturaltime|capfirst}}</span></div>
|
||||
<button type="submit" class="btn btn-phoenix-primary mb-3">Add Note</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<div class="row gy-4 note-list">
|
||||
<div class="col-12 col-xl-auto flex-1">
|
||||
{% for note in opportunity.get_notes %}
|
||||
<div class="border-2 border-dashed mb-4 pb-4 border-bottom border-translucent">
|
||||
<p class="mb-1 text-body-highlight">{{ note.note }}</p>
|
||||
<div class="d-flex">
|
||||
<div class="fs-9 text-body-tertiary text-opacity-85"><span class="fa-solid fa-clock me-2"></span><span class="fw-semibold me-1">{{note.created|naturaltime|capfirst}}</span></div>
|
||||
<p class="fs-9 mb-0 text-body-tertiary text-opacity-85">by<a class="ms-1 fw-semibold" href="#!">{{note.created_by}}</a></p>
|
||||
</div>
|
||||
</div>
|
||||
@ -828,15 +829,14 @@
|
||||
</button>
|
||||
</a> {% endcomment %}
|
||||
{% if opportunity.lead %}
|
||||
<button class="btn btn-phoenix-primary btn-sm"
|
||||
type="button"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#emailModal"
|
||||
hx-get="{% url 'send_lead_email' request.dealer.slug opportunity.lead.slug %}"
|
||||
hx-target="#emailModalBody"
|
||||
hx-select=".email-form"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<button class="btn btn-phoenix-primary btn-sm"
|
||||
type="button"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#emailModal"
|
||||
hx-get="{% url 'send_lead_email' request.dealer.slug opportunity.lead.slug %}"
|
||||
hx-target="#emailModalBody"
|
||||
hx-select=".email-form"
|
||||
hx-swap="innerHTML">
|
||||
<span class="fas fa-plus me-1"></span>{{ _("Send Email") }}
|
||||
</button>
|
||||
{% endif %}
|
||||
@ -858,7 +858,6 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="profileTabContent">
|
||||
<div class="tab-pane fade show active"
|
||||
id="tab-mail"
|
||||
@ -871,7 +870,6 @@
|
||||
<table class="table fs-9 mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
<th class="sort white-space-nowrap align-middle pe-3 ps-0 text-uppercase"
|
||||
scope="col"
|
||||
data-sort="subject"
|
||||
@ -896,14 +894,12 @@
|
||||
<tbody class="list" id="all-email-table-body">
|
||||
{% for email in opportunity.lead.get_emails %}
|
||||
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
|
||||
|
||||
<td class="subject order align-middle white-space-nowrap py-2 ps-0">
|
||||
<a class="fw-semibold text-primary" href="#!">{{ email.subject }}</a>
|
||||
<div class="fs-10 d-block">{{ email.to_email }}</div>
|
||||
</td>
|
||||
<td class="sent align-middle white-space-nowrap text-start fw-bold text-body-tertiary py-2">{{ email.from_email }}</td>
|
||||
<td class="date align-middle white-space-nowrap text-body py-2">{{ email.created }}</td>
|
||||
|
||||
<td class="status align-middle fw-semibold text-end py-2">
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-success">sent</span>
|
||||
</td>
|
||||
@ -1023,10 +1019,10 @@
|
||||
<div class="tab-pane fade"
|
||||
id="tab-activity"
|
||||
hx-get="{% url 'opportunity_detail' request.dealer.slug opportunity.slug %}"
|
||||
hx-trigger="htmx:afterRequest from:"
|
||||
hx-select="#tab-activity"
|
||||
hx-target="this"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="htmx:afterRequest from:"
|
||||
hx-select="#tab-activity"
|
||||
hx-target="this"
|
||||
hx-swap="outerHTML"
|
||||
role="tabpanel"
|
||||
aria-labelledby="activity-tab">
|
||||
<h2 class="mb-4">Activity</h2>
|
||||
@ -1097,37 +1093,36 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="updateStageModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post"
|
||||
action="{% url 'update_opportunity_stage' request.dealer.slug opportunity.slug %}"
|
||||
hx-post="{% url 'update_opportunity_stage' request.dealer.slug opportunity.slug %}"
|
||||
hx-swap="none"
|
||||
hx-on::after-request="location.reload()">
|
||||
{% csrf_token %}
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="updateStageModalLabel">{{ _("Update Opportunity Stage") }}</h5>
|
||||
<button class="btn btn-close p-1"
|
||||
type="button"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{{ stage_form|crispy }}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-phoenix-primary" type="submit">{{ _("Save") }}</button>
|
||||
<button class="btn btn-phoenix-secondary"
|
||||
type="button"
|
||||
data-bs-dismiss="modal">{{ _("Cancel") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal fade"
|
||||
id="updateStageModal"
|
||||
tabindex="-1"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post"
|
||||
action="{% url 'update_opportunity_stage' request.dealer.slug opportunity.slug %}"
|
||||
hx-post="{% url 'update_opportunity_stage' request.dealer.slug opportunity.slug %}"
|
||||
hx-swap="none"
|
||||
hx-on::after-request="location.reload()">
|
||||
{% csrf_token %}
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="updateStageModalLabel">{{ _("Update Opportunity Stage") }}</h5>
|
||||
<button class="btn btn-close p-1"
|
||||
type="button"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">{{ stage_form|crispy }}</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-phoenix-primary" type="submit">{{ _("Save") }}</button>
|
||||
<button class="btn btn-phoenix-secondary"
|
||||
type="button"
|
||||
data-bs-dismiss="modal">{{ _("Cancel") }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
{% include 'modal/delete_modal.html' %}
|
||||
<!-- email Modal -->
|
||||
{% include "components/email_modal.html" %}
|
||||
@ -1138,28 +1133,26 @@
|
||||
<!-- schedule Modal -->
|
||||
{% include "components/schedule_modal.html" with content_type="opportunity" slug=opportunity.slug %}
|
||||
{% endblock %}
|
||||
|
||||
{% block customJS %}
|
||||
<script>
|
||||
|
||||
document.body.addEventListener('htmx:afterSwap', function(evt) {
|
||||
if (evt.detail.target.id === 'main_content') {
|
||||
var modal = bootstrap.Modal.getInstance(document.getElementById('exampleModal'));
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
<script>
|
||||
document.body.addEventListener('htmx:afterSwap', function(evt) {
|
||||
if (evt.detail.target.id === 'main_content') {
|
||||
var modal = bootstrap.Modal.getInstance(document.getElementById('exampleModal'));
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup modal backdrop if needed
|
||||
document.body.addEventListener('htmx:beforeSwap', function(evt) {
|
||||
if (evt.detail.target.id === 'main_content') {
|
||||
var backdrops = document.querySelectorAll('.modal-backdrop');
|
||||
backdrops.forEach(function(backdrop) {
|
||||
backdrop.remove();
|
||||
});
|
||||
}
|
||||
});
|
||||
document.body.addEventListener('htmx:beforeSwap', function(evt) {
|
||||
if (evt.detail.target.id === 'main_content') {
|
||||
var backdrops = document.querySelectorAll('.modal-backdrop');
|
||||
backdrops.forEach(function(backdrop) {
|
||||
backdrop.remove();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock customJS %}
|
||||
</script>
|
||||
{% endblock customJS %}
|
||||
|
||||
@ -9,201 +9,162 @@
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container-fluid mt-4 mb-3">
|
||||
<div class="row g-3 mb-4 align-items-center">
|
||||
<div class="col">
|
||||
<h2 class="mb-0">
|
||||
{% if form.instance.pk %}
|
||||
{% trans "Edit Opportunity" %}
|
||||
{% else %}
|
||||
{% trans "Create New Opportunity" %}
|
||||
{% endif %}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="{% url 'opportunity_list' request.dealer.slug %}" class="btn btn-phoenix-secondary">
|
||||
<span class="fas fa-arrow-left me-2"></span>{% trans "Back to list" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-body p-4 p-sm-5">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger">
|
||||
{{ form.non_field_errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Lead Field -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="{{ form.lead.id_for_label }}">
|
||||
{{ form.lead.label }}
|
||||
<span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.lead|add_class:"form-control" }}
|
||||
{% if form.lead.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.lead.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Car Field -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="{{ form.car.id_for_label }}">
|
||||
{{ form.car.label }}
|
||||
<span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.car|add_class:"form-control" }}
|
||||
{% if form.car.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.car.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Stage Field -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="{{ form.stage.id_for_label }}">
|
||||
{{ form.stage.label }}
|
||||
<span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.stage|add_class:"form-control" }}
|
||||
{% if form.stage.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.stage.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Amount Field -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="{{ form.amount.id_for_label }}">
|
||||
{{ form.amount.label }}
|
||||
<span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><span class="icon-saudi_riyal"></span></span>
|
||||
{{ form.amount|add_class:"form-control" }}
|
||||
</div>
|
||||
{% if form.amount.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.amount.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Probability Field -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="{{ form.probability.id_for_label }}">
|
||||
{{ form.probability.label }}
|
||||
<span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<input type="range"
|
||||
name="{{ form.probability.name }}"
|
||||
id="{{ form.probability.id_for_label }}"
|
||||
min="0" max="100" step="1"
|
||||
value="{{ form.probability.value|default:'50' }}"
|
||||
class="form-control form-range"
|
||||
oninput="updateProbabilityValue(this.value)">
|
||||
<span id="probability-value" class="badge badge-phoenix fs-6 badge-phoenix-primary">
|
||||
{{ form.probability.value|default:'50' }}%
|
||||
</span>
|
||||
</div>
|
||||
{% if form.probability.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.probability.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Expected Revenue -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="{{ form.expected_revenue.id_for_label }}">
|
||||
{{ form.expected_revenue.label }}
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><span class="icon-saudi_riyal"></span></span>
|
||||
{{ form.expected_revenue|add_class:"form-control" }}
|
||||
</div>
|
||||
{% if form.expected_revenue.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.expected_revenue.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Closing Date -->
|
||||
<div class="mb-5">
|
||||
<label class="form-label" for="{{ form.closing_date.id_for_label }}">
|
||||
{{ form.closing_date.label }}
|
||||
</label>
|
||||
<div class="input-group">
|
||||
{{ form.expected_close_date|add_class:"form-control" }}
|
||||
<span class="input-group-text"><span class="far fa-calendar"></span></span>
|
||||
</div>
|
||||
{% if form.expected_close_date.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.expected_close_date.errors }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="d-flex justify-content-end gap-3">
|
||||
<button type="reset" class="btn btn-phoenix-danger px-4">
|
||||
<span class="fas fa-redo me-1"></span>{% trans "Reset" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-phoenix-primary px-6">
|
||||
{% if form.instance.pk %}
|
||||
<span class="fas fa-save me-1"></span>{% trans "Update" %}
|
||||
{% else %}
|
||||
<span class="fas fa-plus me-1"></span>{% trans "Create" %}
|
||||
{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="container-fluid mt-4 mb-3">
|
||||
<div class="row g-3 mb-4 align-items-center">
|
||||
<div class="col">
|
||||
<h2 class="mb-0">
|
||||
{% if form.instance.pk %}
|
||||
{% trans "Edit Opportunity" %}
|
||||
{% else %}
|
||||
{% trans "Create New Opportunity" %}
|
||||
{% endif %}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="{% url 'opportunity_list' request.dealer.slug %}"
|
||||
class="btn btn-phoenix-secondary">
|
||||
<span class="fas fa-arrow-left me-2"></span>{% trans "Back to list" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-body p-4">
|
||||
<h4 class="mb-3">{% trans "Opportunity Guidelines" %}</h4>
|
||||
<ul class="nav flex-column gap-2 nav-guide">
|
||||
<li class="nav-item">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="fas fa-circle text-primary fs-11 me-2"></span>
|
||||
<span class="text-body-highlight">{% trans "Probability indicates conversion chance" %}</span>
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-body p-4 p-sm-5">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% if form.non_field_errors %}<div class="alert alert-danger">{{ form.non_field_errors }}</div>{% endif %}
|
||||
<!-- Lead Field -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="{{ form.lead.id_for_label }}">
|
||||
{{ form.lead.label }}
|
||||
<span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.lead|add_class:"form-control" }}
|
||||
{% if form.lead.errors %}<div class="invalid-feedback d-block">{{ form.lead.errors }}</div>{% endif %}
|
||||
</div>
|
||||
<!-- Car Field -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="{{ form.car.id_for_label }}">
|
||||
{{ form.car.label }}
|
||||
<span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.car|add_class:"form-control" }}
|
||||
{% if form.car.errors %}<div class="invalid-feedback d-block">{{ form.car.errors }}</div>{% endif %}
|
||||
</div>
|
||||
<!-- Stage Field -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="{{ form.stage.id_for_label }}">
|
||||
{{ form.stage.label }}
|
||||
<span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.stage|add_class:"form-control" }}
|
||||
{% if form.stage.errors %}<div class="invalid-feedback d-block">{{ form.stage.errors }}</div>{% endif %}
|
||||
</div>
|
||||
<!-- Amount Field -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="{{ form.amount.id_for_label }}">
|
||||
{{ form.amount.label }}
|
||||
<span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><span class="icon-saudi_riyal"></span></span>
|
||||
{{ form.amount|add_class:"form-control" }}
|
||||
</div>
|
||||
{% if form.amount.errors %}<div class="invalid-feedback d-block">{{ form.amount.errors }}</div>{% endif %}
|
||||
</div>
|
||||
<!-- Probability Field -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="{{ form.probability.id_for_label }}">
|
||||
{{ form.probability.label }}
|
||||
<span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<input type="range"
|
||||
name="{{ form.probability.name }}"
|
||||
id="{{ form.probability.id_for_label }}"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
value="{{ form.probability.value|default:'50' }}"
|
||||
class="form-control form-range"
|
||||
oninput="updateProbabilityValue(this.value)">
|
||||
<span id="probability-value"
|
||||
class="badge badge-phoenix fs-6 badge-phoenix-primary">
|
||||
{{ form.probability.value|default:'50' }}%
|
||||
</span>
|
||||
</div>
|
||||
{% if form.probability.errors %}<div class="invalid-feedback d-block">{{ form.probability.errors }}</div>{% endif %}
|
||||
</div>
|
||||
<!-- Expected Revenue -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="{{ form.expected_revenue.id_for_label }}">{{ form.expected_revenue.label }}</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><span class="icon-saudi_riyal"></span></span>
|
||||
{{ form.expected_revenue|add_class:"form-control" }}
|
||||
</div>
|
||||
{% if form.expected_revenue.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.expected_revenue.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Closing Date -->
|
||||
<div class="mb-5">
|
||||
<label class="form-label" for="{{ form.closing_date.id_for_label }}">{{ form.closing_date.label }}</label>
|
||||
<div class="input-group">
|
||||
{{ form.expected_close_date|add_class:"form-control" }}
|
||||
<span class="input-group-text"><span class="far fa-calendar"></span></span>
|
||||
</div>
|
||||
{% if form.expected_close_date.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.expected_close_date.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Form Actions -->
|
||||
<div class="d-flex justify-content-end gap-3">
|
||||
<button type="reset" class="btn btn-phoenix-danger px-4">
|
||||
<span class="fas fa-redo me-1"></span>{% trans "Reset" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-phoenix-primary px-6">
|
||||
{% if form.instance.pk %}
|
||||
<span class="fas fa-save me-1"></span>{% trans "Update" %}
|
||||
{% else %}
|
||||
<span class="fas fa-plus me-1"></span>{% trans "Create" %}
|
||||
{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="fas fa-circle text-warning fs-11 me-2"></span>
|
||||
<span class="text-body-highlight">{% trans "Update stage as deal progresses" %}</span>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-body p-4">
|
||||
<h4 class="mb-3">{% trans "Opportunity Guidelines" %}</h4>
|
||||
<ul class="nav flex-column gap-2 nav-guide">
|
||||
<li class="nav-item">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="fas fa-circle text-primary fs-11 me-2"></span>
|
||||
<span class="text-body-highlight">{% trans "Probability indicates conversion chance" %}</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="fas fa-circle text-warning fs-11 me-2"></span>
|
||||
<span class="text-body-highlight">{% trans "Update stage as deal progresses" %}</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="fas fa-circle text-success fs-11 me-2"></span>
|
||||
<span class="text-body-highlight">{% trans "Set realistic closing dates" %}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="fas fa-circle text-success fs-11 me-2"></span>
|
||||
<span class="text-body-highlight">{% trans "Set realistic closing dates" %}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
<script>
|
||||
function updateProbabilityValue(value) {
|
||||
const amount = document.getElementById('id_amount');
|
||||
const expectedRevenue = document.getElementById('id_expected_revenue');
|
||||
|
||||
@ -75,7 +75,8 @@
|
||||
height: 12px;
|
||||
width: 12px"></span>{{ opportunity.get_stage_display }}
|
||||
</p>
|
||||
<p class="ms-auto fs-9 text-body-emphasis fw-semibold mb-0 deals-revenue">{{ opportunity.car.total }}</p># TODO : check later
|
||||
<p class="ms-auto fs-9 text-body-emphasis fw-semibold mb-0 deals-revenue">{{ opportunity.car.total }}</p>
|
||||
# TODO : check later
|
||||
</div>
|
||||
<div class="deals-company-agent d-flex flex-between-center">
|
||||
<div class="d-flex align-items-center">
|
||||
|
||||
@ -5,136 +5,137 @@
|
||||
{{ _("Opportunities") }}
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
|
||||
{% if opportunities or request.GET.q%}
|
||||
<div class="row g-3 mt-4">
|
||||
<div class="row g-3 justify-content-between mb-4">
|
||||
<div class="col-auto">
|
||||
<div class="d-md-flex justify-content-between">
|
||||
<h2 class="mb-3">
|
||||
{{ _("Opportunities") }}
|
||||
<li class="fas fas fa-rocket text-primary ms-2"></li>
|
||||
</h2>
|
||||
{% if opportunities or request.GET.q %}
|
||||
<div class="row g-3 mt-4">
|
||||
<div class="row g-3 justify-content-between mb-4">
|
||||
<div class="col-auto">
|
||||
<div class="d-md-flex justify-content-between">
|
||||
<h2 class="mb-3">
|
||||
{{ _("Opportunities") }}
|
||||
<li class="fas fas fa-rocket text-primary ms-2"></li>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
{% if perms.inventory.add_opportunity %}
|
||||
<div class="d-flex justify-content-between">
|
||||
<a class="btn btn-phoenix-primary btn-sm"
|
||||
href="{% url 'opportunity_create' request.dealer.slug %}">
|
||||
<span class="fas fa-plus me-2"></span>{{ _("Add Opportunity") }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
{% if perms.inventory.add_opportunity %}
|
||||
<div class="d-flex justify-content-between">
|
||||
<a class="btn btn-phoenix-primary btn-sm"
|
||||
href="{% url 'opportunity_create' request.dealer.slug %}">
|
||||
<span class="fas fa-plus me-2"></span>{{ _("Add Opportunity") }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4">
|
||||
<!-- Filter Controls -->
|
||||
<div class="d-flex flex-column flex-lg-row align-items-start align-items-lg-center gap-3 w-100"
|
||||
id="filter-container">
|
||||
<!-- Search Input - Wider and properly aligned -->
|
||||
<div class="search-box position-relative flex-grow-1 me-2" style="min-width: 200px">
|
||||
<form class="position-relative show" id="search-form"
|
||||
hx-get=""
|
||||
hx-boost="false"
|
||||
hx-trigger="keyup changed delay:500ms, search">
|
||||
|
||||
<div class="col-12">
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4">
|
||||
<!-- Filter Controls -->
|
||||
<div class="d-flex flex-column flex-lg-row align-items-start align-items-lg-center gap-3 w-100"
|
||||
id="filter-container">
|
||||
<!-- Search Input - Wider and properly aligned -->
|
||||
<div class="search-box position-relative flex-grow-1 me-2"
|
||||
style="min-width: 200px">
|
||||
<form class="position-relative show"
|
||||
id="search-form"
|
||||
hx-get=""
|
||||
hx-boost="false"
|
||||
hx-trigger="keyup changed delay:500ms, search">
|
||||
<input name="q"
|
||||
id="search-input"
|
||||
class="form-control form-control-sm search-input search"
|
||||
type="search"
|
||||
aria-label="Search"
|
||||
placeholder="{{ _("Search") }}..."
|
||||
value="{{ request.GET.q}}" />
|
||||
|
||||
id="search-input"
|
||||
class="form-control form-control-sm search-input search"
|
||||
type="search"
|
||||
aria-label="Search"
|
||||
placeholder="{{ _("Search") }}..."
|
||||
value="{{ request.GET.q }}" />
|
||||
<span class="fa fa-magnifying-glass search-box-icon"></span>
|
||||
|
||||
{% if request.GET.q %}
|
||||
<button type="button"
|
||||
class="btn-close position-absolute end-0 top-50 translate-middle cursor-pointer shadow-none"
|
||||
id="clear-search"
|
||||
aria-label="Clear Search"></button>
|
||||
{% endif %}
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Filter Dropdowns - Aligned in a row -->
|
||||
<div class="d-flex flex-column flex-sm-row gap-3 w-100"
|
||||
style="max-width: 400px">
|
||||
<!-- Stage Filter -->
|
||||
<!-- Stage Filter -->
|
||||
|
||||
<div class="flex-grow-1">
|
||||
<select class="form-select"
|
||||
name="stage"
|
||||
hx-get="{% url 'opportunity_list' request.dealer.slug %}"
|
||||
hx-trigger="change"
|
||||
hx-target="#opportunities-grid"
|
||||
hx-select="#opportunities-grid"
|
||||
hx-swap="outerHTML"
|
||||
hx-include="#filter-container input, #filter-container select">
|
||||
<option value="">{% trans "All Stages" %}</option>
|
||||
{% for value, label in stage_choices %}
|
||||
<option value="{{ value }}"
|
||||
{% if request.GET.stage == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<!-- Sort Filter -->
|
||||
<div class="flex-grow-1">
|
||||
<select class="form-select"
|
||||
name="sort"
|
||||
hx-get="{% url 'opportunity_list' request.dealer.slug %}"
|
||||
hx-trigger="change"
|
||||
hx-target="#opportunities-grid"
|
||||
hx-select="#opportunities-grid"
|
||||
hx-swap="outerHTML"
|
||||
hx-include="#filter-container input, #filter-container select">
|
||||
<option value="newest"
|
||||
{% if request.GET.sort == 'newest' %}selected{% endif %}>{% trans "Newest First" %}</option>
|
||||
<option value="highest"
|
||||
{% if request.GET.sort == 'highest' %}selected{% endif %}>{% trans "Highest Value" %}</option>
|
||||
<option value="closing"
|
||||
{% if request.GET.sort == 'closing' %}selected{% endif %}>{% trans "Earliest Close Date" %}</option>
|
||||
</select>
|
||||
<!-- Filter Dropdowns - Aligned in a row -->
|
||||
<div class="d-flex flex-column flex-sm-row gap-3 w-100"
|
||||
style="max-width: 400px">
|
||||
<!-- Stage Filter -->
|
||||
<!-- Stage Filter -->
|
||||
<div class="flex-grow-1">
|
||||
<select class="form-select"
|
||||
name="stage"
|
||||
hx-get="{% url 'opportunity_list' request.dealer.slug %}"
|
||||
hx-trigger="change"
|
||||
hx-target="#opportunities-grid"
|
||||
hx-select="#opportunities-grid"
|
||||
hx-swap="outerHTML"
|
||||
hx-include="#filter-container input, #filter-container select">
|
||||
<option value="">{% trans "All Stages" %}</option>
|
||||
{% for value, label in stage_choices %}
|
||||
<option value="{{ value }}"
|
||||
{% if request.GET.stage == value %}selected{% endif %}>
|
||||
{{ label }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<!-- Sort Filter -->
|
||||
<div class="flex-grow-1">
|
||||
<select class="form-select"
|
||||
name="sort"
|
||||
hx-get="{% url 'opportunity_list' request.dealer.slug %}"
|
||||
hx-trigger="change"
|
||||
hx-target="#opportunities-grid"
|
||||
hx-select="#opportunities-grid"
|
||||
hx-swap="outerHTML"
|
||||
hx-include="#filter-container input, #filter-container select">
|
||||
<option value="newest"
|
||||
{% if request.GET.sort == 'newest' %}selected{% endif %}>
|
||||
{% trans "Newest First" %}
|
||||
</option>
|
||||
<option value="highest"
|
||||
{% if request.GET.sort == 'highest' %}selected{% endif %}>
|
||||
{% trans "Highest Value" %}
|
||||
</option>
|
||||
<option value="closing"
|
||||
{% if request.GET.sort == 'closing' %}selected{% endif %}>
|
||||
{% trans "Earliest Close Date" %}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div id="opportunities-grid" class="row g-4 px-2 px-lg-4 mt-1 mb-4">
|
||||
{% include 'crm/opportunities/partials/opportunity_grid.html' %}
|
||||
</div>
|
||||
{% if page_obj.paginator.num_pages > 1 %}
|
||||
<div class="d-flex justify-content-end mt-3">
|
||||
<div class="d-flex">{% include 'partials/pagination.html' %}</div>
|
||||
<div id="opportunities-grid" class="row g-4 px-2 px-lg-4 mt-1 mb-4">
|
||||
{% include 'crm/opportunities/partials/opportunity_grid.html' %}
|
||||
</div>
|
||||
{% if page_obj.paginator.num_pages > 1 %}
|
||||
<div class="d-flex justify-content-end mt-3">
|
||||
<div class="d-flex">{% include 'partials/pagination.html' %}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% url 'opportunity_create' request.dealer.slug as create_opportunity_url %}
|
||||
{% include "empty-illustration-page.html" with value="opportunity" url=create_opportunity_url %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% url 'opportunity_create' request.dealer.slug as create_opportunity_url %}
|
||||
{% include "empty-illustration-page.html" with value="opportunity" url=create_opportunity_url %}
|
||||
{% endif %}
|
||||
{% block customJS %}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const searchInput = document.getElementById("search-input");
|
||||
const clearButton = document.getElementById("clear-search");
|
||||
const searchForm = document.getElementById("search-form");
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const searchInput = document.getElementById("search-input");
|
||||
const clearButton = document.getElementById("clear-search");
|
||||
const searchForm = document.getElementById("search-form");
|
||||
|
||||
if (clearButton) {
|
||||
clearButton.addEventListener("click", function() {
|
||||
searchInput.value = "";
|
||||
if (clearButton) {
|
||||
clearButton.addEventListener("click", function() {
|
||||
searchInput.value = "";
|
||||
// This clears the search and triggers the htmx search
|
||||
// by submitting the form with an empty query.
|
||||
searchForm.submit();
|
||||
searchForm.submit();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@ -2,24 +2,19 @@
|
||||
{% load custom_filters %}
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
.bg-success-soft {
|
||||
background-color: rgba(25, 135, 84, 0.1) !important;
|
||||
opacity: .8;
|
||||
}
|
||||
.bg-danger-soft {
|
||||
background-color: rgba(220, 53, 69, 0.1) !important;
|
||||
opacity: .8;
|
||||
}
|
||||
.bg-success-soft {
|
||||
background-color: rgba(25, 135, 84, 0.1) !important;
|
||||
opacity: .8;
|
||||
}
|
||||
.bg-danger-soft {
|
||||
background-color: rgba(220, 53, 69, 0.1) !important;
|
||||
opacity: .8;
|
||||
}
|
||||
</style>
|
||||
{% endblock customCSS %}
|
||||
{% for opportunity in opportunities %}
|
||||
<div class="col-12 col-md-6 col-lg-4 col-xl-3">
|
||||
<div class="card h-100
|
||||
{% if opportunity.get_stage_display == 'Closed Won' %}
|
||||
bg-success-soft
|
||||
{% elif opportunity.get_stage_display == 'Closed Lost' %}
|
||||
bg-danger-soft
|
||||
{% endif %}">
|
||||
<div class="card h-100 {% if opportunity.get_stage_display == 'Closed Won' %} bg-success-soft {% elif opportunity.get_stage_display == 'Closed Lost' %} bg-danger-soft {% endif %}">
|
||||
<div class="card-body">
|
||||
<div class="avatar avatar-xl me-3 mb-3">
|
||||
{% if opportunity.car.id_car_make.logo %}
|
||||
@ -53,12 +48,7 @@
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-secondary">
|
||||
{% endif %}
|
||||
{{ opportunity.stage }}</span>
|
||||
<span class="badge badge-phoenix fs-10
|
||||
{% if opportunity.get_stage_display == 'Won' %}
|
||||
badge-phoenix-success
|
||||
{% elif opportunity.get_stage_display == 'Lost' %}
|
||||
badge-phoenix-danger
|
||||
{% endif %}">
|
||||
<span class="badge badge-phoenix fs-10 {% if opportunity.get_stage_display == 'Won' %} badge-phoenix-success {% elif opportunity.get_stage_display == 'Lost' %} badge-phoenix-danger {% endif %}">
|
||||
{{ opportunity.get_status_display }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
{% block title %}
|
||||
{% trans "Car Bulk Upload"|capfirst %}
|
||||
{% endblock %}
|
||||
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
.color-card {
|
||||
@ -74,13 +73,13 @@
|
||||
</style>
|
||||
{% endblock customCSS %}
|
||||
{% block content %}
|
||||
|
||||
<div class="container mt-4">
|
||||
<h2>
|
||||
Upload Cars CSV <i class="fa-solid fa-file-csv text-primary"></i>
|
||||
</h2>
|
||||
<div class="d-flex justify-content-end">
|
||||
<a href="{% static 'sample/cars_sample.csv' %}" class="btn btn-phoenix-primary mt-4">
|
||||
<a href="{% static 'sample/cars_sample.csv' %}"
|
||||
class="btn btn-phoenix-primary mt-4">
|
||||
<i class="fa-solid fa-file-csv me-2"></i>Download Sample CSV
|
||||
</a>
|
||||
</div>
|
||||
@ -182,7 +181,8 @@
|
||||
<div class="form-text">{{ _("CSV should include columns: vin") }}</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-phoenix-primary mb-2">Upload</button>
|
||||
<a href="{% url 'car_list' request.dealer.slug %}" class="btn btn-phoenix-secondary mb-2">Cancel</a>
|
||||
<a href="{% url 'car_list' request.dealer.slug %}"
|
||||
class="btn btn-phoenix-secondary mb-2">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n static %}
|
||||
{% load crispy_forms_filters %}
|
||||
|
||||
{% block title %}
|
||||
{% if object %}
|
||||
{% trans 'Update Customer' %}
|
||||
@ -9,52 +8,53 @@
|
||||
{% trans 'Add New Customer' %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="d-flex align-items-center justify-content-center min-vh-100 py-5 ">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
|
||||
<div class="card-header bg-gradient py-4 border-0 rounded-top-4">
|
||||
<h3 class="mb-0 fs-4 fw-bold text-center">
|
||||
{% if object %}
|
||||
{% trans "Update Customer" %}
|
||||
<i class="fa-solid fa-user-edit ms-2"></i>
|
||||
{% else %}
|
||||
{% trans "Add New Customer" %}
|
||||
<i class="fa-solid fa-user-plus ms-2"></i>
|
||||
{% endif %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<form method="post" class="form" enctype="multipart/form-data" novalidate>
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger mt-4" role="alert">
|
||||
<h4 class="alert-heading small">{% trans "Please correct the following errors:" %}</h4>
|
||||
<ul class="mb-0">
|
||||
{% for field, errors in form.errors.items %}
|
||||
<li><strong>{{ field|capfirst }}:</strong> {% for error in errors %}{{ error }}{% endfor %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<main class="d-flex align-items-center justify-content-center min-vh-100 py-5 ">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
|
||||
<div class="card-header bg-gradient py-4 border-0 rounded-top-4">
|
||||
<h3 class="mb-0 fs-4 fw-bold text-center">
|
||||
{% if object %}
|
||||
{% trans "Update Customer" %}
|
||||
<i class="fa-solid fa-user-edit ms-2"></i>
|
||||
{% else %}
|
||||
{% trans "Add New Customer" %}
|
||||
<i class="fa-solid fa-user-plus ms-2"></i>
|
||||
{% endif %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<form method="post" class="form" enctype="multipart/form-data" novalidate>
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger mt-4" role="alert">
|
||||
<h4 class="alert-heading small">{% trans "Please correct the following errors:" %}</h4>
|
||||
<ul class="mb-0">
|
||||
{% for field, errors in form.errors.items %}
|
||||
<li>
|
||||
<strong>{{ field|capfirst }}:</strong>
|
||||
{% for error in errors %}{{ error }}{% endfor %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
<hr class="my-4">
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
|
||||
<button class="btn btn-phoenix-primary btn-lg me-md-2" type="submit">
|
||||
<i class="fa-solid fa-floppy-disk me-1"></i>
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
<a href="{% url 'customer_list' request.dealer.slug %}"
|
||||
class="btn btn-phoenix-secondary btn-lg">
|
||||
<i class="fa-solid fa-ban me-1"></i>
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<hr class="my-4">
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
|
||||
<button class="btn btn-phoenix-primary btn-lg me-md-2" type="submit">
|
||||
<i class="fa-solid fa-floppy-disk me-1"></i>
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
<a href="{% url 'customer_list' request.dealer.slug %}" class="btn btn-phoenix-secondary btn-lg">
|
||||
<i class="fa-solid fa-ban me-1"></i>
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@ -6,176 +6,175 @@
|
||||
{% endblock title %}
|
||||
{% block vendors %}<a class="nav-link active">{{ _("Customers") |capfirst }}</a>{% endblock %}
|
||||
{% block content %}
|
||||
{% if customers or request.GET.q %}
|
||||
<div class="row g-3 mt-4">
|
||||
<h2 class="mb-2">
|
||||
{{ _("Customers") |capfirst }}
|
||||
<li class="fas fa-people-group text-primary ms-2"></li>
|
||||
</h2>
|
||||
<div class="row g-3 justify-content-between mb-4">
|
||||
<div class="col-auto">
|
||||
<div class="d-md-flex justify-content-between">
|
||||
{% if perms.inventory.add_customer %}
|
||||
<div>
|
||||
<a href="{% url 'customer_create' request.dealer.slug %}"
|
||||
class="btn btn-sm btn-phoenix-primary me-4"><span class="fas fa-plus me-2"></span>{{ _("Add Customer") }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if customers or request.GET.q %}
|
||||
<div class="row g-3 mt-4">
|
||||
<h2 class="mb-2">
|
||||
{{ _("Customers") |capfirst }}
|
||||
<li class="fas fa-people-group text-primary ms-2"></li>
|
||||
</h2>
|
||||
<div class="row g-3 justify-content-between mb-4">
|
||||
<div class="col-auto">
|
||||
<div class="d-md-flex justify-content-between">
|
||||
{% if perms.inventory.add_customer %}
|
||||
<div>
|
||||
<a href="{% url 'customer_create' request.dealer.slug %}"
|
||||
class="btn btn-sm btn-phoenix-primary me-4"><span class="fas fa-plus me-2"></span>{{ _("Add Customer") }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="d-flex">{% include 'partials/search_box.html' %}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="d-flex">{% include 'partials/search_box.html' %}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if page_obj.object_list or request.GET.q%}
|
||||
<div class="table-responsive scrollbar transition">
|
||||
<table class="table align-items-center table-flush table-hover">
|
||||
<thead>
|
||||
<tr class="bg-body-highlight">
|
||||
<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="home"></span>
|
||||
</div>
|
||||
<span>{{ _("Address") |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>{{ _("Active") |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") }} <span class="text-warning-dark" data-feather="clock"></span>
|
||||
</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 -->
|
||||
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
|
||||
<td></td>
|
||||
{% if perms.inventory.view_customer %}
|
||||
<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' request.dealer.slug customer.slug %}">{{ customer.full_name }}</a>
|
||||
<div class="d-flex align-items-center"></div>
|
||||
</div>
|
||||
{% if page_obj.object_list or request.GET.q %}
|
||||
<div class="table-responsive scrollbar transition">
|
||||
<table class="table align-items-center table-flush table-hover">
|
||||
<thead>
|
||||
<tr class="bg-body-highlight">
|
||||
<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>
|
||||
</td>
|
||||
{% endif %}
|
||||
<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 }}">{{ 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">
|
||||
{% if customer.active %}
|
||||
<span class="badge badge-phoenix badge-phoenix-success"><i class="fas fa-check"></i> {{ customer.active }}</span>
|
||||
{% else %}
|
||||
<span class="badge badge-phoenix badge-phoenix-danger"><i class="fas fa-times"></i> {{ customer.active }}</span>
|
||||
{% endif %}
|
||||
</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">
|
||||
{% if perms.inventory.change_customer %}
|
||||
<a href="{% url 'customer_update' request.dealer.slug customer.slug %}"
|
||||
class="btn btn-sm btn-phoenix-primary me-2"
|
||||
data-url="{% url 'customer_update' request.dealer.slug customer.slug %}">
|
||||
<i class="fas fa-pen"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.inventory.delete_customer %}
|
||||
<button class="btn btn-phoenix-danger btn-sm delete-btn"
|
||||
data-url="{% url 'customer_delete' request.dealer.slug customer.slug %}"
|
||||
data-message="{{ _("Are you sure you want to delete this customer") }}"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#deleteModal">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
<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="home"></span>
|
||||
</div>
|
||||
<span>{{ _("Address") |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>{{ _("Active") |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") }} <span class="text-warning-dark" data-feather="clock"></span>
|
||||
</th>
|
||||
<th class="sort text-end align-middle pe-0 ps-4" scope="col"></th>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">{% trans "No Customers found." %}</td>
|
||||
</tr>
|
||||
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</thead>
|
||||
<tbody class="list" id="lead-tables-body">
|
||||
{% for customer in customers %}
|
||||
<!-- Delete Modal -->
|
||||
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
|
||||
<td></td>
|
||||
{% if perms.inventory.view_customer %}
|
||||
<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' request.dealer.slug customer.slug %}">{{ customer.full_name }}</a>
|
||||
<div class="d-flex align-items-center"></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
{% endif %}
|
||||
<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 }}">{{ 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">
|
||||
{% if customer.active %}
|
||||
<span class="badge badge-phoenix badge-phoenix-success"><i class="fas fa-check"></i> {{ customer.active }}</span>
|
||||
{% else %}
|
||||
<span class="badge badge-phoenix badge-phoenix-danger"><i class="fas fa-times"></i> {{ customer.active }}</span>
|
||||
{% endif %}
|
||||
</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">
|
||||
{% if perms.inventory.change_customer %}
|
||||
<a href="{% url 'customer_update' request.dealer.slug customer.slug %}"
|
||||
class="btn btn-sm btn-phoenix-primary me-2"
|
||||
data-url="{% url 'customer_update' request.dealer.slug customer.slug %}">
|
||||
<i class="fas fa-pen"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.inventory.delete_customer %}
|
||||
<button class="btn btn-phoenix-danger btn-sm delete-btn"
|
||||
data-url="{% url 'customer_delete' request.dealer.slug customer.slug %}"
|
||||
data-message="{{ _("Are you sure you want to delete this customer") }}"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#deleteModal">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">{% trans "No Customers found." %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endif %}
|
||||
</table>
|
||||
{% if page_obj.paginator.num_pages > 1 %}
|
||||
<div class="d-flex justify-content-end mt-3">
|
||||
<div class="d-flex">{% include 'partials/pagination.html' %}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</table>
|
||||
{% if page_obj.paginator.num_pages > 1 %}
|
||||
<div class="d-flex justify-content-end mt-3">
|
||||
<div class="d-flex">{% include 'partials/pagination.html' %}</div>
|
||||
</div>
|
||||
{% include 'modal/delete_modal.html' %}
|
||||
{% else %}
|
||||
{% url "customer_create" request.dealer.slug as create_customer_url %}
|
||||
{% include "empty-illustration-page.html" with value="customer" url=create_customer_url %}
|
||||
{% endif %}
|
||||
{% include 'modal/delete_modal.html' %}
|
||||
{% else %}
|
||||
{% url "customer_create" request.dealer.slug as create_customer_url %}
|
||||
{% include "empty-illustration-page.html" with value="customer" url=create_customer_url %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@ -6,11 +6,12 @@
|
||||
{% block content %}
|
||||
{% include 'modal/delete_modal.html' %}
|
||||
{% include 'components/note_modal.html' with content_type="customer" slug=customer.slug %}
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="row align-items-center justify-content-between g-3 mb-4">
|
||||
<div class="col-auto">
|
||||
<h3 class="mb-0">{% trans 'Customer details' %}<i class="fas fa-user ms-2 text-primary"></i></h3>
|
||||
<h3 class="mb-0">
|
||||
{% trans 'Customer details' %}<i class="fas fa-user ms-2 text-primary"></i>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="col-auto d-flex gap-2">
|
||||
{% if perms.inventory.change_customer %}
|
||||
@ -30,7 +31,6 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="card h-100 shadow-sm">
|
||||
@ -39,7 +39,9 @@
|
||||
<div class="col-12 col-sm-auto mb-sm-2">
|
||||
<div class="avatar avatar-5xl">
|
||||
{% if customer.image %}
|
||||
<img class="rounded-circle border border-2 border-primary" src="{{ customer.image.url }}" alt="{{ customer.full_name }}"/>
|
||||
<img class="rounded-circle border border-2 border-primary"
|
||||
src="{{ customer.image.url }}"
|
||||
alt="{{ customer.full_name }}" />
|
||||
{% else %}
|
||||
<div class="avatar-text rounded-circle bg-secondary text-white border border-2 border-primary">
|
||||
<span class="fs-4">{{ customer.full_name|first|default:"?" }}</span>
|
||||
@ -65,7 +67,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-8">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
@ -79,18 +80,19 @@
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<strong class="text-body-secondary d-block">{% trans 'Email' %}:</strong>
|
||||
<a href="mailto:{{ customer.email|default:"" }}" class="text-decoration-none">{{ customer.email|default:_("N/A") }}</a>
|
||||
<a href="mailto:{{ customer.email|default:"" }}"
|
||||
class="text-decoration-none">{{ customer.email|default:_("N/A") }}</a>
|
||||
</li>
|
||||
<li class="mb-0">
|
||||
<strong class="text-body-secondary d-block">{% trans 'Phone Number' %}:</strong>
|
||||
<a href="tel:{{ customer.phone_number|default:"" }}" class="text-decoration-none">{{ customer.phone_number|default:_("N/A") }}</a>
|
||||
<a href="tel:{{ customer.phone_number|default:"" }}"
|
||||
class="text-decoration-none">{{ customer.phone_number|default:_("N/A") }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
@ -116,16 +118,16 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for note in notes %}
|
||||
<tr class="align-middle">
|
||||
<td class="text-body-secondary">{{ note.note|default_if_none:""|linebreaksbr }}</td>
|
||||
<td class="text-body-secondary text-nowrap">{{ note.created|date:"d M Y" }}</td>
|
||||
</tr>
|
||||
<tr class="align-middle">
|
||||
<td class="text-body-secondary">{{ note.note|default_if_none:""|linebreaksbr }}</td>
|
||||
<td class="text-body-secondary text-nowrap">{{ note.created|date:"d M Y" }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-body-secondary">
|
||||
<i class="fas fa-info-circle me-2"></i>{% trans 'No notes found for this customer.' %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-body-secondary">
|
||||
<i class="fas fa-info-circle me-2"></i>{% trans 'No notes found for this customer.' %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
@ -134,7 +136,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-3">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
@ -142,48 +143,82 @@
|
||||
<h5 class="card-title mb-3">{% trans 'Sales History' %}</h5>
|
||||
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
||||
<li class="nav-item me-6" role="presentation">
|
||||
<button class="nav-link active" id="leads-tab" data-bs-toggle="tab" data-bs-target="#leads-tab-pane" type="button" role="tab" aria-controls="leads-tab-pane" aria-selected="true">{% trans 'Leads' %}</button>
|
||||
<button class="nav-link active"
|
||||
id="leads-tab"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#leads-tab-pane"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="leads-tab-pane"
|
||||
aria-selected="true">{% trans 'Leads' %}</button>
|
||||
</li>
|
||||
<li class="nav-item me-6" role="presentation">
|
||||
<button class="nav-link" id="opportunities-tab" data-bs-toggle="tab" data-bs-target="#opportunities-tab-pane" type="button" role="tab" aria-controls="opportunities-tab-pane" aria-selected="false">{% trans 'Opportunities' %}</button>
|
||||
<button class="nav-link"
|
||||
id="opportunities-tab"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#opportunities-tab-pane"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="opportunities-tab-pane"
|
||||
aria-selected="false">{% trans 'Opportunities' %}</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="estimates-tab" data-bs-toggle="tab" data-bs-target="#estimates-tab-pane" type="button" role="tab" aria-controls="estimates-tab-pane" aria-selected="false">{% trans 'Estimates' %}</button>
|
||||
<button class="nav-link"
|
||||
id="estimates-tab"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#estimates-tab-pane"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="estimates-tab-pane"
|
||||
aria-selected="false">{% trans 'Estimates' %}</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content pt-3" id="myTabContent">
|
||||
<div class="tab-pane fade show active" id="leads-tab-pane" role="tabpanel" aria-labelledby="leads-tab" tabindex="0">
|
||||
<div class="tab-pane fade show active"
|
||||
id="leads-tab-pane"
|
||||
role="tabpanel"
|
||||
aria-labelledby="leads-tab"
|
||||
tabindex="0">
|
||||
{% for lead in leads %}
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="fas fa-handshake me-2 text-primary"></i>
|
||||
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}" class="fw-bold">{{ lead }}</a>
|
||||
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}"
|
||||
class="fw-bold">{{ lead }}</a>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="text-body-secondary">{% trans 'No leads found for this customer.' %}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="opportunities-tab-pane" role="tabpanel" aria-labelledby="opportunities-tab" tabindex="0">
|
||||
<div class="tab-pane fade"
|
||||
id="opportunities-tab-pane"
|
||||
role="tabpanel"
|
||||
aria-labelledby="opportunities-tab"
|
||||
tabindex="0">
|
||||
{% for lead in leads %}
|
||||
{% if lead.opportunity %}
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="fas fa-chart-line me-2 text-success"></i>
|
||||
<a href="{% url 'opportunity_detail' request.dealer.slug lead.opportunity.slug %}" class="fw-bold">{{ lead.opportunity }}</a>
|
||||
<a href="{% url 'opportunity_detail' request.dealer.slug lead.opportunity.slug %}"
|
||||
class="fw-bold">{{ lead.opportunity }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% empty %}
|
||||
<p class="text-body-secondary">{% trans 'No opportunities found for this customer.' %}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="estimates-tab-pane" role="tabpanel" aria-labelledby="estimates-tab" tabindex="0">
|
||||
<div class="tab-pane fade"
|
||||
id="estimates-tab-pane"
|
||||
role="tabpanel"
|
||||
aria-labelledby="estimates-tab"
|
||||
tabindex="0">
|
||||
{% for estimate in estimates %}
|
||||
<div class="card mb-3 shadow-sm">
|
||||
<div class="card-body p-3">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-file-invoice me-2 text-info"></i>
|
||||
<a href="{% url 'estimate_detail' request.dealer.slug estimate.pk %}" class="text-decoration-none">{{ estimate }}</a>
|
||||
<a href="{% url 'estimate_detail' request.dealer.slug estimate.pk %}"
|
||||
class="text-decoration-none">{{ estimate }}</a>
|
||||
</h6>
|
||||
<span class="badge bg-success">{{ estimate.created|date:"d M Y" }}</span>
|
||||
</div>
|
||||
@ -197,16 +232,22 @@
|
||||
{% for invoice in estimate.invoicemodel_set.all %}
|
||||
<li class="mb-2">
|
||||
<i class="fas fa-receipt me-2 {% if invoice.is_paid %}text-success{% else %}text-warning{% endif %}"></i>
|
||||
<a href="{% url 'invoice_detail' request.dealer.slug request.entity.slug invoice.pk %}" class="text-decoration-none">{{ invoice }}</a>
|
||||
<span class="badge rounded-pill {% if invoice.is_paid %}bg-success{% else %}bg-warning{% endif %} ms-2">{% if invoice.is_paid %}{% trans "Paid" %}{% else %}{% trans "Unpaid" %}{% endif %}</span>
|
||||
<a href="{% url 'invoice_detail' request.dealer.slug request.entity.slug invoice.pk %}"
|
||||
class="text-decoration-none">{{ invoice }}</a>
|
||||
<span class="badge rounded-pill {% if invoice.is_paid %}bg-success{% else %}bg-warning{% endif %} ms-2">
|
||||
{% if invoice.is_paid %}
|
||||
{% trans "Paid" %}
|
||||
{% else %}
|
||||
{% trans "Unpaid" %}
|
||||
{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% for item in estimate.itemtransactionmodel_set.all %}
|
||||
<li>
|
||||
<i class="fas fa-car me-2 text-primary"></i>
|
||||
<a href="{% url 'car_detail' request.dealer.slug item.item_model.car.slug %}">{{ item.item_model.car.vin}} | {{item.item_model.car.id_car_make.name}} | {{item.item_model.car.id_car_model.name}}</a>
|
||||
<a href="{% url 'car_detail' request.dealer.slug item.item_model.car.slug %}">{{ item.item_model.car.vin }} | {{ item.item_model.car.id_car_make.name }} | {{ item.item_model.car.id_car_model.name }}</a>
|
||||
</li>
|
||||
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
@ -221,37 +262,32 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include "components/note_modal.html" with content_type="customer" slug=customer.slug %}
|
||||
|
||||
|
||||
|
||||
{% include "components/note_modal.html" with content_type="customer" slug=customer.slug %}
|
||||
<!---->
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const noteModal = document.getElementById("noteModal");
|
||||
const modalTitle = document.getElementById("noteModalLabel");
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const noteModal = document.getElementById("noteModal");
|
||||
const modalTitle = document.getElementById("noteModalLabel");
|
||||
|
||||
const modalBody = noteModal.querySelector(".modal-body");
|
||||
const modalBody = noteModal.querySelector(".modal-body");
|
||||
|
||||
noteModal.addEventListener("", function (event) {
|
||||
const button = event.relatedTarget;
|
||||
const url = button.getAttribute("data-url");
|
||||
const title = button.getAttribute("data-note-title");
|
||||
noteModal.addEventListener("", function (event) {
|
||||
const button = event.relatedTarget;
|
||||
const url = button.getAttribute("data-url");
|
||||
const title = button.getAttribute("data-note-title");
|
||||
|
||||
fetch(url)
|
||||
.then((response) => response.text())
|
||||
.then((html) => {
|
||||
modalBody.innerHTML = html;
|
||||
modalTitle.innerHTML = title;
|
||||
})
|
||||
.catch((error) => {
|
||||
modalBody.innerHTML = '<p class="text-danger">{% trans 'Error loading form. Please try again later' %}.</p>';
|
||||
console.error("Error loading form:", error);
|
||||
});
|
||||
});
|
||||
});
|
||||
fetch(url)
|
||||
.then((response) => response.text())
|
||||
.then((html) => {
|
||||
modalBody.innerHTML = html;
|
||||
modalTitle.innerHTML = title;
|
||||
})
|
||||
.catch((error) => {
|
||||
modalBody.innerHTML = '<p class="text-danger">{% trans 'Error loading form. Please try again later' %}.</p>';
|
||||
console.error("Error loading form:", error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@ -1,124 +1,126 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load tenhal_tag %}
|
||||
{% block content %}
|
||||
<div class="main-content flex-grow-1 container-fluid mt-4 mb-3">
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 pb-3 border-bottom">
|
||||
<h2 class="h3 fw-bold mb-3 mb-md-0">
|
||||
{% trans "Aging Inventory" %}
|
||||
<i class="fas fa-box-open text-danger ms-2"></i>
|
||||
</h2>
|
||||
<h4 class="text-muted mb-3 ">{% trans "Aging Inventory Total" %} :: <span class=" text-danger">{{total_aging_inventory_value|default:0.00}}<span class="icon-saudi_riyal"></span></span></h4>
|
||||
<p class="text-muted mb-0">{% trans "Cars in inventory for more than 60 days." %}</p>
|
||||
|
||||
</div>
|
||||
|
||||
<form method="GET" class="d-flex flex-wrap align-items-center mb-4 g-3">
|
||||
<div class="col-sm-6 col-md-2 me-2">
|
||||
<label for="make-filter" class="form-label mb-0 small text-uppercase fw-bold">{% trans "Make:" %}</label>
|
||||
<select class="form-select" name="make" id="make-filter">
|
||||
<option value="">{% trans "All" %}</option>
|
||||
{% for make in all_makes %}
|
||||
<option value="{{ make }}" {% if make == selected_make %}selected{% endif %}>{{ make }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6 col-md-2 me-2">
|
||||
<label for="model-filter" class="form-label mb-0 small text-uppercase fw-bold">{% trans "Model:" %}</label>
|
||||
<select class="form-select" name="model" id="model-filter">
|
||||
<option value="">{% trans "All" %}</option>
|
||||
{% for model in all_models %}
|
||||
<option value="{{ model }}" {% if model == selected_model %}selected{% endif %}>{{ model }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6 col-md-2 me-2">
|
||||
<label for="series-filter" class="form-label mb-0 small text-uppercase fw-bold">{% trans "Series:" %}</label>
|
||||
<select class="form-select" name="series" id="series-filter">
|
||||
<option value="">{% trans "All" %}</option>
|
||||
{% for series in all_series %}
|
||||
<option value="{{ series }}" {% if series == selected_series %}selected{% endif %}>{{ series }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6 col-md-2 me-2">
|
||||
<label for="year-filter" class="form-label mb-0 small text-uppercase fw-bold">{% trans "Year:" %}</label>
|
||||
<select class="form-select" name="year" id="year-filter">
|
||||
<option value="">{% trans "All" %}</option>
|
||||
{% for year in all_years %}
|
||||
<option value="{{ year }}" {% if year|stringformat:"s" == selected_year %}selected{% endif %}>{{ year }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6 col-md-2 me-2">
|
||||
<label for="stock-type-filter" class="form-label mb-0 small text-uppercase fw-bold">{% trans "Stock Type:" %}</label>
|
||||
<select class="form-select" name="stock_type" id="stock-type-filter">
|
||||
<option value="">{% trans "All" %}</option>
|
||||
{% for stock_type in all_stock_types %}
|
||||
<option value="{{ stock_type }}" {% if stock_type == selected_stock_type %}selected{% endif %}>{{ stock_type|title }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<button type="submit" class="btn btn-primary mt-4">{% trans "Filter" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if is_paginated %}
|
||||
<div class="d-flex justify-content-between mb-4">
|
||||
<span class="text-muted">{% trans "Page" %} {{ page_obj.number }} {% trans "of" %} {{ page_obj.paginator.num_pages }}</span>
|
||||
<span class="text-muted">{% trans "Total Aging Cars:" %} {{ page_obj.paginator.count }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if cars %}
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
|
||||
{% for car in cars %}
|
||||
<div class="col">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h5 class="card-title text-danger fw-bold">
|
||||
<img src="{{car.logo}}" width="40" height="40" class=""> {{ car.id_car_make.name }} {{ car.id_car_model.name }} {{ car.id_car_serie.name }} {{ car.year}}
|
||||
</h5>
|
||||
<p class="card-text text-muted mb-2">
|
||||
<strong>{% trans "VIN:" %}</strong> {{ car.vin }}
|
||||
</p>
|
||||
<p class="card-text mb-2">
|
||||
<strong>{% trans "Age:" %}</strong>
|
||||
<span class="badge bg-danger">{{ car.age_in_days }} {% trans "days" %}</span>
|
||||
</p>
|
||||
<p class="card-text mb-2">
|
||||
<strong>{% trans "Acquisition Date:" %}</strong> {{ car.receiving_date|date:"F j, Y" }}
|
||||
</p>
|
||||
<div class="mt-auto pt-3 border-top">
|
||||
<a href="{% url 'car_detail' request.dealer.slug car.slug %}" class="btn btn-outline-primary btn-sm w-100">
|
||||
{% trans "View Details" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-success d-flex align-items-center" role="alert">
|
||||
<i class="fas fa-check-circle me-2"></i>
|
||||
<div>
|
||||
{% trans "Excellent! There are no cars in the aging inventory at the moment." %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="d-flex justify-content-end mt-3">
|
||||
<div class="d-flex">
|
||||
{% if is_paginated %}
|
||||
{% include 'partials/pagination.html' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load tenhal_tag %}
|
||||
{% block content %}
|
||||
<div class="main-content flex-grow-1 container-fluid mt-4 mb-3">
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 pb-3 border-bottom">
|
||||
<h2 class="h3 fw-bold mb-3 mb-md-0">
|
||||
{% trans "Aging Inventory" %}
|
||||
<i class="fas fa-box-open text-danger ms-2"></i>
|
||||
</h2>
|
||||
<h4 class="text-muted mb-3 ">
|
||||
{% trans "Aging Inventory Total" %} :: <span class=" text-danger">{{ total_aging_inventory_value|default:0.00 }}<span class="icon-saudi_riyal"></span></span>
|
||||
</h4>
|
||||
<p class="text-muted mb-0">{% trans "Cars in inventory for more than 60 days." %}</p>
|
||||
</div>
|
||||
<form method="GET" class="d-flex flex-wrap align-items-center mb-4 g-3">
|
||||
<div class="col-sm-6 col-md-2 me-2">
|
||||
<label for="make-filter"
|
||||
class="form-label mb-0 small text-uppercase fw-bold">{% trans "Make:" %}</label>
|
||||
<select class="form-select" name="make" id="make-filter">
|
||||
<option value="">{% trans "All" %}</option>
|
||||
{% for make in all_makes %}
|
||||
<option value="{{ make }}" {% if make == selected_make %}selected{% endif %}>{{ make }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-2 me-2">
|
||||
<label for="model-filter"
|
||||
class="form-label mb-0 small text-uppercase fw-bold">{% trans "Model:" %}</label>
|
||||
<select class="form-select" name="model" id="model-filter">
|
||||
<option value="">{% trans "All" %}</option>
|
||||
{% for model in all_models %}
|
||||
<option value="{{ model }}"
|
||||
{% if model == selected_model %}selected{% endif %}>{{ model }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-2 me-2">
|
||||
<label for="series-filter"
|
||||
class="form-label mb-0 small text-uppercase fw-bold">{% trans "Series:" %}</label>
|
||||
<select class="form-select" name="series" id="series-filter">
|
||||
<option value="">{% trans "All" %}</option>
|
||||
{% for series in all_series %}
|
||||
<option value="{{ series }}"
|
||||
{% if series == selected_series %}selected{% endif %}>{{ series }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-2 me-2">
|
||||
<label for="year-filter"
|
||||
class="form-label mb-0 small text-uppercase fw-bold">{% trans "Year:" %}</label>
|
||||
<select class="form-select" name="year" id="year-filter">
|
||||
<option value="">{% trans "All" %}</option>
|
||||
{% for year in all_years %}
|
||||
<option value="{{ year }}"
|
||||
{% if year|stringformat:"s" == selected_year %}selected{% endif %}>{{ year }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-2 me-2">
|
||||
<label for="stock-type-filter"
|
||||
class="form-label mb-0 small text-uppercase fw-bold">{% trans "Stock Type:" %}</label>
|
||||
<select class="form-select" name="stock_type" id="stock-type-filter">
|
||||
<option value="">{% trans "All" %}</option>
|
||||
{% for stock_type in all_stock_types %}
|
||||
<option value="{{ stock_type }}"
|
||||
{% if stock_type == selected_stock_type %}selected{% endif %}>
|
||||
{{ stock_type|title }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="submit" class="btn btn-primary mt-4">{% trans "Filter" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% if is_paginated %}
|
||||
<div class="d-flex justify-content-between mb-4">
|
||||
<span class="text-muted">{% trans "Page" %} {{ page_obj.number }} {% trans "of" %} {{ page_obj.paginator.num_pages }}</span>
|
||||
<span class="text-muted">{% trans "Total Aging Cars:" %} {{ page_obj.paginator.count }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if cars %}
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
|
||||
{% for car in cars %}
|
||||
<div class="col">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h5 class="card-title text-danger fw-bold">
|
||||
<img src="{{ car.logo }}" width="40" height="40" class="">
|
||||
{{ car.id_car_make.name }} {{ car.id_car_model.name }} {{ car.id_car_serie.name }} {{ car.year }}
|
||||
</h5>
|
||||
<p class="card-text text-muted mb-2">
|
||||
<strong>{% trans "VIN:" %}</strong> {{ car.vin }}
|
||||
</p>
|
||||
<p class="card-text mb-2">
|
||||
<strong>{% trans "Age:" %}</strong>
|
||||
<span class="badge bg-danger">{{ car.age_in_days }} {% trans "days" %}</span>
|
||||
</p>
|
||||
<p class="card-text mb-2">
|
||||
<strong>{% trans "Acquisition Date:" %}</strong> {{ car.receiving_date|date:"F j, Y" }}
|
||||
</p>
|
||||
<div class="mt-auto pt-3 border-top">
|
||||
<a href="{% url 'car_detail' request.dealer.slug car.slug %}"
|
||||
class="btn btn-outline-primary btn-sm w-100">{% trans "View Details" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-success d-flex align-items-center" role="alert">
|
||||
<i class="fas fa-check-circle me-2"></i>
|
||||
<div>{% trans "Excellent! There are no cars in the aging inventory at the moment." %}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="d-flex justify-content-end mt-3">
|
||||
<div class="d-flex">
|
||||
{% if is_paginated %}
|
||||
{% include 'partials/pagination.html' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
@ -1,109 +1,112 @@
|
||||
{% load i18n %}
|
||||
{% if request.is_dealer or request.is_manager or request.is_accountant %}
|
||||
<h3 class="fw-bold mb-3">
|
||||
{% blocktrans with start_date=start_date|date:"F j, Y" end_date=end_date|date:"F j, Y" %}
|
||||
Monthly Performance Trends ({{ start_date }} - {{ end_date }})
|
||||
{% endblocktrans %}
|
||||
</h3>
|
||||
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-12">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">{% trans "Monthly Revenue & Profit" %}</h5>
|
||||
</div>
|
||||
<div class="card-body" style="height: 400px;">
|
||||
<canvas id="revenueProfitChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">{% trans "Monthly Cars Sold" %}</h5>
|
||||
</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;">
|
||||
<canvas id="CarsSoldByMonthChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-lg-6 col-12">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">{% trans "Sales by Make" %}</h5>
|
||||
</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;">
|
||||
<canvas id="salesByBrandChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6 col-12">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0 d-flex justify-content-between align-items-center">
|
||||
<h5 class="fw-bold mb-0 text-dark">{% trans "Models Sold" %}</h5>
|
||||
<form method="GET" class="d-flex align-items-center">
|
||||
<div class="form-group d-flex align-items-center me-2">
|
||||
<label for="carMakeSelectSales" class="form-label mb-0 me-2">{% trans "Select Make:" %}</label>
|
||||
<select id="carMakeSelectSales" class="form-select" name="make_sold">
|
||||
<option value="">{% trans "All Makes" %}</option>
|
||||
{% for make_sold in all_makes_sold %}
|
||||
<option value="{{ make_sold }}" {% if make_sold == selected_make_sales %}selected{% endif %}>{{ make_sold }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">{% trans "Go" %}</button>
|
||||
<input type="hidden" name="start_date" value="{{ start_date|date:'Y-m-d' }}">
|
||||
<input type="hidden" name="end_date" value="{{ end_date|date:'Y-m-d' }}">
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-body" style="height: 400px;">
|
||||
<canvas id="salesChartByModel"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if request.is_dealer or request.is_manager or request.is_inventory %}
|
||||
<h3 class="fw-bold mb-3">{% trans "Inventory Trends" %}</h3>
|
||||
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-lg-6 col-12">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">{% trans "Inventory by Make" %}</h5>
|
||||
</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;">
|
||||
<canvas id="inventoryByMakeChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6 col-12">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0 d-flex justify-content-between align-items-center">
|
||||
<h5 class="fw-bold mb-0 text-dark">{% trans "Models in Inventory" %}</h5>
|
||||
<form method="GET" class="d-flex align-items-center">
|
||||
<div class="form-group d-flex align-items-center me-2">
|
||||
<label for="carMakeSelectInventory" class="form-label mb-0 me-2">{% trans "Select Make:" %}</label>
|
||||
<select id="carMakeSelectInventory" class="form-select" name="make_inventory">
|
||||
<option value="">{% trans "All Makes" %}</option>
|
||||
{% for make_inv in all_makes_inventory %}
|
||||
<option value="{{ make_inv }}" {% if make_inv == selected_make_inventory %}selected{% endif %}>{{ make_inv }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">{% trans "Go" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-body" style="height: 400px;">
|
||||
<canvas id="inventoryByModelChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% load i18n %}
|
||||
{% if request.is_dealer or request.is_manager or request.is_accountant %}
|
||||
<h3 class="fw-bold mb-3">
|
||||
{% blocktrans with start_date=start_date|date:"F j, Y" end_date=end_date|date:"F j, Y" %}
|
||||
Monthly Performance Trends ({{ start_date }} - {{ end_date }})
|
||||
{% endblocktrans %}
|
||||
</h3>
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-12">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">{% trans "Monthly Revenue & Profit" %}</h5>
|
||||
</div>
|
||||
<div class="card-body" style="height: 400px;">
|
||||
<canvas id="revenueProfitChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">{% trans "Monthly Cars Sold" %}</h5>
|
||||
</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center"
|
||||
style="height: 400px">
|
||||
<canvas id="CarsSoldByMonthChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-lg-6 col-12">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">{% trans "Sales by Make" %}</h5>
|
||||
</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center"
|
||||
style="height: 400px">
|
||||
<canvas id="salesByBrandChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 col-12">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0 d-flex justify-content-between align-items-center">
|
||||
<h5 class="fw-bold mb-0 text-dark">{% trans "Models Sold" %}</h5>
|
||||
<form method="GET" class="d-flex align-items-center">
|
||||
<div class="form-group d-flex align-items-center me-2">
|
||||
<label for="carMakeSelectSales" class="form-label mb-0 me-2">{% trans "Select Make:" %}</label>
|
||||
<select id="carMakeSelectSales" class="form-select" name="make_sold">
|
||||
<option value="">{% trans "All Makes" %}</option>
|
||||
{% for make_sold in all_makes_sold %}
|
||||
<option value="{{ make_sold }}"
|
||||
{% if make_sold == selected_make_sales %}selected{% endif %}>
|
||||
{{ make_sold }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">{% trans "Go" %}</button>
|
||||
<input type="hidden" name="start_date" value="{{ start_date|date:'Y-m-d' }}">
|
||||
<input type="hidden" name="end_date" value="{{ end_date|date:'Y-m-d' }}">
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-body" style="height: 400px;">
|
||||
<canvas id="salesChartByModel"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if request.is_dealer or request.is_manager or request.is_inventory %}
|
||||
<h3 class="fw-bold mb-3">{% trans "Inventory Trends" %}</h3>
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-lg-6 col-12">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">{% trans "Inventory by Make" %}</h5>
|
||||
</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center"
|
||||
style="height: 400px">
|
||||
<canvas id="inventoryByMakeChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 col-12">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0 d-flex justify-content-between align-items-center">
|
||||
<h5 class="fw-bold mb-0 text-dark">{% trans "Models in Inventory" %}</h5>
|
||||
<form method="GET" class="d-flex align-items-center">
|
||||
<div class="form-group d-flex align-items-center me-2">
|
||||
<label for="carMakeSelectInventory" class="form-label mb-0 me-2">{% trans "Select Make:" %}</label>
|
||||
<select id="carMakeSelectInventory" class="form-select" name="make_inventory">
|
||||
<option value="">{% trans "All Makes" %}</option>
|
||||
{% for make_inv in all_makes_inventory %}
|
||||
<option value="{{ make_inv }}"
|
||||
{% if make_inv == selected_make_inventory %}selected{% endif %}>
|
||||
{{ make_inv }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">{% trans "Go" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-body" style="height: 400px;">
|
||||
<canvas id="inventoryByModelChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@ -1,266 +1,312 @@
|
||||
{% load i18n %}
|
||||
{% if request.is_dealer or request.is_manager or request.is_accountant %}
|
||||
<h3 class="fw-bold mb-3">
|
||||
{% blocktrans with start_date=start_date|date:"F j, Y" end_date=end_date|date:"F j, Y" %}
|
||||
Sales KPIs ({{ start_date }} - {{ end_date }})
|
||||
{% endblocktrans %}
|
||||
</h3>
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Cars Sold" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_cars_sold }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Revenue from Cars" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_revenue_from_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Net Profit from Cars" %}</p>
|
||||
<h4 class="fw-bolder text-success mb-3">{{ net_profit_from_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Discount on Cars" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_discount_on_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Cost of Cars Sold" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_cost_of_cars_sold|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total VAT from Cars" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_vat_collected_from_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="fw-bold my-4">{% trans "Sales of New Cars" %}</h4>
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Sold" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_new_cars_sold }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Revenue" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_revenue_from_new_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Net Profit" %}</p>
|
||||
<h4 class="fw-bolder text-success mb-3">{{ net_profit_from_new_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars VAT" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_vat_collected_from_new_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Cost" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_cost_of_new_cars_sold|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="fw-bold my-4">{% trans "Sales of Used Cars" %}</h4>
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Sold" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_used_cars_sold }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Revenue" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_revenue_from_used_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Net Profit" %}</p>
|
||||
<h4 class="fw-bolder text-success mb-3">{{ net_profit_from_used_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars VAT" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_vat_collected_from_used_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Cost" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_cost_of_used_cars_sold|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if request.is_dealer or request.is_manager or request.is_inventory %}
|
||||
<h3 class="fw-bold mb-3">{% trans "Inventory KPIs" %}</h3>
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Cars in Inventory" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_cars_in_inventory }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Inventory Value" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_inventory_value|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars in Inventory" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_new_cars_in_inventory }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars in Inventory" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_used_cars_in_inventory }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Inventory Value" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ new_car_value|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Inventory Value" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ used_car_value|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-danger fw-bold small mb-1"><a class="text-danger" href="{% url 'aging_inventory_list' request.dealer.slug %}">{% trans "Aging Inventory (> 60 days)" %}</a></p>
|
||||
<h4 class="fw-bolder text-danger mb-3"><a class="text-danger" href="{% url 'aging_inventory_list' request.dealer.slug %}">{{ aging_inventory_count }}</a></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if request.is_dealer or request.is_manager or request.is_accountant %}
|
||||
<h3 class="fw-bold mb-3">
|
||||
{% blocktrans with start_date=start_date|date:"F j, Y" end_date=end_date|date:"F j, Y" %}
|
||||
Financial Health KPIs ({{ start_date }} - {{ end_date }})
|
||||
{% endblocktrans %}
|
||||
</h3>
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Revenue from Services" %}</p>
|
||||
<h4 class="fw-bolder text-info mb-3">{{ total_revenue_from_services|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total VAT from Services" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_vat_collected_from_services|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Revenue Generated" %}</p>
|
||||
<h4 class="fw-bolder text-success mb-3">{{ total_revenue_generated|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total VAT Collected" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_vat_collected|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Expenses" %}</p>
|
||||
<h4 class="fw-bolder text-danger mb-3">{{ total_expenses|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Gross Profit" %}</p>
|
||||
<h4 class="fw-bolder text-success mb-3">{{ gross_profit|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% load i18n %}
|
||||
{% if request.is_dealer or request.is_manager or request.is_accountant %}
|
||||
<h3 class="fw-bold mb-3">
|
||||
{% blocktrans with start_date=start_date|date:"F j, Y" end_date=end_date|date:"F j, Y" %}
|
||||
Sales KPIs ({{ start_date }} - {{ end_date }})
|
||||
{% endblocktrans %}
|
||||
</h3>
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Cars Sold" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_cars_sold }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Revenue from Cars" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">
|
||||
{{ total_revenue_from_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Net Profit from Cars" %}</p>
|
||||
<h4 class="fw-bolder text-success mb-3">
|
||||
{{ net_profit_from_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Discount on Cars" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">
|
||||
{{ total_discount_on_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Cost of Cars Sold" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">
|
||||
{{ total_cost_of_cars_sold|floatformat:2 }}<span class="icon-saudi_riyal"></span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total VAT from Cars" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">
|
||||
{{ total_vat_collected_from_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h4 class="fw-bold my-4">{% trans "Sales of New Cars" %}</h4>
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Sold" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_new_cars_sold }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Revenue" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">
|
||||
{{ total_revenue_from_new_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Net Profit" %}</p>
|
||||
<h4 class="fw-bolder text-success mb-3">
|
||||
{{ net_profit_from_new_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars VAT" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">
|
||||
{{ total_vat_collected_from_new_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Cost" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">
|
||||
{{ total_cost_of_new_cars_sold|floatformat:2 }}<span class="icon-saudi_riyal"></span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h4 class="fw-bold my-4">{% trans "Sales of Used Cars" %}</h4>
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Sold" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_used_cars_sold }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Revenue" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">
|
||||
{{ total_revenue_from_used_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Net Profit" %}</p>
|
||||
<h4 class="fw-bolder text-success mb-3">
|
||||
{{ net_profit_from_used_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars VAT" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">
|
||||
{{ total_vat_collected_from_used_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Cost" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">
|
||||
{{ total_cost_of_used_cars_sold|floatformat:2 }}<span class="icon-saudi_riyal"></span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if request.is_dealer or request.is_manager or request.is_inventory %}
|
||||
<h3 class="fw-bold mb-3">{% trans "Inventory KPIs" %}</h3>
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Cars in Inventory" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_cars_in_inventory }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Inventory Value" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">
|
||||
{{ total_inventory_value|floatformat:2 }}<span class="icon-saudi_riyal"></span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars in Inventory" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_new_cars_in_inventory }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars in Inventory" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_used_cars_in_inventory }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Inventory Value" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">
|
||||
{{ new_car_value|floatformat:2 }}<span class="icon-saudi_riyal"></span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Inventory Value" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">
|
||||
{{ used_car_value|floatformat:2 }}<span class="icon-saudi_riyal"></span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-danger fw-bold small mb-1">
|
||||
<a class="text-danger"
|
||||
href="{% url 'aging_inventory_list' request.dealer.slug %}">{% trans "Aging Inventory (> 60 days)" %}</a>
|
||||
</p>
|
||||
<h4 class="fw-bolder text-danger mb-3">
|
||||
<a class="text-danger"
|
||||
href="{% url 'aging_inventory_list' request.dealer.slug %}">{{ aging_inventory_count }}</a>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if request.is_dealer or request.is_manager or request.is_accountant %}
|
||||
<h3 class="fw-bold mb-3">
|
||||
{% blocktrans with start_date=start_date|date:"F j, Y" end_date=end_date|date:"F j, Y" %}
|
||||
Financial Health KPIs ({{ start_date }} - {{ end_date }})
|
||||
{% endblocktrans %}
|
||||
</h3>
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Revenue from Services" %}</p>
|
||||
<h4 class="fw-bolder text-info mb-3">
|
||||
{{ total_revenue_from_services|floatformat:2 }}<span class="icon-saudi_riyal"></span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total VAT from Services" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">
|
||||
{{ total_vat_collected_from_services|floatformat:2 }}<span class="icon-saudi_riyal"></span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Revenue Generated" %}</p>
|
||||
<h4 class="fw-bolder text-success mb-3">
|
||||
{{ total_revenue_generated|floatformat:2 }}<span class="icon-saudi_riyal"></span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total VAT Collected" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">
|
||||
{{ total_vat_collected|floatformat:2 }}<span class="icon-saudi_riyal"></span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Expenses" %}</p>
|
||||
<h4 class="fw-bolder text-danger mb-3">
|
||||
{{ total_expenses|floatformat:2 }}<span class="icon-saudi_riyal"></span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Gross Profit" %}</p>
|
||||
<h4 class="fw-bolder text-success mb-3">
|
||||
{{ gross_profit|floatformat:2 }}<span class="icon-saudi_riyal"></span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@ -1,429 +1,427 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load tenhal_tag %}
|
||||
|
||||
{% block title %}
|
||||
{% trans "Dealership Dashboard"|capfirst %}
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="main-content flex-grow-1 container-fluid mt-4 mb-3">
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 pb-3 border-bottom">
|
||||
<h2 class="h3 fw-bold mb-3 mb-md-0">
|
||||
{% if request.is_dealer %}
|
||||
{% trans "Business Health Dashboard" %}
|
||||
{% elif request.is_manger %}
|
||||
{% trans "Manager Dashboard" %}
|
||||
{% elif request.is_inventory %}
|
||||
{% trans "Inventory Dashboard" %}
|
||||
{% else %}
|
||||
{% trans "Accountant Dashboard" %}
|
||||
{% endif %}
|
||||
<i class="fas fa-chart-area text-primary ms-2"></i>
|
||||
</h2>
|
||||
<form method="GET" class="date-filter-form">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="start-date" class="form-label">{% trans "Start Date" %}</label>
|
||||
<input type="date" class="form-control" id="start-date" name="start_date"
|
||||
value="{{ start_date|date:'Y-m-d' }}" required>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="end-date" class="form-label">{% trans "End Date" %}</label>
|
||||
<input type="date" class="form-control" id="end-date" name="end_date"
|
||||
value="{{ end_date|date:'Y-m-d' }}" required>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-primary w-100">{% trans "Apply Filter" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="make_sold" value="{{ selected_make_sales }}">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-5">
|
||||
{% include 'dashboards/financial_data_cards.html' %}
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-5">
|
||||
{% include 'dashboards/chart.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
{% endblock content %}
|
||||
|
||||
{% block customJS %}
|
||||
<script>
|
||||
// Define a color palette that aligns with the Phoenix template
|
||||
const primaryColor = '#7249b6';
|
||||
const secondaryColor = '#8193a6';
|
||||
const successColor = '#00d074';
|
||||
const dangerColor = '#e63757';
|
||||
const chartColors = [
|
||||
'#7249b6', '#00d074', '#e63757', '#17a2b8', '#ffc107',
|
||||
'#8193a6', '#28a745', '#6c757d', '#fd7e14', '#dc3545',
|
||||
'#20c997', '#6f42c1', '#e83e8c', '#6610f2', '#007bff',
|
||||
'#495057'
|
||||
];
|
||||
|
||||
// Pass translated strings from Django to JavaScript
|
||||
const translatedStrings = {
|
||||
monthlyCarsSoldLabel: "{% trans 'Total Cars Sold' %}",
|
||||
monthlyRevenueLabel: "{% trans 'Monthly Revenue' %}",
|
||||
monthlyNetProfitLabel: "{% trans 'Monthly Net Profit' %}",
|
||||
salesByMakeLabel: "{% trans 'Car Count by Make' %}",
|
||||
salesByModelPrefix: "{% trans 'Cars Sold for' %}",
|
||||
inventoryByMakeLabel: "{% trans 'Car Count by Make' %}",
|
||||
inventoryByModelLabel: "{% trans 'Cars in Inventory' %}",
|
||||
jan: "{% trans 'Jan' %}",
|
||||
feb: "{% trans 'Feb' %}",
|
||||
mar: "{% trans 'Mar' %}",
|
||||
apr: "{% trans 'Apr' %}",
|
||||
may: "{% trans 'May' %}",
|
||||
jun: "{% trans 'Jun' %}",
|
||||
jul: "{% trans 'Jul' %}",
|
||||
aug: "{% trans 'Aug' %}",
|
||||
sep: "{% trans 'Sep' %}",
|
||||
oct: "{% trans 'Oct' %}",
|
||||
nov: "{% trans 'Nov' %}",
|
||||
dec: "{% trans 'Dec' %}",
|
||||
cars: "{% trans 'cars' %}"
|
||||
};
|
||||
|
||||
|
||||
// Monthly Cars Sold (Bar Chart)
|
||||
const ctx1 = document.getElementById('CarsSoldByMonthChart').getContext('2d');
|
||||
new Chart(ctx1, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: [
|
||||
translatedStrings.jan, translatedStrings.feb, translatedStrings.mar, translatedStrings.apr,
|
||||
translatedStrings.may, translatedStrings.jun, translatedStrings.jul, translatedStrings.aug,
|
||||
translatedStrings.sep, translatedStrings.oct, translatedStrings.nov, translatedStrings.dec
|
||||
],
|
||||
datasets: [{
|
||||
label: translatedStrings.monthlyCarsSoldLabel,
|
||||
data: {{ monthly_cars_sold_json|safe }},
|
||||
backgroundColor: primaryColor,
|
||||
borderColor: primaryColor,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false }
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
||||
ticks: {
|
||||
color: secondaryColor,
|
||||
callback: function(value) {
|
||||
if (Number.isInteger(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
x: {
|
||||
grid: { display: false },
|
||||
ticks: { color: secondaryColor }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Monthly Revenue & Profit (Line Chart)
|
||||
const ctx2 = document.getElementById('revenueProfitChart').getContext('2d');
|
||||
new Chart(ctx2, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [
|
||||
translatedStrings.jan, translatedStrings.feb, translatedStrings.mar, translatedStrings.apr,
|
||||
translatedStrings.may, translatedStrings.jun, translatedStrings.jul, translatedStrings.aug,
|
||||
translatedStrings.sep, translatedStrings.oct, translatedStrings.nov, translatedStrings.dec
|
||||
],
|
||||
datasets: [
|
||||
{
|
||||
label: translatedStrings.monthlyRevenueLabel,
|
||||
data: {{ monthly_revenue_json|safe }},
|
||||
borderColor: primaryColor,
|
||||
backgroundColor: 'rgba(114, 73, 182, 0.1)', // Using primaryColor with transparency
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointBackgroundColor: primaryColor,
|
||||
pointRadius: 5,
|
||||
pointHoverRadius: 8
|
||||
},
|
||||
{
|
||||
label: translatedStrings.monthlyNetProfitLabel,
|
||||
data: {{ monthly_net_profit_json|safe }},
|
||||
borderColor: successColor,
|
||||
backgroundColor: 'rgba(0, 208, 116, 0.1)', // Using successColor with transparency
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointBackgroundColor: successColor,
|
||||
pointRadius: 5,
|
||||
pointHoverRadius: 8
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
labels: { color: '#495057', boxWidth: 20 }
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(33, 37, 41, 0.9)',
|
||||
titleColor: 'white',
|
||||
bodyColor: 'white',
|
||||
padding: 10,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
if (context.parsed.y !== null) {
|
||||
label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'SAR' }).format(context.parsed.y);
|
||||
}
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
||||
ticks: { color: secondaryColor },
|
||||
border: { color: secondaryColor }
|
||||
},
|
||||
y: {
|
||||
grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
||||
ticks: { color: secondaryColor },
|
||||
border: { color: secondaryColor }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Sales by Make (Pie Chart)
|
||||
function getChartColors(count) {
|
||||
const colors = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
colors.push(chartColors[i % chartColors.length]);
|
||||
}
|
||||
return colors;
|
||||
}
|
||||
|
||||
const ctx3 = document.getElementById('salesByBrandChart').getContext('2d');
|
||||
new Chart(ctx3, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: {{ sales_by_make_labels_json|safe }},
|
||||
datasets: [{
|
||||
label: translatedStrings.salesByMakeLabel,
|
||||
data: {{ sales_by_make_counts_json|safe }},
|
||||
backgroundColor: getChartColors({{ sales_by_make_counts_json|safe }}.length),
|
||||
hoverOffset: 15,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
labels: { color: '#343a40', font: { size: 14 } }
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(33, 37, 41, 0.9)',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#fff',
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const label = context.label || '';
|
||||
const value = context.parsed || 0;
|
||||
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
||||
const percentage = ((value / total) * 100).toFixed(2);
|
||||
return `${label}: ${value} ${translatedStrings.cars} (${percentage}%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------
|
||||
// 4. Sales by Model (Bar Chart)
|
||||
// -----------------------------------------------------------
|
||||
const salesDataByModel = JSON.parse('{{ sales_data_by_model_json|safe }}');
|
||||
const canvasElementSales = document.getElementById('salesChartByModel');
|
||||
let chartInstanceSales = null;
|
||||
|
||||
if (salesDataByModel.length > 0) {
|
||||
const labels = salesDataByModel.map(item => item.id_car_model__name);
|
||||
const counts = salesDataByModel.map(item => item.count);
|
||||
const backgroundColor = labels.map((_, index) => getChartColors(labels.length)[index]);
|
||||
|
||||
chartInstanceSales = new Chart(canvasElementSales, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: `${translatedStrings.salesByModelPrefix} {{ selected_make_sales }}`,
|
||||
data: counts,
|
||||
backgroundColor: backgroundColor,
|
||||
borderColor: backgroundColor,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
if (Number.isInteger(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
label += Math.round(context.parsed.y);
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------
|
||||
// 5. Inventory by Make (Pie Chart)
|
||||
// -----------------------------------------------------------
|
||||
const ctxInventoryMake = document.getElementById('inventoryByMakeChart').getContext('2d');
|
||||
new Chart(ctxInventoryMake, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: {{ inventory_by_make_labels_json|safe }},
|
||||
datasets: [{
|
||||
label: translatedStrings.inventoryByMakeLabel,
|
||||
data: {{ inventory_by_make_counts_json|safe }},
|
||||
backgroundColor: getChartColors({{ inventory_by_make_counts_json|safe }}.length),
|
||||
hoverOffset: 15,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
labels: { color: '#343a40', font: { size: 14 } }
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(33, 37, 41, 0.9)',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#fff',
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const label = context.label || '';
|
||||
const value = context.parsed || 0;
|
||||
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
||||
const percentage = ((value / total) * 100).toFixed(2);
|
||||
return `${label}: ${value} ${translatedStrings.cars} (${percentage}%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------
|
||||
// 6. Inventory by Model (Bar Chart)
|
||||
// -----------------------------------------------------------
|
||||
const inventoryDataByModel = JSON.parse('{{ inventory_data_by_model_json|safe }}');
|
||||
const canvasInventoryModel = document.getElementById('inventoryByModelChart');
|
||||
const messageInventoryModel = document.getElementById('inventoryByModelMessage');
|
||||
|
||||
if (inventoryDataByModel.length > 0) {
|
||||
canvasInventoryModel.style.display = 'block';
|
||||
if (messageInventoryModel) {
|
||||
messageInventoryModel.style.display = 'none';
|
||||
}
|
||||
|
||||
const labels = inventoryDataByModel.map(item => item.id_car_model__name);
|
||||
const counts = inventoryDataByModel.map(item => item.count);
|
||||
const backgroundColor = getChartColors(labels.length);
|
||||
|
||||
new Chart(canvasInventoryModel, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: translatedStrings.inventoryByModelLabel,
|
||||
data: counts,
|
||||
backgroundColor: backgroundColor,
|
||||
borderColor: backgroundColor,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
if (Number.isInteger(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
label += Math.round(context.parsed.y);
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
canvasInventoryModel.style.display = 'none';
|
||||
if (messageInventoryModel) {
|
||||
messageInventoryModel.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load tenhal_tag %}
|
||||
{% block title %}
|
||||
{% trans "Dealership Dashboard"|capfirst %}
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
<div class="main-content flex-grow-1 container-fluid mt-4 mb-3">
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 pb-3 border-bottom">
|
||||
<h2 class="h3 fw-bold mb-3 mb-md-0">
|
||||
{% if request.is_dealer %}
|
||||
{% trans "Business Health Dashboard" %}
|
||||
{% elif request.is_manger %}
|
||||
{% trans "Manager Dashboard" %}
|
||||
{% elif request.is_inventory %}
|
||||
{% trans "Inventory Dashboard" %}
|
||||
{% else %}
|
||||
{% trans "Accountant Dashboard" %}
|
||||
{% endif %}
|
||||
<i class="fas fa-chart-area text-primary ms-2"></i>
|
||||
</h2>
|
||||
<form method="GET" class="date-filter-form">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="start-date" class="form-label">{% trans "Start Date" %}</label>
|
||||
<input type="date"
|
||||
class="form-control"
|
||||
id="start-date"
|
||||
name="start_date"
|
||||
value="{{ start_date|date:'Y-m-d' }}"
|
||||
required>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="end-date" class="form-label">{% trans "End Date" %}</label>
|
||||
<input type="date"
|
||||
class="form-control"
|
||||
id="end-date"
|
||||
name="end_date"
|
||||
value="{{ end_date|date:'Y-m-d' }}"
|
||||
required>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-primary w-100">{% trans "Apply Filter" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="make_sold" value="{{ selected_make_sales }}">
|
||||
</form>
|
||||
</div>
|
||||
<div class="row g-4 mb-5">{% include 'dashboards/financial_data_cards.html' %}</div>
|
||||
<div class="row g-4 mb-5">{% include 'dashboards/chart.html' %}</div>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
{% endblock content %}
|
||||
{% block customJS %}
|
||||
<script>
|
||||
// Define a color palette that aligns with the Phoenix template
|
||||
const primaryColor = '#7249b6';
|
||||
const secondaryColor = '#8193a6';
|
||||
const successColor = '#00d074';
|
||||
const dangerColor = '#e63757';
|
||||
const chartColors = [
|
||||
'#7249b6', '#00d074', '#e63757', '#17a2b8', '#ffc107',
|
||||
'#8193a6', '#28a745', '#6c757d', '#fd7e14', '#dc3545',
|
||||
'#20c997', '#6f42c1', '#e83e8c', '#6610f2', '#007bff',
|
||||
'#495057'
|
||||
];
|
||||
|
||||
// Pass translated strings from Django to JavaScript
|
||||
const translatedStrings = {
|
||||
monthlyCarsSoldLabel: "{% trans 'Total Cars Sold' %}",
|
||||
monthlyRevenueLabel: "{% trans 'Monthly Revenue' %}",
|
||||
monthlyNetProfitLabel: "{% trans 'Monthly Net Profit' %}",
|
||||
salesByMakeLabel: "{% trans 'Car Count by Make' %}",
|
||||
salesByModelPrefix: "{% trans 'Cars Sold for' %}",
|
||||
inventoryByMakeLabel: "{% trans 'Car Count by Make' %}",
|
||||
inventoryByModelLabel: "{% trans 'Cars in Inventory' %}",
|
||||
jan: "{% trans 'Jan' %}",
|
||||
feb: "{% trans 'Feb' %}",
|
||||
mar: "{% trans 'Mar' %}",
|
||||
apr: "{% trans 'Apr' %}",
|
||||
may: "{% trans 'May' %}",
|
||||
jun: "{% trans 'Jun' %}",
|
||||
jul: "{% trans 'Jul' %}",
|
||||
aug: "{% trans 'Aug' %}",
|
||||
sep: "{% trans 'Sep' %}",
|
||||
oct: "{% trans 'Oct' %}",
|
||||
nov: "{% trans 'Nov' %}",
|
||||
dec: "{% trans 'Dec' %}",
|
||||
cars: "{% trans 'cars' %}"
|
||||
};
|
||||
|
||||
|
||||
// Monthly Cars Sold (Bar Chart)
|
||||
const ctx1 = document.getElementById('CarsSoldByMonthChart').getContext('2d');
|
||||
new Chart(ctx1, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: [
|
||||
translatedStrings.jan, translatedStrings.feb, translatedStrings.mar, translatedStrings.apr,
|
||||
translatedStrings.may, translatedStrings.jun, translatedStrings.jul, translatedStrings.aug,
|
||||
translatedStrings.sep, translatedStrings.oct, translatedStrings.nov, translatedStrings.dec
|
||||
],
|
||||
datasets: [{
|
||||
label: translatedStrings.monthlyCarsSoldLabel,
|
||||
data: {{ monthly_cars_sold_json|safe }},
|
||||
backgroundColor: primaryColor,
|
||||
borderColor: primaryColor,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false }
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
||||
ticks: {
|
||||
color: secondaryColor,
|
||||
callback: function(value) {
|
||||
if (Number.isInteger(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
x: {
|
||||
grid: { display: false },
|
||||
ticks: { color: secondaryColor }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Monthly Revenue & Profit (Line Chart)
|
||||
const ctx2 = document.getElementById('revenueProfitChart').getContext('2d');
|
||||
new Chart(ctx2, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [
|
||||
translatedStrings.jan, translatedStrings.feb, translatedStrings.mar, translatedStrings.apr,
|
||||
translatedStrings.may, translatedStrings.jun, translatedStrings.jul, translatedStrings.aug,
|
||||
translatedStrings.sep, translatedStrings.oct, translatedStrings.nov, translatedStrings.dec
|
||||
],
|
||||
datasets: [
|
||||
{
|
||||
label: translatedStrings.monthlyRevenueLabel,
|
||||
data: {{ monthly_revenue_json|safe }},
|
||||
borderColor: primaryColor,
|
||||
backgroundColor: 'rgba(114, 73, 182, 0.1)', // Using primaryColor with transparency
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointBackgroundColor: primaryColor,
|
||||
pointRadius: 5,
|
||||
pointHoverRadius: 8
|
||||
},
|
||||
{
|
||||
label: translatedStrings.monthlyNetProfitLabel,
|
||||
data: {{ monthly_net_profit_json|safe }},
|
||||
borderColor: successColor,
|
||||
backgroundColor: 'rgba(0, 208, 116, 0.1)', // Using successColor with transparency
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointBackgroundColor: successColor,
|
||||
pointRadius: 5,
|
||||
pointHoverRadius: 8
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
labels: { color: '#495057', boxWidth: 20 }
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(33, 37, 41, 0.9)',
|
||||
titleColor: 'white',
|
||||
bodyColor: 'white',
|
||||
padding: 10,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
if (context.parsed.y !== null) {
|
||||
label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'SAR' }).format(context.parsed.y);
|
||||
}
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
||||
ticks: { color: secondaryColor },
|
||||
border: { color: secondaryColor }
|
||||
},
|
||||
y: {
|
||||
grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
||||
ticks: { color: secondaryColor },
|
||||
border: { color: secondaryColor }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Sales by Make (Pie Chart)
|
||||
function getChartColors(count) {
|
||||
const colors = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
colors.push(chartColors[i % chartColors.length]);
|
||||
}
|
||||
return colors;
|
||||
}
|
||||
|
||||
const ctx3 = document.getElementById('salesByBrandChart').getContext('2d');
|
||||
new Chart(ctx3, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: {{ sales_by_make_labels_json|safe }},
|
||||
datasets: [{
|
||||
label: translatedStrings.salesByMakeLabel,
|
||||
data: {{ sales_by_make_counts_json|safe }},
|
||||
backgroundColor: getChartColors({{ sales_by_make_counts_json|safe }}.length),
|
||||
hoverOffset: 15,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
labels: { color: '#343a40', font: { size: 14 } }
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(33, 37, 41, 0.9)',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#fff',
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const label = context.label || '';
|
||||
const value = context.parsed || 0;
|
||||
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
||||
const percentage = ((value / total) * 100).toFixed(2);
|
||||
return `${label}: ${value} ${translatedStrings.cars} (${percentage}%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------
|
||||
// 4. Sales by Model (Bar Chart)
|
||||
// -----------------------------------------------------------
|
||||
const salesDataByModel = JSON.parse('{{ sales_data_by_model_json|safe }}');
|
||||
const canvasElementSales = document.getElementById('salesChartByModel');
|
||||
let chartInstanceSales = null;
|
||||
|
||||
if (salesDataByModel.length > 0) {
|
||||
const labels = salesDataByModel.map(item => item.id_car_model__name);
|
||||
const counts = salesDataByModel.map(item => item.count);
|
||||
const backgroundColor = labels.map((_, index) => getChartColors(labels.length)[index]);
|
||||
|
||||
chartInstanceSales = new Chart(canvasElementSales, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: `${translatedStrings.salesByModelPrefix} {{ selected_make_sales }}`,
|
||||
data: counts,
|
||||
backgroundColor: backgroundColor,
|
||||
borderColor: backgroundColor,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
if (Number.isInteger(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
label += Math.round(context.parsed.y);
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------
|
||||
// 5. Inventory by Make (Pie Chart)
|
||||
// -----------------------------------------------------------
|
||||
const ctxInventoryMake = document.getElementById('inventoryByMakeChart').getContext('2d');
|
||||
new Chart(ctxInventoryMake, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: {{ inventory_by_make_labels_json|safe }},
|
||||
datasets: [{
|
||||
label: translatedStrings.inventoryByMakeLabel,
|
||||
data: {{ inventory_by_make_counts_json|safe }},
|
||||
backgroundColor: getChartColors({{ inventory_by_make_counts_json|safe }}.length),
|
||||
hoverOffset: 15,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
labels: { color: '#343a40', font: { size: 14 } }
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(33, 37, 41, 0.9)',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#fff',
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const label = context.label || '';
|
||||
const value = context.parsed || 0;
|
||||
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
||||
const percentage = ((value / total) * 100).toFixed(2);
|
||||
return `${label}: ${value} ${translatedStrings.cars} (${percentage}%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------
|
||||
// 6. Inventory by Model (Bar Chart)
|
||||
// -----------------------------------------------------------
|
||||
const inventoryDataByModel = JSON.parse('{{ inventory_data_by_model_json|safe }}');
|
||||
const canvasInventoryModel = document.getElementById('inventoryByModelChart');
|
||||
const messageInventoryModel = document.getElementById('inventoryByModelMessage');
|
||||
|
||||
if (inventoryDataByModel.length > 0) {
|
||||
canvasInventoryModel.style.display = 'block';
|
||||
if (messageInventoryModel) {
|
||||
messageInventoryModel.style.display = 'none';
|
||||
}
|
||||
|
||||
const labels = inventoryDataByModel.map(item => item.id_car_model__name);
|
||||
const counts = inventoryDataByModel.map(item => item.count);
|
||||
const backgroundColor = getChartColors(labels.length);
|
||||
|
||||
new Chart(canvasInventoryModel, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: translatedStrings.inventoryByModelLabel,
|
||||
data: counts,
|
||||
backgroundColor: backgroundColor,
|
||||
borderColor: backgroundColor,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
if (Number.isInteger(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
label += Math.round(context.parsed.y);
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
canvasInventoryModel.style.display = 'none';
|
||||
if (messageInventoryModel) {
|
||||
messageInventoryModel.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -1,367 +1,371 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
|
||||
<div class="main-content flex-grow-1 container-fluid mt-4 mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-5 pb-3 border-bottom">
|
||||
<h2 class="h3 fw-bold mb-0">Manager Dashboard<i class="fas fa-chart-area text-primary ms-2"></i></h2>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Last 30 Days
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow">
|
||||
<li><a class="dropdown-item" href="#">Today</a></li>
|
||||
<li><a class="dropdown-item" href="#">Last 7 Days</a></li>
|
||||
<li><a class="dropdown-item" href="#">Last 90 Days</a></li>
|
||||
</ul>
|
||||
<div class="main-content flex-grow-1 container-fluid mt-4 mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-5 pb-3 border-bottom">
|
||||
<h2 class="h3 fw-bold mb-0">
|
||||
Manager Dashboard<i class="fas fa-chart-area text-primary ms-2"></i>
|
||||
</h2>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false">Last 30 Days</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow">
|
||||
<li>
|
||||
<a class="dropdown-item" href="#">Today</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#">Last 7 Days</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#">Last 90 Days</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Total Revenue</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">$1.25M</h4>
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Total Revenue</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">$1.25M</h4>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+8% from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Net Profit</p>
|
||||
<h4 class="fw-bolder text-success mb-3">$1.25M</h4>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+8% from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Total Expense</p>
|
||||
<h4 class="fw-bolder text-danger mb-3">$1.25M</h4>
|
||||
</div>
|
||||
<span class="badge bg-danger-subtle text-danger fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+3% from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Total Cars Sold</p>
|
||||
<h4 class="fw-bolder text-success mb-3">{{ sold_cars }}</h4>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+5 units from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Avg. Time on Lot</p>
|
||||
<h4 class="fw-bolder text-warning mb-3">10 days</h4>
|
||||
</div>
|
||||
<span class="badge bg-danger-subtle text-danger fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+2 days from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Inventory Value</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">$5.8M</h4>
|
||||
</div>
|
||||
<span class="badge bg-danger-subtle text-danger fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
-2% from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Aging Inventory</p>
|
||||
<h4 class="fw-bolder text-danger mb-3">12 units</h4>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
-4 cars from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Gross Profit</p>
|
||||
<h4 class="fw-bolder text-info mb-3">$1.25M</h4>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+8% from last month
|
||||
</span>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+8% from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Net Profit</p>
|
||||
<h4 class="fw-bolder text-success mb-3">$1.25M</h4>
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-lg-8">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">Monthly Revenue & Profit</h5>
|
||||
</div>
|
||||
<div class="card-body" style="height: 400px;">
|
||||
<canvas id="revenueProfitChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">Monthly Cars Sold</h5>
|
||||
</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center"
|
||||
style="height: 400px">
|
||||
<canvas id="CarsSoldByMonthChart"></canvas>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+8% from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Total Expense</p>
|
||||
<h4 class="fw-bolder text-danger mb-3">$1.25M</h4>
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">Sales by Make</h5>
|
||||
</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center"
|
||||
style="height: 400px">
|
||||
<canvas id="salesByBrandChart"></canvas>
|
||||
</div>
|
||||
<span class="badge bg-danger-subtle text-danger fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+3% from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Total Cars Sold</p>
|
||||
<h4 class="fw-bolder text-success mb-3">{{ sold_cars }}</h4>
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">Top Salesperson Performance</h5>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+5 units from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Avg. Time on Lot</p>
|
||||
<h4 class="fw-bolder text-warning mb-3">10 days</h4>
|
||||
<div class="card-body" style="height: 400px;">
|
||||
<canvas id="salespersonChart"></canvas>
|
||||
</div>
|
||||
<span class="badge bg-danger-subtle text-danger fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+2 days from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Inventory Value</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">$5.8M</h4>
|
||||
</div>
|
||||
<span class="badge bg-danger-subtle text-danger fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
-2% from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Aging Inventory</p>
|
||||
<h4 class="fw-bolder text-danger mb-3">12 units</h4>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
-4 cars from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Gross Profit</p>
|
||||
<h4 class="fw-bolder text-info mb-3">$1.25M</h4>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+8% from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-lg-8">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">Monthly Revenue & Profit</h5>
|
||||
</div>
|
||||
<div class="card-body" style="height: 400px;">
|
||||
<canvas id="revenueProfitChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">Monthly Cars Sold</h5>
|
||||
</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;">
|
||||
<canvas id="CarsSoldByMonthChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">Sales by Make</h5>
|
||||
</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;">
|
||||
<canvas id="salesByBrandChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">Top Salesperson Performance</h5>
|
||||
</div>
|
||||
<div class="card-body" style="height: 400px;">
|
||||
<canvas id="salespersonChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
{% endblock content %}
|
||||
|
||||
|
||||
{% block customJS %}
|
||||
<script>
|
||||
<script>
|
||||
// Define a color palette that aligns with the Phoenix template
|
||||
const primaryColor = '#7249b6'; // A vibrant purple
|
||||
const secondaryColor = '#8193a6'; // A muted gray/blue
|
||||
const successColor = '#00d074'; // A bright green
|
||||
const warningColor = '#ffc107'; // A strong yellow
|
||||
const dangerColor = '#e63757'; // A deep red
|
||||
const chartColors = ['#00d27a', '#7249b6', '#32b9ff', '#e63757', '#ffc107'];
|
||||
const primaryColor = '#7249b6'; // A vibrant purple
|
||||
const secondaryColor = '#8193a6'; // A muted gray/blue
|
||||
const successColor = '#00d074'; // A bright green
|
||||
const warningColor = '#ffc107'; // A strong yellow
|
||||
const dangerColor = '#e63757'; // A deep red
|
||||
const chartColors = ['#00d27a', '#7249b6', '#32b9ff', '#e63757', '#ffc107'];
|
||||
|
||||
// Monthly Cars Sold (Bar Chart)
|
||||
const ctx1 = document.getElementById('CarsSoldByMonthChart').getContext('2d');
|
||||
new Chart(ctx1, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
||||
datasets: [{
|
||||
label: 'Total Cars Sold',
|
||||
data: [2, 3, 10, 4, 30, 12, 8, 9, 20, 12, 15, 35],
|
||||
backgroundColor: primaryColor,
|
||||
borderColor: primaryColor,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false }
|
||||
const ctx1 = document.getElementById('CarsSoldByMonthChart').getContext('2d');
|
||||
new Chart(ctx1, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
||||
datasets: [{
|
||||
label: 'Total Cars Sold',
|
||||
data: [2, 3, 10, 4, 30, 12, 8, 9, 20, 12, 15, 35],
|
||||
backgroundColor: primaryColor,
|
||||
borderColor: primaryColor,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
||||
ticks: { color: secondaryColor }
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false }
|
||||
},
|
||||
x: {
|
||||
grid: { display: false },
|
||||
ticks: { color: secondaryColor }
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
||||
ticks: { color: secondaryColor }
|
||||
},
|
||||
x: {
|
||||
grid: { display: false },
|
||||
ticks: { color: secondaryColor }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Monthly Revenue & Profit (Line Chart)
|
||||
const ctx2 = document.getElementById('revenueProfitChart').getContext('2d');
|
||||
new Chart(ctx2, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Monthly Revenue',
|
||||
data: [120000, 150000, 130000, 180000, 200000, 175000, 190000, 220000, 210000, 250000, 240000, 280000],
|
||||
borderColor: primaryColor,
|
||||
backgroundColor: 'rgba(114, 73, 182, 0.1)', // Using primaryColor with transparency
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointBackgroundColor: primaryColor,
|
||||
pointRadius: 5,
|
||||
pointHoverRadius: 8
|
||||
},
|
||||
{
|
||||
label: 'Monthly Net Profit',
|
||||
data: [25000, 35000, 28000, 40000, 45000, 38000, 42000, 50000, 48000, 55000, 52000, 60000],
|
||||
borderColor: successColor,
|
||||
backgroundColor: 'rgba(0, 208, 116, 0.1)', // Using successColor with transparency
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointBackgroundColor: successColor,
|
||||
pointRadius: 5,
|
||||
pointHoverRadius: 8
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
labels: { color: '#495057', boxWidth: 20 }
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(33, 37, 41, 0.9)',
|
||||
titleColor: 'white',
|
||||
bodyColor: 'white',
|
||||
padding: 10,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
const ctx2 = document.getElementById('revenueProfitChart').getContext('2d');
|
||||
new Chart(ctx2, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Monthly Revenue',
|
||||
data: [120000, 150000, 130000, 180000, 200000, 175000, 190000, 220000, 210000, 250000, 240000, 280000],
|
||||
borderColor: primaryColor,
|
||||
backgroundColor: 'rgba(114, 73, 182, 0.1)', // Using primaryColor with transparency
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointBackgroundColor: primaryColor,
|
||||
pointRadius: 5,
|
||||
pointHoverRadius: 8
|
||||
},
|
||||
{
|
||||
label: 'Monthly Net Profit',
|
||||
data: [25000, 35000, 28000, 40000, 45000, 38000, 42000, 50000, 48000, 55000, 52000, 60000],
|
||||
borderColor: successColor,
|
||||
backgroundColor: 'rgba(0, 208, 116, 0.1)', // Using successColor with transparency
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointBackgroundColor: successColor,
|
||||
pointRadius: 5,
|
||||
pointHoverRadius: 8
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
labels: { color: '#495057', boxWidth: 20 }
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(33, 37, 41, 0.9)',
|
||||
titleColor: 'white',
|
||||
bodyColor: 'white',
|
||||
padding: 10,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
if (context.parsed.y !== null) {
|
||||
label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(context.parsed.y);
|
||||
}
|
||||
return label;
|
||||
}
|
||||
if (context.parsed.y !== null) {
|
||||
label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(context.parsed.y);
|
||||
}
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
||||
ticks: { color: secondaryColor },
|
||||
border: { color: secondaryColor }
|
||||
},
|
||||
y: {
|
||||
grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
||||
ticks: { color: secondaryColor },
|
||||
border: { color: secondaryColor }
|
||||
scales: {
|
||||
x: {
|
||||
grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
||||
ticks: { color: secondaryColor },
|
||||
border: { color: secondaryColor }
|
||||
},
|
||||
y: {
|
||||
grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
||||
ticks: { color: secondaryColor },
|
||||
border: { color: secondaryColor }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Sales by Make (Pie Chart)
|
||||
const ctx3 = document.getElementById('salesByBrandChart').getContext('2d');
|
||||
new Chart(ctx3, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: ['Toyota', 'Ford', 'Honda', 'BMW', 'Other'],
|
||||
datasets: [{
|
||||
label: 'Car Count by Make',
|
||||
data: [45, 30, 25, 15, 10],
|
||||
backgroundColor: chartColors,
|
||||
hoverOffset: 15,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
labels: { color: '#343a40', font: { size: 14 } }
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(33, 37, 41, 0.9)',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#fff',
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const label = context.label || '';
|
||||
const value = context.parsed || 0;
|
||||
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
||||
const percentage = ((value / total) * 100).toFixed(2);
|
||||
return `${label}: ${value} cars (${percentage}%)`;
|
||||
const ctx3 = document.getElementById('salesByBrandChart').getContext('2d');
|
||||
new Chart(ctx3, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: ['Toyota', 'Ford', 'Honda', 'BMW', 'Other'],
|
||||
datasets: [{
|
||||
label: 'Car Count by Make',
|
||||
data: [45, 30, 25, 15, 10],
|
||||
backgroundColor: chartColors,
|
||||
hoverOffset: 15,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
labels: { color: '#343a40', font: { size: 14 } }
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(33, 37, 41, 0.9)',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#fff',
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const label = context.label || '';
|
||||
const value = context.parsed || 0;
|
||||
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
||||
const percentage = ((value / total) * 100).toFixed(2);
|
||||
return `${label}: ${value} cars (${percentage}%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Salesperson Performance (Bar Chart)
|
||||
const ctx_salesperson = document.getElementById('salespersonChart').getContext('2d');
|
||||
new Chart(ctx_salesperson, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['John Doe', 'Jane Smith', 'Peter Jones', 'Mary Brown'],
|
||||
datasets: [{
|
||||
label: 'Cars Sold',
|
||||
data: [15, 22, 18, 25],
|
||||
backgroundColor: chartColors,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
title: { display: true, text: 'Top Salesperson Performance', font: { size: 16 } }
|
||||
const ctx_salesperson = document.getElementById('salespersonChart').getContext('2d');
|
||||
new Chart(ctx_salesperson, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['John Doe', 'Jane Smith', 'Peter Jones', 'Mary Brown'],
|
||||
datasets: [{
|
||||
label: 'Cars Sold',
|
||||
data: [15, 22, 18, 25],
|
||||
backgroundColor: chartColors,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: { display: true, text: 'Salesperson Name', color: secondaryColor },
|
||||
ticks: { color: secondaryColor }
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
title: { display: true, text: 'Top Salesperson Performance', font: { size: 16 } }
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: { display: true, text: 'Number of Cars Sold', color: secondaryColor },
|
||||
ticks: { color: secondaryColor }
|
||||
scales: {
|
||||
x: {
|
||||
title: { display: true, text: 'Salesperson Name', color: secondaryColor },
|
||||
ticks: { color: secondaryColor }
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: { display: true, text: 'Number of Cars Sold', color: secondaryColor },
|
||||
ticks: { color: secondaryColor }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -1,269 +1,275 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="main-content flex-grow-1 container-fluid mt-4 mb-3">
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 pb-3 border-bottom">
|
||||
<h2 class="h3 fw-bold mb-3 mb-md-0">{% trans "Sales Dashboard" %} <i class="fas fa-chart-area text-primary ms-2"></i></h2>
|
||||
<form method="GET" class="date-filter-form">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="start-date" class="form-label">{% trans "Start Date" %}</label>
|
||||
<input type="date" class="form-control" id="start-date" name="start_date"
|
||||
value="{{ start_date|date:'Y-m-d' }}" required>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="end-date" class="form-label">{% trans "End Date" %}</label>
|
||||
<input type="date" class="form-control" id="end-date" name="end_date"
|
||||
value="{{ end_date|date:'Y-m-d' }}" required>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-primary w-100">{% trans "Apply Filter" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-5">
|
||||
<h3 class="fw-bold mb-3">{% trans "Inventory KPIs" %}</h3>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Cars in Inventory" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_cars_in_inventory }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars in Inventory" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_new_cars_in_inventory }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars in Inventory" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_used_cars_in_inventory }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-danger fw-bold small mb-1"><a class="text-danger" href="{% url 'aging_inventory_list' request.dealer.slug %}">{% trans "Aging Inventory (> 60 days)" %}</a></p>
|
||||
<h4 class="fw-bolder text-danger mb-3"><a class="text-danger" href="{% url 'aging_inventory_list' request.dealer.slug %}">{{ aging_inventory_count }}</a></h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">{% trans "Top Lead Sources" %}</h5>
|
||||
</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;">
|
||||
<canvas id="leadSourcesChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">{% trans "Lead Conversion Funnel" %}</h5>
|
||||
</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;">
|
||||
<canvas id="leadFunnelChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
{% endblock content %}
|
||||
|
||||
|
||||
{% block customJS%}
|
||||
<script>
|
||||
// Define your color palette at the top
|
||||
const primaryColor = '#7249b6'; // A vibrant purple
|
||||
const secondaryColor = '#8193a6'; // A muted gray/blue
|
||||
const successColor = '#00d074'; // A bright green
|
||||
const dangerColor = '#e63757'; // A deep red
|
||||
const infoColor = '#17a2b8'; // Correcting the missing variable
|
||||
const warningColor = '#ffc107'; // Add other colors if needed
|
||||
|
||||
const chartColors = [
|
||||
'#7249b6', '#00d074', '#e63757', '#17a2b8', '#ffc107',
|
||||
'#8193a6', '#28a745', '#6c757d', '#fd7e14', '#dc3545',
|
||||
'#20c997', '#6f42c1', '#e83e8c', '#6610f2', '#007bff',
|
||||
'#495057'
|
||||
];
|
||||
|
||||
// Pass translated strings from Django to JavaScript
|
||||
const translatedStrings = {
|
||||
numberOfLeads: "{% trans 'Number of Leads' %}",
|
||||
leads: "{% trans 'Leads' %}",
|
||||
numberOfOpportunities: "{% trans 'Number of Opportunities' %}"
|
||||
};
|
||||
|
||||
// Get the canvas and message elements
|
||||
const ctx_leadSources = document.getElementById('leadSourcesChart').getContext('2d');
|
||||
const leadSourcesMessage = document.getElementById('leadSourcesMessage');
|
||||
|
||||
// Parse the JSON data from Django
|
||||
const leadSourcesLabels = JSON.parse('{{ lead_sources_labels_json|safe }}');
|
||||
const leadSourcesCounts = JSON.parse('{{ lead_sources_counts_json|safe }}');
|
||||
|
||||
// Check if there is any data to display
|
||||
if (leadSourcesCounts.length > 0) {
|
||||
// Show the chart and hide the message
|
||||
ctx_leadSources.canvas.style.display = 'block';
|
||||
if (leadSourcesMessage) {
|
||||
leadSourcesMessage.style.display = 'none';
|
||||
}
|
||||
|
||||
new Chart(ctx_leadSources, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: leadSourcesLabels,
|
||||
datasets: [{
|
||||
label: translatedStrings.numberOfLeads,
|
||||
data: leadSourcesCounts,
|
||||
backgroundColor: infoColor,
|
||||
borderColor: infoColor,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
indexAxis: 'y',
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
title: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(33, 37, 41, 0.9)',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#fff',
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
return `${translatedStrings.leads}: ${context.parsed.x}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
beginAtZero: true,
|
||||
title: { display: true, text: translatedStrings.numberOfLeads, color: secondaryColor },
|
||||
ticks: {
|
||||
color: secondaryColor,
|
||||
callback: function(value) {
|
||||
if (Number.isInteger(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: { color: 'rgba(0, 0, 0, 0.05)' }
|
||||
},
|
||||
y: {
|
||||
grid: { display: false },
|
||||
ticks: { color: secondaryColor }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Hide the chart and show the message
|
||||
ctx_leadSources.canvas.style.display = 'none';
|
||||
if (leadSourcesMessage) {
|
||||
leadSourcesMessage.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
// Lead Conversion Funnel (Horizontal Bar Chart)
|
||||
const ctx_funnel = document.getElementById('leadFunnelChart').getContext('2d');
|
||||
const leadFunnelMessage = document.getElementById('leadFunnelMessage');
|
||||
|
||||
// Parse the dynamic data from Django
|
||||
const opportunityStagesLabels = JSON.parse('{{ opportunity_stage_labels_json|safe }}');
|
||||
const opportunityStagesCounts = JSON.parse('{{ opportunity_stage_counts_json|safe }}');
|
||||
|
||||
if (opportunityStagesCounts.length > 0) {
|
||||
// Show the chart and hide the message
|
||||
ctx_funnel.canvas.style.display = 'block';
|
||||
if (leadFunnelMessage) {
|
||||
leadFunnelMessage.style.display = 'none';
|
||||
}
|
||||
|
||||
// Get a subset of colors based on the number of data points
|
||||
const backgroundColors = chartColors.slice(0, opportunityStagesCounts.length);
|
||||
|
||||
new Chart(ctx_funnel, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: opportunityStagesLabels,
|
||||
datasets: [{
|
||||
label: translatedStrings.numberOfOpportunities,
|
||||
data: opportunityStagesCounts,
|
||||
// Use the new backgroundColors array
|
||||
backgroundColor: backgroundColors,
|
||||
// Set borders to match the fill color
|
||||
borderColor: backgroundColors,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
indexAxis: 'y',
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
title: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(33, 37, 41, 0.9)',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#fff',
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const totalOpportunities = opportunityStagesCounts[0] || 0;
|
||||
const currentOpportunities = context.parsed.x;
|
||||
const percentage = totalOpportunities > 0 ? ((currentOpportunities / totalOpportunities) * 100).toFixed(1) : 0;
|
||||
return `${translatedStrings.leads}: ${currentOpportunities} (${percentage}%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
beginAtZero: true,
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
grid: { display: false },
|
||||
ticks: { color: secondaryColor }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Hide the chart and show the message
|
||||
ctx_funnel.canvas.style.display = 'none';
|
||||
if (leadFunnelMessage) {
|
||||
leadFunnelMessage.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block content %}
|
||||
<div class="main-content flex-grow-1 container-fluid mt-4 mb-3">
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 pb-3 border-bottom">
|
||||
<h2 class="h3 fw-bold mb-3 mb-md-0">
|
||||
{% trans "Sales Dashboard" %} <i class="fas fa-chart-area text-primary ms-2"></i>
|
||||
</h2>
|
||||
<form method="GET" class="date-filter-form">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="start-date" class="form-label">{% trans "Start Date" %}</label>
|
||||
<input type="date"
|
||||
class="form-control"
|
||||
id="start-date"
|
||||
name="start_date"
|
||||
value="{{ start_date|date:'Y-m-d' }}"
|
||||
required>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label for="end-date" class="form-label">{% trans "End Date" %}</label>
|
||||
<input type="date"
|
||||
class="form-control"
|
||||
id="end-date"
|
||||
name="end_date"
|
||||
value="{{ end_date|date:'Y-m-d' }}"
|
||||
required>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-primary w-100">{% trans "Apply Filter" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="row g-4 mb-5">
|
||||
<h3 class="fw-bold mb-3">{% trans "Inventory KPIs" %}</h3>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Cars in Inventory" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_cars_in_inventory }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars in Inventory" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_new_cars_in_inventory }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars in Inventory" %}</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_used_cars_in_inventory }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body p-4">
|
||||
<p class="text-uppercase text-danger fw-bold small mb-1">
|
||||
<a class="text-danger"
|
||||
href="{% url 'aging_inventory_list' request.dealer.slug %}">{% trans "Aging Inventory (> 60 days)" %}</a>
|
||||
</p>
|
||||
<h4 class="fw-bolder text-danger mb-3">
|
||||
<a class="text-danger"
|
||||
href="{% url 'aging_inventory_list' request.dealer.slug %}">{{ aging_inventory_count }}</a>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">{% trans "Top Lead Sources" %}</h5>
|
||||
</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center"
|
||||
style="height: 400px">
|
||||
<canvas id="leadSourcesChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">{% trans "Lead Conversion Funnel" %}</h5>
|
||||
</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center"
|
||||
style="height: 400px">
|
||||
<canvas id="leadFunnelChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
{% endblock content %}
|
||||
{% block customJS %}
|
||||
<script>
|
||||
// Define your color palette at the top
|
||||
const primaryColor = '#7249b6'; // A vibrant purple
|
||||
const secondaryColor = '#8193a6'; // A muted gray/blue
|
||||
const successColor = '#00d074'; // A bright green
|
||||
const dangerColor = '#e63757'; // A deep red
|
||||
const infoColor = '#17a2b8'; // Correcting the missing variable
|
||||
const warningColor = '#ffc107'; // Add other colors if needed
|
||||
|
||||
const chartColors = [
|
||||
'#7249b6', '#00d074', '#e63757', '#17a2b8', '#ffc107',
|
||||
'#8193a6', '#28a745', '#6c757d', '#fd7e14', '#dc3545',
|
||||
'#20c997', '#6f42c1', '#e83e8c', '#6610f2', '#007bff',
|
||||
'#495057'
|
||||
];
|
||||
|
||||
// Pass translated strings from Django to JavaScript
|
||||
const translatedStrings = {
|
||||
numberOfLeads: "{% trans 'Number of Leads' %}",
|
||||
leads: "{% trans 'Leads' %}",
|
||||
numberOfOpportunities: "{% trans 'Number of Opportunities' %}"
|
||||
};
|
||||
|
||||
// Get the canvas and message elements
|
||||
const ctx_leadSources = document.getElementById('leadSourcesChart').getContext('2d');
|
||||
const leadSourcesMessage = document.getElementById('leadSourcesMessage');
|
||||
|
||||
// Parse the JSON data from Django
|
||||
const leadSourcesLabels = JSON.parse('{{ lead_sources_labels_json|safe }}');
|
||||
const leadSourcesCounts = JSON.parse('{{ lead_sources_counts_json|safe }}');
|
||||
|
||||
// Check if there is any data to display
|
||||
if (leadSourcesCounts.length > 0) {
|
||||
// Show the chart and hide the message
|
||||
ctx_leadSources.canvas.style.display = 'block';
|
||||
if (leadSourcesMessage) {
|
||||
leadSourcesMessage.style.display = 'none';
|
||||
}
|
||||
|
||||
new Chart(ctx_leadSources, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: leadSourcesLabels,
|
||||
datasets: [{
|
||||
label: translatedStrings.numberOfLeads,
|
||||
data: leadSourcesCounts,
|
||||
backgroundColor: infoColor,
|
||||
borderColor: infoColor,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
indexAxis: 'y',
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
title: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(33, 37, 41, 0.9)',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#fff',
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
return `${translatedStrings.leads}: ${context.parsed.x}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
beginAtZero: true,
|
||||
title: { display: true, text: translatedStrings.numberOfLeads, color: secondaryColor },
|
||||
ticks: {
|
||||
color: secondaryColor,
|
||||
callback: function(value) {
|
||||
if (Number.isInteger(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: { color: 'rgba(0, 0, 0, 0.05)' }
|
||||
},
|
||||
y: {
|
||||
grid: { display: false },
|
||||
ticks: { color: secondaryColor }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Hide the chart and show the message
|
||||
ctx_leadSources.canvas.style.display = 'none';
|
||||
if (leadSourcesMessage) {
|
||||
leadSourcesMessage.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
// Lead Conversion Funnel (Horizontal Bar Chart)
|
||||
const ctx_funnel = document.getElementById('leadFunnelChart').getContext('2d');
|
||||
const leadFunnelMessage = document.getElementById('leadFunnelMessage');
|
||||
|
||||
// Parse the dynamic data from Django
|
||||
const opportunityStagesLabels = JSON.parse('{{ opportunity_stage_labels_json|safe }}');
|
||||
const opportunityStagesCounts = JSON.parse('{{ opportunity_stage_counts_json|safe }}');
|
||||
|
||||
if (opportunityStagesCounts.length > 0) {
|
||||
// Show the chart and hide the message
|
||||
ctx_funnel.canvas.style.display = 'block';
|
||||
if (leadFunnelMessage) {
|
||||
leadFunnelMessage.style.display = 'none';
|
||||
}
|
||||
|
||||
// Get a subset of colors based on the number of data points
|
||||
const backgroundColors = chartColors.slice(0, opportunityStagesCounts.length);
|
||||
|
||||
new Chart(ctx_funnel, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: opportunityStagesLabels,
|
||||
datasets: [{
|
||||
label: translatedStrings.numberOfOpportunities,
|
||||
data: opportunityStagesCounts,
|
||||
// Use the new backgroundColors array
|
||||
backgroundColor: backgroundColors,
|
||||
// Set borders to match the fill color
|
||||
borderColor: backgroundColors,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
indexAxis: 'y',
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
title: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(33, 37, 41, 0.9)',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#fff',
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const totalOpportunities = opportunityStagesCounts[0] || 0;
|
||||
const currentOpportunities = context.parsed.x;
|
||||
const percentage = totalOpportunities > 0 ? ((currentOpportunities / totalOpportunities) * 100).toFixed(1) : 0;
|
||||
return `${translatedStrings.leads}: ${currentOpportunities} (${percentage}%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
beginAtZero: true,
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
grid: { display: false },
|
||||
ticks: { color: secondaryColor }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Hide the chart and show the message
|
||||
ctx_funnel.canvas.style.display = 'none';
|
||||
if (leadFunnelMessage) {
|
||||
leadFunnelMessage.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -1,93 +1,94 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n static %}
|
||||
{% block title %}
|
||||
{% trans 'Activity' %}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="ol-auto pt-5 pb-9">
|
||||
<div class="row-sm">
|
||||
<div class="row d-flex-center">
|
||||
<div class="col-8">
|
||||
<div class="tab-content" id="myTabContent">
|
||||
<div class="tab-pane fade active show"
|
||||
id="tab-activity"
|
||||
role="tabpanel"
|
||||
aria-labelledby="activity-tab">
|
||||
<h3 class="mb-4">{{ _("Activity") }}</h3>
|
||||
<div class="border-bottom py-4">
|
||||
{% for log in logs %}
|
||||
<div class="d-flex">
|
||||
<div class="d-flex bg-primary-subtle rounded-circle flex-center me-3 bg-primary-subtle"
|
||||
style="width: 25px;
|
||||
height: 25px">
|
||||
<span class="fa-solid text-primary-dark fs-9 fa-clipboard text-primary-dark"></span>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="d-flex justify-content-between flex-column flex-xl-row mb-2 mb-sm-0">
|
||||
<div class="flex-1 me-2">
|
||||
<h5 class="text-body-highlight lh-sm">{{ log.user }}</h5>
|
||||
</div>
|
||||
<div class="fs-9">
|
||||
<span class="fa-regular fa-calendar-days text-primary me-2"></span><span class="fw-semibold">{{ log.timestamp }}</span>
|
||||
</div>
|
||||
{% trans 'Activity' %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="ol-auto pt-5 pb-9">
|
||||
<div class="row-sm">
|
||||
<div class="row d-flex-center">
|
||||
<div class="col-8">
|
||||
<div class="tab-content" id="myTabContent">
|
||||
<div class="tab-pane fade active show"
|
||||
id="tab-activity"
|
||||
role="tabpanel"
|
||||
aria-labelledby="activity-tab">
|
||||
<h3 class="mb-4">{{ _("Activity") }}</h3>
|
||||
<div class="border-bottom py-4">
|
||||
{% for log in logs %}
|
||||
<div class="d-flex">
|
||||
<div class="d-flex bg-primary-subtle rounded-circle flex-center me-3 bg-primary-subtle"
|
||||
style="width: 25px;
|
||||
height: 25px">
|
||||
<span class="fa-solid text-primary-dark fs-9 fa-clipboard text-primary-dark"></span>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="d-flex justify-content-between flex-column flex-xl-row mb-2 mb-sm-0">
|
||||
<div class="flex-1 me-2">
|
||||
<h5 class="text-body-highlight lh-sm">{{ log.user }}</h5>
|
||||
</div>
|
||||
<div class="fs-9">
|
||||
<span class="fa-regular fa-calendar-days text-primary me-2"></span><span class="fw-semibold">{{ log.timestamp }}</span>
|
||||
</div>
|
||||
<p class="fs-9 mb-0">{{ log.action }}</p>
|
||||
</div>
|
||||
<p class="fs-9 mb-0">{{ log.action }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-bottom border-translucent py-4">{% endfor %}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-bottom border-translucent py-4">{% endfor %}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if is_paginated %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination mb-0">
|
||||
{% 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 class="fas fa-chevron-left"></span></span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Previous">
|
||||
<span aria-hidden="true"><span class="fas fa-chevron-left"></span></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 class="fas fa-chevron-right"></span></span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Next">
|
||||
<span aria-hidden="true"><span class="fas fa-chevron-right"></span></span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if is_paginated %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination mb-0">
|
||||
{% 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 class="fas fa-chevron-left"></span></span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Previous">
|
||||
<span aria-hidden="true"><span class="fas fa-chevron-left"></span></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 class="fas fa-chevron-right"></span></span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Next">
|
||||
<span aria-hidden="true"><span class="fas fa-chevron-right"></span></span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n static %}
|
||||
{% block title %}
|
||||
{% trans 'Car Makes' %}{% endblock %}
|
||||
{% block content %}
|
||||
<style>
|
||||
{% trans 'Car Makes' %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<style>
|
||||
/* Your existing CSS styles here */
|
||||
.car-makes-grid {
|
||||
display: grid;
|
||||
@ -55,37 +56,36 @@
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
<h2 class="text-center text-primary">{{ _("Select Car Makes You Sell") }}</h2>
|
||||
<form method="post" class="mb-3"
|
||||
action="{% url 'assign_car_makes' request.dealer.slug %}">
|
||||
{% csrf_token %}
|
||||
<div class="car-makes-grid">
|
||||
{% for car_make in form.fields.car_makes.queryset %}
|
||||
<label class="car-make-option">
|
||||
<input type="checkbox"
|
||||
name="car_makes"
|
||||
value="{{ car_make.pk }}"
|
||||
{% if car_make.pk in form.initial.car_makes or car_make.pk|stringformat:"s" in form.car_makes.value %}
|
||||
checked
|
||||
{% endif %}>
|
||||
<div class="car-make-image-container">
|
||||
{% if car_make.logo and car_make.logo.url %}
|
||||
<img src="{{ car_make.logo.url }}"
|
||||
alt="{{ car_make.name }}"
|
||||
class="car-make-image">
|
||||
{% else %}
|
||||
<div class="logo-placeholder">{{ car_make.name }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="car-make-name">{{ car_make.get_local_name }}</div>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-outline-primary btn-lg" type="submit">
|
||||
<i class="fa fa-save me-2"></i>{{ _("Save") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
</style>
|
||||
<h2 class="text-center text-primary">{{ _("Select Car Makes You Sell") }}</h2>
|
||||
<form method="post"
|
||||
class="mb-3"
|
||||
action="{% url 'assign_car_makes' request.dealer.slug %}">
|
||||
{% csrf_token %}
|
||||
<div class="car-makes-grid">
|
||||
{% for car_make in form.fields.car_makes.queryset %}
|
||||
<label class="car-make-option">
|
||||
<input type="checkbox"
|
||||
name="car_makes"
|
||||
value="{{ car_make.pk }}"
|
||||
{% if car_make.pk in form.initial.car_makes or car_make.pk|stringformat:"s" in form.car_makes.value %} checked {% endif %}>
|
||||
<div class="car-make-image-container">
|
||||
{% if car_make.logo and car_make.logo.url %}
|
||||
<img src="{{ car_make.logo.url }}"
|
||||
alt="{{ car_make.name }}"
|
||||
class="car-make-image">
|
||||
{% else %}
|
||||
<div class="logo-placeholder">{{ car_make.name }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="car-make-name">{{ car_make.get_local_name }}</div>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-outline-primary btn-lg" type="submit">
|
||||
<i class="fa fa-save me-2"></i>{{ _("Save") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@ -3,64 +3,92 @@
|
||||
{% block title %}
|
||||
{% trans 'Profile' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid mb-3">
|
||||
<div class="row align-items-center justify-content-between g-3 mb-4">
|
||||
<div class="col-auto">
|
||||
<h2 class="mb-0">{% trans 'Profile' %}</h2>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-phoenix-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<span class="fas fa-cog me-2"></span>{{ _("Manage Profile") }}
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><a class="dropdown-item" href="{% url 'dealer_update' dealer.slug %}"><span class="fas fa-edit me-2"></span>{{ _("Edit Profile") }}</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'billing_info' %}"><span class="fas fa-credit-card me-2"></span>{{ _("Billing Information") }}</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'order_list' %}"><span class="fas fa-clipboard-list me-2"></span>{{ _("Plans History") }}</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item text-danger" href="{% url 'account_change_password' %}"><span class="fas fa-key me-2"></span>{{ _("Change Password") }}</a></li>
|
||||
</ul>
|
||||
<div class="container-fluid mb-3">
|
||||
<div class="row align-items-center justify-content-between g-3 mb-4">
|
||||
<div class="col-auto">
|
||||
<h2 class="mb-0">{% trans 'Profile' %}</h2>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-phoenix-primary dropdown-toggle"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<span class="fas fa-cog me-2"></span>{{ _("Manage Profile") }}
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'dealer_update' dealer.slug %}"><span class="fas fa-edit me-2"></span>{{ _("Edit Profile") }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'billing_info' %}"><span class="fas fa-credit-card me-2"></span>{{ _("Billing Information") }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'order_list' %}"><span class="fas fa-clipboard-list me-2"></span>{{ _("Plans History") }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item text-danger"
|
||||
href="{% url 'account_change_password' %}"><span class="fas fa-key me-2"></span>{{ _("Change Password") }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-column flex-sm-row align-items-sm-center g-3 g-sm-5 text-center text-sm-start">
|
||||
<div class="col-12 col-sm-auto mb-3 mb-sm-0">
|
||||
<input class="d-none" id="avatarFile" type="file" />
|
||||
<label class="cursor-pointer avatar avatar-5xl border rounded-circle shadow-sm" for="avatarFile">
|
||||
{% 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" />
|
||||
{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 col-12 col-sm ms-2">
|
||||
<h3>{{ dealer.get_local_name }}</h3>
|
||||
<p class="text-body-secondary mb-1">{% trans 'Joined' %} {{ dealer.joined_at|timesince }} {% trans 'ago' %}</p>
|
||||
<span class="badge bg-primary-subtle text-primary">{% trans 'Last login' %}: {{ dealer.user.last_login|date:"D M d, Y H:i" }}</span>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-sm-auto d-flex align-items-center justify-content-around flex-wrap mt-3 mt-sm-0">
|
||||
<div class="text-center mx-3 mb-2 mb-sm-0">
|
||||
<h6 class="mb-2 text-body-secondary">{% trans 'Total users'|capfirst %}</h6>
|
||||
<h4 class="fs-7 text-body-highlight mb-2">{{ dealer.staff_count }} / {{ allowed_users }}</h4>
|
||||
<div class="progress" style="height: 5px; width: 100px;">
|
||||
<div class="progress-bar bg-success" role="progressbar" style="width: {{ dealer.staff_count|get_percentage:allowed_users }}%;" aria-valuenow="{{ dealer.staff_count|get_percentage:allowed_users }}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-column flex-sm-row align-items-sm-center g-3 g-sm-5 text-center text-sm-start">
|
||||
<div class="col-12 col-sm-auto mb-3 mb-sm-0">
|
||||
<input class="d-none" id="avatarFile" type="file" />
|
||||
<label class="cursor-pointer avatar avatar-5xl border rounded-circle shadow-sm"
|
||||
for="avatarFile">
|
||||
{% 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" />
|
||||
{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="text-center mx-3 mb-2 mb-sm-0">
|
||||
<h6 class="mb-2 text-body-secondary">{% trans 'Total cars'|capfirst %}</h6>
|
||||
<h4 class="fs-7 text-body-highlight mb-2">{{ cars_count }} / {{ allowed_cars }}</h4>
|
||||
<div class="progress" style="height: 5px; width: 100px;">
|
||||
<div class="progress-bar bg-info" role="progressbar" style="width: {{ cars_count|get_percentage:allowed_cars }}%;" aria-valuenow="{{ cars_count|get_percentage:allowed_cars }}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
<div class="flex-1 col-12 col-sm ms-2">
|
||||
<h3>{{ dealer.get_local_name }}</h3>
|
||||
<p class="text-body-secondary mb-1">{% trans 'Joined' %} {{ dealer.joined_at|timesince }} {% trans 'ago' %}</p>
|
||||
<span class="badge bg-primary-subtle text-primary">{% trans 'Last login' %}: {{ dealer.user.last_login|date:"D M d, Y H:i" }}</span>
|
||||
</div>
|
||||
<div class="col-12 col-sm-auto d-flex align-items-center justify-content-around flex-wrap mt-3 mt-sm-0">
|
||||
<div class="text-center mx-3 mb-2 mb-sm-0">
|
||||
<h6 class="mb-2 text-body-secondary">{% trans 'Total users'|capfirst %}</h6>
|
||||
<h4 class="fs-7 text-body-highlight mb-2">{{ dealer.staff_count }} / {{ allowed_users }}</h4>
|
||||
<div class="progress" style="height: 5px; width: 100px;">
|
||||
<div class="progress-bar bg-success"
|
||||
role="progressbar"
|
||||
style="width: {{ dealer.staff_count|get_percentage:allowed_users }}%"
|
||||
aria-valuenow="{{ dealer.staff_count|get_percentage:allowed_users }}"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center mx-3 mb-2 mb-sm-0">
|
||||
<h6 class="mb-2 text-body-secondary">{% trans 'Total cars'|capfirst %}</h6>
|
||||
<h4 class="fs-7 text-body-highlight mb-2">{{ cars_count }} / {{ allowed_cars }}</h4>
|
||||
<div class="progress" style="height: 5px; width: 100px;">
|
||||
<div class="progress-bar bg-info"
|
||||
role="progressbar"
|
||||
style="width: {{ cars_count|get_percentage:allowed_cars }}%"
|
||||
aria-valuenow="{{ cars_count|get_percentage:allowed_cars }}"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -68,64 +96,89 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<ul class="nav nav-tabs nav-justified" id="profileTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="subscription-tab" data-bs-toggle="tab" data-bs-target="#subscription-pane" type="button" role="tab" aria-controls="subscription-pane" aria-selected="true"><span class="fas fa-star me-2"></span>{{ _("Plan & Subscription") }}</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="contact-tab" data-bs-toggle="tab" data-bs-target="#contact-pane" type="button" role="tab" aria-controls="contact-pane" aria-selected="false"><span class="fas fa-info-circle me-2"></span>{{ _("Company Details") }}</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="makes-tab" data-bs-toggle="tab" data-bs-target="#makes-pane" type="button" role="tab" aria-controls="makes-pane" aria-selected="false"><span class="fas fa-car me-2"></span>{{ _("Car Brands") }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content pt-4" id="profileTabsContent">
|
||||
<div class="tab-pane fade show active" id="subscription-pane" role="tabpanel" aria-labelledby="subscription-tab">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h3 class="mb-0">{{ dealer.user.userplan.plan|capfirst }}</h3>
|
||||
{% if dealer.user.userplan and not dealer.user.userplan.is_expired %}
|
||||
<span class="badge bg-success-subtle text-success">{{ _("Active") }}</span>
|
||||
{% elif dealer.user.userplan and dealer.user.userplan.is_expired %}
|
||||
<span class="badge bg-danger-subtle text-danger">{{ _("Expired") }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning-subtle text-warning">{{ _("No Active Plan") }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p class="fs-9 text-body-secondary">
|
||||
{% if dealer.user.userplan and not dealer.user.userplan.is_expired %}
|
||||
{% trans 'Active until' %}: {{ dealer.user.userplan.expire }} <small>{% trans 'Days left' %}: {{ dealer.user.userplan.days_left }}</small>
|
||||
{% else %}
|
||||
{% trans 'Please subscribe or renew your plan to continue using our services.' %}
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
<div class="d-flex align-items-end mb-3">
|
||||
<h4 class="fw-bolder me-1">
|
||||
{{ dealer.user.userplan.plan.planpricing_set.first.price }} <span class="icon-saudi_riyal"></span>
|
||||
</h4>
|
||||
<h5 class="fs-9 fw-normal text-body-tertiary ms-1">{{ _("Per month") }}</h5>
|
||||
</div>
|
||||
|
||||
<ul class="list-unstyled mb-4">
|
||||
{% for line in dealer.user.userplan.plan.description|splitlines %}
|
||||
<li class="d-flex align-items-center mb-1">
|
||||
<span class="uil uil-check-circle text-success me-2"></span>
|
||||
<span class="text-body-secondary">{{ line }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% comment %} <div class="d-flex justify-content-end gap-2">
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<ul class="nav nav-tabs nav-justified" id="profileTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active"
|
||||
id="subscription-tab"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#subscription-pane"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="subscription-pane"
|
||||
aria-selected="true">
|
||||
<span class="fas fa-star me-2"></span>{{ _("Plan & Subscription") }}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link"
|
||||
id="contact-tab"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#contact-pane"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="contact-pane"
|
||||
aria-selected="false">
|
||||
<span class="fas fa-info-circle me-2"></span>{{ _("Company Details") }}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link"
|
||||
id="makes-tab"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#makes-pane"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="makes-pane"
|
||||
aria-selected="false">
|
||||
<span class="fas fa-car me-2"></span>{{ _("Car Brands") }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content pt-4" id="profileTabsContent">
|
||||
<div class="tab-pane fade show active"
|
||||
id="subscription-pane"
|
||||
role="tabpanel"
|
||||
aria-labelledby="subscription-tab">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h3 class="mb-0">{{ dealer.user.userplan.plan|capfirst }}</h3>
|
||||
{% if dealer.user.userplan and not dealer.user.userplan.is_expired %}
|
||||
<span class="badge bg-success-subtle text-success">{{ _("Active") }}</span>
|
||||
{% elif dealer.user.userplan and dealer.user.userplan.is_expired %}
|
||||
<span class="badge bg-danger-subtle text-danger">{{ _("Expired") }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning-subtle text-warning">{{ _("No Active Plan") }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p class="fs-9 text-body-secondary">
|
||||
{% if dealer.user.userplan and not dealer.user.userplan.is_expired %}
|
||||
{% trans 'Active until' %}: {{ dealer.user.userplan.expire }} <small>{% trans 'Days left' %}: {{ dealer.user.userplan.days_left }}</small>
|
||||
{% else %}
|
||||
{% trans 'Please subscribe or renew your plan to continue using our services.' %}
|
||||
{% endif %}
|
||||
</p>
|
||||
<div class="d-flex align-items-end mb-3">
|
||||
<h4 class="fw-bolder me-1">
|
||||
{{ dealer.user.userplan.plan.planpricing_set.first.price }} <span class="icon-saudi_riyal"></span>
|
||||
</h4>
|
||||
<h5 class="fs-9 fw-normal text-body-tertiary ms-1">{{ _("Per month") }}</h5>
|
||||
</div>
|
||||
<ul class="list-unstyled mb-4">
|
||||
{% for line in dealer.user.userplan.plan.description|splitlines %}
|
||||
<li class="d-flex align-items-center mb-1">
|
||||
<span class="uil uil-check-circle text-success me-2"></span>
|
||||
<span class="text-body-secondary">{{ line }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% comment %} <div class="d-flex justify-content-end gap-2">
|
||||
{% if dealer.user.userplan.is_expired %}
|
||||
<a href="{% url 'pricing_page' request.dealer.slug %}" class="btn btn-warning"><span class="fas fa-redo-alt me-2"></span>{{ _("Renew") }}</a>
|
||||
{% endif %}
|
||||
@ -135,114 +188,141 @@
|
||||
{% if not dealer.user.userplan %}
|
||||
<a href="{% url 'pricing_page' request.dealer.slug %}" class="btn btn-success"><span class="fas fa-cart-plus me-2"></span>{{ _("Subscribe Now") }}</a>
|
||||
{% endif %}
|
||||
</div> {% endcomment %}
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
</div> {% endcomment %}
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
{% if not dealer.user.userplan %}
|
||||
<a href="{% url 'pricing_page' request.dealer.slug %}" class="btn btn-outline-primary"><span class="fas fa-cart-plus me-2"></span>{{ _("Subscribe Now") }}</a>
|
||||
<a href="{% url 'pricing_page' request.dealer.slug %}"
|
||||
class="btn btn-outline-primary"><span class="fas fa-cart-plus me-2"></span>{{ _("Subscribe Now") }}</a>
|
||||
{% elif dealer.user.userplan.is_expired %}
|
||||
<a href="{% url 'pricing_page' request.dealer.slug %}" class="btn btn-outline-warning"><span class="fas fa-redo-alt me-2"></span>{{ _("Renew") }}</a>
|
||||
<a href="{% url 'pricing_page' request.dealer.slug %}"
|
||||
class="btn btn-outline-warning"><span class="fas fa-redo-alt me-2"></span>{{ _("Renew") }}</a>
|
||||
{% elif dealer.user.userplan.plan.name != "Enterprise" %}
|
||||
<a href="{% url 'pricing_page' request.dealer.slug %}" class="btn btn-outline-primary"><span class="fas fa-rocket me-2"></span>{{ _("Upgrade Plan") }}</a>
|
||||
<a href="{% url 'pricing_page' request.dealer.slug %}"
|
||||
class="btn btn-outline-primary"><span class="fas fa-rocket me-2"></span>{{ _("Upgrade Plan") }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="card-body d-flex flex-column justify-content-center">
|
||||
<div class="d-flex justify-content-between mb-4">
|
||||
<h5 class="mb-0 text-body-highlight">{{ _("Manage Users & Cars") }}</h5>
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="card-body d-flex flex-column justify-content-center">
|
||||
<div class="d-flex justify-content-between mb-4">
|
||||
<h5 class="mb-0 text-body-highlight">{{ _("Manage Users & Cars") }}</h5>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<h6 class="text-body-secondary">{{ _("Total users") }}</h6>
|
||||
<div class="progress" style="height: 10px;">
|
||||
<div class="progress-bar bg-success"
|
||||
role="progressbar"
|
||||
style="width: {{ dealer.staff_count|get_percentage:allowed_users }}%"
|
||||
aria-valuenow="{{ dealer.staff_count|get_percentage:allowed_users }}"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"></div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between text-body-secondary fs-9 mt-2">
|
||||
<span>{{ _("Used") }}: {{ dealer.staff_count }}</span>
|
||||
<span>{{ _("Limit") }}: {{ allowed_users }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<h6 class="text-body-secondary">{{ _("Total cars") }}</h6>
|
||||
<div class="progress" style="height: 10px;">
|
||||
<div class="progress-bar bg-info"
|
||||
role="progressbar"
|
||||
style="width: {{ cars_count|get_percentage:allowed_cars }}%"
|
||||
aria-valuenow="{{ cars_count|get_percentage:allowed_cars }}"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"></div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between text-body-secondary fs-9 mt-2">
|
||||
<span>{{ _("Used") }}: {{ cars_count }}</span>
|
||||
<span>{{ _("Limit") }}: {{ allowed_cars }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-body-secondary mt-auto">{{ _("Contact support to increase your limits") }}</small>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<h6 class="text-body-secondary">{{ _("Total users") }}</h6>
|
||||
<div class="progress" style="height: 10px;">
|
||||
<div class="progress-bar bg-success" role="progressbar" style="width: {{ dealer.staff_count|get_percentage:allowed_users }}%;" aria-valuenow="{{ dealer.staff_count|get_percentage:allowed_users }}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between text-body-secondary fs-9 mt-2">
|
||||
<span>{{ _("Used") }}: {{ dealer.staff_count }}</span>
|
||||
<span>{{ _("Limit") }}: {{ allowed_users }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<h6 class="text-body-secondary">{{ _("Total cars") }}</h6>
|
||||
<div class="progress" style="height: 10px;">
|
||||
<div class="progress-bar bg-info" role="progressbar" style="width: {{ cars_count|get_percentage:allowed_cars }}%;" aria-valuenow="{{ cars_count|get_percentage:allowed_cars }}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between text-body-secondary fs-9 mt-2">
|
||||
<span>{{ _("Used") }}: {{ cars_count }}</span>
|
||||
<span>{{ _("Limit") }}: {{ allowed_cars }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-body-secondary mt-auto">{{ _("Contact support to increase your limits") }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="contact-pane" role="tabpanel" aria-labelledby="contact-tab">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="mb-3">{% trans 'Contact Information' %}</h5>
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<span class="fas fa-location-dot me-3 text-primary"></span>
|
||||
<div>
|
||||
<h6 class="mb-0">{% trans 'Address' %}</h6>
|
||||
<p class="mb-0 text-body-secondary">{{ dealer.address }}</p>
|
||||
<div class="tab-pane fade"
|
||||
id="contact-pane"
|
||||
role="tabpanel"
|
||||
aria-labelledby="contact-tab">
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="mb-3">{% trans 'Contact Information' %}</h5>
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<span class="fas fa-location-dot me-3 text-primary"></span>
|
||||
<div>
|
||||
<h6 class="mb-0">{% trans 'Address' %}</h6>
|
||||
<p class="mb-0 text-body-secondary">{{ dealer.address }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<span class="fas fa-envelope me-3 text-info"></span>
|
||||
<div>
|
||||
<h6 class="mb-0">{% trans 'Email' %}</h6>
|
||||
<p class="mb-0 text-body-secondary">{{ dealer.user.email }}</p>
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<span class="fas fa-envelope me-3 text-info"></span>
|
||||
<div>
|
||||
<h6 class="mb-0">{% trans 'Email' %}</h6>
|
||||
<p class="mb-0 text-body-secondary">{{ dealer.user.email }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="fas fa-phone me-3 text-success"></span>
|
||||
<div>
|
||||
<h6 class="mb-0">{% trans 'Phone' %}</h6>
|
||||
<p class="mb-0 text-body-secondary" dir="ltr">{{ dealer.phone_number }}</p>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="fas fa-phone me-3 text-success"></span>
|
||||
<div>
|
||||
<h6 class="mb-0">{% trans 'Phone' %}</h6>
|
||||
<p class="mb-0 text-body-secondary" dir="ltr">{{ dealer.phone_number }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="mb-3">{{ _("VAT Information") }}</h5>
|
||||
<form action="{% url 'dealer_vat_rate_update' request.dealer.slug %}" method="post">
|
||||
{% csrf_token %}
|
||||
{{ vatform|crispy }}
|
||||
<button class="btn btn-phoenix-primary mt-3" type="submit"><i class="fa-solid fa-pen-to-square me-1"></i>{% trans 'Update VAT' %}</button>
|
||||
</form>
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="mb-3">{{ _("VAT Information") }}</h5>
|
||||
<form action="{% url 'dealer_vat_rate_update' request.dealer.slug %}"
|
||||
method="post">
|
||||
{% csrf_token %}
|
||||
{{ vatform|crispy }}
|
||||
<button class="btn btn-phoenix-primary mt-3" type="submit">
|
||||
<i class="fa-solid fa-pen-to-square me-1"></i>{% trans 'Update VAT' %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="makes-pane" role="tabpanel" aria-labelledby="makes-tab">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="mb-4">{{ _("Makes you are selling") }}</h5>
|
||||
<div class="d-flex flex-wrap gap-3 mb-4">
|
||||
{% for make in car_makes %}
|
||||
<div class="text-center p-2 border rounded-3">
|
||||
{% if make.logo %}
|
||||
<img src="{{ make.logo.url }}" alt="{{ make.get_local_name }}" class="rounded" style="height: 48px; width: auto; background-color:white;" />
|
||||
{% endif %}
|
||||
<p class="fs-8 text-body-secondary mt-1 mb-0">{{ make.get_local_name }}</p>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="text-body-secondary">{{ _("No car makes selected.") }}</p>
|
||||
{% endfor %}
|
||||
<div class="tab-pane fade"
|
||||
id="makes-pane"
|
||||
role="tabpanel"
|
||||
aria-labelledby="makes-tab">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="mb-4">{{ _("Makes you are selling") }}</h5>
|
||||
<div class="d-flex flex-wrap gap-3 mb-4">
|
||||
{% for make in car_makes %}
|
||||
<div class="text-center p-2 border rounded-3">
|
||||
{% if make.logo %}
|
||||
<img src="{{ make.logo.url }}"
|
||||
alt="{{ make.get_local_name }}"
|
||||
class="rounded"
|
||||
style="height: 48px;
|
||||
width: auto;
|
||||
background-color:white" />
|
||||
{% endif %}
|
||||
<p class="fs-8 text-body-secondary mt-1 mb-0">{{ make.get_local_name }}</p>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="text-body-secondary">{{ _("No car makes selected.") }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<a class="btn btn-phoenix-warning"
|
||||
href="{% url 'assign_car_makes' request.dealer.slug %}"><span class="fas fa-plus me-2"></span>{{ _("Select Makes") }}</a>
|
||||
</div>
|
||||
<a class="btn btn-phoenix-warning" href="{% url 'assign_car_makes' request.dealer.slug %}"><span class="fas fa-plus me-2"></span>{{ _("Select Makes") }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -251,5 +331,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@ -5,34 +5,38 @@
|
||||
{{ _("Update Dealer Information") }}
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
<main class="d-flex align-items-center justify-content-center min-vh-80 py-5">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
|
||||
<div class="card-header bg-gradient py-4 border-0 rounded-top-4">
|
||||
<h3 class="mb-0 fs-4 fw-bold text-center">
|
||||
{{ _("Update Dealer Information") }}
|
||||
<i class="fas fa-car ms-2"></i>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<form hx-boost="false" method="post" enctype="multipart/form-data" class="needs-validation" novalidate>
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<hr class="my-4">
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
|
||||
<button class="btn btn-phoenix-primary btn-lg me-md-2" type="submit">
|
||||
<i class="fa-solid fa-floppy-disk me-1"></i>
|
||||
{{ _("Save") }}
|
||||
</button>
|
||||
<a href="{% url 'dealer_detail' request.dealer.slug %}"
|
||||
class="btn btn-phoenix-secondary btn-lg">
|
||||
<i class="fa-solid fa-ban me-1"></i>
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
<main class="d-flex align-items-center justify-content-center min-vh-80 py-5">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
|
||||
<div class="card-header bg-gradient py-4 border-0 rounded-top-4">
|
||||
<h3 class="mb-0 fs-4 fw-bold text-center">
|
||||
{{ _("Update Dealer Information") }}
|
||||
<i class="fas fa-car ms-2"></i>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<form hx-boost="false"
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
class="needs-validation"
|
||||
novalidate>
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<hr class="my-4">
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
|
||||
<button class="btn btn-phoenix-primary btn-lg me-md-2" type="submit">
|
||||
<i class="fa-solid fa-floppy-disk me-1"></i>
|
||||
{{ _("Save") }}
|
||||
</button>
|
||||
<a href="{% url 'dealer_detail' request.dealer.slug %}"
|
||||
class="btn btn-phoenix-secondary btn-lg">
|
||||
<i class="fa-solid fa-ban me-1"></i>
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@ -170,10 +170,7 @@
|
||||
valign="top"
|
||||
style="font-family: Open Sans, Helvetica, Arial, sans-serif;
|
||||
padding-bottom: 30px">
|
||||
<p style="color: #ffffff;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
margin: 0">{% trans 'Thank you for choosing us.' %}</p>
|
||||
<p style="color: #ffffff; font-size: 14px; line-height: 24px; margin: 0">{% trans 'Thank you for choosing us.' %}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body style="font-family: 'Segoe UI', Tahoma, sans-serif; direction: rtl;">
|
||||
<p>مرحباً {{ user.get_full_name }}،</p>
|
||||
|
||||
<p>
|
||||
اشتراكك في <strong>{{ plan.name }}</strong> سينتهي خلال
|
||||
{{ days_until_expire }} يوم في {{ expiration_date|date:"j F Y" }}.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="{{ RENEWAL_URL }}">جدد اشتراكك الآن</a> لمواصلة الخدمة.
|
||||
</p>
|
||||
|
||||
<p>مع أطيب التحيات،<br>
|
||||
فريق تنحل</p>
|
||||
</body>
|
||||
</html>
|
||||
<body style="font-family: 'Segoe UI', Tahoma, sans-serif; direction: rtl;">
|
||||
<p>مرحباً {{ user.get_full_name }}،</p>
|
||||
<p>
|
||||
اشتراكك في <strong>{{ plan.name }}</strong> سينتهي خلال
|
||||
{{ days_until_expire }} يوم في {{ expiration_date|date:"j F Y" }}.
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{ RENEWAL_URL }}">جدد اشتراكك الآن</a> لمواصلة الخدمة.
|
||||
</p>
|
||||
<p>
|
||||
مع أطيب التحيات،
|
||||
<br>
|
||||
فريق تنحل
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,14 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; direction: {{ direction }};">
|
||||
<p>Hello {{ user.get_full_name }},</p>
|
||||
|
||||
<p>Your <strong>{{ plan.name }}</strong> subscription will expire
|
||||
in {{ days_until_expire }} days on {{ expiration_date|date:"F j, Y" }}.</p>
|
||||
|
||||
<p><a href="{{ RENEWAL_URL }}">Renew now</a> to continue service.</p>
|
||||
|
||||
<p>Best regards,<br>
|
||||
The Team at Tenhal</p>
|
||||
</body>
|
||||
</html>
|
||||
<body style="font-family: Arial, sans-serif; direction: {{ direction }};">
|
||||
<p>Hello {{ user.get_full_name }},</p>
|
||||
<p>
|
||||
Your <strong>{{ plan.name }}</strong> subscription will expire
|
||||
in {{ days_until_expire }} days on {{ expiration_date|date:"F j, Y" }}.
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{ RENEWAL_URL }}">Renew now</a> to continue service.
|
||||
</p>
|
||||
<p>
|
||||
Best regards,
|
||||
<br>
|
||||
The Team at Tenhal
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,34 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 0; }
|
||||
.container { max-width: 600px; margin: 20px auto; background-color: #ffffff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); }
|
||||
h2 { color: #333333; }
|
||||
p { color: #555555; line-height: 1.6; }
|
||||
.footer { text-align: center; margin-top: 20px; font-size: 0.8em; color: #888888; }
|
||||
.highlight { font-weight: bold; color: #007bff; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>Hello {{ user_name }},</h2>
|
||||
<p>{% trans "This is a friendly reminder for your upcoming schedule" %}:</p>
|
||||
<p>
|
||||
<span class="highlight">{% trans "Purpose" %}:</span> {{ schedule_purpose }}<br>
|
||||
<span class="highlight">{% trans "Scheduled At" %}:</span> {{ scheduled_at }}<br>
|
||||
<span class="highlight">{% trans "Type" %}:</span> {{ schedule_type }}<br>
|
||||
{% if customer_name != 'N/A' %}<span class="highlight">{% trans "Customer" %}:</span> {{ customer_name }}<br>{% endif %}
|
||||
{% if notes %}<span class="highlight">{% trans "Notes" %}:</span> {{ notes }}<br>{% endif %}
|
||||
</p>
|
||||
<p>{% trans "Please be prepared for your schedule" %}.</p>
|
||||
<p>{% trans "Thank you" %}!</p>
|
||||
<p class="fs-4">{% trans "The team at Tenhal" %}.</p>
|
||||
<div class="footer">
|
||||
<p>{% trans "This is an automated reminder. Please do not reply to this email." %}</p>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 0; }
|
||||
.container { max-width: 600px; margin: 20px auto; background-color: #ffffff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); }
|
||||
h2 { color: #333333; }
|
||||
p { color: #555555; line-height: 1.6; }
|
||||
.footer { text-align: center; margin-top: 20px; font-size: 0.8em; color: #888888; }
|
||||
.highlight { font-weight: bold; color: #007bff; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>Hello {{ user_name }},</h2>
|
||||
<p>{% trans "This is a friendly reminder for your upcoming schedule" %}:</p>
|
||||
<p>
|
||||
<span class="highlight">{% trans "Purpose" %}:</span> {{ schedule_purpose }}
|
||||
<br>
|
||||
<span class="highlight">{% trans "Scheduled At" %}:</span> {{ scheduled_at }}
|
||||
<br>
|
||||
<span class="highlight">{% trans "Type" %}:</span> {{ schedule_type }}
|
||||
<br>
|
||||
{% if customer_name != 'N/A' %}
|
||||
<span class="highlight">{% trans "Customer" %}:</span> {{ customer_name }}
|
||||
<br>
|
||||
{% endif %}
|
||||
{% if notes %}
|
||||
<span class="highlight">{% trans "Notes" %}:</span> {{ notes }}
|
||||
<br>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>{% trans "Please be prepared for your schedule" %}.</p>
|
||||
<p>{% trans "Thank you" %}!</p>
|
||||
<p class="fs-4">{% trans "The team at Tenhal" %}.</p>
|
||||
<div class="footer">
|
||||
<p>{% trans "This is an automated reminder. Please do not reply to this email." %}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,70 +1,62 @@
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
<style>
|
||||
|
||||
.empty-state-container {
|
||||
background-color: #ffffff;
|
||||
padding: 50px;
|
||||
border-radius: 5px; /* Rounded corners */
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08); /* Subtle shadow */
|
||||
text-align: center;
|
||||
max-width: 70rem; /* Max width for content - made wider */
|
||||
width: 90%; /* Fluid width */
|
||||
margin: 0px auto; /* Added margin-top and auto for horizontal centering */
|
||||
max-height: 80vh; /* Added min-height to control the height */
|
||||
display: flex; /* Use flexbox for vertical centering of content */
|
||||
flex-direction: column; /* Stack children vertically */
|
||||
justify-content: center; /* Center content vertically */
|
||||
align-items: center; /* Center content horizontally */
|
||||
}
|
||||
.empty-state-image {
|
||||
max-width: 50%; /* Responsive image size */
|
||||
height: auto%;
|
||||
|
||||
border-radius: 10px; /* Rounded corners for image */
|
||||
}
|
||||
.empty-state-title {
|
||||
color: #343a40; /* Dark text for title */
|
||||
font-weight: 600;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.empty-state-text {
|
||||
color: #6c757d; /* Muted text for description */
|
||||
margin-bottom: 30px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
/* No specific styles for .btn-add-new or .message-box are needed here as per previous updates */
|
||||
</style>
|
||||
|
||||
|
||||
<div class="empty-state-container">
|
||||
<!-- Empty State Illustration -->
|
||||
|
||||
{% if image %}
|
||||
{% static image as final_image_path %}
|
||||
{% else %}
|
||||
{% static 'images/no_content/no_item.jpg' as final_image_path %}
|
||||
{% endif %}
|
||||
<p class="sm">
|
||||
<img src="{{ final_image_path }}" alt="No-empty-state-image" class="empty-state-image">
|
||||
<p>
|
||||
|
||||
<!-- Title -->
|
||||
<h3 class="empty-state-title">
|
||||
{% blocktrans %}No {{ value}} Yet{% endblocktrans %}
|
||||
</h3>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="empty-state-text">
|
||||
{% blocktrans %}It looks like you haven't added any {{ value }} to your account.
|
||||
Click the button below to get started and add your first {{ value }}!{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<!-- Call to Action Button -->
|
||||
<a class="btn btn-lg btn-primary" href="{{ url }}">
|
||||
<i class="fa fa-plus me-2"></i>
|
||||
{% blocktrans %}Create New {{value}}{% endblocktrans %}
|
||||
|
||||
</a>
|
||||
</div>
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
<style>
|
||||
|
||||
.empty-state-container {
|
||||
background-color: #ffffff;
|
||||
padding: 50px;
|
||||
border-radius: 5px; /* Rounded corners */
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08); /* Subtle shadow */
|
||||
text-align: center;
|
||||
max-width: 70rem; /* Max width for content - made wider */
|
||||
width: 90%; /* Fluid width */
|
||||
margin: 0px auto; /* Added margin-top and auto for horizontal centering */
|
||||
max-height: 80vh; /* Added min-height to control the height */
|
||||
display: flex; /* Use flexbox for vertical centering of content */
|
||||
flex-direction: column; /* Stack children vertically */
|
||||
justify-content: center; /* Center content vertically */
|
||||
align-items: center; /* Center content horizontally */
|
||||
}
|
||||
.empty-state-image {
|
||||
max-width: 50%; /* Responsive image size */
|
||||
height: auto%;
|
||||
|
||||
border-radius: 10px; /* Rounded corners for image */
|
||||
}
|
||||
.empty-state-title {
|
||||
color: #343a40; /* Dark text for title */
|
||||
font-weight: 600;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.empty-state-text {
|
||||
color: #6c757d; /* Muted text for description */
|
||||
margin-bottom: 30px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
/* No specific styles for .btn-add-new or .message-box are needed here as per previous updates */
|
||||
</style>
|
||||
<div class="empty-state-container">
|
||||
<!-- Empty State Illustration -->
|
||||
{% if image %}
|
||||
{% static image as final_image_path %}
|
||||
{% else %}
|
||||
{% static 'images/no_content/no_item.jpg' as final_image_path %}
|
||||
{% endif %}
|
||||
<p class="sm">
|
||||
<img src="{{ final_image_path }}"
|
||||
alt="No-empty-state-image"
|
||||
class="empty-state-image">
|
||||
<p>
|
||||
<!-- Title -->
|
||||
<h3 class="empty-state-title">{% blocktrans %}No {{ value}} Yet{% endblocktrans %}</h3>
|
||||
<!-- Description -->
|
||||
<p class="empty-state-text">
|
||||
{% blocktrans %}It looks like you haven't added any {{ value }} to your account.
|
||||
Click the button below to get started and add your first {{ value }}!{% endblocktrans %}
|
||||
</p>
|
||||
<!-- Call to Action Button -->
|
||||
<a class="btn btn-lg btn-primary" href="{{ url }}">
|
||||
<i class="fa fa-plus me-2"></i>
|
||||
{% blocktrans %}Create New {{value}}{% endblocktrans %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@ -61,19 +61,19 @@
|
||||
rel="stylesheet"
|
||||
id="user-style-default">
|
||||
<script>
|
||||
var phoenixIsRTL = window.config.config.phoenixIsRTL;
|
||||
if (phoenixIsRTL) {
|
||||
var linkDefault = document.getElementById('style-default');
|
||||
var userLinkDefault = document.getElementById('user-style-default');
|
||||
linkDefault.setAttribute('disabled', true);
|
||||
userLinkDefault.setAttribute('disabled', true);
|
||||
document.querySelector('html').setAttribute('dir', 'rtl');
|
||||
} else {
|
||||
var linkRTL = document.getElementById('style-rtl');
|
||||
var userLinkRTL = document.getElementById('user-style-rtl');
|
||||
linkRTL.setAttribute('disabled', true);
|
||||
userLinkRTL.setAttribute('disabled', true);
|
||||
}
|
||||
var phoenixIsRTL = window.config.config.phoenixIsRTL;
|
||||
if (phoenixIsRTL) {
|
||||
var linkDefault = document.getElementById('style-default');
|
||||
var userLinkDefault = document.getElementById('user-style-default');
|
||||
linkDefault.setAttribute('disabled', true);
|
||||
userLinkDefault.setAttribute('disabled', true);
|
||||
document.querySelector('html').setAttribute('dir', 'rtl');
|
||||
} else {
|
||||
var linkRTL = document.getElementById('style-rtl');
|
||||
var userLinkRTL = document.getElementById('user-style-rtl');
|
||||
linkRTL.setAttribute('disabled', true);
|
||||
userLinkRTL.setAttribute('disabled', true);
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
@ -115,17 +115,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
var navbarTopStyle = window.config.config.phoenixNavbarTopStyle;
|
||||
var navbarTop = document.querySelector('.navbar-top');
|
||||
if (navbarTopStyle === 'darker') {
|
||||
navbarTop.setAttribute('data-navbar-appearance', 'darker');
|
||||
}
|
||||
var navbarTopStyle = window.config.config.phoenixNavbarTopStyle;
|
||||
var navbarTop = document.querySelector('.navbar-top');
|
||||
if (navbarTopStyle === 'darker') {
|
||||
navbarTop.setAttribute('data-navbar-appearance', 'darker');
|
||||
}
|
||||
|
||||
var navbarVerticalStyle = window.config.config.phoenixNavbarVerticalStyle;
|
||||
var navbarVertical = document.querySelector('.navbar-vertical');
|
||||
if (navbarVertical && navbarVerticalStyle === 'darker') {
|
||||
navbarVertical.setAttribute('data-navbar-appearance', 'darker');
|
||||
}
|
||||
var navbarVerticalStyle = window.config.config.phoenixNavbarVerticalStyle;
|
||||
var navbarVertical = document.querySelector('.navbar-vertical');
|
||||
if (navbarVertical && navbarVerticalStyle === 'darker') {
|
||||
navbarVertical.setAttribute('data-navbar-appearance', 'darker');
|
||||
}
|
||||
</script>
|
||||
<div class="support-chat-row">
|
||||
<div class="row-fluid support-chat">
|
||||
|
||||
@ -61,19 +61,19 @@
|
||||
rel="stylesheet"
|
||||
id="user-style-default">
|
||||
<script>
|
||||
var phoenixIsRTL = window.config.config.phoenixIsRTL;
|
||||
if (phoenixIsRTL) {
|
||||
var linkDefault = document.getElementById('style-default');
|
||||
var userLinkDefault = document.getElementById('user-style-default');
|
||||
linkDefault.setAttribute('disabled', true);
|
||||
userLinkDefault.setAttribute('disabled', true);
|
||||
document.querySelector('html').setAttribute('dir', 'rtl');
|
||||
} else {
|
||||
var linkRTL = document.getElementById('style-rtl');
|
||||
var userLinkRTL = document.getElementById('user-style-rtl');
|
||||
linkRTL.setAttribute('disabled', true);
|
||||
userLinkRTL.setAttribute('disabled', true);
|
||||
}
|
||||
var phoenixIsRTL = window.config.config.phoenixIsRTL;
|
||||
if (phoenixIsRTL) {
|
||||
var linkDefault = document.getElementById('style-default');
|
||||
var userLinkDefault = document.getElementById('user-style-default');
|
||||
linkDefault.setAttribute('disabled', true);
|
||||
userLinkDefault.setAttribute('disabled', true);
|
||||
document.querySelector('html').setAttribute('dir', 'rtl');
|
||||
} else {
|
||||
var linkRTL = document.getElementById('style-rtl');
|
||||
var userLinkRTL = document.getElementById('user-style-rtl');
|
||||
linkRTL.setAttribute('disabled', true);
|
||||
userLinkRTL.setAttribute('disabled', true);
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
@ -111,17 +111,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
var navbarTopStyle = window.config.config.phoenixNavbarTopStyle;
|
||||
var navbarTop = document.querySelector('.navbar-top');
|
||||
if (navbarTopStyle === 'darker') {
|
||||
navbarTop.setAttribute('data-navbar-appearance', 'darker');
|
||||
}
|
||||
var navbarTopStyle = window.config.config.phoenixNavbarTopStyle;
|
||||
var navbarTop = document.querySelector('.navbar-top');
|
||||
if (navbarTopStyle === 'darker') {
|
||||
navbarTop.setAttribute('data-navbar-appearance', 'darker');
|
||||
}
|
||||
|
||||
var navbarVerticalStyle = window.config.config.phoenixNavbarVerticalStyle;
|
||||
var navbarVertical = document.querySelector('.navbar-vertical');
|
||||
if (navbarVertical && navbarVerticalStyle === 'darker') {
|
||||
navbarVertical.setAttribute('data-navbar-appearance', 'darker');
|
||||
}
|
||||
var navbarVerticalStyle = window.config.config.phoenixNavbarVerticalStyle;
|
||||
var navbarVertical = document.querySelector('.navbar-vertical');
|
||||
if (navbarVertical && navbarVerticalStyle === 'darker') {
|
||||
navbarVertical.setAttribute('data-navbar-appearance', 'darker');
|
||||
}
|
||||
</script>
|
||||
<div class="support-chat-row">
|
||||
<div class="row-fluid support-chat">
|
||||
|
||||
@ -15,7 +15,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</footer> {% endcomment %}
|
||||
|
||||
{% comment %} <footer class="footer position-absolute fs-9 bg-info-subtle">
|
||||
<div class="row g-0 justify-content-between align-items-center h-100">
|
||||
<div class="col-12 col-sm-auto text-center text-warning">
|
||||
@ -32,7 +31,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</footer> {% endcomment %}
|
||||
|
||||
{% comment %} <footer class="footer position-absolute fs-9 bg-white text-secondary">
|
||||
<div class="row g-0 justify-content-between align-items-center h-100">
|
||||
<div class="col-12 col-sm-auto text-center">
|
||||
@ -51,73 +49,64 @@
|
||||
</div>
|
||||
</div>
|
||||
</footer> {% endcomment %}
|
||||
<style>
|
||||
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
|
||||
|
||||
.improved-footer {
|
||||
.improved-footer {
|
||||
/* Kept `position-absolute` and adjusted padding */
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 90%;
|
||||
padding: 1.5rem;
|
||||
|
||||
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.improved-footer .text-body {
|
||||
color: var(--text-color) !important;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.improved-footer .fw-bold {
|
||||
font-weight: 600 !important;
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
.improved-footer a {
|
||||
color: var(--link-color) !important;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease-in-out, transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.improved-footer a:hover {
|
||||
color: #d1d5db !important; /* A slightly softer white on hover */
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.improved-footer .fas.fa-registered {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-color);
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 90%;
|
||||
padding: 1.5rem;
|
||||
|
||||
|
||||
|
||||
<footer class="improved-footer">
|
||||
<div class="container">
|
||||
<div class="row g-0 justify-content-between align-items-center h-100">
|
||||
<div class="col-12 col-sm-auto text-center">
|
||||
<span class="text-body"> © 2025 All rights reserved</span>
|
||||
<span class="fw-bold">Haikal</span> | <span class="fw-bold">هيكل</span>
|
||||
</div>
|
||||
<div class="col-12 col-sm-auto text-center">
|
||||
<span class="text-body">Powered by </span>
|
||||
<span>
|
||||
<a class="mx-1 text-secondary" href="https://tenhal.sa">
|
||||
<span>TENHAL</span> | <span>تنحل</span>
|
||||
</a>
|
||||
</span>
|
||||
<span class="fas fa-registered fw-light"></span>
|
||||
</div>
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.improved-footer .text-body {
|
||||
color: var(--text-color) !important;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.improved-footer .fw-bold {
|
||||
font-weight: 600 !important;
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
.improved-footer a {
|
||||
color: var(--link-color) !important;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease-in-out, transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.improved-footer a:hover {
|
||||
color: #d1d5db !important; /* A slightly softer white on hover */
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.improved-footer .fas.fa-registered {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-color);
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
<footer class="improved-footer">
|
||||
<div class="container">
|
||||
<div class="row g-0 justify-content-between align-items-center h-100">
|
||||
<div class="col-12 col-sm-auto text-center">
|
||||
<span class="text-body">© 2025 All rights reserved</span>
|
||||
<span class="fw-bold">Haikal</span> | <span class="fw-bold">هيكل</span>
|
||||
</div>
|
||||
<div class="col-12 col-sm-auto text-center">
|
||||
<span class="text-body">Powered by</span>
|
||||
<span>
|
||||
<a class="mx-1 text-secondary" href="https://tenhal.sa">
|
||||
<span>TENHAL</span> | <span>تنحل</span>
|
||||
</a>
|
||||
</span>
|
||||
<span class="fas fa-registered fw-light"></span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@ -10,49 +10,51 @@
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<main class="d-flex align-items-center justify-content-center min-vh-80 py-5">
|
||||
<div class="col-12 col-sm-10 col-md-8 col-lg-6 col-xl-5">
|
||||
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
|
||||
<div class="card-header bg-gradient py-4 border-0 rounded-top-4">
|
||||
<h3 class="mb-0 fs-4 fw-bold text-center">
|
||||
{% if object %}
|
||||
{% trans "Update Group" %}
|
||||
<i class="fa-solid fa-user-group ms-2"></i>
|
||||
{% else %}
|
||||
{% trans "Create Group" %}
|
||||
<i class="fa-solid fa-user-plus ms-2"></i>
|
||||
{% endif %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<form method="post" class="needs-validation" novalidate>
|
||||
{% csrf_token %}
|
||||
{{ redirect_field }}
|
||||
{{ form|crispy }}
|
||||
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger mt-4" role="alert">
|
||||
<h4 class="alert-heading small">{% trans "Please correct the following errors:" %}</h4>
|
||||
<ul class="mb-0">
|
||||
{% for field, errors in form.errors.items %}
|
||||
<li><strong>{{ field|capfirst }}:</strong> {% for error in errors %}{{ error }}{% endfor %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<main class="d-flex align-items-center justify-content-center min-vh-80 py-5">
|
||||
<div class="col-12 col-sm-10 col-md-8 col-lg-6 col-xl-5">
|
||||
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
|
||||
<div class="card-header bg-gradient py-4 border-0 rounded-top-4">
|
||||
<h3 class="mb-0 fs-4 fw-bold text-center">
|
||||
{% if object %}
|
||||
{% trans "Update Group" %}
|
||||
<i class="fa-solid fa-user-group ms-2"></i>
|
||||
{% else %}
|
||||
{% trans "Create Group" %}
|
||||
<i class="fa-solid fa-user-plus ms-2"></i>
|
||||
{% endif %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<form method="post" class="needs-validation" novalidate>
|
||||
{% csrf_token %}
|
||||
{{ redirect_field }}
|
||||
{{ form|crispy }}
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger mt-4" role="alert">
|
||||
<h4 class="alert-heading small">{% trans "Please correct the following errors:" %}</h4>
|
||||
<ul class="mb-0">
|
||||
{% for field, errors in form.errors.items %}
|
||||
<li>
|
||||
<strong>{{ field|capfirst }}:</strong>
|
||||
{% for error in errors %}{{ error }}{% endfor %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
<hr class="my-4">
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
|
||||
<button class="btn btn-phoenix-primary btn-lg md-me-2" type="submit">
|
||||
<i class="fa-solid fa-floppy-disk me-1"></i>{% trans "Save" %}
|
||||
</button>
|
||||
<a href="{% url 'group_list' request.dealer.slug %}"
|
||||
class="btn btn-phoenix-secondary btn-lg">
|
||||
<i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<hr class="my-4">
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
|
||||
<button class="btn btn-phoenix-primary btn-lg md-me-2" type="submit">
|
||||
<i class="fa-solid fa-floppy-disk me-1"></i>{% trans "Save" %}
|
||||
</button>
|
||||
<a href="{% url 'group_list' request.dealer.slug %}" class="btn btn-phoenix-secondary btn-lg">
|
||||
<i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@ -6,70 +6,73 @@
|
||||
{% trans "Groups" %}
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
<main class="py-5">
|
||||
<div class="container">
|
||||
{% if groups or request.GET.q %}
|
||||
<div class="card border-0 rounded-4 animate__animated animate__fadeInUp">
|
||||
<div class="card-header border-bottom d-flex flex-column flex-md-row justify-content-between align-items-md-center p-4">
|
||||
<h5 class="card-title mb-2 mb-md-0 me-md-4 fw-bold"> <i class="fa-solid fa-user-group fs-3 me-1 text-primary "></i>{% trans "Groups" %}</h5>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'group_create' request.dealer.slug %}"
|
||||
class="btn btn-phoenix-primary">
|
||||
<i class="fa-solid fa-user-group fs-9 me-1"></i>
|
||||
<span class="fas fa-plus me-2"></span>{% trans "Add Group" %}
|
||||
</a>
|
||||
<a href="{% url 'user_list' request.dealer.slug %}"
|
||||
class="btn btn-phoenix-secondary">
|
||||
<span class="fas fas fa-arrow-left me-2"></span>{% trans "Back to Staffs" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive scrollbar mx-n1 px-1 mt-3">
|
||||
<table class="table align-items-center table-hover mb-0">
|
||||
<thead>
|
||||
<tr class="bg-light">
|
||||
<th scope="col" class="text-secondary text-uppercase fw-bold ps-4">{% trans 'name'|capfirst %}</th>
|
||||
<th scope="col" class="text-secondary text-uppercase fw-bold">{% trans 'total Users'|capfirst %}</th>
|
||||
<th scope="col" class="text-secondary text-uppercase fw-bold">{% trans 'total permission'|capfirst %}</th>
|
||||
<th scope="col" class="text-secondary text-uppercase fw-bold text-end pe-4">{% trans 'actions'|capfirst %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for group in groups %}
|
||||
<tr>
|
||||
<td class="align-middle white-space-nowrap ps-4">{{ group.name }}</td>
|
||||
<td class="align-middle white-space-nowrap">
|
||||
<i class="fa-solid fa-users me-1"></i> {{ group.users.count }}
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap">
|
||||
<i class="fa-solid fa-unlock me-1"></i> {{ group.permissions.count }}
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap text-end pe-4">
|
||||
<a class="btn btn-phoenix-secondary btn-sm"
|
||||
href="{% url 'group_detail' request.dealer.slug group.id %}">
|
||||
<i class="fa-solid fa-eye me-1"></i>
|
||||
{% trans 'view'|capfirst %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% if page_obj.paginator.num_pages > 1 %}
|
||||
<div class="card-footer bg-light border-top">
|
||||
<div class="d-flex justify-content-end">
|
||||
{% include 'partials/pagination.html' %}
|
||||
<main class="py-5">
|
||||
<div class="container">
|
||||
{% if groups or request.GET.q %}
|
||||
<div class="card border-0 rounded-4 animate__animated animate__fadeInUp">
|
||||
<div class="card-header border-bottom d-flex flex-column flex-md-row justify-content-between align-items-md-center p-4">
|
||||
<h5 class="card-title mb-2 mb-md-0 me-md-4 fw-bold">
|
||||
<i class="fa-solid fa-user-group fs-3 me-1 text-primary "></i>{% trans "Groups" %}
|
||||
</h5>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'group_create' request.dealer.slug %}"
|
||||
class="btn btn-phoenix-primary">
|
||||
<i class="fa-solid fa-user-group fs-9 me-1"></i>
|
||||
<span class="fas fa-plus me-2"></span>{% trans "Add Group" %}
|
||||
</a>
|
||||
<a href="{% url 'user_list' request.dealer.slug %}"
|
||||
class="btn btn-phoenix-secondary">
|
||||
<span class="fas fas fa-arrow-left me-2"></span>{% trans "Back to Staffs" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% url "group_create" request.dealer.slug as create_group_url %}
|
||||
{% include "empty-illustration-page.html" with value="group" url=create_group_url %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive scrollbar mx-n1 px-1 mt-3">
|
||||
<table class="table align-items-center table-hover mb-0">
|
||||
<thead>
|
||||
<tr class="bg-light">
|
||||
<th scope="col" class="text-secondary text-uppercase fw-bold ps-4">{% trans 'name'|capfirst %}</th>
|
||||
<th scope="col" class="text-secondary text-uppercase fw-bold">{% trans 'total Users'|capfirst %}</th>
|
||||
<th scope="col" class="text-secondary text-uppercase fw-bold">{% trans 'total permission'|capfirst %}</th>
|
||||
<th scope="col"
|
||||
class="text-secondary text-uppercase fw-bold text-end pe-4">
|
||||
{% trans 'actions'|capfirst %}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for group in groups %}
|
||||
<tr>
|
||||
<td class="align-middle white-space-nowrap ps-4">{{ group.name }}</td>
|
||||
<td class="align-middle white-space-nowrap">
|
||||
<i class="fa-solid fa-users me-1"></i> {{ group.users.count }}
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap">
|
||||
<i class="fa-solid fa-unlock me-1"></i> {{ group.permissions.count }}
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap text-end pe-4">
|
||||
<a class="btn btn-phoenix-secondary btn-sm"
|
||||
href="{% url 'group_detail' request.dealer.slug group.id %}">
|
||||
<i class="fa-solid fa-eye me-1"></i>
|
||||
{% trans 'view'|capfirst %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% if page_obj.paginator.num_pages > 1 %}
|
||||
<div class="card-footer bg-light border-top">
|
||||
<div class="d-flex justify-content-end">{% include 'partials/pagination.html' %}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% url "group_create" request.dealer.slug as create_group_url %}
|
||||
{% include "empty-illustration-page.html" with value="group" url=create_group_url %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@ -21,144 +21,138 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<form method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
||||
<input type="text" class="form-control" id="permissionSearch"
|
||||
placeholder="{% trans 'Search permissions...' %}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="alert alert-info py-2 mb-0">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
{% trans "Checked items are currently assigned permissions" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4" id="permissionsGrid">
|
||||
{% for app_label, models in grouped_permissions.items %}
|
||||
<div class="col"> {# This div opens for each app_label #}
|
||||
<div class="card h-100 border-{% if app_label in group_permission_apps %}primary{% else %}light{% endif %}">
|
||||
<div class="card-header bg-{% if app_label in group_permission_apps %}primary text-white{% else %}light{% endif %}">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-{% if app_label in group_permission_apps %}check-circle{% else %}cube{% endif %} me-2"></i>
|
||||
{{ app_label|capfirst }}
|
||||
</h5>
|
||||
<span class="badge bg-{% if app_label in group_permission_apps %}light text-primary{% else %}secondary{% endif %}">
|
||||
{{ models|length }} {% trans "categories" %}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<form method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="permissionSearch"
|
||||
placeholder="{% trans 'Search permissions...' %}">
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="accordion" id="accordion-{{ app_label|slugify }}">
|
||||
{% for model, perms in models.items %}
|
||||
<div class="accordion-item border-0 mb-2">
|
||||
<h6 class="accordion-header" id="heading-{{ app_label|slugify }}-{{ model|slugify }}">
|
||||
<button class="accordion-button collapsed bg-white shadow-none py-2"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#collapse-{{ app_label|slugify }}-{{ model|slugify }}"
|
||||
aria-expanded="false"
|
||||
aria-controls="collapse-{{ app_label|slugify }}-{{ model|slugify }}">
|
||||
<i class="fas fa-{% if model == 'Custom' %}star{% else %}table{% endif %} me-2"></i>
|
||||
{{ model|capfirst }}
|
||||
<span class="badge bg-{% if model in group_permission_models %}primary{% else %}secondary{% endif %} rounded-pill ms-2">
|
||||
{# This is where you might need the custom filter 'count_checked' #}
|
||||
{{ perms|length }} / {{ perms|count_checked:group_permission_ids }}
|
||||
</span>
|
||||
</button>
|
||||
</h6>
|
||||
<div id="collapse-{{ app_label|slugify }}-{{ model|slugify }}"
|
||||
class="accordion-collapse collapse"
|
||||
aria-labelledby="heading-{{ app_label|slugify }}-{{ model|slugify }}"
|
||||
data-bs-parent="#accordion-{{ app_label|slugify }}">
|
||||
<div class="accordion-body pt-0 px-0">
|
||||
<div class="list-group list-group-flush">
|
||||
{% for perm in perms %}
|
||||
<label class="list-group-item d-flex gap-2 {% if perm.id in group_permission_ids %}bg-light-primary{% endif %}">
|
||||
<input class="form-check-input flex-shrink-0 mt-0"
|
||||
type="checkbox"
|
||||
name="permissions"
|
||||
value="{{ perm.id }}"
|
||||
id="perm_{{ perm.id }}"
|
||||
{% if perm.id in group_permission_ids %}checked{% endif %}>
|
||||
<span>
|
||||
<span class="d-block fw-bold">{{ perm.name|capfirst }}</span>
|
||||
<small class="d-block text-muted">{{ perm.codename }}</small>
|
||||
{% if model == 'Custom' %}
|
||||
<span class="badge bg-info mt-1">
|
||||
<i class="fas fa-star me-1"></i>{% trans "Custom" %}
|
||||
</span>
|
||||
{% elif perm.id in group_permission_ids %}
|
||||
<span class="badge bg-success mt-1">
|
||||
<i class="fas fa-check me-1"></i>{% trans "Assigned" %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="alert alert-info py-2 mb-0">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
{% trans "Checked items are currently assigned permissions" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4"
|
||||
id="permissionsGrid">
|
||||
{% for app_label, models in grouped_permissions.items %}
|
||||
<div class="col">
|
||||
{# This div opens for each app_label #}
|
||||
<div class="card h-100 border-{% if app_label in group_permission_apps %}primary{% else %}light{% endif %}">
|
||||
<div class="card-header bg-{% if app_label in group_permission_apps %}primary text-white{% else %}light{% endif %}">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-{% if app_label in group_permission_apps %}check-circle{% else %}cube{% endif %} me-2"></i>
|
||||
{{ app_label|capfirst }}
|
||||
</h5>
|
||||
<span class="badge bg-{% if app_label in group_permission_apps %}light text-primary{% else %}secondary{% endif %}">
|
||||
{{ models|length }} {% trans "categories" %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="card-body">
|
||||
<div class="accordion" id="accordion-{{ app_label|slugify }}">
|
||||
{% for model, perms in models.items %}
|
||||
<div class="accordion-item border-0 mb-2">
|
||||
<h6 class="accordion-header"
|
||||
id="heading-{{ app_label|slugify }}-{{ model|slugify }}">
|
||||
<button class="accordion-button collapsed bg-white shadow-none py-2"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#collapse-{{ app_label|slugify }}-{{ model|slugify }}"
|
||||
aria-expanded="false"
|
||||
aria-controls="collapse-{{ app_label|slugify }}-{{ model|slugify }}">
|
||||
<i class="fas fa-{% if model == 'Custom' %}star{% else %}table{% endif %} me-2"></i>
|
||||
{{ model|capfirst }}
|
||||
<span class="badge bg-{% if model in group_permission_models %}primary{% else %}secondary{% endif %} rounded-pill ms-2">
|
||||
{# This is where you might need the custom filter 'count_checked' #}
|
||||
{{ perms|length }} / {{ perms|count_checked:group_permission_ids }}
|
||||
</span>
|
||||
</button>
|
||||
</h6>
|
||||
<div id="collapse-{{ app_label|slugify }}-{{ model|slugify }}"
|
||||
class="accordion-collapse collapse"
|
||||
aria-labelledby="heading-{{ app_label|slugify }}-{{ model|slugify }}"
|
||||
data-bs-parent="#accordion-{{ app_label|slugify }}">
|
||||
<div class="accordion-body pt-0 px-0">
|
||||
<div class="list-group list-group-flush">
|
||||
{% for perm in perms %}
|
||||
<label class="list-group-item d-flex gap-2 {% if perm.id in group_permission_ids %}bg-light-primary{% endif %}">
|
||||
<input class="form-check-input flex-shrink-0 mt-0"
|
||||
type="checkbox"
|
||||
name="permissions"
|
||||
value="{{ perm.id }}"
|
||||
id="perm_{{ perm.id }}"
|
||||
{% if perm.id in group_permission_ids %}checked{% endif %}>
|
||||
<span>
|
||||
<span class="d-block fw-bold">{{ perm.name|capfirst }}</span>
|
||||
<small class="d-block text-muted">{{ perm.codename }}</small>
|
||||
{% if model == 'Custom' %}
|
||||
<span class="badge bg-info mt-1">
|
||||
<i class="fas fa-star me-1"></i>{% trans "Custom" %}
|
||||
</span>
|
||||
{% elif perm.id in group_permission_ids %}
|
||||
<span class="badge bg-success mt-1">
|
||||
<i class="fas fa-check me-1"></i>{% trans "Assigned" %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="row mt-4 mb-4">
|
||||
<div class="col">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<span class="badge bg-primary rounded-pill me-2">{{ group_permission_ids|length }} {% trans "selected" %}</span>
|
||||
<span class="text-muted">{% trans "Permissions will be updated immediately" %}</span>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="btn btn-lg btn-primary me-2">
|
||||
<i class="fas fa-save me-1"></i>{% trans "Save Changes" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mt-4 mb-4">
|
||||
<div class="col">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<span class="badge bg-primary rounded-pill me-2">
|
||||
{{ group_permission_ids|length }} {% trans "selected" %}
|
||||
</span>
|
||||
<span class="text-muted">
|
||||
{% trans "Permissions will be updated immediately" %}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
<button type="submit" class="btn btn-lg btn-primary me-2">
|
||||
<i class="fas fa-save me-1"></i>{% trans "Save Changes" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.bg-light-primary {
|
||||
background-color: rgba(13, 110, 253, 0.1);
|
||||
}
|
||||
.list-group-item:hover {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
.accordion-button:not(.collapsed) {
|
||||
box-shadow: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
.accordion-button:focus {
|
||||
box-shadow: none;
|
||||
border-color: rgba(0,0,0,.125);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
</form>
|
||||
</div>
|
||||
<style>
|
||||
.bg-light-primary {
|
||||
background-color: rgba(13, 110, 253, 0.1);
|
||||
}
|
||||
.list-group-item:hover {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
.accordion-button:not(.collapsed) {
|
||||
box-shadow: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
.accordion-button:focus {
|
||||
box-shadow: none;
|
||||
border-color: rgba(0,0,0,.125);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize all accordions
|
||||
document.querySelectorAll('.accordion-button').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
|
||||
@ -71,20 +71,12 @@
|
||||
<div class="d-flex gap-3">
|
||||
<span id="clearChatBtn"
|
||||
class="translate-middle-y cursor-pointer"
|
||||
title="{% if LANGUAGE_CODE == 'ar' %}
|
||||
مسح المحادثة
|
||||
{% else %}
|
||||
Clear Chat
|
||||
{% endif %}">
|
||||
title="{% if LANGUAGE_CODE == 'ar' %} مسح المحادثة {% else %} Clear Chat {% endif %}">
|
||||
<i class="fas fa-trash-alt text-danger"></i>
|
||||
</span>
|
||||
<span id="exportChatBtn"
|
||||
class="translate-middle-y cursor-pointer"
|
||||
title="{% if LANGUAGE_CODE == 'ar' %}
|
||||
تصدير المحادثة
|
||||
{% else %}
|
||||
Export Chat
|
||||
{% endif %}">
|
||||
title="{% if LANGUAGE_CODE == 'ar' %} تصدير المحادثة {% else %} Export Chat {% endif %}">
|
||||
<i class="fas fa-download text-success"></i>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -3,11 +3,7 @@
|
||||
{% block content %}
|
||||
{% if request.user.is_authenticated %}
|
||||
<div id="dashboard-content"
|
||||
hx-get="{% if request.is_sales and not request.is_manager %}
|
||||
{% url 'sales_dashboard' request.dealer.slug %}
|
||||
{% else %}
|
||||
{% url 'general_dashboard' request.dealer.slug %}
|
||||
{% endif %}"
|
||||
hx-get="{% if request.is_sales and not request.is_manager %} {% url 'sales_dashboard' request.dealer.slug %} {% else %} {% url 'general_dashboard' request.dealer.slug %} {% endif %}"
|
||||
hx-trigger="load"
|
||||
hx-target="#dashboard-content"
|
||||
hx-swap="innerHTML">
|
||||
@ -17,6 +13,4 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@ -1,67 +1,69 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{%block title%} {%trans 'Add Colors'%} {% endblock%}
|
||||
{% block content %}
|
||||
<div class="row mt-4 mb-3">
|
||||
<h3 class="text-center">{% trans "Add Colors" %}</h3>
|
||||
<p class="text-center">
|
||||
{% trans "Select exterior and interior colors for" %} {{ car.id_car_make.get_local_name }} {{ car.id_car_model.get_local_name }}
|
||||
</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<!-- Exterior Colors -->
|
||||
<div class="row g-4">
|
||||
<p class="fs-5 mb-0">{% trans 'Exterior Colors' %}</p>
|
||||
{% for color in form.fields.exterior.queryset %}
|
||||
<div class="col-lg-4 col-xl-2">
|
||||
<div class="card rounded shadow-sm color-card">
|
||||
<label class="color-option">
|
||||
<input class="color-radio"
|
||||
type="radio"
|
||||
name="exterior"
|
||||
value="{{ color.id }}" {% if color.id == form.instance.exterior.id %}checked{% endif %}>
|
||||
|
||||
<div class="card-body color-display"
|
||||
style="background-color: rgb({{ color.rgb }})">
|
||||
<div class="">
|
||||
<small>{{ color.get_local_name }}</small>
|
||||
{% block title %}
|
||||
{% trans 'Add Colors' %} {% endblock %}
|
||||
{% block content %}
|
||||
<div class="row mt-4 mb-3">
|
||||
<h3 class="text-center">{% trans "Add Colors" %}</h3>
|
||||
<p class="text-center">
|
||||
{% trans "Select exterior and interior colors for" %} {{ car.id_car_make.get_local_name }} {{ car.id_car_model.get_local_name }}
|
||||
</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<!-- Exterior Colors -->
|
||||
<div class="row g-4">
|
||||
<p class="fs-5 mb-0">{% trans 'Exterior Colors' %}</p>
|
||||
{% for color in form.fields.exterior.queryset %}
|
||||
<div class="col-lg-4 col-xl-2">
|
||||
<div class="card rounded shadow-sm color-card">
|
||||
<label class="color-option">
|
||||
<input class="color-radio"
|
||||
type="radio"
|
||||
name="exterior"
|
||||
value="{{ color.id }}"
|
||||
{% if color.id == form.instance.exterior.id %}checked{% endif %}>
|
||||
<div class="card-body color-display"
|
||||
style="background-color: rgb({{ color.rgb }})">
|
||||
<div class="">
|
||||
<small>{{ color.get_local_name }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<!-- Interior Colors -->
|
||||
<p class="fs-5 mt-3 mb-0">{% trans 'Interior Colors' %}</p>
|
||||
{% for color in form.fields.interior.queryset %}
|
||||
<div class="col-lg-4 col-xl-2">
|
||||
<div class="card rounded shadow-sm color-card">
|
||||
<label class="color-option">
|
||||
<input class="color-radio"
|
||||
type="radio"
|
||||
name="interior"
|
||||
value="{{ color.id }}" {% if color.id == form.instance.interior.id %}checked{% endif %}>
|
||||
<div class="card-body color-display"
|
||||
style="background-color: rgb({{ color.rgb }})">
|
||||
<div class="">
|
||||
<small>{{ color.get_local_name }}</small>
|
||||
{% endfor %}
|
||||
<!-- Interior Colors -->
|
||||
<p class="fs-5 mt-3 mb-0">{% trans 'Interior Colors' %}</p>
|
||||
{% for color in form.fields.interior.queryset %}
|
||||
<div class="col-lg-4 col-xl-2">
|
||||
<div class="card rounded shadow-sm color-card">
|
||||
<label class="color-option">
|
||||
<input class="color-radio"
|
||||
type="radio"
|
||||
name="interior"
|
||||
value="{{ color.id }}"
|
||||
{% if color.id == form.instance.interior.id %}checked{% endif %}>
|
||||
<div class="card-body color-display"
|
||||
style="background-color: rgb({{ color.rgb }})">
|
||||
<div class="">
|
||||
<small>{{ color.get_local_name }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-center mt-4">
|
||||
<button class="btn btn-lg btn-phoenix-primary me-2" type="submit">
|
||||
<i class="fa-solid fa-floppy-disk me-1"></i>{{ _("Save") }}
|
||||
</button>
|
||||
<a href="{% url 'car_detail' request.dealer.slug car.slug %}" class="btn btn-lg btn-phoenix-secondary"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<style>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="d-flex justify-content-center mt-4">
|
||||
<button class="btn btn-lg btn-phoenix-primary me-2" type="submit">
|
||||
<i class="fa-solid fa-floppy-disk me-1"></i>{{ _("Save") }}
|
||||
</button>
|
||||
<a href="{% url 'car_detail' request.dealer.slug car.slug %}"
|
||||
class="btn btn-lg btn-phoenix-secondary"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<style>
|
||||
.color-card {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
@ -93,7 +95,7 @@
|
||||
}
|
||||
|
||||
.color-radio:focus + .color-display {
|
||||
border: 3px solid rgb(44, 229, 44);
|
||||
border: 3px solid rgb(44, 229, 44);
|
||||
box-shadow: 0 0 10px rgba(44, 123, 229, 0.5);
|
||||
}
|
||||
|
||||
|
||||
@ -1,41 +1,34 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}Delete Car{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="d-flex align-items-center justify-content-center min-vh-50 py-5">
|
||||
<div class="col-md-6 ">
|
||||
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
|
||||
|
||||
<div class="card-body p-4 p-md-5 text-center bg-gradient">
|
||||
<div class="mb-4">
|
||||
<i class="fa-solid fa-triangle-exclamation text-danger" style="font-size: 2rem;"></i>
|
||||
<main class="d-flex align-items-center justify-content-center min-vh-50 py-5">
|
||||
<div class="col-md-6 ">
|
||||
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
|
||||
<div class="card-body p-4 p-md-5 text-center bg-gradient">
|
||||
<div class="mb-4">
|
||||
<i class="fa-solid fa-triangle-exclamation text-danger"
|
||||
style="font-size: 2rem"></i>
|
||||
</div>
|
||||
<h1 class="card-title fw-bold mb-3 fs-4">{% trans 'Confirm Deletion' %}</h1>
|
||||
<p class="fs-7 mb-4">{% trans "Are you absolutely sure you want to delete the car" %}</p>
|
||||
<p class="fs-6 mb-4">
|
||||
"<strong class="">{{ car }}</strong>"?
|
||||
</p>
|
||||
<p class="fs-7 mb-4">{% trans "This action is permanent and cannot be undone." %}</p>
|
||||
<form method="post"
|
||||
class="d-grid gap-3 d-sm-flex justify-content-sm-center">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-phoenix-danger btn-lg px-5">
|
||||
<i class="fa-solid fa-trash-can me-2"></i>{% trans 'Confirm Delete' %}
|
||||
</button>
|
||||
<a href="{% url 'car_list' request.dealer.slug %}"
|
||||
class="btn btn-phoenix-secondary btn-lg px-5">
|
||||
<i class="fa-solid fa-ban me-2"></i>{% trans 'Cancel' %}
|
||||
</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<h1 class="card-title fw-bold mb-3 fs-4">{% trans 'Confirm Deletion' %}</h1>
|
||||
|
||||
<p class="fs-7 mb-4">
|
||||
{% trans "Are you absolutely sure you want to delete the car" %}
|
||||
</p>
|
||||
<p class="fs-6 mb-4">
|
||||
"<strong class="">{{ car }}</strong>"?
|
||||
</p>
|
||||
<p class="fs-7 mb-4">
|
||||
{% trans "This action is permanent and cannot be undone." %}
|
||||
</p>
|
||||
|
||||
<form method="post" class="d-grid gap-3 d-sm-flex justify-content-sm-center">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-phoenix-danger btn-lg px-5">
|
||||
<i class="fa-solid fa-trash-can me-2"></i>{% trans 'Confirm Delete' %}
|
||||
</button>
|
||||
<a href="{% url 'car_list' request.dealer.slug %}" class="btn btn-phoenix-secondary btn-lg px-5">
|
||||
<i class="fa-solid fa-ban me-2"></i>{% trans 'Cancel' %}
|
||||
</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user