Compare commits
11 Commits
88769d4a89
...
966633fe3b
| Author | SHA1 | Date | |
|---|---|---|---|
| 966633fe3b | |||
| 36f279a386 | |||
| a697d00dd1 | |||
| ba180dcbd0 | |||
| e9f301ee20 | |||
| 4b6de7794b | |||
| ac01d944dd | |||
| c880eb88b6 | |||
| 2bbcae2e7a | |||
| 4d63b17e68 | |||
| 1b5d0fbf7d |
@ -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")
|
||||
)
|
||||
@ -757,7 +762,8 @@ class Car(Base):
|
||||
make = self.id_car_make.name if self.id_car_make else "Unknown Make"
|
||||
model = self.id_car_model.name if self.id_car_model else "Unknown Model"
|
||||
trim = self.id_car_trim.name if self.id_car_trim else "Unknown Trim"
|
||||
return f"{self.year} - {make} - {model} - {trim}"
|
||||
vin=self.vin if self.vin else None
|
||||
return f"{self.year} - {make} - {model} - {trim}-{vin}"
|
||||
|
||||
@property
|
||||
def product(self):
|
||||
@ -773,6 +779,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 +794,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()
|
||||
|
||||
@ -803,8 +816,8 @@ class Car(Base):
|
||||
color = ""
|
||||
try:
|
||||
color = self.colors.exterior.name if self.colors else ""
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting color for car {self.vin} error: {e}")
|
||||
make = self.id_car_make.name if self.id_car_make else ""
|
||||
model = self.id_car_model.name if self.id_car_model else ""
|
||||
year = self.year if self.year else 0
|
||||
@ -873,39 +886,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 +941,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 +992,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 +1028,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 +1338,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 +1396,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 +1447,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 +1462,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 +1480,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 +1519,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 +1584,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 +1687,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 +1719,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 +1755,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 +1825,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 +1868,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 +1881,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 +2023,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 +2047,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 +2241,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 +2313,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 +2323,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 +2742,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 +3302,6 @@ class CustomGroup(models.Model):
|
||||
"activity",
|
||||
"payment",
|
||||
"vendor",
|
||||
|
||||
],
|
||||
other_perms=[
|
||||
"view_car",
|
||||
@ -3236,8 +3312,7 @@ class CustomGroup(models.Model):
|
||||
"view_saleorder",
|
||||
"view_leads",
|
||||
"view_opportunity",
|
||||
'view_customer'
|
||||
|
||||
"view_customer",
|
||||
],
|
||||
)
|
||||
self.set_permissions(
|
||||
@ -3533,7 +3608,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 +3621,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 +3636,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 +3659,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 +3672,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 +3697,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 +3716,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 +3726,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 +3744,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 +3778,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 +3810,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 +3835,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
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
from django.urls import reverse
|
||||
@ -28,6 +29,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 +179,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 +279,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 +1045,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 +1096,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 +1144,7 @@ def estimate_in_review_notification(sender, instance, created, **kwargs):
|
||||
url=reverse(
|
||||
"estimate_detail",
|
||||
kwargs={"dealer_slug": dealer.slug, "pk": instance.pk},
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@ -1188,7 +1201,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 +1241,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 +1265,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 +1292,40 @@ 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.hash = car.get_hash
|
||||
car.save()
|
||||
# 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 = os.path.exists(os.path.join(settings.MEDIA_ROOT, "car_images",car.get_hash + ".png"))
|
||||
# existing = (
|
||||
# models.CarImage.objects.filter(
|
||||
# image_hash=car.get_hash, image__isnull=False
|
||||
# )
|
||||
# .first()
|
||||
# )
|
||||
|
||||
if existing:
|
||||
logger.info(f"Found existing image for car {car.vin}")
|
||||
# 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:
|
||||
logger.info(f"Generating image for car {car.vin}")
|
||||
# Schedule async generation
|
||||
async_task(
|
||||
'inventory.tasks.generate_car_image_task',
|
||||
car_image.id,
|
||||
task_name=f"generate_car_image_{car.vin}"
|
||||
"inventory.tasks.generate_car_image_task",
|
||||
car.pk,
|
||||
task_name=f"generate_car_image_{car.vin}",
|
||||
)
|
||||
# async_task(
|
||||
# "inventory.tasks.generate_car_image_task",
|
||||
# car_image.id,
|
||||
# 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,20 @@ 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,
|
||||
Car
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
@ -52,6 +61,7 @@ def create_settings(pk):
|
||||
.first(),
|
||||
)
|
||||
|
||||
|
||||
def create_coa_accounts(**kwargs):
|
||||
logger.info("creating all accounts are created")
|
||||
instance = kwargs.get("dealer")
|
||||
@ -62,9 +72,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 +807,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 +814,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 +866,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 +875,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 +884,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 +952,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 +970,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 +980,21 @@ 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)
|
||||
# car_image = CarImage.objects.get(id=car_image_id)
|
||||
car = Car.objects.get(pk=car_image_id)
|
||||
result = generate_car_image_simple(car)
|
||||
|
||||
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}
|
||||
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,
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import os
|
||||
import json
|
||||
import secrets
|
||||
import logging
|
||||
@ -73,15 +74,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 +167,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 +234,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 +255,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 +1036,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 +1069,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 +1092,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 +1116,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 +1136,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 +1147,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 +1194,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 +1227,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 +1250,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 +1274,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 +1294,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 +1306,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 +1574,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 +1585,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 +1594,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 +1634,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 +1650,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 +1674,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 +1683,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 +1693,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 +1712,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 +1720,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 +1733,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 +1829,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 +1899,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 +1986,6 @@ def handle_payment(request, order):
|
||||
# return user.dealer.quota
|
||||
|
||||
|
||||
|
||||
def get_accounts_data():
|
||||
return [
|
||||
# Current Assets (must start with 1)
|
||||
@ -2339,6 +2381,7 @@ def get_accounts_data():
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def create_account(entity, coa, account_data):
|
||||
try:
|
||||
account = entity.create_account(
|
||||
@ -2371,17 +2414,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 +2436,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 +2457,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 +2480,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 +2488,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 +2517,39 @@ class CarImageAPIClient:
|
||||
|
||||
try:
|
||||
headers = {
|
||||
'X-CSRFToken': self.csrf_token,
|
||||
'Referer': self.BASE_URL,
|
||||
"X-CSRFToken": self.csrf_token,
|
||||
"Referer": self.BASE_URL,
|
||||
}
|
||||
print(payload)
|
||||
logger.info(f"Generating image with payload: {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")
|
||||
logger.info(f"Generated image URL: {image_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 +2565,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 +2586,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,18 +2618,18 @@ def resize_image(image_data, max_size=(800, 600)):
|
||||
logger.error(error_msg)
|
||||
return None, error_msg
|
||||
|
||||
def generate_car_image_simple(car_image):
|
||||
|
||||
def generate_car_image_simple(car):
|
||||
"""
|
||||
Simple function to generate car image with authentication and resizing
|
||||
"""
|
||||
car = car_image.car
|
||||
|
||||
# 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 +2638,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 +2655,24 @@ 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
|
||||
)
|
||||
logger.info(f" {car.vin}")
|
||||
with open(
|
||||
os.path.join(settings.MEDIA_ROOT, f"car_images/{car.get_hash}.{file_extension}"),
|
||||
"wb",
|
||||
) as f:
|
||||
f.write(resized_data)
|
||||
logger.info(f"Saved image for car {car.vin}")
|
||||
|
||||
# Update CarImage record
|
||||
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)
|
||||
|
||||
@ -4979,7 +4979,6 @@ def create_estimate(request, dealer_slug, slug=None):
|
||||
.annotate(hash_count=Count("hash"))
|
||||
.distinct()
|
||||
)
|
||||
|
||||
context = {
|
||||
"form": form,
|
||||
"items": [
|
||||
|
||||
@ -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)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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="true"
|
||||
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,26 @@
|
||||
{% 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.active.label_tag }}
|
||||
{{ form.active }}
|
||||
{% if form.active.help_text %}<small class="form-text text-muted">{{ form.active.help_text }}</small>{% endif %}
|
||||
{% for error in form.active.errors %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
{{ form.description.label_tag }}
|
||||
@ -26,19 +29,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 +43,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 or request.is_manager%}
|
||||
{% trans "Business Health Dashboard" %}
|
||||
{% endif %}
|
||||
{% if request.is_inventory and not request.is_manager and not request.is_dealer %}
|
||||
{% trans "Inventory Dashboard" %}
|
||||
{% endif %}
|
||||
{% if request.is_accountant and not request.is_manager and not request.is_dealer %}
|
||||
{% 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 %}
|
||||
|
||||
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