Compare commits

...

2 Commits

Author SHA1 Message Date
2bbcae2e7a lint and formate 2025-08-27 13:04:41 +03:00
4d63b17e68 merge complete 2025-08-27 12:59:19 +03:00
199 changed files with 13812 additions and 13143 deletions

View File

@ -10,9 +10,11 @@ https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
# asgi.py # asgi.py
import os import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings")
import django import django
django.setup() django.setup()
@ -30,11 +32,17 @@ from django.core.asgi import get_asgi_application
# # "websocket": AuthMiddlewareStack(URLRouter(routing.websocket_urlpatterns)), # # "websocket": AuthMiddlewareStack(URLRouter(routing.websocket_urlpatterns)),
# } # }
# ) # )
application = ProtocolTypeRouter({ application = ProtocolTypeRouter(
"http": AuthMiddlewareStack( {
URLRouter([ "http": AuthMiddlewareStack(
path("sse/notifications/", NotificationSSEApp()), URLRouter(
re_path(r"", get_asgi_application()), # All other routes go to Django [
]) path("sse/notifications/", NotificationSSEApp()),
), re_path(
}) r"", get_asgi_application()
), # All other routes go to Django
]
)
),
}
)

View File

@ -56,7 +56,7 @@ from .models import (
DealerSettings, DealerSettings,
Tasks, Tasks,
Recall, Recall,
Ticket Ticket,
) )
from django_ledger import models as ledger_models from django_ledger import models as ledger_models
from django.forms import ( from django.forms import (
@ -146,9 +146,16 @@ class StaffForm(forms.ModelForm):
) )
class Meta: class Meta:
model = Staff 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 # Dealer Form
@ -439,13 +446,15 @@ class CarFinanceForm(forms.ModelForm):
marked_price = cleaned_data.get("marked_price") marked_price = cleaned_data.get("marked_price")
if cost_price > 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 return cleaned_data
class Meta: class Meta:
model = Car model = Car
fields = ["cost_price","marked_price"] fields = ["cost_price", "marked_price"]
class CarLocationForm(forms.ModelForm): class CarLocationForm(forms.ModelForm):
@ -1168,7 +1177,7 @@ class ScheduleForm(forms.ModelForm):
scheduled_at = forms.DateTimeField( scheduled_at = forms.DateTimeField(
widget=DateTimeInput(attrs={"type": "datetime-local"}) 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: class Meta:
model = Schedule model = Schedule
@ -1289,6 +1298,7 @@ class OpportunityForm(forms.ModelForm):
if self.instance and self.instance.pk: if self.instance and self.instance.pk:
self.fields["probability"].initial = self.instance.probability self.fields["probability"].initial = self.instance.probability
class OpportunityStageForm(forms.ModelForm): class OpportunityStageForm(forms.ModelForm):
""" """
Represents a form for creating or editing Opportunity instances. Represents a form for creating or editing Opportunity instances.
@ -1305,17 +1315,13 @@ class OpportunityStageForm(forms.ModelForm):
:type Meta.fields: list :type Meta.fields: list
""" """
class Meta: class Meta:
model = Opportunity model = Opportunity
fields = [ fields = [
"stage", "stage",
] ]
class InvoiceModelCreateForm(InvoiceModelCreateFormBase): class InvoiceModelCreateForm(InvoiceModelCreateFormBase):
""" """
Represents a form for creating an Invoice model that inherits from a base 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.billmodeldjango_ledger.itemmodel",
"django_ledger.invoicemodel", "django_ledger.invoicemodel",
"django_ledger.vendormodel", "django_ledger.vendormodel",
"django_ledger.journalentrymodel" "django_ledger.journalentrymodeldjango_ledger.purchaseordermodel",
"django_ledger.purchaseordermodel",
] ]
permissions = cache.get( permissions = cache.get(
@ -2138,91 +2143,115 @@ class VatRateForm(forms.ModelForm):
class CustomSetPasswordForm(SetPasswordForm): class CustomSetPasswordForm(SetPasswordForm):
new_password1 = forms.CharField( new_password1 = forms.CharField(
label="New Password", 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( new_password2 = forms.CharField(
label="Confirm New Password", 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 # forms.py
class RecallFilterForm(forms.Form): class RecallFilterForm(forms.Form):
make = forms.ModelChoiceField( make = forms.ModelChoiceField(
queryset=CarMake.objects.all(), queryset=CarMake.objects.all(),
required=False, required=False,
label=_("Make"), label=_("Make"),
widget=forms.Select(attrs={'class': 'form-control'}) widget=forms.Select(attrs={"class": "form-control"}),
) )
model = forms.ModelChoiceField( model = forms.ModelChoiceField(
queryset=CarModel.objects.none(), queryset=CarModel.objects.none(),
required=False, required=False,
label=_("Model"), label=_("Model"),
widget=forms.Select(attrs={'class': 'form-control'}) widget=forms.Select(attrs={"class": "form-control"}),
) )
serie = forms.ModelChoiceField( serie = forms.ModelChoiceField(
queryset=CarSerie.objects.none(), queryset=CarSerie.objects.none(),
required=False, required=False,
label=_("Series"), label=_("Series"),
widget=forms.Select(attrs={'class': 'form-control'}) widget=forms.Select(attrs={"class": "form-control"}),
) )
trim = forms.ModelChoiceField( trim = forms.ModelChoiceField(
queryset=CarTrim.objects.none(), queryset=CarTrim.objects.none(),
required=False, required=False,
label=_("Trim"), 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): def __init__(self, *args, **kwargs):
make_id = kwargs.pop('make_id', None) make_id = kwargs.pop("make_id", None)
model_id = kwargs.pop('model_id', None) model_id = kwargs.pop("model_id", None)
serie_id = kwargs.pop('serie_id', None) serie_id = kwargs.pop("serie_id", None)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if make_id: 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: 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: 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 RecallCreateForm(forms.ModelForm):
class Meta: class Meta:
model = Recall 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 = { widgets = {
'make': forms.Select(attrs={'class': 'form-control'}), "make": forms.Select(attrs={"class": "form-control"}),
'model': forms.Select(attrs={'class': 'form-control'}), "model": forms.Select(attrs={"class": "form-control"}),
'serie': forms.Select(attrs={'class': 'form-control'}), "serie": forms.Select(attrs={"class": "form-control"}),
'trim': forms.Select(attrs={'class': 'form-control'}), "trim": forms.Select(attrs={"class": "form-control"}),
'title': forms.TextInput(attrs={'class': 'form-control'}), "title": forms.TextInput(attrs={"class": "form-control"}),
'description': forms.Textarea(attrs={'class': 'form-control'}), "description": forms.Textarea(attrs={"class": "form-control"}),
'year_from': forms.NumberInput(attrs={'class': 'form-control'}), "year_from": forms.NumberInput(attrs={"class": "form-control"}),
'year_to': forms.NumberInput(attrs={'class': 'form-control'}), "year_to": forms.NumberInput(attrs={"class": "form-control"}),
} }
class TicketForm(forms.ModelForm): class TicketForm(forms.ModelForm):
class Meta: class Meta:
model = Ticket model = Ticket
fields = ['subject', 'description', 'priority'] fields = ["subject", "description", "priority"]
widgets = { widgets = {
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 10}), "description": forms.Textarea(attrs={"class": "form-control", "rows": 10}),
} }
class TicketResolutionForm(forms.ModelForm): class TicketResolutionForm(forms.ModelForm):
class Meta: class Meta:
model = Ticket model = Ticket
fields = ['status', 'resolution_notes'] fields = ["status", "resolution_notes"]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Limit status choices to resolution options # Limit status choices to resolution options
self.fields['status'].choices = [ self.fields["status"].choices = [("resolved", "Resolved"), ("closed", "Closed")]
('resolved', 'Resolved'),
('closed', 'Closed')
]

View File

@ -1,9 +1,10 @@
import logging import logging
from inventory.models import Dealer 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__) logger = logging.getLogger(__name__)
def check_create_coa_accounts(task): def check_create_coa_accounts(task):
logger.info("Checking if all accounts are created") logger.info("Checking if all accounts are created")
instance = task.kwargs["dealer"] 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']}") logger.info(f"Default account does not exist: {account_data['code']}")
create_account(entity, coa, account_data) create_account(entity, coa, account_data)
def print_results(task): def print_results(task):
dealer= task.kwargs["dealer"] dealer = task.kwargs["dealer"]
print("HOOK: ",dealer) print("HOOK: ", dealer)
print("HOOK: ",dealer.pk) print("HOOK: ", dealer.pk)

View File

@ -8,11 +8,12 @@ from django.core.management.base import BaseCommand
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django_ledger.models import InvoiceModel,EstimateModel from django_ledger.models import InvoiceModel, EstimateModel
from inventory.models import ExtraInfo,Notification,CustomGroup from inventory.models import ExtraInfo, Notification, CustomGroup
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Command(BaseCommand): class Command(BaseCommand):
help = "Handles invoices due date reminders" help = "Handles invoices due date reminders"
@ -33,27 +34,30 @@ class Command(BaseCommand):
def invocie_expiration_reminders(self): def invocie_expiration_reminders(self):
"""Queue email reminders for expiring plans""" """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() today = timezone.now().date()
for days in reminder_days: for days in reminder_days:
target_date = today + timedelta(days=days) target_date = today + timedelta(days=days)
expiring_plans = InvoiceModel.objects.filter( expiring_plans = InvoiceModel.objects.filter(
date_due=target_date date_due=target_date
).select_related('customer','ce_model') ).select_related("customer", "ce_model")
for inv in expiring_plans: for inv in expiring_plans:
# dealer = inv.customer.customer_set.first().dealer # dealer = inv.customer.customer_set.first().dealer
subject = f"Your invoice is due in {days} days" subject = f"Your invoice is due in {days} days"
message = render_to_string('emails/invoice_past_due_reminder.txt', { message = render_to_string(
'customer_name': inv.customer.customer_name, "emails/invoice_past_due_reminder.txt",
'invoice_number': inv.invoice_number, {
'amount_due': inv.amount_due, "customer_name": inv.customer.customer_name,
'days_past_due': inv.due_in_days(), "invoice_number": inv.invoice_number,
'SITE_NAME': settings.SITE_NAME "amount_due": inv.amount_due,
}) "days_past_due": inv.due_in_days(),
"SITE_NAME": settings.SITE_NAME,
},
)
send_email( send_email(
'noreply@yourdomain.com', "noreply@yourdomain.com",
inv.customer.email, inv.customer.email,
subject, subject,
message, message,
@ -65,21 +69,24 @@ class Command(BaseCommand):
"""Queue email reminders for expiring plans""" """Queue email reminders for expiring plans"""
today = timezone.now().date() today = timezone.now().date()
expiring_plans = InvoiceModel.objects.filter( expiring_plans = InvoiceModel.objects.filter(
date_due__lte = today date_due__lte=today
).select_related('customer','ce_model') ).select_related("customer", "ce_model")
# Send email # Send email
for inv in expiring_plans: for inv in expiring_plans:
dealer = inv.customer.customer_set.first().dealer dealer = inv.customer.customer_set.first().dealer
subject = f"Your invoice is past due" subject = f"Your invoice is past due"
message = render_to_string('emails/invoice_past_due.txt', { message = render_to_string(
'customer_name': inv.customer.customer_name, "emails/invoice_past_due.txt",
'invoice_number': inv.invoice_number, {
'amount_due': inv.amount_due, "customer_name": inv.customer.customer_name,
'days_past_due': (today - inv.date_due).days, "invoice_number": inv.invoice_number,
'SITE_NAME': settings.SITE_NAME "amount_due": inv.amount_due,
}) "days_past_due": (today - inv.date_due).days,
"SITE_NAME": settings.SITE_NAME,
},
)
# send notification to accountatnt # send notification to accountatnt
recipients = ( recipients = (
@ -90,24 +97,28 @@ class Command(BaseCommand):
) )
for rec in recipients: for rec in recipients:
Notification.objects.create( Notification.objects.create(
user=rec, user=rec,
message=_( message=_(
""" """
Invoice {invoice_number} is past due,please your Invoice {invoice_number} is past due,please your
<a href="{url}" target="_blank">View</a>. <a href="{url}" target="_blank">View</a>.
""" """
).format( ).format(
invoice_number=inv.invoice_number, invoice_number=inv.invoice_number,
url=reverse( url=reverse(
"invoice_detail", "invoice_detail",
kwargs={"dealer_slug": dealer.slug, "entity_slug": dealer.entity.slug, "pk": inv.pk}, kwargs={
"dealer_slug": dealer.slug,
"entity_slug": dealer.entity.slug,
"pk": inv.pk,
},
), ),
), ),
) )
# send email to customer # send email to customer
send_email( send_email(
'noreply@yourdomain.com', "noreply@yourdomain.com",
inv.customer.email, inv.customer.email,
subject, subject,
message, message,
@ -131,4 +142,4 @@ class Command(BaseCommand):
# created__lt=cutoff, # created__lt=cutoff,
# status=Order.STATUS.NEW # status=Order.STATUS.NEW
# ).delete() # ).delete()
# self.stdout.write(f"Cleaned up {count} old incomplete orders") # self.stdout.write(f"Cleaned up {count} old incomplete orders")

View File

@ -2,9 +2,11 @@ from decimal import Decimal
import random import random
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from inventory.models import Car 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 inventory.utils import CarFinanceCalculator
from rich import print from rich import print
class Command(BaseCommand): class Command(BaseCommand):
help = "" help = ""
@ -14,27 +16,43 @@ class Command(BaseCommand):
admin = e.admin admin = e.admin
# estimate = e.get_estimates().first() # estimate = e.get_estimates().first()
# e.create_invoice(coa_model=e.get_default_coa(), customer_model=customer, terms="net_30") # 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) calc = CarFinanceCalculator(i)
data = calc.get_finance_data() data = calc.get_finance_data()
for car_data in data['cars']: for car_data in data["cars"]:
car = i.get_itemtxs_data()[0].filter( car = (
item_model__car__vin=car_data['vin'] i.get_itemtxs_data()[0]
).first().item_model.car .filter(item_model__car__vin=car_data["vin"])
.first()
.item_model.car
)
print("car", car) print("car", car)
qty = Decimal(car_data['quantity']) qty = Decimal(car_data["quantity"])
print("qty", qty) print("qty", qty)
# amounts from calculator # amounts from calculator
net_car_price = Decimal(car_data['total']) # after discount net_car_price = Decimal(car_data["total"]) # after discount
net_add_price = Decimal(data['total_additionals']) # per car or split however you want net_add_price = Decimal(
vat_amount = Decimal(data['total_vat_amount']) * qty # prorate if multi-qty 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 = net_car_price + net_add_price + vat_amount
grand_total = Decimal(data['grand_total']) grand_total = Decimal(data["grand_total"])
cost_total = Decimal(car_data['cost_price']) * qty 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_cars = e.get_coa_accounts().get(name="Inventory (Cars)")
# acc_sales = e.get_coa_accounts().get(name="Car Sales") # acc_sales = e.get_coa_accounts().get(name="Car Sales")
@ -76,4 +94,4 @@ class Command(BaseCommand):
# operation=InvoiceModel.ITEMIZE_APPEND) # operation=InvoiceModel.ITEMIZE_APPEND)
# print(i.amount_due) # print(i.amount_due)
# i.save() # i.save()

View File

@ -2,8 +2,11 @@ from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
import datetime import datetime
from inventory.models import Dealer from inventory.models import Dealer
from plans.models import Plan, Order,PlanPricing from plans.models import Plan, Order, PlanPricing
User = get_user_model() User = get_user_model()
class Command(BaseCommand): class Command(BaseCommand):
help = "" help = ""
@ -25,4 +28,4 @@ class Command(BaseCommand):
) )
order.complete_order() order.complete_order()
print(user.userplan) print(user.userplan)

View File

@ -11,6 +11,7 @@ from inventory.tasks import send_bilingual_reminder, handle_email_result
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Command(BaseCommand): class Command(BaseCommand):
help = "Handles subscription plan maintenance tasks" help = "Handles subscription plan maintenance tasks"
@ -30,17 +31,18 @@ class Command(BaseCommand):
def send_expiration_reminders(self): def send_expiration_reminders(self):
"""Queue email reminders for expiring plans""" """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() today = timezone.now().date()
for days in reminder_days: for days in reminder_days:
target_date = today + timedelta(days=days) target_date = today + timedelta(days=days)
expiring_plans = UserPlan.objects.filter( expiring_plans = UserPlan.objects.filter(
active=True, active=True, expire=target_date
expire=target_date ).select_related("user", "plan")
).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: for user_plan in expiring_plans:
# Queue email task # Queue email task
@ -50,14 +52,13 @@ class Command(BaseCommand):
user_plan.plan_id, user_plan.plan_id,
user_plan.expire, user_plan.expire,
days, days,
hook=handle_email_result hook=handle_email_result,
) )
def deactivate_expired_plans(self): def deactivate_expired_plans(self):
"""Deactivate plans that have expired (synchronous)""" """Deactivate plans that have expired (synchronous)"""
expired_plans = UserPlan.objects.filter( expired_plans = UserPlan.objects.filter(
active=True, active=True, expire__lt=timezone.now().date()
expire__lt=timezone.now().date()
) )
count = expired_plans.update(active=False) count = expired_plans.update(active=False)
self.stdout.write(f"Deactivated {count} expired plans") self.stdout.write(f"Deactivated {count} expired plans")
@ -66,7 +67,6 @@ class Command(BaseCommand):
"""Delete incomplete orders older than 30 days""" """Delete incomplete orders older than 30 days"""
cutoff = timezone.now() - timedelta(days=30) cutoff = timezone.now() - timedelta(days=30)
count, _ = Order.objects.filter( count, _ = Order.objects.filter(
created__lt=cutoff, created__lt=cutoff, status=Order.STATUS.NEW
status=Order.STATUS.NEW
).delete() ).delete()
self.stdout.write(f"Cleaned up {count} old incomplete orders") self.stdout.write(f"Cleaned up {count} old incomplete orders")

View File

@ -5,5 +5,10 @@ from django_q.tasks import async_task, result
class Command(BaseCommand): class Command(BaseCommand):
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
from inventory.models import Dealer from inventory.models import Dealer
instance = Dealer.objects.first() 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",
)

View File

@ -3,21 +3,24 @@ import json, random, string, decimal
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.test import Client from django.test import Client
from django.contrib.auth import get_user_model 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.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 from django_q.tasks import async_task
User = get_user_model() User = get_user_model()
class Command(BaseCommand): class Command(BaseCommand):
help = "Seed a full dealership via the real signup & downstream views" help = "Seed a full dealership via the real signup & downstream views"
def add_arguments(self, parser): 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): def handle(self, *args, **opts):
count = opts['count'] count = opts["count"]
client = Client() # lives inside management command client = Client() # lives inside management command
for n in range(6, 9): for n in range(6, 9):
@ -43,7 +46,16 @@ class Command(BaseCommand):
"address": f"Street {n}, Riyadh", "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 user = dealer.user
self._assign_random_plan(user) self._assign_random_plan(user)
self._services(dealer) self._services(dealer)
@ -61,7 +73,7 @@ class Command(BaseCommand):
return payload["email"] 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. Pick a random Plan and create + initialize a UserPlan for the user.
""" """
@ -72,14 +84,13 @@ class Command(BaseCommand):
plan = random.choice(plans) plan = random.choice(plans)
user_plan, created = UserPlan.objects.get_or_create( user_plan, created = UserPlan.objects.get_or_create(
user=user, user=user, defaults={"plan": plan, "active": True}
defaults={'plan': plan, 'active': True}
) )
if created: if created:
user_plan.initialize() user_plan.initialize()
return user_plan return user_plan
def _services(self,dealer): def _services(self, dealer):
additional_services = [ additional_services = [
{ {
"name": "Vehicle registration transfer assistance", "name": "Vehicle registration transfer assistance",
@ -114,5 +125,5 @@ class Command(BaseCommand):
price=additional_service["price"], price=additional_service["price"],
description=additional_service["description"], description=additional_service["description"],
dealer=dealer, dealer=dealer,
uom="Unit" uom="Unit",
) )

View File

@ -4,11 +4,31 @@ import json, random, string, decimal
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.test import Client from django.test import Client
from django.contrib.auth import get_user_model 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.services import decodevin
from inventory.tasks import create_user_dealer 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 inventory.models import (
from django_ledger.models import PurchaseOrderModel,ItemTransactionModel,ItemModel,EntityModel 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 django_q.tasks import async_task
from faker import Faker from faker import Faker
from appointment.models import Appointment, AppointmentRequest, Service, StaffMember from appointment.models import Appointment, AppointmentRequest, Service, StaffMember
@ -16,6 +36,7 @@ from appointment.models import Appointment, AppointmentRequest, Service, StaffMe
User = get_user_model() User = get_user_model()
fake = Faker() fake = Faker()
class Command(BaseCommand): class Command(BaseCommand):
help = "Seed a full dealership via the real signup & downstream views" 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_randome_services(dealer)
# self._create_random_lead(dealer) # self._create_random_lead(dealer)
# dealer = Dealer.objects.get(name="Dealer #6") # dealer = Dealer.objects.get(name="Dealer #6")
# coa_model = dealer.entity.get_default_coa() # coa_model = dealer.entity.get_default_coa()
# inventory_account = dealer.entity.get_all_accounts().get(name="Inventory (Cars)") # 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}")) self.stdout.write(self.style.SUCCESS(f"✅ PO created for {dealers}"))
def _create_random_po(self, dealer): def _create_random_po(self, dealer):
for i in range(random.randint(1,70)): for i in range(random.randint(1, 70)):
try: try:
e: EntityModel = dealer.entity 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: except Exception as e:
self.stderr.write(self.style.ERROR(f"Error : {e}")) self.stderr.write(self.style.ERROR(f"Error : {e}"))
def _create_random_vendors(self, dealer): def _create_random_vendors(self, dealer):
for i in range(random.randint(1,50)): for i in range(random.randint(1, 50)):
try: try:
name = fake.name() name = fake.name()
n = random.randint(1,9999) 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)}" 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}") 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: except Exception as e:
pass pass
@ -65,7 +97,9 @@ class Command(BaseCommand):
name = f"{fake.name()}{i}" name = f"{fake.name()}{i}"
email = fake.email() email = fake.email()
password = "Tenhal@123" 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.is_staff = True
user.save() user.save()
@ -74,17 +108,24 @@ class Command(BaseCommand):
# for service in services: # for service in services:
# staff_member.services_offered.add(service) # 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) groups = CustomGroup.objects.filter(dealer=dealer)
random_group = random.choice(list(groups)) random_group = random.choice(list(groups))
staff.add_group(random_group.group) staff.add_group(random_group.group)
# for i in range(random.randint(1,15)): # for i in range(random.randint(1,15)):
# n = random.randint(1,9999) # 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)}" # 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}") # 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() vendors = Vendor.objects.filter(dealer=dealer).all()
vin_list = [ vin_list = [
@ -103,18 +144,20 @@ class Command(BaseCommand):
] ]
for vin in vin_list: for vin in vin_list:
try: 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)}" vin = f"{vin[:-4]}{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}"
result = decodevin(vin) result = decodevin(vin)
make = CarMake.objects.get(name=result["maker"]) 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 == "": if not model or model == "":
model = random.choice(make.carmodel_set.all()) model = random.choice(make.carmodel_set.all())
year = result["modelYear"] year = result["modelYear"]
serie = random.choice(model.carserie_set.all()) serie = random.choice(model.carserie_set.all())
trim = random.choice(serie.cartrim_set.all()) trim = random.choice(serie.cartrim_set.all())
vendor = random.choice(vendors) vendor = random.choice(vendors)
print(make, model, serie, trim, vendor,vin) print(make, model, serie, trim, vendor, vin)
car = Car.objects.create( car = Car.objects.create(
vin=vin, vin=vin,
id_car_make=make, id_car_make=make,
@ -128,9 +171,12 @@ class Command(BaseCommand):
mileage=0, mileage=0,
) )
print(car) print(car)
cp=random.randint(10000, 100000) cp = random.randint(10000, 100000)
CarFinance.objects.create( 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( CarColors.objects.create(
car=car, car=car,
@ -141,8 +187,8 @@ class Command(BaseCommand):
except Exception as e: except Exception as e:
print(e) print(e)
def _create_random_customers(self,dealer): def _create_random_customers(self, dealer):
for i in range(random.randint(1,60)): for i in range(random.randint(1, 60)):
try: try:
c = Customer( c = Customer(
dealer=dealer, dealer=dealer,
@ -161,7 +207,7 @@ class Command(BaseCommand):
except Exception as e: except Exception as e:
pass pass
def _create_randome_services(self,dealer): def _create_randome_services(self, dealer):
additional_services = [ additional_services = [
{ {
"name": "Vehicle registration transfer assistance", "name": "Vehicle registration transfer assistance",
@ -196,12 +242,11 @@ class Command(BaseCommand):
price=additional_service["price"], price=additional_service["price"],
description=additional_service["description"], description=additional_service["description"],
dealer=dealer, dealer=dealer,
uom=uom uom=uom,
) )
def _create_random_lead(self, dealer):
def _create_random_lead(self,dealer): for i in range(random.randint(1, 60)):
for i in range(random.randint(1,60)):
try: try:
first_name = fake.name() first_name = fake.name()
last_name = fake.last_name() last_name = fake.last_name()
@ -224,7 +269,7 @@ class Command(BaseCommand):
id_car_model=model, id_car_model=model,
source="website", source="website",
channel="website", channel="website",
staff=staff staff=staff,
) )
c = Customer( c = Customer(
dealer=dealer, dealer=dealer,
@ -243,4 +288,4 @@ class Command(BaseCommand):
lead.customer = c lead.customer = c
lead.save() lead.save()
except Exception as e: except Exception as e:
pass pass

View File

@ -152,11 +152,22 @@ class DealerSlugMiddleware:
def process_view(self, request, view_func, view_args, view_kwargs): def process_view(self, request, view_func, view_args, view_kwargs):
paths = [ paths = [
"/ar/signup/", "/en/signup/", "/ar/login/", "/en/login/", "/ar/signup/",
"/ar/logout/", "/en/logout/", "/en/ledger/", "/ar/ledger/", "/en/signup/",
"/en/notifications/", "/ar/notifications/", "/en/appointment/", "/ar/login/",
"/ar/appointment/", "/en/feature/recall/","/ar/feature/recall/", "/en/login/",
"/ar/help_center/", "/en/help_center/", "/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("------------------------------------")
print(request.path in paths) print(request.path in paths)

View File

@ -42,12 +42,14 @@ from django_ledger.models import (
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
# from appointment.models import StaffMember # from appointment.models import StaffMember
from plans.quota import get_user_quota from plans.quota import get_user_quota
from plans.models import UserPlan from plans.models import UserPlan
from django.db.models import Q from django.db.models import Q
from imagekit.models import ImageSpecField from imagekit.models import ImageSpecField
from imagekit.processors import ResizeToFill from imagekit.processors import ResizeToFill
# from plans.models import AbstractPlan # from plans.models import AbstractPlan
# from simple_history.models import HistoricalRecords # from simple_history.models import HistoricalRecords
from plans.models import Invoice from plans.models import Invoice
@ -229,7 +231,9 @@ class CarMake(models.Model, LocalizedNameMixin):
name = models.CharField(max_length=255, blank=True, null=True) name = models.CharField(max_length=255, blank=True, null=True)
slug = models.SlugField(max_length=255, unique=True, 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) 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) is_sa_import = models.BooleanField(default=False)
car_type = models.SmallIntegerField(choices=CarType.choices, blank=True, null=True) car_type = models.SmallIntegerField(choices=CarType.choices, blank=True, null=True)
@ -589,7 +593,7 @@ class AdditionalServices(models.Model, LocalizedNameMixin):
"price_": str(self.price_), "price_": str(self.price_),
"taxable": self.taxable, "taxable": self.taxable,
"uom": self.uom, "uom": self.uom,
"service_tax":str(self.service_tax) "service_tax": str(self.service_tax),
} }
@property @property
@ -604,9 +608,7 @@ class AdditionalServices(models.Model, LocalizedNameMixin):
@property @property
def service_tax(self): def service_tax(self):
vat = VatRate.objects.filter(dealer=self.dealer, is_active=True).first() vat = VatRate.objects.filter(dealer=self.dealer, is_active=True).first()
return ( return Decimal(self.price * vat.rate)
Decimal(self.price * vat.rate)
)
class Meta: class Meta:
verbose_name = _("Additional Services") verbose_name = _("Additional Services")
@ -683,10 +685,13 @@ class Car(Base):
) )
# #
additional_services = models.ManyToManyField( additional_services = models.ManyToManyField(
AdditionalServices, related_name="additionals", blank=True,null=True AdditionalServices, related_name="additionals", blank=True, null=True
) )
cost_price = models.DecimalField( 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( selling_price = models.DecimalField(
max_digits=14, max_digits=14,
@ -710,7 +715,7 @@ class Car(Base):
remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks")) remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks"))
mileage = models.IntegerField(blank=True, null=True, verbose_name=_("Mileage")) mileage = models.IntegerField(blank=True, null=True, verbose_name=_("Mileage"))
receiving_date = models.DateTimeField(verbose_name=_("Receiving Date")) 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( hash = models.CharField(
max_length=64, blank=True, null=True, verbose_name=_("Hash") max_length=64, blank=True, null=True, verbose_name=_("Hash")
) )
@ -773,6 +778,7 @@ class Car(Base):
@property @property
def logo(self): def logo(self):
return getattr(self.id_car_make, "logo", "") return getattr(self.id_car_make, "logo", "")
# @property # @property
# def additional_services(self): # def additional_services(self):
# return self.additional_services.all() # return self.additional_services.all()
@ -787,9 +793,15 @@ class Car(Base):
) )
except Exception: except Exception:
return False return False
@property @property
def invoice(self): 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): def get_transfer(self):
return self.transfer_logs.filter(active=True).first() return self.transfer_logs.filter(active=True).first()
@ -873,39 +885,54 @@ class Car(Base):
car=self, exterior=exterior, interior=interior car=self, exterior=exterior, interior=interior
) )
self.save() self.save()
@property @property
def logo(self): def logo(self):
return self.id_car_make.logo.url if self.id_car_make.logo else None return self.id_car_make.logo.url if self.id_car_make.logo else None
# #
@property @property
def get_additional_services_amount(self): def get_additional_services_amount(self):
return sum([Decimal(x.price) for x in self.additional_services.all()]) return sum([Decimal(x.price) for x in self.additional_services.all()])
@property @property
def get_additional_services_amount_(self): def get_additional_services_amount_(self):
return sum([Decimal(x.price_) for x in self.additional_services.all()]) return sum([Decimal(x.price_) for x in self.additional_services.all()])
@property @property
def get_additional_services_vat(self): def get_additional_services_vat(self):
vat = VatRate.objects.filter(dealer=self.dealer,is_active=True).first() 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)]) return sum(
[
Decimal((x.price) * (vat.rate))
for x in self.additional_services.filter(taxable=True)
]
)
def get_additional_services(self): def get_additional_services(self):
vat = VatRate.objects.filter(dealer=self.dealer,is_active=True).first() 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()], return {
"total_":self.get_additional_services_amount_, "services": [
"total":self.get_additional_services_amount, [x, ((x.price) * (vat.rate) if x.taxable else 0)]
"services_vat":self.get_additional_services_vat} 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 @property
def final_price(self): def final_price(self):
return Decimal(self.marked_price -self.discount) return Decimal(self.marked_price - self.discount)
@property @property
def vat_amount(self): 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) return Decimal(self.final_price) * (vat.rate)
@property @property
def total_services_and_car_vat(self): 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 @property
def final_price_plus_vat(self): def final_price_plus_vat(self):
@ -913,29 +940,32 @@ class Car(Base):
@property @property
def final_price_plus_services_plus_vat(self): 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 # to be used after invoice is created
@property @property
def invoice(self): def invoice(self):
return self.item_model.invoicemodel_set.first() or None return self.item_model.invoicemodel_set.first() or None
@property @property
def estimate(self): def estimate(self):
return getattr(self.invoice,'ce_model',None) return getattr(self.invoice, "ce_model", None)
@property @property
def discount(self): def discount(self):
if not self.estimate: if not self.estimate:
return 0 return 0
try: try:
instance = ExtraInfo.objects.get( instance = ExtraInfo.objects.get(
dealer=self.dealer, dealer=self.dealer,
content_type=ContentType.objects.get_for_model(EstimateModel), content_type=ContentType.objects.get_for_model(EstimateModel),
object_id=self.estimate.pk, object_id=self.estimate.pk,
) )
return Decimal(instance.data.get('discount',0)) return Decimal(instance.data.get("discount", 0))
except ExtraInfo.DoesNotExist: except ExtraInfo.DoesNotExist:
return Decimal(0) return Decimal(0)
# def get_discount_amount(self,estimate,user): # def get_discount_amount(self,estimate,user):
# try: # try:
@ -961,10 +991,6 @@ class Car(Base):
# return round(self.total_discount + self.vat_amount + self.total_additionals, 2) # return round(self.total_discount + self.vat_amount + self.total_additionals, 2)
class CarTransfer(models.Model): class CarTransfer(models.Model):
car = models.ForeignKey( car = models.ForeignKey(
"Car", "Car",
@ -1001,7 +1027,7 @@ class CarTransfer(models.Model):
@property @property
def total_price(self): 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: class Meta:
verbose_name = _("Car Transfer Log") verbose_name = _("Car Transfer Log")
@ -1311,7 +1337,11 @@ class Dealer(models.Model, LocalizedNameMixin):
) )
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
name = models.CharField(max_length=255, verbose_name=_("English 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( address = models.CharField(
max_length=200, blank=True, null=True, verbose_name=_("Address") max_length=200, blank=True, null=True, verbose_name=_("Address")
) )
@ -1365,6 +1395,7 @@ class Dealer(models.Model, LocalizedNameMixin):
@property @property
def customers(self): def customers(self):
return models.Customer.objects.filter(dealer=self) return models.Customer.objects.filter(dealer=self)
@property @property
def user_quota(self): def user_quota(self):
try: try:
@ -1415,6 +1446,7 @@ class Dealer(models.Model, LocalizedNameMixin):
def invoices(self): def invoices(self):
return Invoice.objects.filter(order__user=self.user) return Invoice.objects.filter(order__user=self.user)
class StaffTypes(models.TextChoices): class StaffTypes(models.TextChoices):
# MANAGER = "manager", _("Manager") # MANAGER = "manager", _("Manager")
INVENTORY = "inventory", _("Inventory") INVENTORY = "inventory", _("Inventory")
@ -1429,15 +1461,17 @@ class Staff(models.Model):
# staff_member = models.OneToOneField( # staff_member = models.OneToOneField(
# StaffMember, on_delete=models.CASCADE, related_name="staff" # StaffMember, on_delete=models.CASCADE, related_name="staff"
# ) # )
user = models.OneToOneField( user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="staff")
User, on_delete=models.CASCADE, related_name="staff"
)
dealer = models.ForeignKey(Dealer, 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")) first_name = models.CharField(max_length=255, verbose_name=_("First Name"))
last_name = models.CharField(max_length=255, verbose_name=_("Last Name")) last_name = models.CharField(max_length=255, verbose_name=_("Last Name"))
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic 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( staff_type = models.CharField(
choices=StaffTypes.choices, max_length=255, verbose_name=_("Staff Type") choices=StaffTypes.choices, max_length=255, verbose_name=_("Staff Type")
) )
@ -1445,7 +1479,11 @@ class Staff(models.Model):
max_length=200, blank=True, null=True, verbose_name=_("Address") max_length=200, blank=True, null=True, verbose_name=_("Address")
) )
logo = models.ImageField( 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( thumbnail = ImageSpecField(
source="logo", source="logo",
@ -1480,6 +1518,7 @@ class Staff(models.Model):
@property @property
def fullname(self): def fullname(self):
return self.first_name + " " + self.last_name return self.first_name + " " + self.last_name
def deactivate_account(self): def deactivate_account(self):
self.active = False self.active = False
self.user.is_active = False self.user.is_active = False
@ -1544,8 +1583,7 @@ class Staff(models.Model):
permissions = [] permissions = []
constraints = [ constraints = [
models.UniqueConstraint( models.UniqueConstraint(
fields=['dealer', 'user'], fields=["dealer", "user"], name="unique_staff_email_per_dealer"
name='unique_staff_email_per_dealer'
) )
] ]
@ -1648,7 +1686,11 @@ class Customer(models.Model):
CustomerModel, on_delete=models.SET_NULL, null=True CustomerModel, on_delete=models.SET_NULL, null=True
) )
user = models.OneToOneField( 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( title = models.CharField(
choices=Title.choices, default=Title.NA, max_length=10, verbose_name=_("Title") choices=Title.choices, default=Title.NA, max_length=10, verbose_name=_("Title")
@ -1676,7 +1718,11 @@ class Customer(models.Model):
) )
active = models.BooleanField(default=True, verbose_name=_("Active")) active = models.BooleanField(default=True, verbose_name=_("Active"))
image = models.ImageField( 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( thumbnail = ImageSpecField(
source="image", source="image",
@ -1708,8 +1754,7 @@ class Customer(models.Model):
class Meta: class Meta:
constraints = [ constraints = [
models.UniqueConstraint( models.UniqueConstraint(
fields=['dealer', 'email'], fields=["dealer", "email"], name="unique_customer_email_per_dealer"
name='unique_customer_email_per_dealer'
) )
] ]
verbose_name = _("Customer") verbose_name = _("Customer")
@ -1779,13 +1824,13 @@ class Customer(models.Model):
user, created = User.objects.get_or_create( user, created = User.objects.get_or_create(
username=self.email, username=self.email,
defaults={ defaults={
'email': self.email, "email": self.email,
'first_name': self.first_name, "first_name": self.first_name,
'last_name': self.last_name, "last_name": self.last_name,
'password': make_random_password(), "password": make_random_password(),
'is_staff': False, "is_staff": False,
'is_superuser': False, "is_superuser": False,
'is_active': False if for_lead else True, "is_active": False if for_lead else True,
}, },
) )
self.user = user self.user = user
@ -1822,7 +1867,11 @@ class Organization(models.Model, LocalizedNameMixin):
CustomerModel, on_delete=models.SET_NULL, null=True CustomerModel, on_delete=models.SET_NULL, null=True
) )
user = models.OneToOneField( 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")) name = models.CharField(max_length=255, verbose_name=_("Name"))
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
@ -1831,12 +1880,20 @@ class Organization(models.Model, LocalizedNameMixin):
) )
vrn = models.CharField(max_length=15, verbose_name=_("VAT Registration Number")) vrn = models.CharField(max_length=15, verbose_name=_("VAT Registration Number"))
email = models.EmailField(verbose_name=_("Email")) 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( address = models.CharField(
max_length=200, blank=True, null=True, verbose_name=_("Address") max_length=200, blank=True, null=True, verbose_name=_("Address")
) )
logo = models.ImageField( 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( thumbnail = ImageSpecField(
source="logo", source="logo",
@ -1965,7 +2022,11 @@ class Representative(models.Model, LocalizedNameMixin):
id_number = models.CharField( id_number = models.CharField(
max_length=10, unique=True, verbose_name=_("ID Number") 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")) email = models.EmailField(max_length=255, verbose_name=_("Email Address"))
address = models.CharField( address = models.CharField(
max_length=200, blank=True, null=True, verbose_name=_("Address") max_length=200, blank=True, null=True, verbose_name=_("Address")
@ -1985,7 +2046,11 @@ class Lead(models.Model):
first_name = models.CharField(max_length=50, verbose_name=_("First Name")) first_name = models.CharField(max_length=50, verbose_name=_("First Name"))
last_name = models.CharField(max_length=50, verbose_name=_("Last Name")) last_name = models.CharField(max_length=50, verbose_name=_("Last Name"))
email = models.EmailField(verbose_name=_("Email")) 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( address = models.CharField(
max_length=200, blank=True, null=True, verbose_name=_("Address") max_length=200, blank=True, null=True, verbose_name=_("Address")
) )
@ -2175,8 +2240,9 @@ class Lead(models.Model):
.order_by("-updated") .order_by("-updated")
.first() .first()
) )
def get_absolute_url(self): 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): def save(self, *args, **kwargs):
if not self.slug: if not self.slug:
@ -2246,6 +2312,7 @@ class Schedule(models.Model):
@property @property
def duration(self): def duration(self):
return (self.end_time - self.start_time).seconds return (self.end_time - self.start_time).seconds
@property @property
def schedule_past_date(self): def schedule_past_date(self):
if self.scheduled_at < now(): if self.scheduled_at < now():
@ -2255,6 +2322,7 @@ class Schedule(models.Model):
@property @property
def get_purpose(self): def get_purpose(self):
return self.purpose.replace("_", " ").title() return self.purpose.replace("_", " ").title()
class Meta: class Meta:
ordering = ["-scheduled_at"] ordering = ["-scheduled_at"]
verbose_name = _("Schedule") verbose_name = _("Schedule")
@ -2673,11 +2741,19 @@ class Vendor(models.Model, LocalizedNameMixin):
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name")) arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
name = models.CharField(max_length=255, verbose_name=_("English Name")) name = models.CharField(max_length=255, verbose_name=_("English Name"))
contact_person = models.CharField(max_length=100, verbose_name=_("Contact Person")) 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")) email = models.EmailField(max_length=255, verbose_name=_("Email Address"))
address = models.CharField(max_length=200, verbose_name=_("Address")) address = models.CharField(max_length=200, verbose_name=_("Address"))
logo = models.ImageField( 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( thumbnail = ImageSpecField(
source="logo", source="logo",
@ -3225,7 +3301,6 @@ class CustomGroup(models.Model):
"activity", "activity",
"payment", "payment",
"vendor", "vendor",
], ],
other_perms=[ other_perms=[
"view_car", "view_car",
@ -3236,8 +3311,7 @@ class CustomGroup(models.Model):
"view_saleorder", "view_saleorder",
"view_leads", "view_leads",
"view_opportunity", "view_opportunity",
'view_customer' "view_customer",
], ],
) )
self.set_permissions( self.set_permissions(
@ -3533,7 +3607,7 @@ class ExtraInfo(models.Model):
return f"ExtraInfo for {self.content_object} ({self.content_type})" return f"ExtraInfo for {self.content_object} ({self.content_type})"
@classmethod @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: if not staff and not is_dealer:
return [] return []
@ -3546,11 +3620,13 @@ class ExtraInfo(models.Model):
content_type=content_type, content_type=content_type,
related_content_type=related_content_type, related_content_type=related_content_type,
related_object_id__isnull=False, related_object_id__isnull=False,
).union(cls.objects.filter( ).union(
dealer=dealer, cls.objects.filter(
content_type=ContentType.objects.get_for_model(EstimateModel), dealer=dealer,
related_content_type=ContentType.objects.get_for_model(User), content_type=ContentType.objects.get_for_model(EstimateModel),
)) related_content_type=ContentType.objects.get_for_model(User),
)
)
else: else:
qs = cls.objects.filter( qs = cls.objects.filter(
dealer=dealer, dealer=dealer,
@ -3559,7 +3635,17 @@ class ExtraInfo(models.Model):
related_object_id=staff.pk, related_object_id=staff.pk,
) )
# qs = qs.select_related("customer","estimate","invoice") # 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 return data
@ -3572,7 +3658,7 @@ class ExtraInfo(models.Model):
# ] # ]
@classmethod @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: if not staff and not is_dealer:
return [] return []
@ -3585,11 +3671,13 @@ class ExtraInfo(models.Model):
content_type=content_type, content_type=content_type,
related_content_type=related_content_type, related_content_type=related_content_type,
related_object_id__isnull=False, related_object_id__isnull=False,
).union(cls.objects.filter( ).union(
dealer=dealer, cls.objects.filter(
content_type=content_type, dealer=dealer,
related_content_type=ContentType.objects.get_for_model(User), content_type=content_type,
)) related_content_type=ContentType.objects.get_for_model(User),
)
)
else: else:
qs = cls.objects.filter( qs = cls.objects.filter(
dealer=dealer, dealer=dealer,
@ -3608,32 +3696,16 @@ class Recall(models.Model):
title = models.CharField(max_length=200, verbose_name=_("Recall Title")) title = models.CharField(max_length=200, verbose_name=_("Recall Title"))
description = models.TextField(verbose_name=_("Description")) description = models.TextField(verbose_name=_("Description"))
make = models.ForeignKey( make = models.ForeignKey(
CarMake, CarMake, models.DO_NOTHING, verbose_name=_("Make"), null=True, blank=True
models.DO_NOTHING,
verbose_name=_("Make"),
null=True,
blank=True
) )
model = models.ForeignKey( model = models.ForeignKey(
CarModel, CarModel, models.DO_NOTHING, verbose_name=_("Model"), null=True, blank=True
models.DO_NOTHING,
verbose_name=_("Model"),
null=True,
blank=True
) )
serie = models.ForeignKey( serie = models.ForeignKey(
CarSerie, CarSerie, models.DO_NOTHING, verbose_name=_("Series"), null=True, blank=True
models.DO_NOTHING,
verbose_name=_("Series"),
null=True,
blank=True
) )
trim = models.ForeignKey( trim = models.ForeignKey(
CarTrim, CarTrim, models.DO_NOTHING, verbose_name=_("Trim"), null=True, blank=True
models.DO_NOTHING,
verbose_name=_("Trim"),
null=True,
blank=True
) )
year_from = models.IntegerField(verbose_name=_("From Year"), 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) year_to = models.IntegerField(verbose_name=_("To Year"), null=True, blank=True)
@ -3643,7 +3715,7 @@ class Recall(models.Model):
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
blank=True, blank=True,
verbose_name=_("Created By") verbose_name=_("Created By"),
) )
class Meta: class Meta:
@ -3653,11 +3725,16 @@ class Recall(models.Model):
def __str__(self): def __str__(self):
return self.title return self.title
class RecallNotification(models.Model): class RecallNotification(models.Model):
recall = models.ForeignKey(Recall, on_delete=models.CASCADE, related_name='notifications') recall = models.ForeignKey(
dealer = models.ForeignKey("Dealer", on_delete=models.CASCADE, related_name='recall_notifications') 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) 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: class Meta:
verbose_name = _("Recall Notification") verbose_name = _("Recall Notification")
@ -3666,27 +3743,30 @@ class RecallNotification(models.Model):
def __str__(self): def __str__(self):
return f"Notification for {self.dealer} about {self.recall}" return f"Notification for {self.dealer} about {self.recall}"
class Ticket(models.Model): class Ticket(models.Model):
STATUS_CHOICES = [ STATUS_CHOICES = [
('open', 'Open'), ("open", "Open"),
('in_progress', 'In Progress'), ("in_progress", "In Progress"),
('resolved', 'Resolved'), ("resolved", "Resolved"),
('closed', 'Closed'), ("closed", "Closed"),
] ]
PRIORITY_CHOICES = [ PRIORITY_CHOICES = [
('low', 'Low'), ("low", "Low"),
('medium', 'Medium'), ("medium", "Medium"),
('high', 'High'), ("high", "High"),
('critical', 'Critical'), ("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) subject = models.CharField(max_length=200)
description = models.TextField() description = models.TextField()
resolution_notes = models.TextField(blank=True, null=True) resolution_notes = models.TextField(blank=True, null=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='open') status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="open")
priority = models.CharField(max_length=20, choices=PRIORITY_CHOICES, default='medium') priority = models.CharField(
max_length=20, choices=PRIORITY_CHOICES, default="medium"
)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
@ -3697,7 +3777,7 @@ class Ticket(models.Model):
Returns None if ticket isn't resolved/closed. Returns None if ticket isn't resolved/closed.
Returns timedelta if 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 self.updated_at - self.created_at
return None return None
@ -3729,9 +3809,11 @@ class Ticket(models.Model):
class CarImage(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_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) is_generating = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
@ -3752,5 +3834,5 @@ class CarImage(models.Model):
async_task( async_task(
generate_car_image_task, generate_car_image_task,
self.id, self.id,
task_name=f"generate_car_image_{self.car.vin}" task_name=f"generate_car_image_{self.car.vin}",
) )

View File

@ -87,6 +87,7 @@ from inventory.models import Notification
import asyncio import asyncio
from datetime import datetime from datetime import datetime
@database_sync_to_async @database_sync_to_async
def get_user(user_id): def get_user(user_id):
User = get_user_model() User = get_user_model()
@ -95,24 +96,24 @@ def get_user(user_id):
except User.DoesNotExist: except User.DoesNotExist:
return AnonymousUser() return AnonymousUser()
@database_sync_to_async @database_sync_to_async
def get_notifications(user, last_id): def get_notifications(user, last_id):
notifications = Notification.objects.filter( notifications = Notification.objects.filter(
user=user, user=user, id__gt=last_id, is_read=False
id__gt=last_id,
is_read=False
).order_by("created") ).order_by("created")
return [ return [
{ {
'id': n.id, "id": n.id,
'message': n.message, "message": n.message,
'created': n.created.isoformat(), # Convert datetime to string "created": n.created.isoformat(), # Convert datetime to string
'is_read': n.is_read "is_read": n.is_read,
} }
for n in notifications for n in notifications
] ]
class NotificationSSEApp: class NotificationSSEApp:
async def __call__(self, scope, receive, send): async def __call__(self, scope, receive, send):
if scope["type"] != "http": if scope["type"] != "http":
@ -143,15 +144,17 @@ class NotificationSSEApp:
for notification in notifications: for notification in notifications:
await self._send_notification(send, notification) await self._send_notification(send, notification)
if notification['id'] > last_id: if notification["id"] > last_id:
last_id = notification['id'] last_id = notification["id"]
# Send keep-alive comment every 15 seconds # Send keep-alive comment every 15 seconds
await send({ await send(
"type": "http.response.body", {
"body": b":keep-alive\n\n", "type": "http.response.body",
"more_body": True "body": b":keep-alive\n\n",
}) "more_body": True,
}
)
# await asyncio.sleep(3) # await asyncio.sleep(3)
@ -161,16 +164,18 @@ class NotificationSSEApp:
await self._close_connection(send) await self._close_connection(send)
async def _send_headers(self, send): async def _send_headers(self, send):
await send({ await send(
"type": "http.response.start", {
"status": 200, "type": "http.response.start",
"headers": [ "status": 200,
(b"content-type", b"text/event-stream"), "headers": [
(b"cache-control", b"no-cache"), (b"content-type", b"text/event-stream"),
(b"connection", b"keep-alive"), (b"cache-control", b"no-cache"),
(b"x-accel-buffering", b"no"), (b"connection", b"keep-alive"),
] (b"x-accel-buffering", b"no"),
}) ],
}
)
async def _send_notification(self, send, notification): async def _send_notification(self, send, notification):
try: try:
@ -179,27 +184,25 @@ class NotificationSSEApp:
f"event: notification\n" f"event: notification\n"
f"data: {json.dumps(notification)}\n\n" f"data: {json.dumps(notification)}\n\n"
) )
await send({ await send(
"type": "http.response.body", {
"body": event_str.encode("utf-8"), "type": "http.response.body",
"more_body": True "body": event_str.encode("utf-8"),
}) "more_body": True,
}
)
except Exception as e: except Exception as e:
print(f"Error sending notification: {e}") print(f"Error sending notification: {e}")
async def _send_response(self, send, status, body): async def _send_response(self, send, status, body):
await send({ await send(
"type": "http.response.start", {
"status": status, "type": "http.response.start",
"headers": [(b"content-type", b"text/plain")] "status": status,
}) "headers": [(b"content-type", b"text/plain")],
await send({ }
"type": "http.response.body", )
"body": body await send({"type": "http.response.body", "body": body})
})
async def _close_connection(self, send): async def _close_connection(self, send):
await send({ await send({"type": "http.response.body", "body": b""})
"type": "http.response.body",
"body": b""
})

View File

@ -20,7 +20,12 @@ from django.contrib import messages
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse 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.detail import DetailView
from django.views.generic.edit import CreateView from django.views.generic.edit import CreateView
from django_ledger.forms.chart_of_accounts import ( from django_ledger.forms.chart_of_accounts import (
@ -35,17 +40,28 @@ from django_ledger.forms.purchase_order import (
get_po_itemtxs_formset_class, get_po_itemtxs_formset_class,
) )
from django_ledger.views.purchase_order import PurchaseOrderModelModelViewQuerySetMixIn 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.detail import SingleObjectMixin
from django.views.generic.edit import UpdateView from django.views.generic.edit import UpdateView
from django.views.generic.base import RedirectView from django.views.generic.base import RedirectView
from django.views.generic.list import ListView from django.views.generic.list import ListView
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_ledger.forms.invoice import (BaseInvoiceModelUpdateForm, InvoiceModelCreateForEstimateForm, from django_ledger.forms.invoice import (
get_invoice_itemtxs_formset_class, BaseInvoiceModelUpdateForm,
DraftInvoiceModelUpdateForm, InReviewInvoiceModelUpdateForm, InvoiceModelCreateForEstimateForm,
ApprovedInvoiceModelUpdateForm, PaidInvoiceModelUpdateForm, get_invoice_itemtxs_formset_class,
AccruedAndApprovedInvoiceModelUpdateForm, InvoiceModelCreateForm) DraftInvoiceModelUpdateForm,
InReviewInvoiceModelUpdateForm,
ApprovedInvoiceModelUpdateForm,
PaidInvoiceModelUpdateForm,
AccruedAndApprovedInvoiceModelUpdateForm,
InvoiceModelCreateForm,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -71,7 +87,11 @@ class PurchaseOrderModelUpdateView(
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["entity_slug"] = dealer.entity.slug 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: if not itemtxs_formset:
itemtxs_qs = self.get_po_itemtxs_qs(po_model) itemtxs_qs = self.get_po_itemtxs_qs(po_model)
@ -776,12 +796,12 @@ class InventoryListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
class InvoiceModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): class InvoiceModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
slug_url_kwarg = 'invoice_pk' slug_url_kwarg = "invoice_pk"
slug_field = 'uuid' slug_field = "uuid"
context_object_name = 'invoice' context_object_name = "invoice"
# template_name = 'inventory/sales/invoices/invoice_update.html' # template_name = 'inventory/sales/invoices/invoice_update.html'
form_class = BaseInvoiceModelUpdateForm form_class = BaseInvoiceModelUpdateForm
http_method_names = ['get', 'post'] http_method_names = ["get", "post"]
action_update_items = False action_update_items = False
@ -802,115 +822,137 @@ class InvoiceModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Update
def get_form(self, form_class=None): def get_form(self, form_class=None):
form_class = self.get_form_class() 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( return form_class(
entity_slug=self.kwargs['entity_slug'], entity_slug=self.kwargs["entity_slug"],
user_model=self.request.dealer.user, user_model=self.request.dealer.user,
instance=self.object instance=self.object,
) )
return form_class( return form_class(
entity_slug=self.kwargs['entity_slug'], entity_slug=self.kwargs["entity_slug"],
user_model=self.request.dealer.user, user_model=self.request.dealer.user,
**self.get_form_kwargs() **self.get_form_kwargs(),
) )
def get_context_data(self, itemtxs_formset=None, **kwargs): def get_context_data(self, itemtxs_formset=None, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
invoice_model: InvoiceModel = self.object invoice_model: InvoiceModel = self.object
title = f'Invoice {invoice_model.invoice_number}' title = f"Invoice {invoice_model.invoice_number}"
context['page_title'] = title context["page_title"] = title
context['header_title'] = title context["header_title"] = title
ledger_model: LedgerModel = self.object.ledger ledger_model: LedgerModel = self.object.ledger
if not invoice_model.is_configured(): if not invoice_model.is_configured():
messages.add_message( messages.add_message(
request=self.request, 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, level=messages.ERROR,
extra_tags='is-danger' extra_tags="is-danger",
) )
if not invoice_model.is_paid(): if not invoice_model.is_paid():
if ledger_model.locked: if ledger_model.locked:
messages.add_message(self.request, messages.add_message(
messages.ERROR, self.request,
f'Warning! This invoice is locked. Must unlock before making any changes.', messages.ERROR,
extra_tags='is-danger') f"Warning! This invoice is locked. Must unlock before making any changes.",
extra_tags="is-danger",
)
if ledger_model.locked: if ledger_model.locked:
messages.add_message(self.request, messages.add_message(
messages.ERROR, self.request,
f'Warning! This Invoice is Locked. Must unlock before making any changes.', messages.ERROR,
extra_tags='is-danger') f"Warning! This Invoice is Locked. Must unlock before making any changes.",
extra_tags="is-danger",
)
if not ledger_model.is_posted(): if not ledger_model.is_posted():
messages.add_message(self.request, messages.add_message(
messages.INFO, self.request,
f'This Invoice has not been posted. Must post to see ledger changes.', messages.INFO,
extra_tags='is-info') f"This Invoice has not been posted. Must post to see ledger changes.",
extra_tags="is-info",
)
if not itemtxs_formset: if not itemtxs_formset:
itemtxs_qs = invoice_model.itemtransactionmodel_set.all().select_related('item_model') itemtxs_qs = invoice_model.itemtransactionmodel_set.all().select_related(
itemtxs_qs, itemtxs_agg = invoice_model.get_itemtxs_data(queryset=itemtxs_qs) "item_model"
invoice_itemtxs_formset_class = get_invoice_itemtxs_formset_class(invoice_model) )
itemtxs_formset = invoice_itemtxs_formset_class( itemtxs_qs, itemtxs_agg = invoice_model.get_itemtxs_data(
entity_slug=self.kwargs['entity_slug'],
user_model=self.request.dealer.user,
invoice_model=invoice_model,
queryset=itemtxs_qs 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: 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["itemtxs_formset"] = itemtxs_formset
context['total_amount__sum'] = itemtxs_agg['total_amount__sum'] context["total_amount__sum"] = itemtxs_agg["total_amount__sum"]
return context return context
def get_success_url(self): def get_success_url(self):
entity_slug = self.kwargs['entity_slug'] entity_slug = self.kwargs["entity_slug"]
invoice_pk = self.kwargs['invoice_pk'] invoice_pk = self.kwargs["invoice_pk"]
return reverse('invoice_detail', return reverse(
kwargs={ "invoice_detail",
'dealer_slug': self.request.dealer.slug, kwargs={
'entity_slug': entity_slug, "dealer_slug": self.request.dealer.slug,
'pk': invoice_pk "entity_slug": entity_slug,
}) "pk": invoice_pk,
},
)
# def get_queryset(self): # def get_queryset(self):
# qs = super().get_queryset() # qs = super().get_queryset()
# return qs.prefetch_related('itemtransactionmodel_set') # return qs.prefetch_related('itemtransactionmodel_set')
def get_queryset(self): def get_queryset(self):
if self.queryset is None: if self.queryset is None:
self.queryset = InvoiceModel.objects.for_entity( self.queryset = (
entity_slug=self.kwargs['entity_slug'], InvoiceModel.objects.for_entity(
user_model=self.request.user 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') .select_related("customer", "ledger")
.order_by("-created")
)
return super().get_queryset().prefetch_related("itemtransactionmodel_set")
def form_valid(self, form): def form_valid(self, form):
invoice_model: InvoiceModel = form.save(commit=False) invoice_model: InvoiceModel = form.save(commit=False)
if invoice_model.can_migrate(): if invoice_model.can_migrate():
invoice_model.migrate_state( invoice_model.migrate_state(
user_model=self.request.dealer.user, user_model=self.request.dealer.user,
entity_slug=self.kwargs['entity_slug'] entity_slug=self.kwargs["entity_slug"],
) )
messages.add_message(self.request, messages.add_message(
messages.SUCCESS, self.request,
f'Invoice {self.object.invoice_number} successfully updated.', messages.SUCCESS,
extra_tags='is-success') f"Invoice {self.object.invoice_number} successfully updated.",
extra_tags="is-success",
)
return super().form_valid(form) return super().form_valid(form)
def get(self, request, entity_slug, invoice_pk, *args, **kwargs): def get(self, request, entity_slug, invoice_pk, *args, **kwargs):
if self.action_update_items: if self.action_update_items:
return HttpResponseRedirect( return HttpResponseRedirect(
redirect_to=reverse('invoice_update', redirect_to=reverse(
kwargs={ "invoice_update",
'dealer_slug': request.dealer.slug, kwargs={
'entity_slug': entity_slug, "dealer_slug": request.dealer.slug,
'pk': invoice_pk "entity_slug": entity_slug,
}) "pk": invoice_pk,
},
)
) )
return super(InvoiceModelUpdateView, self).get(request, *args, **kwargs) return super(InvoiceModelUpdateView, self).get(request, *args, **kwargs)
@ -922,18 +964,22 @@ class InvoiceModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Update
queryset = self.get_queryset() queryset = self.get_queryset()
invoice_model = self.get_object(queryset=queryset) invoice_model = self.get_object(queryset=queryset)
self.object = invoice_model self.object = invoice_model
invoice_itemtxs_formset_class = get_invoice_itemtxs_formset_class(invoice_model) invoice_itemtxs_formset_class = get_invoice_itemtxs_formset_class(
itemtxs_formset = invoice_itemtxs_formset_class(request.POST, invoice_model
user_model=self.request.dealer.user, )
invoice_model=invoice_model, itemtxs_formset = invoice_itemtxs_formset_class(
entity_slug=entity_slug) request.POST,
user_model=self.request.dealer.user,
invoice_model=invoice_model,
entity_slug=entity_slug,
)
if not invoice_model.can_edit_items(): if not invoice_model.can_edit_items():
messages.add_message( messages.add_message(
request, 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, level=messages.ERROR,
extra_tags='is-danger' extra_tags="is-danger",
) )
context = self.get_context_data(itemtxs_formset=itemtxs_formset) context = self.get_context_data(itemtxs_formset=itemtxs_formset)
return self.render_to_response(context=context) return self.render_to_response(context=context)
@ -941,8 +987,12 @@ class InvoiceModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Update
if itemtxs_formset.has_changed(): if itemtxs_formset.has_changed():
if itemtxs_formset.is_valid(): if itemtxs_formset.is_valid():
itemtxs_list = itemtxs_formset.save(commit=False) itemtxs_list = itemtxs_formset.save(commit=False)
entity_qs = EntityModel.objects.for_user(user_model=self.request.dealer.user) entity_qs = EntityModel.objects.for_user(
entity_model: EntityModel = get_object_or_404(entity_qs, slug__exact=entity_slug) user_model=self.request.dealer.user
)
entity_model: EntityModel = get_object_or_404(
entity_qs, slug__exact=entity_slug
)
for itemtxs in itemtxs_list: for itemtxs in itemtxs_list:
itemtxs.invoice_model_id = invoice_model.uuid itemtxs.invoice_model_id = invoice_model.uuid
@ -953,53 +1003,72 @@ class InvoiceModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Update
invoice_model.get_state(commit=True) invoice_model.get_state(commit=True)
invoice_model.clean() invoice_model.clean()
invoice_model.save( invoice_model.save(
update_fields=['amount_due', update_fields=[
'amount_receivable', "amount_due",
'amount_unearned', "amount_receivable",
'amount_earned', "amount_unearned",
'updated'] "amount_earned",
"updated",
]
) )
invoice_model.migrate_state( invoice_model.migrate_state(
entity_slug=entity_slug, entity_slug=entity_slug,
user_model=self.request.user, user_model=self.request.user,
raise_exception=False, raise_exception=False,
itemtxs_qs=itemtxs_qs itemtxs_qs=itemtxs_qs,
) )
messages.add_message(request, messages.add_message(
message=f'Items for Invoice {invoice_model.invoice_number} saved.', request,
level=messages.SUCCESS, message=f"Items for Invoice {invoice_model.invoice_number} saved.",
extra_tags='is-success') level=messages.SUCCESS,
extra_tags="is-success",
)
return HttpResponseRedirect( return HttpResponseRedirect(
redirect_to=reverse('django_ledger:invoice-update', redirect_to=reverse(
kwargs={ "django_ledger:invoice-update",
'entity_slug': entity_slug, kwargs={
'invoice_pk': invoice_pk "entity_slug": entity_slug,
}) "invoice_pk": invoice_pk,
},
)
) )
# if not valid, return formset with errors... # 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) return super(InvoiceModelUpdateView, self).post(request, **kwargs)
class ChartOfAccountModelModelBaseViewMixIn(
class ChartOfAccountModelModelBaseViewMixIn(LoginRequiredMixin, PermissionRequiredMixin): LoginRequiredMixin, PermissionRequiredMixin
):
queryset = None queryset = None
permission_required = [] permission_required = []
def get_queryset(self): def get_queryset(self):
if self.queryset is None: if self.queryset is None:
entity_model = self.request.dealer.entity 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() return super().get_queryset()
def get_redirect_url(self, *args, **kwargs): def get_redirect_url(self, *args, **kwargs):
return reverse('coa-list', kwargs={'dealer_slug': self.request.dealer.slug, return reverse(
'entity_slug': self.request.entity.slug}) "coa-list",
kwargs={
"dealer_slug": self.request.dealer.slug,
"entity_slug": self.request.entity.slug,
},
)
class ChartOfAccountModelListView(ChartOfAccountModelModelBaseViewMixIn, ListView): class ChartOfAccountModelListView(ChartOfAccountModelModelBaseViewMixIn, ListView):
template_name = 'chart_of_accounts/coa_list.html' template_name = "chart_of_accounts/coa_list.html"
context_object_name = 'coa_list' context_object_name = "coa_list"
inactive = False inactive = False
def get_queryset(self): def get_queryset(self):
@ -1010,84 +1079,116 @@ class ChartOfAccountModelListView(ChartOfAccountModelModelBaseViewMixIn, ListVie
def get_context_data(self, *, object_list=None, **kwargs): def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=None, **kwargs) context = super().get_context_data(object_list=None, **kwargs)
context['inactive'] = self.inactive context["inactive"] = self.inactive
context['header_subtitle'] = self.request.entity.name context["header_subtitle"] = self.request.entity.name
context['header_subtitle_icon'] = 'gravity-ui:hierarchy' context["header_subtitle_icon"] = "gravity-ui:hierarchy"
context['page_title'] = 'Inactive Chart of Account List' if self.inactive else 'Chart of Accounts List' context["page_title"] = (
context['header_title'] = 'Inactive Chart of Account List' if self.inactive else 'Chart of Accounts List' "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 return context
class ChartOfAccountModelCreateView(ChartOfAccountModelModelBaseViewMixIn, CreateView): class ChartOfAccountModelCreateView(ChartOfAccountModelModelBaseViewMixIn, CreateView):
template_name = 'chart_of_accounts/coa_create.html' template_name = "chart_of_accounts/coa_create.html"
extra_context = { extra_context = {
'header_title': _('Create Chart of Accounts'), "header_title": _("Create Chart of Accounts"),
'page_title': _('Create Chart of Account'), "page_title": _("Create Chart of Account"),
} }
def get_initial(self): def get_initial(self):
return { return {
'entity': self.request.entity, "entity": self.request.entity,
} }
def get_form(self, form_class=None): def get_form(self, form_class=None):
return ChartOfAccountsModelCreateForm( return ChartOfAccountsModelCreateForm(
entity_model=self.request.entity, entity_model=self.request.entity, **self.get_form_kwargs()
**self.get_form_kwargs()
) )
def get_context_data(self, *, object_list=None, **kwargs): def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(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"] = (
context['header_subtitle_icon'] = 'gravity-ui:hierarchy' f"New Chart of Accounts: {self.request.entity.name}"
)
context["header_subtitle_icon"] = "gravity-ui:hierarchy"
return context return context
def get_success_url(self): def get_success_url(self):
return reverse('coa-list', kwargs={'dealer_slug': self.request.dealer.slug, return reverse(
'entity_slug': self.request.entity.slug}) "coa-list",
kwargs={
"dealer_slug": self.request.dealer.slug,
"entity_slug": self.request.entity.slug,
},
)
class ChartOfAccountModelUpdateView(ChartOfAccountModelModelBaseViewMixIn, UpdateView): class ChartOfAccountModelUpdateView(ChartOfAccountModelModelBaseViewMixIn, UpdateView):
context_object_name = 'coa_model' context_object_name = "coa_model"
slug_url_kwarg = 'coa_slug' slug_url_kwarg = "coa_slug"
template_name = 'chart_of_accounts/coa_update.html' template_name = "chart_of_accounts/coa_update.html"
form_class = ChartOfAccountsModelUpdateForm form_class = ChartOfAccountsModelUpdateForm
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
chart_of_accounts_model: ChartOfAccountModel = self.object chart_of_accounts_model: ChartOfAccountModel = self.object
context['page_title'] = f'Update Chart of Account {chart_of_accounts_model.name}' context["page_title"] = (
context['header_title'] = f'Update Chart of Account {chart_of_accounts_model.name}' 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 return context
def get_success_url(self): def get_success_url(self):
return reverse('coa-list', kwargs={'dealer_slug': self.request.dealer.slug, return reverse(
'entity_slug': self.request.entity.slug}) "coa-list",
kwargs={
"dealer_slug": self.request.dealer.slug,
"entity_slug": self.request.entity.slug,
},
)
class CharOfAccountModelActionView(ChartOfAccountModelModelBaseViewMixIn, class CharOfAccountModelActionView(
RedirectView, ChartOfAccountModelModelBaseViewMixIn, RedirectView, SingleObjectMixin
SingleObjectMixin): ):
http_method_names = ['get'] http_method_names = ["get"]
slug_url_kwarg = 'coa_slug' slug_url_kwarg = "coa_slug"
action_name = None action_name = None
commit = True commit = True
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
kwargs['user_model'] = self.request.user kwargs["user_model"] = self.request.user
if not self.action_name: if not self.action_name:
raise ImproperlyConfigured('View attribute action_name is required.') raise ImproperlyConfigured("View attribute action_name is required.")
response = super(CharOfAccountModelActionView, self).get(request, *args, **kwargs) response = super(CharOfAccountModelActionView, self).get(
request, *args, **kwargs
)
coa_model: ChartOfAccountModel = self.get_object() coa_model: ChartOfAccountModel = self.get_object()
try: try:
getattr(coa_model, self.action_name)(commit=self.commit, **kwargs) getattr(coa_model, self.action_name)(commit=self.commit, **kwargs)
messages.add_message(request, level=messages.SUCCESS, extra_tags='is-success', messages.add_message(
message=_('Successfully updated {} Default Chart of Account to '.format( request,
request.entity.name) + level=messages.SUCCESS,
'{}'.format(coa_model.name))) extra_tags="is-success",
message=_(
"Successfully updated {} Default Chart of Account to ".format(
request.entity.name
)
+ "{}".format(coa_model.name)
),
)
except ValidationError as e: except ValidationError as e:
messages.add_message(request, messages.add_message(
message=e.message, request, message=e.message, level=messages.ERROR, extra_tags="is-danger"
level=messages.ERROR, )
extra_tags='is-danger')
return response return response

View File

@ -28,6 +28,7 @@ from plans.models import UserPlan
from plans.signals import order_completed, activate_user_plan from plans.signals import order_completed, activate_user_plan
from inventory.tasks import send_email from inventory.tasks import send_email
from django.conf import settings from django.conf import settings
# logging # logging
import logging import logging
@ -177,7 +178,11 @@ def create_ledger_entity(sender, instance, created, **kwargs):
entity.create_uom(name=u[1], unit_abbr=u[0]) entity.create_uom(name=u[1], unit_abbr=u[0])
# Create COA accounts, background task # 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)) # async_task('inventory.tasks.check_create_coa_accounts', instance, schedule_type='O', schedule_time=timedelta(seconds=20))
# create_settings(instance.pk) # create_settings(instance.pk)
@ -273,14 +278,14 @@ def create_item_model(sender, instance, created, **kwargs):
else: else:
instance.item_model.default_amount = instance.marked_price instance.item_model.default_amount = instance.marked_price
# inventory = entity.create_item_inventory( # inventory = entity.create_item_inventory(
# name=instance.vin, # name=instance.vin,
# uom_model=uom, # uom_model=uom,
# item_type=ItemModel.ITEM_TYPE_LUMP_SUM # item_type=ItemModel.ITEM_TYPE_LUMP_SUM
# ) # )
# inventory.additional_info = {} # inventory.additional_info = {}
# inventory.additional_info.update({"car_info": instance.to_dict()}) # inventory.additional_info.update({"car_info": instance.to_dict()})
# inventory.save() # inventory.save()
# else: # else:
# instance.item_model.additional_info.update({"car_info": instance.to_dict()}) # instance.item_model.additional_info.update({"car_info": instance.to_dict()})
# instance.item_model.save() # instance.item_model.save()
@ -1039,7 +1044,11 @@ def po_fullfilled_notification(sender, instance, created, **kwargs):
po_number=instance.po_number, po_number=instance.po_number,
url=reverse( url=reverse(
"purchase_order_detail", "purchase_order_detail",
kwargs={"dealer_slug": dealer.slug,"entity_slug":instance.entity.slug, "pk": instance.pk}, kwargs={
"dealer_slug": dealer.slug,
"entity_slug": instance.entity.slug,
"pk": instance.pk,
},
), ),
), ),
) )
@ -1086,7 +1095,10 @@ def sale_order_created_notification(sender, instance, created, **kwargs):
estimate_number=instance.estimate.estimate_number, estimate_number=instance.estimate.estimate_number,
url=reverse( url=reverse(
"estimate_detail", "estimate_detail",
kwargs={"dealer_slug": instance.dealer.slug, "pk": instance.estimate.pk}, kwargs={
"dealer_slug": instance.dealer.slug,
"pk": instance.estimate.pk,
},
), ),
), ),
) )
@ -1131,7 +1143,7 @@ def estimate_in_review_notification(sender, instance, created, **kwargs):
url=reverse( url=reverse(
"estimate_detail", "estimate_detail",
kwargs={"dealer_slug": dealer.slug, "pk": instance.pk}, kwargs={"dealer_slug": dealer.slug, "pk": instance.pk},
) ),
), ),
) )
@ -1188,7 +1200,11 @@ def bill_model_in_approve_notification(sender, instance, created, **kwargs):
bill_number=instance.bill_number, bill_number=instance.bill_number,
url=reverse( url=reverse(
"bill-update", "bill-update",
kwargs={"dealer_slug": dealer.slug, "entity_slug": dealer.entity.slug, "bill_pk": instance.pk}, kwargs={
"dealer_slug": dealer.slug,
"entity_slug": dealer.entity.slug,
"bill_pk": instance.pk,
},
), ),
), ),
) )
@ -1224,7 +1240,6 @@ def bill_model_in_approve_notification(sender, instance, created, **kwargs):
# ) # )
@receiver(post_save, sender=models.Ticket) @receiver(post_save, sender=models.Ticket)
def send_ticket_notification(sender, instance, created, **kwargs): def send_ticket_notification(sender, instance, created, **kwargs):
if created: if created:
@ -1249,20 +1264,23 @@ def send_ticket_notification(sender, instance, created, **kwargs):
) )
else: else:
models.Notification.objects.create( models.Notification.objects.create(
user=instance.dealer.user, user=instance.dealer.user,
message=_( message=_(
""" """
Support Ticket #{ticket_number} has been updated. Support Ticket #{ticket_number} has been updated.
<a href="{url}" target="_blank">View</a>. <a href="{url}" target="_blank">View</a>.
""" """
).format( ).format(
ticket_number=instance.pk, ticket_number=instance.pk,
url=reverse( url=reverse(
"ticket_detail", "ticket_detail",
kwargs={"dealer_slug": instance.dealer.slug, "ticket_id": instance.pk}, kwargs={
), "dealer_slug": instance.dealer.slug,
"ticket_id": instance.pk,
},
), ),
) ),
)
@receiver(post_save, sender=models.CarColors) @receiver(post_save, sender=models.CarColors)
@ -1273,30 +1291,31 @@ def handle_car_image(sender, instance, created, **kwargs):
try: try:
# Create or get car image record # Create or get car image record
car = instance.car car = instance.car
car_image, created = models.CarImage.objects.get_or_create(car=car, defaults={'image_hash': car.get_hash}) car_image, created = models.CarImage.objects.get_or_create(
car=car, defaults={"image_hash": car.get_hash}
)
# Check for existing image with same hash # Check for existing image with same hash
existing = models.CarImage.objects.filter( existing = (
image_hash=car_image.image_hash, models.CarImage.objects.filter(
image__isnull=False image_hash=car_image.image_hash, image__isnull=False
).exclude(car=car).first() )
.exclude(car=car)
.first()
)
if existing: if existing:
# Copy existing image # Copy existing image
car_image.image.save( car_image.image.save(existing.image.name, existing.image.file, save=True)
existing.image.name,
existing.image.file,
save=True
)
logger.info(f"Reused image for car {car.vin}") logger.info(f"Reused image for car {car.vin}")
else: else:
# Schedule async generation # Schedule async generation
async_task( async_task(
'inventory.tasks.generate_car_image_task', "inventory.tasks.generate_car_image_task",
car_image.id, car_image.id,
task_name=f"generate_car_image_{car.vin}" task_name=f"generate_car_image_{car.vin}",
) )
logger.info(f"Scheduled image generation for car {car.vin}") logger.info(f"Scheduled image generation for car {car.vin}")
except Exception as e: 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}")

View File

@ -17,11 +17,19 @@ from django.core.files.base import ContentFile
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from allauth.account.models import EmailAddress from allauth.account.models import EmailAddress
from django.core.mail import EmailMultiAlternatives 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.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import User, Group, Permission from django.contrib.auth.models import User, Group, Permission
from inventory.models import DealerSettings, Dealer,Schedule,Notification,CarReservation,CarStatusChoices,CarImage from inventory.models import (
DealerSettings,
Dealer,
Schedule,
Notification,
CarReservation,
CarStatusChoices,
CarImage,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
@ -52,6 +60,7 @@ def create_settings(pk):
.first(), .first(),
) )
def create_coa_accounts(**kwargs): def create_coa_accounts(**kwargs):
logger.info("creating all accounts are created") logger.info("creating all accounts are created")
instance = kwargs.get("dealer") instance = kwargs.get("dealer")
@ -62,9 +71,6 @@ def create_coa_accounts(**kwargs):
create_account(entity, coa, account_data) create_account(entity, coa, account_data)
# def create_coa_accounts1(pk): # def create_coa_accounts1(pk):
# with transaction.atomic(): # with transaction.atomic():
# instance = Dealer.objects.select_for_update().get(pk=pk) # instance = Dealer.objects.select_for_update().get(pk=pk)
@ -800,8 +806,6 @@ def create_user_dealer(email, password, name, arabic_name, phone, crn, vrn, addr
# transaction.on_commit(run) # transaction.on_commit(run)
def send_bilingual_reminder(user_id, plan_id, expiration_date, days_until_expire): def send_bilingual_reminder(user_id, plan_id, expiration_date, days_until_expire):
"""Send bilingual email reminder using Django-Q""" """Send bilingual email reminder using Django-Q"""
try: try:
@ -809,41 +813,49 @@ def send_bilingual_reminder(user_id, plan_id, expiration_date, days_until_expire
plan = Plan.objects.get(id=plan_id) plan = Plan.objects.get(id=plan_id)
# Determine user language preference # Determine user language preference
user_language = getattr(user, 'language', settings.LANGUAGE_CODE) user_language = getattr(user, "language", settings.LANGUAGE_CODE)
activate(user_language) activate(user_language)
# Context data # Context data
context = { context = {
'user': user, "user": user,
'plan': plan, "plan": plan,
'expiration_date': expiration_date, "expiration_date": expiration_date,
'days_until_expire': days_until_expire, "days_until_expire": days_until_expire,
'SITE_NAME': settings.SITE_NAME, "SITE_NAME": settings.SITE_NAME,
'RENEWAL_URL': "url" ,#settings.RENEWAL_URL, "RENEWAL_URL": "url", # settings.RENEWAL_URL,
'direction': 'rtl' if user_language.startswith('ar') else 'ltr' "direction": "rtl" if user_language.startswith("ar") else "ltr",
} }
# Subject with translation # 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} أيام" subject_ar = f"اشتراكك في {plan.name} ينتهي خلال {days_until_expire} أيام"
# Render templates # Render templates
text_content = render_to_string([ text_content = render_to_string(
f'emails/expiration_reminder_{user_language}.txt', [
'emails/expiration_reminder.txt' f"emails/expiration_reminder_{user_language}.txt",
], context) "emails/expiration_reminder.txt",
],
context,
)
html_content = render_to_string([ html_content = render_to_string(
f'emails/expiration_reminder_{user_language}.html', [
'emails/expiration_reminder.html' f"emails/expiration_reminder_{user_language}.html",
], context) "emails/expiration_reminder.html",
],
context,
)
# Create email # Create email
email = EmailMultiAlternatives( 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, body=text_content,
from_email=settings.DEFAULT_FROM_EMAIL, from_email=settings.DEFAULT_FROM_EMAIL,
to=[user.email] to=[user.email],
) )
email.attach_alternative(html_content, "text/html") email.attach_alternative(html_content, "text/html")
email.send() email.send()
@ -853,6 +865,7 @@ def send_bilingual_reminder(user_id, plan_id, expiration_date, days_until_expire
logger.error(f"Email failed: {str(e)}") logger.error(f"Email failed: {str(e)}")
raise raise
def handle_email_result(task): def handle_email_result(task):
"""Callback for email results""" """Callback for email results"""
if task.success: if task.success:
@ -861,7 +874,6 @@ def handle_email_result(task):
logger.error(f"Email task failed: {task.result}") logger.error(f"Email task failed: {task.result}")
def send_schedule_reminder_email(schedule_id): def send_schedule_reminder_email(schedule_id):
""" """
Sends an email reminder for a specific schedule. Sends an email reminder for a specific schedule.
@ -871,46 +883,67 @@ def send_schedule_reminder_email(schedule_id):
schedule = Schedule.objects.get(pk=schedule_id) schedule = Schedule.objects.get(pk=schedule_id)
# Ensure the user has an email and the schedule is not completed/canceled # 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"]: if not schedule.scheduled_by.email or schedule.status in [
logger.error(f"Skipping email for Schedule ID {schedule_id}: No email or schedule status is {schedule.status}.") "completed",
"canceled",
]:
logger.error(
f"Skipping email for Schedule ID {schedule_id}: No email or schedule status is {schedule.status}."
)
return return
user_email = schedule.scheduled_by.email user_email = schedule.scheduled_by.email
Notification.objects.create( Notification.objects.create(
user=schedule.scheduled_by, user=schedule.scheduled_by,
message=_( message=_(
""" """
Reminder: You have an appointment scheduled for {scheduled_type} After 15 minutes <a href="{url}" target="_blank">View</a>. 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 # Prepare context for email templates
context = { context = {
'schedule_purpose': schedule.purpose, "schedule_purpose": schedule.purpose,
'scheduled_at': schedule.scheduled_at.astimezone(timezone.get_current_timezone()).strftime('%Y-%m-%d %H:%M %Z'), # Format with timezone "scheduled_at": schedule.scheduled_at.astimezone(
'schedule_type': schedule.scheduled_type, timezone.get_current_timezone()
'customer_name': schedule.customer.customer_name if schedule.customer else 'N/A', ).strftime("%Y-%m-%d %H:%M %Z"), # Format with timezone
'notes': schedule.notes, "schedule_type": schedule.scheduled_type,
'user_name': schedule.scheduled_by.get_full_name() or schedule.scheduled_by.email, "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 # Render email content from templates
html_message = render_to_string('emails/schedule_reminder.html', context) html_message = render_to_string("emails/schedule_reminder.html", context)
plain_message = render_to_string('emails/schedule_reminder.txt', context) plain_message = render_to_string("emails/schedule_reminder.txt", context)
send_mail( send_mail(
f'Reminder: Your Upcoming Schedule - {schedule.purpose}', f"Reminder: Your Upcoming Schedule - {schedule.purpose}",
plain_message, plain_message,
settings.DEFAULT_FROM_EMAIL, settings.DEFAULT_FROM_EMAIL,
[user_email], [user_email],
html_message=html_message, 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: 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: except Exception as e:
logger.info(f"Error sending reminder email for Schedule ID {schedule_id}: {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) # Optional: A hook function to log the status of the email task (add to your_app/tasks.py)
def log_email_status(task): def log_email_status(task):
""" """
@ -918,9 +951,14 @@ def log_email_status(task):
It logs whether the task was successful or not. It logs whether the task was successful or not.
""" """
if task.success: 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: 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): def remove_reservation_by_id(reservation_id):
try: try:
@ -931,8 +969,9 @@ def remove_reservation_by_id(reservation_id):
except Exception as e: except Exception as e:
logger.error(f"Error removing reservation with ID {reservation_id}: {e}") logger.error(f"Error removing reservation with ID {reservation_id}: {e}")
def test_task(**kwargs): def test_task(**kwargs):
print("TASK : ",kwargs.get("dealer")) print("TASK : ", kwargs.get("dealer"))
def generate_car_image_task(car_image_id): def generate_car_image_task(car_image_id):
@ -940,22 +979,25 @@ def generate_car_image_task(car_image_id):
Simple async task to generate car image Simple async task to generate car image
""" """
from inventory.utils import generate_car_image_simple from inventory.utils import generate_car_image_simple
try: try:
car_image = CarImage.objects.get(id=car_image_id) car_image = CarImage.objects.get(id=car_image_id)
result = generate_car_image_simple(car_image) result = generate_car_image_simple(car_image)
return { return {
'success': result.get('success', False), "success": result.get("success", False),
'car_image_id': car_image_id, "car_image_id": car_image_id,
'error': result.get('error'), "error": result.get("error"),
'message': 'Image generated' if result.get('success') else 'Generation failed' "message": "Image generated"
if result.get("success")
else "Generation failed",
} }
except CarImage.DoesNotExist: except CarImage.DoesNotExist:
error_msg = f"CarImage with id {car_image_id} not found" error_msg = f"CarImage with id {car_image_id} not found"
logger.error(error_msg) logger.error(error_msg)
return {'success': False, 'error': error_msg} return {"success": False, "error": error_msg}
except Exception as e: except Exception as e:
error_msg = f"Unexpected error: {e}" error_msg = f"Unexpected error: {e}"
logger.error(error_msg) logger.error(error_msg)
return {'success': False, 'error': error_msg} return {"success": False, "error": error_msg}

View File

@ -13,9 +13,9 @@ from django.db.models import Case, Value, When, IntegerField
register = template.Library() register = template.Library()
@register.filter @register.filter
def get_percentage(value, total): def get_percentage(value, total):
try: try:
value = int(value) value = int(value)
total = int(total) total = int(total)
@ -25,6 +25,7 @@ def get_percentage(value, total):
except (ValueError, TypeError): except (ValueError, TypeError):
return 0 return 0
@register.filter(name="percentage") @register.filter(name="percentage")
def percentage(value): def percentage(value):
if value is not None: 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""" # """Count how many permissions are checked from the allowed list"""
# return sum(1 for perm in permissions if perm.id in group_permission_ids) # 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): def invoice_item_formset_table(context, itemtxs_formset):
return { return {
'entity_slug': context['view'].kwargs['entity_slug'], "entity_slug": context["view"].kwargs["entity_slug"],
'invoice_model': context['invoice'], "invoice_model": context["invoice"],
'total_amount__sum': context['total_amount__sum'], "total_amount__sum": context["total_amount__sum"],
'itemtxs_formset': itemtxs_formset, "itemtxs_formset": itemtxs_formset,
} }

View File

@ -40,13 +40,18 @@ urlpatterns = [
views.assign_car_makes, views.assign_car_makes,
name="assign_car_makes", name="assign_car_makes",
), ),
# dashboards for manager, dealer, inventory and accounatant
path(
#dashboards for manager, dealer, inventory and accounatant "dashboards/<slug:dealer_slug>/general/",
path("dashboards/<slug:dealer_slug>/general/", views.general_dashboard,name="general_dashboard"), views.general_dashboard,
#dashboard for sales name="general_dashboard",
path("dashboards/<slug:dealer_slug>/sales/", views.sales_dashboard, name="sales_dashboard"), ),
# dashboard for sales
path(
"dashboards/<slug:dealer_slug>/sales/",
views.sales_dashboard,
name="sales_dashboard",
),
path( path(
"<slug:dealer_slug>/cars/aging-inventory/list", "<slug:dealer_slug>/cars/aging-inventory/list",
views.aging_inventory_list_view, views.aging_inventory_list_view,
@ -777,7 +782,11 @@ urlpatterns = [
views.EstimateDetailView.as_view(), views.EstimateDetailView.as_view(),
name="estimate_detail", name="estimate_detail",
), ),
path('<slug:dealer_slug>/sales/estimates/print/<uuid:pk>/', views.EstimatePrintView.as_view(), name='estimate_print'), path(
"<slug:dealer_slug>/sales/estimates/print/<uuid:pk>/",
views.EstimatePrintView.as_view(),
name="estimate_print",
),
path( path(
"<slug:dealer_slug>/sales/estimates/create/", "<slug:dealer_slug>/sales/estimates/create/",
views.create_estimate, views.create_estimate,
@ -934,7 +943,6 @@ urlpatterns = [
views.ItemServiceUpdateView.as_view(), views.ItemServiceUpdateView.as_view(),
name="item_service_update", name="item_service_update",
), ),
# Expanese # Expanese
path( path(
"<slug:dealer_slug>/items/expeneses/", "<slug:dealer_slug>/items/expeneses/",
@ -1093,32 +1101,47 @@ urlpatterns = [
name="entity-ic-date", name="entity-ic-date",
), ),
# Chart of Accounts... # Chart of Accounts...
path('<slug:dealer_slug>/chart-of-accounts/<slug:entity_slug>/list/', path(
"<slug:dealer_slug>/chart-of-accounts/<slug:entity_slug>/list/",
views.ChartOfAccountModelListView.as_view(), views.ChartOfAccountModelListView.as_view(),
name='coa-list'), name="coa-list",
path('<slug:dealer_slug>/chart-of-accounts/<slug:entity_slug>/list/inactive/', ),
views.ChartOfAccountModelListView.as_view(inactive=True), path(
name='coa-list-inactive'), "<slug:dealer_slug>/chart-of-accounts/<slug:entity_slug>/list/inactive/",
path('<slug:dealer_slug>/<slug:entity_slug>/create/', views.ChartOfAccountModelListView.as_view(inactive=True),
views.ChartOfAccountModelCreateView.as_view(), name="coa-list-inactive",
name='coa-create'), ),
path('<slug:dealer_slug>/<slug:entity_slug>/detail/<slug:coa_slug>/', path(
views.ChartOfAccountModelListView.as_view(), "<slug:dealer_slug>/<slug:entity_slug>/create/",
name='coa-detail'), views.ChartOfAccountModelCreateView.as_view(),
path('<slug:dealer_slug>/<slug:entity_slug>/update/<slug:coa_slug>/', name="coa-create",
views.ChartOfAccountModelUpdateView.as_view(), ),
name='coa-update'), path(
"<slug:dealer_slug>/<slug:entity_slug>/detail/<slug:coa_slug>/",
views.ChartOfAccountModelListView.as_view(),
name="coa-detail",
),
path(
"<slug:dealer_slug>/<slug:entity_slug>/update/<slug:coa_slug>/",
views.ChartOfAccountModelUpdateView.as_view(),
name="coa-update",
),
# ACTIONS.... # ACTIONS....
path('<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-default/', path(
views.CharOfAccountModelActionView.as_view(action_name='mark_as_default'), "<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-default/",
name='coa-action-mark-as-default'), views.CharOfAccountModelActionView.as_view(action_name="mark_as_default"),
path('<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-active/', name="coa-action-mark-as-default",
views.CharOfAccountModelActionView.as_view(action_name='mark_as_active'), ),
name='coa-action-mark-as-active'), path(
path('<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-inactive/', "<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-active/",
views.CharOfAccountModelActionView.as_view(action_name='mark_as_inactive'), views.CharOfAccountModelActionView.as_view(action_name="mark_as_active"),
name='coa-action-mark-as-inactive'), name="coa-action-mark-as-active",
),
path(
"<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-inactive/",
views.CharOfAccountModelActionView.as_view(action_name="mark_as_inactive"),
name="coa-action-mark-as-inactive",
),
# CASH FLOW STATEMENTS... # CASH FLOW STATEMENTS...
# Entities... # Entities...
path( path(
@ -1294,40 +1317,74 @@ urlpatterns = [
views.PurchaseOrderMarkAsVoidView.as_view(), views.PurchaseOrderMarkAsVoidView.as_view(),
name="po-action-mark-as-void", name="po-action-mark-as-void",
), ),
# reports # reports
path( path(
"<slug:dealer_slug>/purchase-report/", "<slug:dealer_slug>/purchase-report/",
views.purchase_report_view, views.purchase_report_view,
name="po-report", name="po-report",
), ),
path('purchase-report/<slug:dealer_slug>/csv/', views.purchase_report_csv_export, name='purchase-report-csv-export'), path(
"purchase-report/<slug:dealer_slug>/csv/",
path( views.purchase_report_csv_export,
name="purchase-report-csv-export",
),
path(
"<slug:dealer_slug>/car-sale-report/", "<slug:dealer_slug>/car-sale-report/",
views.car_sale_report_view, views.car_sale_report_view,
name="car-sale-report", name="car-sale-report",
), ),
path('car-sale-report/<slug:dealer_slug>/csv/', views.car_sale_report_csv_export, name='car-sale-report-csv-export'), path(
"car-sale-report/<slug:dealer_slug>/csv/",
path('feature/recall/', views.RecallListView.as_view(), name='recall_list'), views.car_sale_report_csv_export,
path('feature/recall/filter/', views.RecallFilterView, name='recall_filter'), name="car-sale-report-csv-export",
path('feature/recall/<int:pk>/view/', views.RecallDetailView.as_view(), name='recall_detail'), ),
path('feature/recall/create/', views.RecallCreateView.as_view(), name='recall_create'), path("feature/recall/", views.RecallListView.as_view(), name="recall_list"),
path('feature/recall/success/', views.RecallSuccessView.as_view(), name='recall_success'), path("feature/recall/filter/", views.RecallFilterView, name="recall_filter"),
path(
path('<slug:dealer_slug>/schedules/calendar/', views.schedule_calendar, name='schedule_calendar'), "feature/recall/<int:pk>/view/",
views.RecallDetailView.as_view(),
name="recall_detail",
),
path(
"feature/recall/create/", views.RecallCreateView.as_view(), name="recall_create"
),
path(
"feature/recall/success/",
views.RecallSuccessView.as_view(),
name="recall_success",
),
path(
"<slug:dealer_slug>/schedules/calendar/",
views.schedule_calendar,
name="schedule_calendar",
),
# staff profile # staff profile
path('<slug:dealer_slug>/staff/<slug:slug>detail/', views.StaffDetailView.as_view(), name='staff_detail'), path(
"<slug:dealer_slug>/staff/<slug:slug>detail/",
views.StaffDetailView.as_view(),
name="staff_detail",
),
# tickets # tickets
path('help_center/view/', views.help_center, name='help_center'), path("help_center/view/", views.help_center, name="help_center"),
path('<slug:dealer_slug>/help_center/tickets/', views.ticket_list, name='ticket_list'), path(
path('help_center/tickets/<slug:dealer_slug>/create/', views.create_ticket, name='create_ticket'), "<slug:dealer_slug>/help_center/tickets/", views.ticket_list, name="ticket_list"
path('<slug:dealer_slug>/help_center/tickets/<int:ticket_id>/', views.ticket_detail, name='ticket_detail'), ),
path('help_center/tickets/<int:ticket_id>/update/', views.ticket_update, name='ticket_update'), path(
"help_center/tickets/<slug:dealer_slug>/create/",
views.create_ticket,
name="create_ticket",
),
path(
"<slug:dealer_slug>/help_center/tickets/<int:ticket_id>/",
views.ticket_detail,
name="ticket_detail",
),
path(
"help_center/tickets/<int:ticket_id>/update/",
views.ticket_update,
name="ticket_update",
),
# path('help_center/tickets/<int:ticket_id>/ticket_mark_resolved/', views.ticket_mark_resolved, name='ticket_mark_resolved'), # path('help_center/tickets/<int:ticket_id>/ticket_mark_resolved/', views.ticket_mark_resolved, name='ticket_mark_resolved'),
] ]
handler404 = "inventory.views.custom_page_not_found_view" handler404 = "inventory.views.custom_page_not_found_view"

View File

@ -73,15 +73,12 @@ def get_jwt_token():
try: try:
response = requests.post(url, headers=headers, json=data) response = requests.post(url, headers=headers, json=data)
response.raise_for_status() response.raise_for_status()
#logging for success # logging for success
logger.info("Successfully fetched JWT token.") logger.info("Successfully fetched JWT token.")
return response.text return response.text
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
#logging for error # logging for error
logger.error( logger.error(f"HTTP error fetching JWT token from {url}: ", exc_info=True)
f"HTTP error fetching JWT token from {url}: ",
exc_info=True
)
print(f"Error obtaining JWT token: {e}") print(f"Error obtaining JWT token: {e}")
return None return None
@ -169,7 +166,7 @@ def send_email(from_, to_, subject, message):
message = message message = message
from_email = from_ from_email = from_
recipient_list = [to_] 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): def get_user_type(request):
@ -236,10 +233,10 @@ def reserve_car(car, request):
) )
car.status = models.CarStatusChoices.RESERVED car.status = models.CarStatusChoices.RESERVED
car.save() car.save()
# --- Logging for Success --- # --- Logging for Success ---
DjangoQSchedule.objects.create( DjangoQSchedule.objects.create(
name=f"remove_reservation_for_car_with_vin_{car.vin}", 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, args=reservation.pk,
schedule_type=DjangoQSchedule.ONCE, schedule_type=DjangoQSchedule.ONCE,
next_run=reserved_until, next_run=reserved_until,
@ -257,7 +254,7 @@ def reserve_car(car, request):
f"Error reserving car {car.pk} ('{car.id_car_make} {car.id_car_model}') " f"Error reserving car {car.pk} ('{car.id_car_make} {car.id_car_model}') "
f"for user {request.user} . " f"for user {request.user} . "
f"Error: {e}", f"Error: {e}",
exc_info=True exc_info=True,
) )
messages.error(request, f"Error reserving car: {e}") messages.error(request, f"Error reserving car: {e}")
@ -1038,22 +1035,25 @@ class CarFinanceCalculator1:
self.item_transactions = self._get_item_transactions() self.item_transactions = self._get_item_transactions()
# self.additional_services = self._get_additional_services() # self.additional_services = self._get_additional_services()
def _get_vat_rate(self): 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: if not vat:
raise ObjectDoesNotExist("No active VAT rate found") raise ObjectDoesNotExist("No active VAT rate found")
return vat.rate return vat.rate
def _get_additional_services(self): def _get_additional_services(self):
return [x for item in self.item_transactions return [
for x in item.item_model.car.additional_services x
] for item in self.item_transactions
for x in item.item_model.car.additional_services
]
def _get_item_transactions(self): def _get_item_transactions(self):
return self.model.get_itemtxs_data()[0].all() return self.model.get_itemtxs_data()[0].all()
def get_items(self): def get_items(self):
return self._get_item_transactions() return self._get_item_transactions()
@staticmethod @staticmethod
def _get_quantity(item): def _get_quantity(item):
return item.ce_quantity or item.quantity return item.ce_quantity or item.quantity
@ -1068,17 +1068,17 @@ class CarFinanceCalculator1:
quantity = self._get_quantity(item) quantity = self._get_quantity(item)
car = item.item_model.car car = item.item_model.car
unit_price = Decimal(car.marked_price) 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) sell_price = unit_price - Decimal(discount)
return { return {
"item_number": item.item_model.item_number, "item_number": item.item_model.item_number,
"vin": car.vin, #car_info.get("vin"), "vin": car.vin, # car_info.get("vin"),
"make": car.id_car_make ,#car_info.get("make"), "make": car.id_car_make, # car_info.get("make"),
"model": car.id_car_model ,#car_info.get("model"), "model": car.id_car_model, # car_info.get("model"),
"year": car.year ,# car_info.get("year"), "year": car.year, # car_info.get("year"),
"logo": car.logo, # getattr(car.id_car_make, "logo", ""), "logo": car.logo, # getattr(car.id_car_make, "logo", ""),
"trim": car.id_car_trim ,# car_info.get("trim"), "trim": car.id_car_trim, # car_info.get("trim"),
"mileage": car.mileage ,# car_info.get("mileage"), "mileage": car.mileage, # car_info.get("mileage"),
"cost_price": car.cost_price, "cost_price": car.cost_price,
"selling_price": car.selling_price, "selling_price": car.selling_price,
"marked_price": car.marked_price, "marked_price": car.marked_price,
@ -1091,21 +1091,23 @@ class CarFinanceCalculator1:
"total_discount": discount, "total_discount": discount,
"final_price": sell_price + (sell_price * self.vat_rate), "final_price": sell_price + (sell_price * self.vat_rate),
"total_additionals": car.total_additional_services, "total_additionals": car.total_additional_services,
"grand_total": sell_price + (sell_price * self.vat_rate) + car.total_additional_services, "grand_total": sell_price
"additional_services": car.additional_services,# self._get_nested_value( + (sell_price * self.vat_rate)
#item, self.ADDITIONAL_SERVICES_KEY + car.total_additional_services,
#), "additional_services": car.additional_services, # self._get_nested_value(
# item, self.ADDITIONAL_SERVICES_KEY
# ),
} }
def calculate_totals(self): def calculate_totals(self):
total_price = sum( total_price = sum(
Decimal(item.item_model.car.marked_price) Decimal(item.item_model.car.marked_price) for item in self.item_transactions
for item in self.item_transactions
) )
total_additionals = sum( 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 total_price_discounted = total_price
if total_discount: if total_discount:
total_price_discounted = total_price - Decimal(total_discount) total_price_discounted = total_price - Decimal(total_discount)
@ -1113,13 +1115,15 @@ class CarFinanceCalculator1:
total_vat_amount = total_price_discounted * self.vat_rate total_vat_amount = total_price_discounted * self.vat_rate
return { return {
"total_price_discounted":total_price_discounted, "total_price_discounted": total_price_discounted,
"total_price_before_discount":total_price, "total_price_before_discount": total_price,
"total_price": total_price_discounted, "total_price": total_price_discounted,
"total_vat_amount": total_vat_amount, "total_vat_amount": total_vat_amount,
"total_discount": Decimal(total_discount), "total_discount": Decimal(total_discount),
"total_additionals": total_additionals, "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): def get_finance_data(self):
@ -1131,7 +1135,9 @@ class CarFinanceCalculator1:
), ),
"total_price": round(totals["total_price"], 2), "total_price": round(totals["total_price"], 2),
"total_price_discounted": round(totals["total_price_discounted"], 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": round(totals["total_vat_amount"] + totals["total_price"], 2),
"total_vat_amount": round(totals["total_vat_amount"], 2), "total_vat_amount": round(totals["total_vat_amount"], 2),
"total_discount": round(totals["total_discount"], 2), "total_discount": round(totals["total_discount"], 2),
@ -1140,6 +1146,8 @@ class CarFinanceCalculator1:
"additionals": self._get_additional_services(), "additionals": self._get_additional_services(),
"vat": round(self.vat_rate, 2), "vat": round(self.vat_rate, 2),
} }
class CarFinanceCalculator: class CarFinanceCalculator:
""" """
Class responsible for calculating car financing details. Class responsible for calculating car financing details.
@ -1185,22 +1193,25 @@ class CarFinanceCalculator:
self.item_transactions = self._get_item_transactions() self.item_transactions = self._get_item_transactions()
# self.additional_services = self._get_additional_services() # self.additional_services = self._get_additional_services()
def _get_vat_rate(self): 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: if not vat:
raise ObjectDoesNotExist("No active VAT rate found") raise ObjectDoesNotExist("No active VAT rate found")
return vat.rate return vat.rate
def _get_additional_services(self): def _get_additional_services(self):
return [x for item in self.item_transactions return [
for x in item.item_model.car.additional_services x
] for item in self.item_transactions
for x in item.item_model.car.additional_services
]
def _get_item_transactions(self): def _get_item_transactions(self):
return self.model.get_itemtxs_data()[0].all() return self.model.get_itemtxs_data()[0].all()
def get_items(self): def get_items(self):
return self._get_item_transactions() return self._get_item_transactions()
@staticmethod @staticmethod
def _get_quantity(item): def _get_quantity(item):
return item.ce_quantity or item.quantity return item.ce_quantity or item.quantity
@ -1215,17 +1226,17 @@ class CarFinanceCalculator:
quantity = self._get_quantity(item) quantity = self._get_quantity(item)
car = item.item_model.car car = item.item_model.car
unit_price = Decimal(car.marked_price) 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) sell_price = unit_price - Decimal(discount)
return { return {
"item_number": item.item_model.item_number, "item_number": item.item_model.item_number,
"vin": car.vin, #car_info.get("vin"), "vin": car.vin, # car_info.get("vin"),
"make": car.id_car_make ,#car_info.get("make"), "make": car.id_car_make, # car_info.get("make"),
"model": car.id_car_model ,#car_info.get("model"), "model": car.id_car_model, # car_info.get("model"),
"year": car.year ,# car_info.get("year"), "year": car.year, # car_info.get("year"),
"logo": car.logo, # getattr(car.id_car_make, "logo", ""), "logo": car.logo, # getattr(car.id_car_make, "logo", ""),
"trim": car.id_car_trim ,# car_info.get("trim"), "trim": car.id_car_trim, # car_info.get("trim"),
"mileage": car.mileage ,# car_info.get("mileage"), "mileage": car.mileage, # car_info.get("mileage"),
"cost_price": car.cost_price, "cost_price": car.cost_price,
"selling_price": car.selling_price, "selling_price": car.selling_price,
"marked_price": car.marked_price, "marked_price": car.marked_price,
@ -1238,21 +1249,23 @@ class CarFinanceCalculator:
"total_discount": discount, "total_discount": discount,
"final_price": sell_price + (sell_price * self.vat_rate), "final_price": sell_price + (sell_price * self.vat_rate),
"total_additionals": car.total_additional_services, "total_additionals": car.total_additional_services,
"grand_total": sell_price + (sell_price * self.vat_rate) + car.total_additional_services, "grand_total": sell_price
"additional_services": car.additional_services,# self._get_nested_value( + (sell_price * self.vat_rate)
#item, self.ADDITIONAL_SERVICES_KEY + car.total_additional_services,
#), "additional_services": car.additional_services, # self._get_nested_value(
# item, self.ADDITIONAL_SERVICES_KEY
# ),
} }
def calculate_totals(self): def calculate_totals(self):
total_price = sum( total_price = sum(
Decimal(item.item_model.car.marked_price) Decimal(item.item_model.car.marked_price) for item in self.item_transactions
for item in self.item_transactions
) )
total_additionals = sum( 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 total_price_discounted = total_price
if total_discount: if total_discount:
total_price_discounted = total_price - Decimal(total_discount) total_price_discounted = total_price - Decimal(total_discount)
@ -1260,13 +1273,15 @@ class CarFinanceCalculator:
total_vat_amount = total_price_discounted * self.vat_rate total_vat_amount = total_price_discounted * self.vat_rate
return { return {
"total_price_discounted":total_price_discounted, "total_price_discounted": total_price_discounted,
"total_price_before_discount":total_price, "total_price_before_discount": total_price,
"total_price": total_price_discounted, "total_price": total_price_discounted,
"total_vat_amount": total_vat_amount, "total_vat_amount": total_vat_amount,
"total_discount": Decimal(total_discount), "total_discount": Decimal(total_discount),
"total_additionals": total_additionals, "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): def get_finance_data(self):
@ -1278,7 +1293,9 @@ class CarFinanceCalculator:
), ),
"total_price": round(totals["total_price"], 2), "total_price": round(totals["total_price"], 2),
"total_price_discounted": round(totals["total_price_discounted"], 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": round(totals["total_vat_amount"] + totals["total_price"], 2),
"total_vat_amount": round(totals["total_vat_amount"], 2), "total_vat_amount": round(totals["total_vat_amount"], 2),
"total_discount": round(totals["total_discount"], 2), "total_discount": round(totals["total_discount"], 2),
@ -1288,58 +1305,60 @@ class CarFinanceCalculator:
"vat": round(self.vat_rate, 2), "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( def get_finance_data(estimate, dealer):
dealer=dealer, vat = models.VatRate.objects.filter(dealer=dealer, is_active=True).first()
content_type=ContentType.objects.get_for_model(EstimateModel), item = estimate.get_itemtxs_data()[0].first()
object_id=estimate.pk, car = item.item_model.car
) if isinstance(estimate, InvoiceModel) and hasattr(estimate, "ce_model"):
discount = extra_info.data.get("discount", 0) estimate = estimate.ce_model
discount = Decimal(discount)
additional_services = car.get_additional_services() extra_info = models.ExtraInfo.objects.get(
discounted_price=(Decimal(car.marked_price) - discount) dealer=dealer,
vat_amount = discounted_price * vat.rate content_type=ContentType.objects.get_for_model(EstimateModel),
total_services_vat=sum([x[1] for x in additional_services.get("services")]) object_id=estimate.pk,
total_vat=vat_amount+total_services_vat )
return { discount = extra_info.data.get("discount", 0)
"car": car, discount = Decimal(discount)
"discounted_price": discounted_price or 0,
"price_before_discount": car.marked_price, additional_services = car.get_additional_services()
"vat_amount": vat_amount, discounted_price = Decimal(car.marked_price) - discount
"vat_rate": vat.rate, vat_amount = discounted_price * vat.rate
"discount_amount": discount, total_services_vat = sum([x[1] for x in additional_services.get("services")])
"additional_services": additional_services, total_vat = vat_amount + total_services_vat
"final_price": discounted_price + vat_amount, return {
"total_services_vat":total_services_vat, "car": car,
"total_vat":total_vat, "discounted_price": discounted_price or 0,
"grand_total": discounted_price + total_vat + additional_services.get("total") "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 CarFinanceCalculator:
# """ # """
# Class responsible for calculating car financing details. # Class responsible for calculating car financing details.
@ -1554,7 +1573,6 @@ def get_local_name(self):
return getattr(self, "name", None) return getattr(self, "name", None)
@transaction.atomic @transaction.atomic
def set_invoice_payment(dealer, entity, invoice, amount, payment_method): def set_invoice_payment(dealer, entity, invoice, amount, payment_method):
""" """
@ -1566,6 +1584,7 @@ def set_invoice_payment(dealer, entity, invoice, amount, payment_method):
_post_sale_and_cogs(invoice, dealer) _post_sale_and_cogs(invoice, dealer)
def _post_sale_and_cogs(invoice, dealer): def _post_sale_and_cogs(invoice, dealer):
""" """
For every car line on the invoice: For every car line on the invoice:
@ -1574,15 +1593,39 @@ def _post_sale_and_cogs(invoice, dealer):
""" """
entity = invoice.ledger.entity entity = invoice.ledger.entity
# calc = CarFinanceCalculator(invoice) # calc = CarFinanceCalculator(invoice)
data = get_finance_data(invoice,dealer) data = get_finance_data(invoice, dealer)
car = data.get("car") car = data.get("car")
cash_acc = entity.get_default_coa_accounts().filter(role_default=True, role=roles.ASSET_CA_CASH).first() cash_acc = (
ar_acc = entity.get_default_coa_accounts().filter(role_default=True, role=roles.ASSET_CA_RECEIVABLES).first() entity.get_default_coa_accounts()
vat_acc = entity.get_default_coa_accounts().filter(role_default=True, role=roles.LIABILITY_CL_TAXES_PAYABLE).first() .filter(role_default=True, role=roles.ASSET_CA_CASH)
car_rev = entity.get_default_coa_accounts().filter(role_default=True, role=roles.INCOME_OPERATIONAL).first() .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() ar_acc = (
inv_acc = entity.get_default_coa_accounts().filter(role_default=True, role=roles.ASSET_CA_INVENTORY).first() 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']: # for car_data in data['cars']:
# car = invoice.get_itemtxs_data()[0].filter( # car = invoice.get_itemtxs_data()[0].filter(
@ -1590,12 +1633,12 @@ def _post_sale_and_cogs(invoice, dealer):
# ).first().item_model.car # ).first().item_model.car
# qty = Decimal(car_data['quantity']) # qty = Decimal(car_data['quantity'])
net_car_price = Decimal(data['discounted_price']) net_car_price = Decimal(data["discounted_price"])
net_additionals_price = Decimal(data['additional_services']['total']) net_additionals_price = Decimal(data["additional_services"]["total"])
vat_amount = Decimal(data['vat_amount']) vat_amount = Decimal(data["vat_amount"])
grand_total = net_car_price + car.get_additional_services_amount_ + vat_amount grand_total = net_car_price + car.get_additional_services_amount_ + vat_amount
cost_total = Decimal(car.cost_price) cost_total = Decimal(car.cost_price)
discount_amount =Decimal(data['discount_amount']) discount_amount = Decimal(data["discount_amount"])
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# 2A. Journal: Cash / A-R / VAT / Sales # 2A. Journal: Cash / A-R / VAT / Sales
@ -1606,15 +1649,15 @@ def _post_sale_and_cogs(invoice, dealer):
description=f"Sale {car.vin}", description=f"Sale {car.vin}",
origin=f"Invoice {invoice.invoice_number}", origin=f"Invoice {invoice.invoice_number}",
locked=False, locked=False,
posted=False posted=False,
) )
# Dr Cash (what the customer paid) # Dr Cash (what the customer paid)
TransactionModel.objects.create( TransactionModel.objects.create(
journal_entry=je_sale, journal_entry=je_sale,
account=cash_acc, account=cash_acc,
amount=grand_total, amount=grand_total,
tx_type='debit', tx_type="debit",
description='Debit to Cash on Hand' description="Debit to Cash on Hand",
) )
# # Cr A/R (clear the receivable) # # Cr A/R (clear the receivable)
@ -1630,8 +1673,8 @@ def _post_sale_and_cogs(invoice, dealer):
journal_entry=je_sale, journal_entry=je_sale,
account=vat_acc, account=vat_acc,
amount=vat_amount, amount=vat_amount,
tx_type='credit', tx_type="credit",
description="Credit to Tax Payable" description="Credit to Tax Payable",
) )
# Cr Sales Car # Cr Sales Car
@ -1639,8 +1682,8 @@ def _post_sale_and_cogs(invoice, dealer):
journal_entry=je_sale, journal_entry=je_sale,
account=car_rev, account=car_rev,
amount=net_car_price, amount=net_car_price,
tx_type='credit', tx_type="credit",
description=" Credit to Car Sales" description=" Credit to Car Sales",
) )
if car.get_additional_services_amount > 0: if car.get_additional_services_amount > 0:
@ -1649,16 +1692,15 @@ def _post_sale_and_cogs(invoice, dealer):
journal_entry=je_sale, journal_entry=je_sale,
account=add_rev, account=add_rev,
amount=car.get_additional_services_amount, amount=car.get_additional_services_amount,
tx_type='credit', tx_type="credit",
description="Credit to After-Sales Services" description="Credit to After-Sales Services",
) )
TransactionModel.objects.create( TransactionModel.objects.create(
journal_entry=je_sale, journal_entry=je_sale,
account=vat_acc, account=vat_acc,
amount=car.get_additional_services_vat, amount=car.get_additional_services_vat,
tx_type='credit', tx_type="credit",
description="Credit to Tax Payable (Additional Services)" description="Credit to Tax Payable (Additional Services)",
) )
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@ -1669,7 +1711,7 @@ def _post_sale_and_cogs(invoice, dealer):
description=f"COGS {car.vin}", description=f"COGS {car.vin}",
origin=f"Invoice {invoice.invoice_number}", origin=f"Invoice {invoice.invoice_number}",
locked=False, locked=False,
posted=False posted=False,
) )
# Dr COGS # Dr COGS
@ -1677,15 +1719,12 @@ def _post_sale_and_cogs(invoice, dealer):
journal_entry=je_cogs, journal_entry=je_cogs,
account=cogs_acc, account=cogs_acc,
amount=cost_total, amount=cost_total,
tx_type='debit', tx_type="debit",
) )
# Cr Inventory # Cr Inventory
TransactionModel.objects.create( TransactionModel.objects.create(
journal_entry=je_cogs, journal_entry=je_cogs, account=inv_acc, amount=cost_total, tx_type="credit"
account=inv_acc,
amount=cost_total,
tx_type='credit'
) )
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# 2C. Update car state flags inside the same transaction # 2C. Update car state flags inside the same transaction
@ -1693,10 +1732,12 @@ def _post_sale_and_cogs(invoice, dealer):
entity.get_items_inventory().filter(name=car.vin).update(for_inventory=False) entity.get_items_inventory().filter(name=car.vin).update(for_inventory=False)
# car.item_model.for_inventory = False # car.item_model.for_inventory = False
# car.item_model.save(update_fields=['for_inventory']) # car.item_model.save(update_fields=['for_inventory'])
car.discount_amount=discount_amount car.discount_amount = discount_amount
car.selling_price = grand_total car.selling_price = grand_total
# car.is_sold = True # car.is_sold = True
car.save() car.save()
# def handle_account_process(invoice, amount, finance_data): # def handle_account_process(invoice, amount, finance_data):
# """ # """
# Processes accounting transactions based on an invoice, financial data, # Processes accounting transactions based on an invoice, financial data,
@ -1787,29 +1828,29 @@ def _post_sale_and_cogs(invoice, dealer):
# car.finances.save() # car.finances.save()
# car.item_model.save() # car.item_model.save()
# TransactionModel.objects.create( # TransactionModel.objects.create(
# journal_entry=journal, # journal_entry=journal,
# account=additional_services_account, # Debit Additional Services # account=additional_services_account, # Debit Additional Services
# amount=Decimal(car.finances.total_additionals), # amount=Decimal(car.finances.total_additionals),
# tx_type="debit", # tx_type="debit",
# description="Additional Services", # description="Additional Services",
# ) # )
# TransactionModel.objects.create( # TransactionModel.objects.create(
# journal_entry=journal, # journal_entry=journal,
# account=inventory_account, # Credit Inventory account # account=inventory_account, # Credit Inventory account
# amount=Decimal(finance_data.get("grand_total")), # amount=Decimal(finance_data.get("grand_total")),
# tx_type="credit", # tx_type="credit",
# description="Account Adjustment", # description="Account Adjustment",
# ) # )
# TransactionModel.objects.create( # TransactionModel.objects.create(
# journal_entry=journal, # journal_entry=journal,
# account=vat_payable_account, # Credit VAT Payable # account=vat_payable_account, # Credit VAT Payable
# amount=finance_data.get("total_vat_amount"), # amount=finance_data.get("total_vat_amount"),
# tx_type="credit", # tx_type="credit",
# description="VAT Payable on Invoice", # description="VAT Payable on Invoice",
# ) # )
def create_make_accounts(dealer): def create_make_accounts(dealer):
@ -1857,6 +1898,7 @@ def create_make_accounts(dealer):
active=True, active=True,
) )
def handle_payment(request, order): def handle_payment(request, order):
url = "https://api.moyasar.com/v1/payments" url = "https://api.moyasar.com/v1/payments"
callback_url = request.build_absolute_uri( callback_url = request.build_absolute_uri(
@ -1943,7 +1985,6 @@ def handle_payment(request, order):
# return user.dealer.quota # return user.dealer.quota
def get_accounts_data(): def get_accounts_data():
return [ return [
# Current Assets (must start with 1) # Current Assets (must start with 1)
@ -2339,6 +2380,7 @@ def get_accounts_data():
}, },
] ]
def create_account(entity, coa, account_data): def create_account(entity, coa, account_data):
try: try:
account = entity.create_account( account = entity.create_account(
@ -2371,17 +2413,16 @@ def get_or_generate_car_image(car):
return car_image.image.url return car_image.image.url
# Check for existing image with same hash # Check for existing image with same hash
existing = models.CarImage.objects.filter( existing = (
image_hash=car_image.image_hash, models.CarImage.objects.filter(
image__isnull=False image_hash=car_image.image_hash, image__isnull=False
).exclude(car=car).first() )
.exclude(car=car)
.first()
)
if existing: if existing:
car_image.image.save( car_image.image.save(existing.image.name, existing.image.file, save=True)
existing.image.name,
existing.image.file,
save=True
)
return car_image.image.url return car_image.image.url
# If no image exists and not already generating, schedule generation # If no image exists and not already generating, schedule generation
@ -2394,6 +2435,7 @@ def get_or_generate_car_image(car):
logger.error(f"Error getting/generating car image: {e}") logger.error(f"Error getting/generating car image: {e}")
return None return None
def force_regenerate_car_image(car): def force_regenerate_car_image(car):
""" """
Force regeneration of car image (useful for admin actions) Force regeneration of car image (useful for admin actions)
@ -2414,6 +2456,7 @@ def force_regenerate_car_image(car):
logger.error(f"Error forcing image regeneration: {e}") logger.error(f"Error forcing image regeneration: {e}")
return False return False
class CarImageAPIClient: class CarImageAPIClient:
"""Simple client to handle authenticated requests to the car image API""" """Simple client to handle authenticated requests to the car image API"""
@ -2436,7 +2479,7 @@ class CarImageAPIClient:
response.raise_for_status() response.raise_for_status()
# Get CSRF token from cookies # 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: if not self.csrf_token:
raise Exception("CSRF token not found in cookies") raise Exception("CSRF token not found in cookies")
@ -2444,16 +2487,17 @@ class CarImageAPIClient:
login_data = { login_data = {
"username": self.USERNAME, "username": self.USERNAME,
"password": self.PASSWORD, "password": self.PASSWORD,
"csrfmiddlewaretoken": self.csrf_token "csrfmiddlewaretoken": self.csrf_token,
} }
login_response = self.session.post( login_response = self.session.post(
f"{self.BASE_URL}/login", f"{self.BASE_URL}/login", data=login_data
data=login_data
) )
if login_response.status_code != 200: 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") logger.info("Successfully logged in to car image API")
return True return True
@ -2472,39 +2516,38 @@ class CarImageAPIClient:
try: try:
headers = { headers = {
'X-CSRFToken': self.csrf_token, "X-CSRFToken": self.csrf_token,
'Referer': self.BASE_URL, "Referer": self.BASE_URL,
} }
print(payload) print(payload)
generate_data = { generate_data = {
"year": payload['year'], "year": payload["year"],
"make": payload['make'], "make": payload["make"],
"model": payload['model'], "model": payload["model"],
"exterior_color": payload['color'], "exterior_color": payload["color"],
"angle": "3/4 rear", "angle": "3/4 rear",
"reference_image": "" "reference_image": "",
} }
response = self.session.post( response = self.session.post(
f"{self.BASE_URL}/generate", f"{self.BASE_URL}/generate",
json=generate_data, json=generate_data,
headers=headers, headers=headers,
timeout=160 timeout=160,
) )
response.raise_for_status() response.raise_for_status()
# Parse response # Parse response
result = response.json() result = response.json()
image_url = result.get('url') image_url = result.get("url")
if not image_url: if not image_url:
raise Exception("No image URL in response") raise Exception("No image URL in response")
# Download the actual image # Download the actual image
image_response = self.session.get( image_response = self.session.get(
f"{self.BASE_URL}{image_url}", f"{self.BASE_URL}{image_url}", timeout=160
timeout=160
) )
image_response.raise_for_status() image_response.raise_for_status()
@ -2520,9 +2563,11 @@ class CarImageAPIClient:
logger.error(error_msg) logger.error(error_msg)
return None, error_msg return None, error_msg
# Global client instance # Global client instance
api_client = CarImageAPIClient() api_client = CarImageAPIClient()
def resize_image(image_data, max_size=(800, 600)): def resize_image(image_data, max_size=(800, 600)):
""" """
Resize image to make it smaller while maintaining aspect ratio Resize image to make it smaller while maintaining aspect ratio
@ -2539,29 +2584,31 @@ def resize_image(image_data, max_size=(800, 600)):
# Save back to bytes in original format # Save back to bytes in original format
output_buffer = BytesIO() output_buffer = BytesIO()
if original_format and original_format.upper() in ['JPEG', 'JPG']: if original_format and original_format.upper() in ["JPEG", "JPG"]:
img.save(output_buffer, format='JPEG', quality=95, optimize=True) img.save(output_buffer, format="JPEG", quality=95, optimize=True)
elif original_format and original_format.upper() == 'PNG': elif original_format and original_format.upper() == "PNG":
# Preserve transparency for PNG # Preserve transparency for PNG
if original_mode == 'RGBA': if original_mode == "RGBA":
img.save(output_buffer, format='PNG', optimize=True) img.save(output_buffer, format="PNG", optimize=True)
else: else:
img.save(output_buffer, format='PNG', optimize=True) img.save(output_buffer, format="PNG", optimize=True)
else: else:
# Default to JPEG for other formats # 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 # Convert to RGB if image has transparency
background = Image.new('RGB', img.size, (255, 255, 255)) background = Image.new("RGB", img.size, (255, 255, 255))
if img.mode == 'RGBA': if img.mode == "RGBA":
background.paste(img, mask=img.split()[3]) background.paste(img, mask=img.split()[3])
else: else:
background.paste(img, (0, 0)) background.paste(img, (0, 0))
img = background 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() 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 return resized_data, None
except Exception as e: except Exception as e:
@ -2569,6 +2616,7 @@ def resize_image(image_data, max_size=(800, 600)):
logger.error(error_msg) logger.error(error_msg)
return None, error_msg return None, error_msg
def generate_car_image_simple(car_image): def generate_car_image_simple(car_image):
""" """
Simple function to generate car image with authentication and resizing Simple function to generate car image with authentication and resizing
@ -2577,10 +2625,10 @@ def generate_car_image_simple(car_image):
# Prepare payload # Prepare payload
payload = { payload = {
'make': car.id_car_make.name if car.id_car_make else '', "make": car.id_car_make.name if car.id_car_make else "",
'model': car.id_car_model.name if car.id_car_model else '', "model": car.id_car_model.name if car.id_car_model else "",
'year': car.year, "year": car.year,
'color': car.colors.exterior.name "color": car.colors.exterior.name,
} }
logger.info(f"Generating image for car {car.vin}") logger.info(f"Generating image for car {car.vin}")
@ -2589,10 +2637,10 @@ def generate_car_image_simple(car_image):
image_data, error = api_client.generate_image(payload) image_data, error = api_client.generate_image(payload)
if error: if error:
return {'success': False, 'error': error} return {"success": False, "error": error}
if not image_data: if not image_data:
return {'success': False, 'error': 'No image data received'} return {"success": False, "error": "No image data received"}
try: try:
# Resize the image to make it smaller # Resize the image to make it smaller
@ -2606,21 +2654,21 @@ def generate_car_image_simple(car_image):
# Determine file extension based on content # Determine file extension based on content
try: try:
img = Image.open(BytesIO(resized_data)) 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: except:
file_extension = 'jpg' file_extension = "jpg"
# Save the resized image # Save the resized image
car_image.image.save( car_image.image.save(
f"{car_image.image_hash}.{file_extension}", f"{car_image.image_hash}.{file_extension}",
ContentFile(resized_data), ContentFile(resized_data),
save=False save=False,
) )
logger.info(f"Successfully generated and resized image for car {car.vin}") logger.info(f"Successfully generated and resized image for car {car.vin}")
return {'success': True} return {"success": True}
except Exception as e: except Exception as e:
error_msg = f"Image processing failed: {e}" error_msg = f"Image processing failed: {e}"
logger.error(error_msg) logger.error(error_msg)
return {'success': False, 'error': error_msg} return {"success": False, "error": error_msg}

View File

@ -2,13 +2,15 @@ from django.core.validators import RegexValidator
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
import re import re
class SaudiPhoneNumberValidator(RegexValidator): class SaudiPhoneNumberValidator(RegexValidator):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__( super().__init__(
regex=r"^(\+9665|05)[0-9]{8}$", regex=r"^(\+9665|05)[0-9]{8}$",
message=_("Enter a valid Saudi phone number (05XXXXXXXX or +9665XXXXXXXX)"), message=_("Enter a valid Saudi phone number (05XXXXXXXX or +9665XXXXXXXX)"),
) )
def __call__(self, value): def __call__(self, value):
# Remove any whitespace, dashes, or other separators # Remove any whitespace, dashes, or other separators
cleaned_value = re.sub(r'[\s\-\(\)\.]', '', str(value)) cleaned_value = re.sub(r"[\s\-\(\)\.]", "", str(value))
super().__call__(cleaned_value) super().__call__(cleaned_value)

File diff suppressed because it is too large Load Diff

View File

@ -46,7 +46,7 @@ def run():
# arabic_name=item.get("arabic_name", ""), # arabic_name=item.get("arabic_name", ""),
# logo=item.get("Logo", ""), # logo=item.get("Logo", ""),
# is_sa_import=item.get("is_sa_import", False), # is_sa_import=item.get("is_sa_import", False),
slug=unique_slug slug=unique_slug,
) )
# Step 2: Insert CarModel # Step 2: Insert CarModel
@ -60,7 +60,7 @@ def run():
id_car_make_id=item["id_car_make"], id_car_make_id=item["id_car_make"],
name=item["name"], name=item["name"],
# arabic_name=item.get("arabic_name", ""), # arabic_name=item.get("arabic_name", ""),
slug=unique_slug slug=unique_slug,
) )
# Step 3: Insert CarSerie # Step 3: Insert CarSerie
@ -77,7 +77,7 @@ def run():
year_begin=item.get("year_begin"), year_begin=item.get("year_begin"),
year_end=item.get("year_end"), year_end=item.get("year_end"),
generation_name=item.get("generation_name", ""), generation_name=item.get("generation_name", ""),
slug=unique_slug slug=unique_slug,
) )
# Step 4: Insert CarTrim # Step 4: Insert CarTrim
@ -98,9 +98,10 @@ def run():
# Step 5: Insert CarEquipment # Step 5: Insert CarEquipment
for item in tqdm(data["car_equipment"], desc="Inserting 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(): if CarTrim.objects.filter(id_car_trim=item["id_car_trim"]).exists():
unique_slug = generate_unique_slug(CarEquipment, item["name"]) unique_slug = generate_unique_slug(CarEquipment, item["name"])
CarEquipment.objects.create( CarEquipment.objects.create(
@ -108,7 +109,7 @@ def run():
id_car_trim_id=item["id_car_trim"], id_car_trim_id=item["id_car_trim"],
name=item["name"], name=item["name"],
year_begin=item.get("year"), year_begin=item.get("year"),
slug=unique_slug slug=unique_slug,
) )
# Step 6: Insert CarSpecification (Parent specifications first) # Step 6: Insert CarSpecification (Parent specifications first)

View File

@ -34,7 +34,7 @@ def run():
is_boolean=True, is_boolean=True,
url="pricing", url="pricing",
) )
# Create the plans # Create the plans
free_plan = Plan.objects.create( free_plan = Plan.objects.create(
name="Free", name="Free",
description="Free plan with limited features", description="Free plan with limited features",
@ -46,7 +46,7 @@ def run():
order=1, order=1,
) )
free_plan.quotas.add(free_quota) free_plan.quotas.add(free_quota)
# Create the plans # Create the plans
basic_plan = Plan.objects.create( basic_plan = Plan.objects.create(
name="Basic", name="Basic",
@ -58,7 +58,7 @@ def run():
visible=True, visible=True,
order=1, order=1,
) )
basic_plan.quotas.add(basic_quota,free_quota) basic_plan.quotas.add(basic_quota, free_quota)
pro_plan = Plan.objects.create( pro_plan = Plan.objects.create(
name="Professional", name="Professional",
@ -69,7 +69,7 @@ def run():
visible=True, visible=True,
# order=2 # 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( premium_plan = Plan.objects.create(
name="Premium", name="Premium",
@ -80,4 +80,4 @@ def run():
visible=True, visible=True,
order=3, 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)

View File

@ -1,177 +1,175 @@
{% load i18n %} {% load i18n %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>403 - Access Forbidden</title> <title>403 - Access Forbidden</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
<style> rel="stylesheet">
:root { <style>
--dark-bg: #121212; :root {
--main-color: #ff3864; --dark-bg: #121212;
--secondary-color: #e6e6e6; --main-color: #ff3864;
} --secondary-color: #e6e6e6;
body, html { }
height: 100%; body, html {
margin: 0; height: 100%;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0;
background-color: var(--dark-bg); font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: var(--secondary-color); background-color: var(--dark-bg);
overflow: hidden; /* Hide overflow for the particles */ color: var(--secondary-color);
} overflow: hidden; /* Hide overflow for the particles */
.center-content { }
height: 100%; .center-content {
display: flex; height: 100%;
align-items: center; display: flex;
justify-content: center; align-items: center;
text-align: center; justify-content: center;
flex-direction: column; text-align: center;
z-index: 2; /* Ensure content is on top of particles */ flex-direction: column;
position: relative; z-index: 2; /* Ensure content is on top of particles */
} position: relative;
.glitch { }
font-size: 10rem; .glitch {
font-weight: bold; font-size: 10rem;
color: var(--main-color); font-weight: bold;
position: relative; color: var(--main-color);
animation: glitch-animation 2.5s infinite; position: relative;
} animation: glitch-animation 2.5s infinite;
@keyframes glitch-animation { }
0% { text-shadow: 2px 2px var(--secondary-color); } @keyframes glitch-animation {
20% { text-shadow: -2px -2px var(--secondary-color); } 0% { text-shadow: 2px 2px var(--secondary-color); }
40% { text-shadow: 4px 4px var(--main-color); } 20% { text-shadow: -2px -2px var(--secondary-color); }
60% { text-shadow: -4px -4px var(--main-color); } 40% { text-shadow: 4px 4px var(--main-color); }
80% { text-shadow: 6px 6px var(--secondary-color); } 60% { text-shadow: -4px -4px var(--main-color); }
100% { text-shadow: -6px -6px var(--secondary-color); } 80% { text-shadow: 6px 6px var(--secondary-color); }
} 100% { text-shadow: -6px -6px var(--secondary-color); }
.main-message { }
font-size: 2.5rem; .main-message {
margin-top: -20px; font-size: 2.5rem;
letter-spacing: 5px; margin-top: -20px;
text-transform: uppercase; letter-spacing: 5px;
} text-transform: uppercase;
.sub-message { }
font-size: 1.2rem; .sub-message {
color: #8c8c8c; font-size: 1.2rem;
margin-top: 10px; color: #8c8c8c;
} margin-top: 10px;
.home-button { }
margin-top: 30px; .home-button {
padding: 12px 30px; margin-top: 30px;
background-color: var(--main-color); padding: 12px 30px;
border: none; background-color: var(--main-color);
color: #fff; border: none;
border-radius: 5px; color: #fff;
font-size: 1rem; border-radius: 5px;
text-decoration: none; font-size: 1rem;
transition: background-color 0.3s, transform 0.3s; text-decoration: none;
} transition: background-color 0.3s, transform 0.3s;
.home-button:hover { }
background-color: #d12e52; .home-button:hover {
transform: scale(1.05); background-color: #d12e52;
} transform: scale(1.05);
}
/* Particle Background styles */ /* Particle Background styles */
#particles-js { #particles-js {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
top: 0; top: 0;
left: 0; left: 0;
z-index: 1; z-index: 1;
} }
</style> </style>
</head> </head>
<body> <body>
<div id="particles-js"></div>
<div id="particles-js"></div> <div class="center-content container-fluid">
<h1 class="glitch">403</h1>
<div class="center-content container-fluid"> <h2 class="main-message">{% trans "Access Forbidden" %}</h2>
<h1 class="glitch">403</h1> <p class="sub-message">{% trans "You do not have permission to view this page." %}</p>
<h2 class="main-message">{% trans "Access Forbidden" %}</h2> <p class="sub-message fs-2">{% trans "Powered By Tenhal, Riyadh Saudi Arabia" %}</p>
<p class="sub-message">{% trans "You do not have permission to view this page."%}</p> <a href="{% url 'home' %}" class="home-button">{% trans "Go Home" %}</a>
<p class="sub-message fs-2">{% trans "Powered By Tenhal, Riyadh Saudi Arabia"%}</p> </div>
<a href="{% url 'home' %}" class="home-button">{% trans "Go Home" %}</a> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</div> <script src="https://cdn.jsdelivr.net/npm/particles.js@2.0.0/particles.min.js"></script>
<script>
<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 */ /* Particles.js Configuration */
particlesJS("particles-js", { particlesJS("particles-js", {
"particles": { "particles": {
"number": { "number": {
"value": 80, "value": 80,
"density": { "density": {
"enable": true, "enable": true,
"value_area": 800 "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
} }
}, },
"push": { "color": {
"particles_nb": 4 "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": {
"retina_detect": true "detect_on": "canvas",
}); "events": {
</script> "onhover": {
</body> "enable": true,
</html> "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>

View File

@ -55,27 +55,27 @@
rel="stylesheet" rel="stylesheet"
id="user-style-default" /> id="user-style-default" />
<style> <style>
body, html { body, html {
height: 100%; height: 100%;
margin: 0; margin: 0;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
text-align: center; text-align: center;
} }
.main { .main {
width: 100%; width: 100%;
max-width: 1200px; max-width: 1200px;
margin: auto; margin: auto;
} }
.flex-center { .flex-center {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.text-center { .text-center {
text-align: center; text-align: center;
} }
</style> </style>
</head> </head>
<body> <body>

View File

@ -55,27 +55,27 @@
rel="stylesheet" rel="stylesheet"
id="user-style-default" /> id="user-style-default" />
<style> <style>
body, html { body, html {
height: 100%; height: 100%;
margin: 0; margin: 0;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
text-align: center; text-align: center;
} }
.main { .main {
width: 100%; width: 100%;
max-width: 1200px; max-width: 1200px;
margin: auto; margin: auto;
} }
.flex-center { .flex-center {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.text-center { .text-center {
text-align: center; text-align: center;
} }
</style> </style>
</head> </head>
<body> <body>

View File

@ -61,19 +61,19 @@
rel="stylesheet" rel="stylesheet"
id="user-style-default"> id="user-style-default">
<script> <script>
var phoenixIsRTL = window.config.config.phoenixIsRTL; var phoenixIsRTL = window.config.config.phoenixIsRTL;
if (phoenixIsRTL) { if (phoenixIsRTL) {
var linkDefault = document.getElementById('style-default'); var linkDefault = document.getElementById('style-default');
var userLinkDefault = document.getElementById('user-style-default'); var userLinkDefault = document.getElementById('user-style-default');
linkDefault.setAttribute('disabled', true); linkDefault.setAttribute('disabled', true);
userLinkDefault.setAttribute('disabled', true); userLinkDefault.setAttribute('disabled', true);
document.querySelector('html').setAttribute('dir', 'rtl'); document.querySelector('html').setAttribute('dir', 'rtl');
} else { } else {
var linkRTL = document.getElementById('style-rtl'); var linkRTL = document.getElementById('style-rtl');
var userLinkRTL = document.getElementById('user-style-rtl'); var userLinkRTL = document.getElementById('user-style-rtl');
linkRTL.setAttribute('disabled', true); linkRTL.setAttribute('disabled', true);
userLinkRTL.setAttribute('disabled', true); userLinkRTL.setAttribute('disabled', true);
} }
</script> </script>
</head> </head>
<body> <body>
@ -109,17 +109,17 @@
</div> </div>
</div> </div>
<script> <script>
var navbarTopStyle = window.config.config.phoenixNavbarTopStyle; var navbarTopStyle = window.config.config.phoenixNavbarTopStyle;
var navbarTop = document.querySelector('.navbar-top'); var navbarTop = document.querySelector('.navbar-top');
if (navbarTopStyle === 'darker') { if (navbarTopStyle === 'darker') {
navbarTop.setAttribute('data-navbar-appearance', 'darker'); navbarTop.setAttribute('data-navbar-appearance', 'darker');
} }
var navbarVerticalStyle = window.config.config.phoenixNavbarVerticalStyle; var navbarVerticalStyle = window.config.config.phoenixNavbarVerticalStyle;
var navbarVertical = document.querySelector('.navbar-vertical'); var navbarVertical = document.querySelector('.navbar-vertical');
if (navbarVertical && navbarVerticalStyle === 'darker') { if (navbarVertical && navbarVerticalStyle === 'darker') {
navbarVertical.setAttribute('data-navbar-appearance', 'darker'); navbarVertical.setAttribute('data-navbar-appearance', 'darker');
} }
</script> </script>
<div class="support-chat-row"> <div class="support-chat-row">
<div class="row-fluid support-chat"> <div class="row-fluid support-chat">

View File

@ -6,7 +6,6 @@
{% trans "Sign In" %} {% trans "Sign In" %}
{% endblock head_title %} {% endblock head_title %}
{% block content %} {% block content %}
<section class="main my-2"> <section class="main my-2">
<div class="container" style="max-width:40rem;"> <div class="container" style="max-width:40rem;">
<div class="class="row form-container" id="form-container""> <div class="class="row form-container" id="form-container"">
@ -78,9 +77,7 @@
</div> </div>
</div> </div>
</section> </section>
{% include 'footer.html' %}
{% include 'footer.html' %}
{% if LOGIN_BY_CODE_ENABLED or PASSKEY_LOGIN_ENABLED %} {% if LOGIN_BY_CODE_ENABLED or PASSKEY_LOGIN_ENABLED %}
<hr> <hr>
{% element button_group vertical=True %} {% element button_group vertical=True %}

View File

@ -282,9 +282,7 @@
</div> </div>
</div> </div>
</section> </section>
{% include 'footer.html' %}
{% include 'footer.html' %}
<script src="{% static 'js/phoenix.js' %}"></script> <script src="{% static 'js/phoenix.js' %}"></script>
{% endblock content %} {% endblock content %}
{% block customJS %} {% block customJS %}
@ -293,97 +291,97 @@
<script type="module" <script type="module"
src="https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.0-beta.11/bundles/datastar.js"></script> src="https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.0-beta.11/bundles/datastar.js"></script>
<script> <script>
function validatePassword(password, confirmPassword) { function validatePassword(password, confirmPassword) {
return password === confirmPassword && password.length > 7 && password !== ''; 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 validateEmail(email) {
} const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return emailRegex.test(email) && email !== '';
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 validateform2(name,arabic_name,phone_number) {
return cookieValue; if (name === '' || arabic_name === '' || phone_number === '' || phone_number.length < 10 || !phone_number.startsWith('056')) {
} return false;
async function sendFormData() { }
const formData = getAllFormData(); return true
const url = "{% url 'account_signup' %}"; }
const csrftoken = getCookie('csrftoken'); function validate_sa_phone_number(phone_number) {
try { const phone_numberRegex = /^056[0-9]{7}$/;
showLoading(); return phone_numberRegex.test(phone_number) && phone_numberRegex !== '';
const response = await fetch(url, { }
method: 'POST', function getAllFormData() {
headers: { const forms = document.querySelectorAll('.needs-validation');
'X-CSRFToken': '{{csrf_token}}', const formData = {};
'Content-Type': 'application/json', forms.forEach(form => {
}, const fields = form.querySelectorAll('input,textarea,select');
body: JSON.stringify(formData), fields.forEach(field => {
}); formData[field.name] = field.value;
hideLoading(); });
const data = await response.json(); });
if (response.ok) { return formData;
notify("success","Account created successfully"); }
setTimeout(() => {
window.location.href = "{% url 'account_login' %}"; function showLoading() {
}, 1000); Swal.fire({
} else { title: "{% trans 'Please Wait' %}",
notify("error",data.error); 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> </script>
{% endblock customJS %} {% endblock customJS %}

View File

@ -6,42 +6,42 @@
{% trans 'Dealer Settings' %} {% trans 'Dealer Settings' %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<main class="d-flex align-items-center justify-content-center min-vh-80 py-5"> <main class="d-flex align-items-center justify-content-center min-vh-80 py-5">
<div class="col-md-6"> <div class="col-md-6">
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp"> <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"> <div class="card-header bg-gradient py-4 border-0 rounded-top-4">
<h3 class="mb-0 fs-4 fw-bold text-center"> <h3 class="mb-0 fs-4 fw-bold text-center">
{% trans "Dealer Settings" %} {% trans "Dealer Settings" %}
<i class="fas fa-solid fa-gear ms-2"></i> <i class="fas fa-solid fa-gear ms-2"></i>
</h3> </h3>
</div> </div>
<div class="card-body p-4 p-md-5"> <div class="card-body p-4 p-md-5">
<form action="" method="post" class="needs-validation" novalidate> <form action="" method="post" class="needs-validation" novalidate>
{% csrf_token %} {% csrf_token %}
<div class="row g-1"> <div class="row g-1">
<div class="col-12"> <div class="col-12">
<h4 class="mb-4 text-center">{% trans 'Default Invoice Accounts' %}</h4> <h4 class="mb-4 text-center">{% trans 'Default Invoice Accounts' %}</h4>
{{ form.invoice_cash_account|as_crispy_field }} {{ form.invoice_cash_account|as_crispy_field }}
{{ form.invoice_prepaid_account|as_crispy_field }} {{ form.invoice_prepaid_account|as_crispy_field }}
{{ form.invoice_unearned_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>
<div class="col-12 mt-4"> <hr class="my-4">
<h4 class="mb-4 text-center">{% trans 'Default Bill Accounts' %}</h4> <div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
{{ form.bill_cash_account|as_crispy_field }} <button class="btn btn-phoenix-primary btn-lg" type="submit">
{{ form.bill_prepaid_account|as_crispy_field }} <i class="fa-solid fa-pen-to-square me-1"></i>
{{ form.bill_unearned_account|as_crispy_field }} {% trans 'Update' %}
</button>
</div> </div>
</div> </form>
<hr class="my-4"> </div>
<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>
</div> </div>
</div> </div>
</div> </main>
</main> {% endblock %}
{% endblock %}

View File

@ -23,7 +23,7 @@
<h3 class="mb-0 fs-4 text-center text-white">{% trans 'Activate Account' %}</h3> <h3 class="mb-0 fs-4 text-center text-white">{% trans 'Activate Account' %}</h3>
</div> </div>
<div class="card-body bg-light-subtle"> <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"> <form method="post">
{% csrf_token %} {% csrf_token %}
<hr class="my-2"> <hr class="my-2">

View File

@ -1,29 +1,33 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n %} {% load i18n %}
{% block title %} {% block title %}
{% trans 'Admin Management' %} {% endblock %} {% trans 'Admin Management' %}
{% block content %} {% endblock %}
<h3 class="my-4">{% trans "Admin Management" %}<li class="fa fa-user-cog ms-2 text-primary"></li></h3> {% block content %}
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-4 g-4 mt-10"> <h3 class="my-4">
<div class="col"> {% trans "Admin Management" %}
<a href="{% url 'user_management' request.dealer.slug %}"> <li class="fa fa-user-cog ms-2 text-primary"></li>
<div class="card h-100"> </h3>
<div class="card-header text-center"> <div class="row row-cols-1 row-cols-sm-2 row-cols-md-4 g-4 mt-10">
<h5 class="card-title">{{ _("User Management") }}</h5> <div class="col">
<span class="me-2"><i class="fas fa-user fa-2x"></i></span> <a href="{% url 'user_management' request.dealer.slug %}">
</div> <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> </div>
</a> </div>
</div> </a>
<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> </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 %}

View File

@ -32,15 +32,7 @@
<div class="messages" style="margin: 20px 0"> <div class="messages" style="margin: 20px 0">
{% if messages %} {% if messages %}
{% for message in messages %} {% for message in messages %}
<div class="alert alert-dismissible <div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
{% if message.tags %}
alert-
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
danger
{% else %}
{{ message.tags }}
{% endif %}
{% endif %}"
role="alert">{{ message }}</div> role="alert">{{ message }}</div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}

View File

@ -78,15 +78,7 @@
<div class="messages" style="margin: 20px 0"> <div class="messages" style="margin: 20px 0">
{% if messages %} {% if messages %}
{% for message in messages %} {% for message in messages %}
<div class="alert alert-dismissible <div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
{% if message.tags %}
alert-
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
danger
{% else %}
{{ message.tags }}
{% endif %}
{% endif %}"
role="alert">{{ message }}</div> role="alert">{{ message }}</div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}

View File

@ -65,15 +65,7 @@
<div class="messages" style="margin: 20px 0"> <div class="messages" style="margin: 20px 0">
{% if messages %} {% if messages %}
{% for message in messages %} {% for message in messages %}
<div class="alert alert-dismissible <div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
{% if message.tags %}
alert-
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
danger
{% else %}
{{ message.tags }}
{% endif %}
{% endif %}"
role="alert">{{ message }}</div> role="alert">{{ message }}</div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}

View File

@ -25,15 +25,7 @@
<div class="messages" style="margin: 20px 0"> <div class="messages" style="margin: 20px 0">
{% if messages %} {% if messages %}
{% for message in messages %} {% for message in messages %}
<div class="alert alert-dismissible <div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
{% if message.tags %}
alert-
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
danger
{% else %}
{{ message.tags }}
{% endif %}
{% endif %}"
role="alert">{{ message }}</div> role="alert">{{ message }}</div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}

View File

@ -11,22 +11,9 @@
method="post" method="post"
action="" action=""
id="workingHoursForm" id="workingHoursForm"
data-action="{% if working_hours_instance %} data-action="{% if working_hours_instance %} update {% else %} create {% endif %}"
update data-working-hours-id=" {% if working_hours_instance %} {{ working_hours_instance.id }} {% else %} 0 {% endif %}"
{% else %} data-staff-user-id="{% if staff_user_id %} {{ staff_user_id }} {% else %} 0 {% endif %}">
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 %} {% csrf_token %}
{% if working_hours_form.staff_member %} {% if working_hours_form.staff_member %}
<div class="form-group mb-3"> <div class="form-group mb-3">
@ -94,15 +81,7 @@
<div class="messages" style="margin: 20px 0"> <div class="messages" style="margin: 20px 0">
{% if messages %} {% if messages %}
{% for message in messages %} {% for message in messages %}
<div class="alert alert-dismissible <div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
{% if message.tags %}
alert-
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
danger
{% else %}
{{ message.tags }}
{% endif %}
{% endif %}"
role="alert">{{ message }}</div> role="alert">{{ message }}</div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}

View File

@ -23,15 +23,7 @@
<div class="messages" style="margin: 20px 0"> <div class="messages" style="margin: 20px 0">
{% if messages %} {% if messages %}
{% for message in messages %} {% for message in messages %}
<div class="alert alert-dismissible <div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
{% if message.tags %}
alert-
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
danger
{% else %}
{{ message.tags }}
{% endif %}
{% endif %}"
role="alert">{{ message }}</div> role="alert">{{ message }}</div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}

View File

@ -68,7 +68,9 @@
{% endif %} {% endif %}
{% for sf in all_staff_members %} {% for sf in all_staff_members %}
<option value="{{ sf.id }}" <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 %} {% endfor %}
</select> </select>
</div> </div>
@ -88,15 +90,7 @@
</div> </div>
{% if messages %} {% if messages %}
{% for message in messages %} {% for message in messages %}
<div class="alert alert-dismissible <div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
{% if message.tags %}
alert-
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
danger
{% else %}
{{ message.tags }}
{% endif %}
{% endif %}"
role="alert">{{ message }}</div> role="alert">{{ message }}</div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}

View File

@ -24,15 +24,7 @@
</ul> </ul>
{% if messages %} {% if messages %}
{% for message in messages %} {% for message in messages %}
<div class="alert alert-dismissible <div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
{% if message.tags %}
alert-
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
danger
{% else %}
{{ message.tags }}
{% endif %}
{% endif %}"
role="alert">{{ message }}</div> role="alert">{{ message }}</div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}

View File

@ -29,8 +29,7 @@
</form> </form>
{% if messages %} {% if messages %}
{% for message in messages %} {% for message in messages %}
<div class="vcode-alert vcode-alert- <div class="vcode-alert vcode-alert- {% if message.tags %}{{ message.tags }}{% endif %}">{{ message }}</div>
{% if message.tags %}{{ message.tags }}{% endif %}">{{ message }}</div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
</div> </div>

View File

@ -69,15 +69,7 @@
</div> </div>
{% if messages %} {% if messages %}
{% for message in messages %} {% for message in messages %}
<div class="alert alert-dismissible <div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
{% if message.tags %}
alert-
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
danger
{% else %}
{{ message.tags }}
{% endif %}
{% endif %}"
role="alert">{{ message }}</div> role="alert">{{ message }}</div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}

View File

@ -40,15 +40,7 @@
<p class="message">{{ page_message }}</p> <p class="message">{{ page_message }}</p>
{% if messages %} {% if messages %}
{% for message in messages %} {% for message in messages %}
<div class="alert alert-dismissible <div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
{% if message.tags %}
alert-
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
danger
{% else %}
{{ message.tags }}
{% endif %}
{% endif %}"
role="alert">{{ message }}</div> role="alert">{{ message }}</div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}

View File

@ -3,11 +3,7 @@
<!DOCTYPE html> <!DOCTYPE html>
{% get_current_language as LANGUAGE_CODE %} {% get_current_language as LANGUAGE_CODE %}
<html lang="{{ LANGUAGE_CODE }}" <html lang="{{ LANGUAGE_CODE }}"
dir="{% if LANGUAGE_CODE == 'ar' %} dir="{% if LANGUAGE_CODE == 'ar' %} rtl {% else %} ltr {% endif %}"
rtl
{% else %}
ltr
{% endif %}"
data-bs-theme="" data-bs-theme=""
data-navigation-type="default" data-navigation-type="default"
data-navbar-horizontal-shape="default"> data-navbar-horizontal-shape="default">

View File

@ -2,11 +2,7 @@
<!DOCTYPE html> <!DOCTYPE html>
{% get_current_language as LANGUAGE_CODE %} {% get_current_language as LANGUAGE_CODE %}
<html lang="{{ LANGUAGE_CODE }}" <html lang="{{ LANGUAGE_CODE }}"
{% if LANGUAGE_CODE == 'ar' %} {% if LANGUAGE_CODE == 'ar' %} dir="rtl" {% else %} dir="ltr" {% endif %}
dir="rtl"
{% else %}
dir="ltr"
{% endif %}
data-bs-theme="" data-bs-theme=""
data-navigation-type="default" data-navigation-type="default"
data-navbar-horizontal-shape="default"> data-navbar-horizontal-shape="default">
@ -54,8 +50,14 @@
<link href="{% static 'css/custom.css' %}" rel="stylesheet"> <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 %} {% comment %} <link rel="stylesheet" href="https://unicons.iconscout.com/release/v4.0.8/css/line.css"> {% endcomment %}
{% if LANGUAGE_CODE == 'ar' %} {% if LANGUAGE_CODE == 'ar' %}
<link href="{% static 'css/theme-rtl.min.css' %}" type="text/css" rel="stylesheet" id="style-rtl"> <link href="{% static 'css/theme-rtl.min.css' %}"
<link href="{% static 'css/user-rtl.min.css' %}" type="text/css" rel="stylesheet" id="user-style-rtl"> 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 %} {% else %}
<link href="{% static 'css/theme.min.css' %}" <link href="{% static 'css/theme.min.css' %}"
type="text/css" type="text/css"
@ -66,11 +68,8 @@
rel="stylesheet" rel="stylesheet"
id="user-style-default"> id="user-style-default">
{% endif %} {% endif %}
<script src="{% static 'js/main.js' %}"></script> <script src="{% static 'js/main.js' %}"></script>
<script src="{% static 'js/jquery.min.js' %}"></script> <script src="{% static 'js/jquery.min.js' %}"></script>
{% comment %} <script src="{% static 'js/echarts.js' %}"></script> {% endcomment %} {% comment %} <script src="{% static 'js/echarts.js' %}"></script> {% endcomment %}
{% comment %} {% block customCSS %}{% endblock %} {% endcomment %} {% comment %} {% block customCSS %}{% endblock %} {% endcomment %}
</head> </head>
@ -85,19 +84,25 @@
{% block period_navigation %} {% block period_navigation %}
{% endblock period_navigation %} {% endblock period_navigation %}
<div id="spinner" class="htmx-indicator spinner-bg"> <div id="spinner" class="htmx-indicator spinner-bg">
<img src="{% static 'spinner.svg' %}" width="100" height="100" alt=""> <img src="{% static 'spinner.svg' %}" width="100" height="100" alt="">
</div> </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> <div id="main_content"
class="fade-me-in"
hx-boost="false"
hx-target="#main_content"
hx-select="#main_content"
hx-swap="outerHTML transition:true"
hx-select-oob="#toast-container"
hx-history-elt>
{% block customCSS %}{% endblock %} {% block customCSS %}{% endblock %}
{% block content %}{% endblock content %} {% block content %}
{% endblock content %}
{% block customJS %}{% endblock %} {% block customJS %}{% endblock %}
{% comment %} <script src="{% static 'vendors/feather-icons/feather.min.js' %}"></script> {% comment %} <script src="{% static 'vendors/feather-icons/feather.min.js' %}"></script>
<script src="{% static 'vendors/fontawesome/all.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/popper/popper.min.js' %}"></script>
<script src="{% static 'vendors/bootstrap/bootstrap.min.js' %}"></script> {% endcomment %} <script src="{% static 'vendors/bootstrap/bootstrap.min.js' %}"></script> {% endcomment %}
<script src="{% static 'js/phoenix.js' %}"></script> <script src="{% static 'js/phoenix.js' %}"></script>
</div> </div>
{% block body %} {% block body %}
{% endblock body %} {% endblock body %}
@ -126,7 +131,7 @@
{% comment %} <script src="{% static 'vendors/echarts/echarts.min.js' %}"></script> {% endcomment %} {% 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/crm-analytics.js' %}"></script> {% endcomment %}
{% comment %} <script src="{% static 'js/travel-agency-dashboard.js' %}"></script> {% 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 %} <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/mapbox-gl/mapbox-gl.js' %}"></script> {% endcomment %}
{% comment %} <script src="{% static 'vendors/turf.min.js' %}"></script> {% endcomment %} {% comment %} <script src="{% static 'vendors/turf.min.js' %}"></script> {% endcomment %}
@ -156,57 +161,57 @@
document.getElementById('global-indicator') document.getElementById('global-indicator')
]; ];
});*/ });*/
let Toast = Swal.mixin({ let Toast = Swal.mixin({
toast: true, toast: true,
position: "top-end", position: "top-end",
showConfirmButton: false, showConfirmButton: false,
timer: 3000, timer: 3000,
timerProgressBar: true, timerProgressBar: true,
didOpen: (toast) => { didOpen: (toast) => {
toast.onmouseenter = Swal.stopTimer; toast.onmouseenter = Swal.stopTimer;
toast.onmouseleave = Swal.resumeTimer; toast.onmouseleave = Swal.resumeTimer;
} }
}); });
function notify(tag, msg) { function notify(tag, msg) {
Toast.fire({ Toast.fire({
icon: tag, icon: tag,
titleText: msg 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();
} }
}); 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> </script>
{% comment %} {% block customJS %}{% endblock %} {% endcomment %} {% comment %} {% block customJS %}{% endblock %} {% endcomment %}
</body> </body>

View File

@ -6,48 +6,48 @@
{% block title %} {% block title %}
{{ _("Create Bill") |capfirst }} {{ _("Create Bill") |capfirst }}
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
<main class="d-flex align-items-center justify-content-center min-vh-80 py-5 "> <main class="d-flex align-items-center justify-content-center min-vh-80 py-5 ">
<div class="col-md-6"> <div class="col-md-6">
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp"> <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"> <div class="card-header bg-gradient py-4 border-0 rounded-top-4">
<h3 class="mb-0 fs-4 fw-bold text-center"> <h3 class="mb-0 fs-4 fw-bold text-center">
{% trans 'Create Bill' %} {% trans 'Create Bill' %}
<i class="fas fa-money-bills ms-2"></i> <i class="fas fa-money-bills ms-2"></i>
</h3> </h3>
</div> </div>
<div class="card-body p-4 p-md-5"> <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> <form action="{{ form_action_url }}"
{% csrf_token %} method="post"
{% if po_model %} id="djl-bill-model-create-form-id"
<div class="text-center mb-4"> class="needs-validation"
<h3 class="h5">{% trans 'Bill for' %} {{ po_model.po_number }}</h3> novalidate>
<p class="text-muted mb-3">{% trans 'Bill for' %} {{ po_model.po_title }}</p> {% csrf_token %}
<div class="d-flex flex-column gap-2"> {% if po_model %}
{% for itemtxs in po_itemtxs_qs %} <div class="text-center mb-4">
<span class="badge bg-secondary">{{ itemtxs }}</span> <h3 class="h5">{% trans 'Bill for' %} {{ po_model.po_number }}</h3>
{% endfor %} <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> </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> </div>
{% endif %} </form>
<div class="mb-4"> </div>
{{ 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>
</div> </div>
</div> </div>
</div> </main>
</main> {% endblock %}
{% endblock %}

View File

@ -4,180 +4,160 @@
{% load django_ledger %} {% load django_ledger %}
{% load custom_filters %} {% load custom_filters %}
{% block title %}Bill Details{% endblock %} {% block title %}Bill Details{% endblock %}
{% block content %}
{% block content%}
<div class="row mt-4"> <div class="row mt-4">
<div class="col-12 mb-3"> <div class="col-12 mb-3">
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-body"> <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' %} {% 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> </div>
</div> {% if bill.is_configured %}
<div class="row text-center g-3 mb-3">
<div class="col-12"> <div class="col-12 col-md-3">
<div class="card mb-3 shadow-sm"> <h6 class="text-uppercase text-xs text-muted mb-2">
<div class="card-header pb-0"> {% trans 'Cash Account' %}:
<div class="d-flex align-items-center mb-2"> {% if bill.cash_account %}
<i class="fas fa-receipt me-3 text-primary"></i> <a href="{% url 'account_detail' request.dealer.slug bill.cash_account.coa_model.pk bill.cash_account.uuid %}"
<h5 class="mb-0">{% trans 'Bill Items' %}</h5> class="text-decoration-none ms-1">{{ bill.cash_account.code }}</a>
</div> {% 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>
<div class="card-body px-0 pt-0 pb-2"> {% if bill.accrue %}
<div class="table-responsive"> <div class="col-12 col-md-3">
<table class="table table-hover"> <h6 class="text-uppercase text-xs text-muted mb-2">
<thead> {% trans 'Prepaid Account' %}:
<tr class="bg-body-highlight"> {% if bill.prepaid_account %}
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Item' %}</th> <a href="{% url 'account_detail' request.dealer.slug bill.prepaid_account.coa_model.pk bill.prepaid_account.uuid %}"
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Entity Unit' %}</th> class="text-decoration-none ms-1">{{ bill.prepaid_account.code }}</a>
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Unit Cost' %}</th> {% else %}
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Quantity' %}</th> {{ bill.prepaid_account.code }}
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Total' %}</th> {% endif %}
<th class="sort white-space-nowrap align-middle " scope="col">{% trans 'PO' %}</th> </h6>
</tr> <h4 class="text-success mb-0" id="djl-bill-detail-amount-prepaid">
</thead> {% currency_symbol %}{{ bill.get_amount_prepaid | currency_format }}
<tbody class="list fs-9" id="project-list-table-body"> </h4>
{% 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 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> {% endif %}
<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> </div>
<div> <div class="col-12">
{% include "bill/includes/mark_as.html" %} <div class="card mb-3 shadow-sm">
{% endblock %} <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 %}

View File

@ -14,7 +14,8 @@
<div class="card mb-2"> <div class="card mb-2">
<div class="card-body"> <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 %} {% 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 %} {% csrf_token %}
<div class="mb-3">{{ form|crispy }}</div> <div class="mb-3">{{ form|crispy }}</div>
<button type="submit" class="btn btn-phoenix-primary mb-2 me-2"> <button type="submit" class="btn btn-phoenix-primary mb-2 me-2">

View File

@ -4,7 +4,6 @@
{% if not create_bill %} {% if not create_bill %}
{% if style == 'dashboard' %} {% if style == 'dashboard' %}
<!-- Dashboard Style Card --> <!-- Dashboard Style Card -->
<div class=""> <div class="">
<div class="card-body"> <div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3 text-primary"> <div class="d-flex justify-content-between align-items-center mb-3 text-primary">
@ -51,15 +50,16 @@
</div> </div>
<!-- Modal Action --> <!-- Modal Action -->
{% modal_action bill 'get' entity_slug %} {% modal_action bill 'get' entity_slug %}
<div class="d-grid gap-2 d-md-flex justify-content-md-end"> <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 %}" <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> class="btn btn-sm btn-phoenix-primary me-md-2">{% trans 'View' %}</a>
{% if perms.django_ledger.change_billmodel %} {% 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> class="btn btn-sm btn-phoenix-warning me-md-2">{% trans 'Update' %}</a>
{% if bill.can_pay %} {% 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 %} {% endif %}
{% if bill.can_cancel %} {% if bill.can_cancel %}
<button onclick="djLedger.toggleModal('{{ bill.get_html_id }}')" <button onclick="djLedger.toggleModal('{{ bill.get_html_id }}')"
@ -202,10 +202,11 @@
<div class="d-flex flex-wrap gap-2 mt-2"> <div class="d-flex flex-wrap gap-2 mt-2">
<!-- Update Button --> <!-- Update Button -->
{% if perms.django_ledger.change_billmodel %} {% if perms.django_ledger.change_billmodel %}
{% if "update" not in request.path %} {% 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 %}"> <a hx-boost="true"
<button class="btn btn-phoenix-primary"> href="{% url 'bill-update' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill.uuid %}">
<i class="fas fa-edit me-2"></i>{% trans 'Update' %} <button class="btn btn-phoenix-primary">
<i class="fas fa-edit me-2"></i>{% trans 'Update' %}
</button> </button>
</a> </a>
{% endif %} {% endif %}
@ -219,7 +220,6 @@
{% endif %} {% endif %}
<!-- Mark as Review --> <!-- Mark as Review -->
{% if bill.can_review %} {% if bill.can_review %}
<button class="btn btn-phoenix-warning" <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')"> 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' %} <i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Review' %}
@ -227,41 +227,40 @@
{% endif %} {% endif %}
<!-- Mark as Approved --> <!-- Mark as Approved -->
{% endif %} {% endif %}
{% if bill.can_approve and request.is_accountant %} {% if bill.can_approve and request.is_accountant %}
<button class="btn btn-phoenix-warning" disabled> <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> <i class="fas fa-hourglass-start me-2"></i><span class="text-warning">{% trans 'Waiting for Manager Approval' %}</span>
</button> </button>
{% else %} {% else %}
{% if bill.can_approve and perms.django_ledger.can_approve_billmodel %} {% 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 %}
<button class="btn btn-phoenix-success" <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')"> 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 Paid' %} <i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Approved' %}
</button> </button>
{% endif %} {% endif %}
<!-- Void Button --> {% endif %}
{% if bill.can_void %} <!-- Mark as Paid -->
<button class="btn btn-phoenix-danger" {% if bill.can_pay %}
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')"> <button class="btn btn-phoenix-success"
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Void' %} 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')">
</button> <i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Paid' %}
{% endif %} </button>
<!-- Cancel Button --> {% endif %}
{% if bill.can_cancel %} <!-- Void Button -->
<button class="btn btn-phoenix-danger" {% if bill.can_void %}
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')"> <button class="btn btn-phoenix-danger"
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Canceled' %} 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')">
</button> <i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Void' %}
{% modal_action_v2 bill bill.get_mark_as_canceled_url bill.get_mark_as_canceled_message bill.get_mark_as_canceled_html_id %} </button>
{% endif %} {% 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 %} {% endif %}
</div> </div>
</div> </div>

View File

@ -2,137 +2,142 @@
{% load static %} {% load static %}
{% load django_ledger %} {% load django_ledger %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% if bill.get_itemtxs_data.1.total_amount__sum > 0 %} {% 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 %}" <form id="bill-update-form"
method="post"> action="{% url 'bill-update-items' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill_pk %}"
{% else %} method="post">
<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 %}" {% else %}
method="post"> <form id="bill-update-form"
{% endif %} hx-trigger="load delay:300ms"
<div class="container-fluid py-4"> hx-swap="outerHTML"
<!-- Page Header --> hx-target="#bill-update-form"
<div class="row mb-4"> hx-select="#bill-update-form"
<div class="col-12"> hx-post="{% url 'bill-update-items' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill_pk %}"
<h2 class="text-primary mb-0 d-flex align-items-center"> method="post">
<i class="fas fa-receipt me-2"></i> {% endif %}
{% trans 'Bill Items' %} <div class="container-fluid py-4">
</h2> <!-- Page Header -->
<hr class="my-3"> <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>
</div> <!-- Form Content -->
<!-- Form Content --> <div class="row">
<div class="row"> <div class="col-12">
<div class="col-12"> {% csrf_token %}
{% csrf_token %} {{ item_formset.non_form_errors }}
{{ item_formset.non_form_errors }} {{ item_formset.management_form }}
{{ item_formset.management_form }} <!-- Card Container -->
<!-- Card Container --> <div class="card shadow-sm">
<div class="card shadow-sm"> <div class="card-body p-0">
<div class="card-body p-0"> <!-- Responsive Table -->
<!-- Responsive Table --> <div class="table-responsive">
<div class="table-responsive"> <table class="table table-hover align-middle mb-0">
<table class="table table-hover align-middle mb-0"> <thead class="table-light">
<thead class="table-light"> <tr>
<tr> <th class="text-uppercase text-xxs font-weight-bolder">{% trans 'Item' %}</th>
<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 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 '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 '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 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-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-end">{% trans 'Total' %}</th> <th class="text-uppercase text-xxs font-weight-bolder text-center">{% trans 'Delete' %}</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>
</tr> </tr>
{% endfor %} </thead>
</tbody> <tbody>
<!-- Footer Total --> {% for f in item_formset %}
<tfoot class="total-row"> <tr class="align-middle">
<tr> <!-- Item Column -->
<td colspan="5"></td> <td>
<td class="text-end"> <div class="d-flex flex-column ms-2">
<strong>{% trans 'Total' %}</strong> {% for hidden_field in f.hidden_fields %}{{ hidden_field }}{% endfor %}
</td> {{ f.item_model|add_class:"form-control" }}
<td class="text-end"> {% if f.errors %}<span class="text-danger text-xs">{{ f.errors }}</span>{% endif %}
<strong>{% currency_symbol %}{{ total_amount__sum | currency_format }}</strong> </div>
</td> </td>
<td></td> <!-- PO Quantity -->
</tr> <td class="text-center">
</tfoot> <span class="text-muted text-xs">
</table> {% 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> </div>
</div> </div>
</div> <!-- Action Buttons -->
<!-- Action Buttons --> <div class="row mt-4">
<div class="row mt-4"> <div class="col-12">
<div class="col-12"> <div class="d-flex justify-content-start gap-2">
<div class="d-flex justify-content-start gap-2"> {% if not item_formset.has_po %}
{% if not item_formset.has_po %} <a href="{% url 'django_ledger:product-create' entity_slug=entity_slug %}"
<a href="{% url 'django_ledger:product-create' entity_slug=entity_slug %}" class="btn btn-phoenix-primary">
class="btn btn-phoenix-primary"> <i class="fas fa-plus me-1"></i>
<i class="fas fa-plus me-1"></i> {% trans 'New Item' %}
{% trans 'New Item' %} </a>
</a> {% endif %}
{% endif %} <button type="submit" class="btn btn-phoenix-primary">
<button type="submit" class="btn btn-phoenix-primary"> <i class="fas fa-save me-1"></i>
<i class="fas fa-save me-1"></i> {% trans 'Save Changes' %}
{% trans 'Save Changes' %} </button>
</button> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </form>
</form>

View File

@ -2,58 +2,52 @@
{% load i18n static %} {% load i18n static %}
{% load django_ledger %} {% load django_ledger %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% block content %} {% block content %}
<main class="d-flex align-items-center justify-content-center min-vh-80 py-5"> <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="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 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"> <div class="card-header bg-gradient py-4 border-0 rounded-top-4">
<h3 class="mb-0 fs-4 fw-bold text-center"> <h3 class="mb-0 fs-4 fw-bold text-center">
{% trans 'Create Chart of Accounts' %} {% trans 'Create Chart of Accounts' %}
<i class="fa-solid fa-chart-pie ms-2"></i> <i class="fa-solid fa-chart-pie ms-2"></i>
</h3> </h3>
</div> </div>
<div class="card-body p-4 p-md-5"> <div class="card-body p-4 p-md-5">
<form method="post" id="{{ form.get_form_id }}" class="needs-validation" novalidate> <form method="post"
{% csrf_token %} id="{{ form.get_form_id }}"
class="needs-validation"
{# Bootstrap form rendering #} novalidate>
<div class="mb-3"> {% csrf_token %}
{{ form.name.label_tag }} {# Bootstrap form rendering #}
{{ form.name|add_class:"form-control" }} <div class="mb-3">
{% if form.name.help_text %} {{ form.name.label_tag }}
<small class="form-text text-muted">{{ form.name.help_text }}</small> {{ form.name|add_class:"form-control" }}
{% endif %} {% if form.name.help_text %}<small class="form-text text-muted">{{ form.name.help_text }}</small>{% endif %}
{% for error in form.name.errors %} {% for error in form.name.errors %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %}
<div class="invalid-feedback d-block">{{ error }}</div> </div>
{% endfor %} <div class="mb-3">
</div> {{ form.description.label_tag }}
<div class="mb-3"> {{ form.description|add_class:"form-control" }}
{{ form.description.label_tag }} {% if form.description.help_text %}
{{ form.description|add_class:"form-control" }} <small class="form-text text-muted">{{ form.description.help_text }}</small>
{% if form.description.help_text %} {% endif %}
<small class="form-text text-muted">{{ form.description.help_text }}</small> {% for error in form.description.errors %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %}
{% endif %} </div>
{% for error in form.description.errors %} <hr class="my-4">
<div class="invalid-feedback d-block">{{ error }}</div> <div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
{% endfor %} <button type="submit" class="btn btn-phoenix-primary btn-lg me-md-2">
</div> <i class="fa-solid fa-plus me-1"></i>
{% trans 'Create' %}
<hr class="my-4"> </button>
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3"> <a href="{% url 'coa-list' request.dealer.slug request.entity.slug %}"
<button type="submit" class="btn btn-phoenix-primary btn-lg me-md-2"> class="btn btn-phoenix-secondary btn-lg">
<i class="fa-solid fa-plus me-1"></i> <i class="fa-solid fa-ban me-1"></i>
{% trans 'Create' %} {% trans 'Cancel' %}
</button> </a>
<a href="{% url 'coa-list' request.dealer.slug request.entity.slug %}" </div>
class="btn btn-phoenix-secondary btn-lg"> </form>
<i class="fa-solid fa-ban me-1"></i> </div>
{% trans 'Cancel' %}
</a>
</div>
</form>
</div> </div>
</div> </div>
</div> </main>
</main> {% endblock %}
{% endblock %}

View File

@ -2,7 +2,6 @@
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}
{% load icon from django_ledger %} {% load icon from django_ledger %}
{% block content %} {% block content %}
<div class="card mb-4"> <div class="card mb-4">
<div class="card-body"> <div class="card-body">
@ -10,17 +9,19 @@
<div class="col-12"> <div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="display-4 mb-0">{% trans "Chart of Accounts" %}</h1> <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" %} <i class="fa-solid fa-plus"></i> {% trans "Add New" %}
</a> </a>
</div> </div>
{% if not inactive %} {% 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' %} {% trans 'Show Inactive' %}
</a> </a>
{% else %} {% 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' %} {% trans 'Show Active' %}
</a> </a>
{% endif %} {% endif %}
@ -28,11 +29,9 @@
</div> </div>
<div class="row row-cols-1 row-cols-md-2 g-4"> <div class="row row-cols-1 row-cols-md-2 g-4">
{% for coa_model in coa_list %} {% for coa_model in coa_list %}
<div class="col"> <div class="col">{% include 'chart_of_accounts/includes/coa_card.html' with coa_model=coa_model %}</div>
{% include 'chart_of_accounts/includes/coa_card.html' with coa_model=coa_model %}
</div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -2,23 +2,20 @@
{% load i18n %} {% load i18n %}
{% load static %} {% load static %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% block content %} {% block content %}
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-lg-6 col-md-8"> <div class="col-lg-6 col-md-8">
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-body"> <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 %} {% csrf_token %}
<div class="mb-3"> <div class="mb-3">
{{ form.name.label_tag }} {{ form.name.label_tag }}
{{ form.name|add_class:"form-control" }} {{ form.name|add_class:"form-control" }}
{% if form.name.help_text %} {% if form.name.help_text %}<small class="form-text text-muted">{{ form.name.help_text }}</small>{% endif %}
<small class="form-text text-muted">{{ form.name.help_text }}</small> {% for error in form.name.errors %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %}
{% endif %}
{% for error in form.name.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
</div> </div>
<div class="mb-3"> <div class="mb-3">
{{ form.description.label_tag }} {{ form.description.label_tag }}
@ -26,19 +23,13 @@
{% if form.description.help_text %} {% if form.description.help_text %}
<small class="form-text text-muted">{{ form.description.help_text }}</small> <small class="form-text text-muted">{{ form.description.help_text }}</small>
{% endif %} {% endif %}
{% for error in form.description.errors %} {% for error in form.description.errors %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
</div> </div>
<div class="d-flex justify-content-center gap-2 mt-4"> <div class="d-flex justify-content-center gap-2 mt-4">
<button class="btn btn-phoenix-primary" type="submit"> <button class="btn btn-phoenix-primary" type="submit">{% trans 'Update' %}</button>
{% trans 'Update'%}
</button>
<a class="btn btn-phoenix-secondary" <a class="btn btn-phoenix-secondary"
href="{% url 'coa-list' request.dealer.slug request.entity.slug %}"> href="{% url 'coa-list' request.dealer.slug request.entity.slug %}">
{% trans 'Back'%} {% trans 'Back' %}
</a> </a>
</div> </div>
</form> </form>
@ -46,4 +37,4 @@
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,7 +1,6 @@
{% load django_ledger %} {% load django_ledger %}
{% load i18n %} {% load i18n %}
{% now "Y" as current_year %} {% now "Y" as current_year %}
<div class="card shadow-sm border-0 mb-4"> <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="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"> <div class="me-3">
@ -19,17 +18,16 @@
{% endif %} {% endif %}
</div> </div>
<div class="ms-auto d-flex flex-column align-items-end"> <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> <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> <span class="badge bg-danger"><i class="fas fa-times-circle"></i> {% trans 'Inactive' %}</span>
{% endif %} {% endif %}
{% if coa_model.is_default %} {% if coa_model.is_default %}
<span class="badge bg-primary-subtle text-primary mt-1">{% trans 'Entity Default' %}</span> <span class="badge bg-primary-subtle text-primary mt-1">{% trans 'Entity Default' %}</span>
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row g-3"> <div class="row g-3">
<div class="col-sm-6"> <div class="col-sm-6">
@ -46,27 +44,22 @@
<span class="text-muted ms-2">{{ coa_model.slug }}</span> <span class="text-muted ms-2">{{ coa_model.slug }}</span>
</div> </div>
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="mb-2"> <div class="mb-2">
<span class="fw-bold"><i class="fas fa-list-alt me-1"></i> {% trans 'Total Accounts' %}:</span> <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> <span class="ms-2">{{ coa_model.accountmodel_total__count }}</span>
</div> </div>
<div class="mb-2"> <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="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> <span class="ms-2">{{ coa_model.accountmodel_active__count }}</span>
</div> </div>
<div class="mb-2"> <div class="mb-2">
<span class="fw-bold text-danger"><i class="fas fa-lock me-1"></i> {% trans 'Locked Accounts' %}:</span> <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> <span class="ms-2">{{ coa_model.accountmodel_locked__count }}</span>
</div> </div>
</div> </div>
</div> </div>
<hr class="my-3"> <hr class="my-3">
<div class="row g-2"> <div class="row g-2">
<div class="col-sm-6"> <div class="col-sm-6">
<small class="text-muted d-block"> <small class="text-muted d-block">
@ -84,36 +77,37 @@
</div> </div>
</div> </div>
</div> </div>
<div class="card-footer bg-transparent border-top-0 pt-0 pt-sm-3"> <div class="card-footer bg-transparent border-top-0 pt-0 pt-sm-3">
<div class="d-flex flex-wrap gap-2"> <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' %} <i class="fas fa-edit me-1"></i> {% trans 'Update' %}
</a> </a>
<a href="{% url 'account_list' request.dealer.slug coa_model.pk %}"
<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"> 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' %} <i class="fas fa-book me-1"></i> {% trans 'Accounts' %}
</a> </a>
<a href="{% url 'account_create' request.dealer.slug coa_model.pk %}"
<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"> 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' %} <i class="fas fa-plus-circle me-1"></i> {% trans 'Add Account' %}
</a> </a>
{% if coa_model.can_mark_as_default %} {% 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' %} <i class="fas fa-star me-1"></i> {% trans 'Mark as Default' %}
</a> </a>
{% endif %} {% endif %}
{% if coa_model.can_deactivate %} {% 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' %} <i class="fas fa-toggle-off me-1"></i> {% trans 'Mark as Inactive' %}
</a> </a>
{% elif coa_model.can_activate %} {% 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' %} <i class="fas fa-toggle-on me-1"></i> {% trans 'Mark as Active' %}
</a> </a>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,16 +1,22 @@
{% load i18n crispy_forms_tags %} {% 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-dialog modal-xl">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header justify-content-between align-items-start gap-5 px-4 pt-4 pb-3 border-0"> <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> <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"> <button class="btn p-0 text-body-quaternary fs-6"
<span class="fas fa-times"></span> data-bs-dismiss="modal"
</button> aria-label="Close">
</div> <span class="fas fa-times"></span>
<div id="emailModalBody" class="modal-body"> </button>
<h1>hi</h1> </div>
<div id="emailModalBody" class="modal-body">
<h1>hi</h1>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>

View File

@ -15,13 +15,11 @@
</button> </button>
</div> </div>
<div class="modal-body"> <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-select="#notesTable"
hx-target="#notesTable" hx-target="#notesTable"
hx-on::after-request="{ hx-on::after-request="{ resetSubmitButton(document.querySelector('.add_note_form button[type=submit]')); $('#noteModal').modal('hide'); }"
resetSubmitButton(document.querySelector('.add_note_form button[type=submit]'));
$('#noteModal').modal('hide');
}"
hx-swap="outerHTML" hx-swap="outerHTML"
method="post" method="post"
class="add_note_form"> class="add_note_form">
@ -34,7 +32,7 @@
</div> </div>
</div> </div>
<script> <script>
function updateNote(e) { function updateNote(e) {
let url = e.getAttribute('data-url'); let url = e.getAttribute('data-url');
let note = e.getAttribute('data-note'); let note = e.getAttribute('data-note');

View File

@ -15,13 +15,11 @@
</button> </button>
</div> </div>
<div class="modal-body"> <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-select=".taskTable"
hx-target=".taskTable" hx-target=".taskTable"
hx-on::after-request="{ hx-on::after-request="{ resetSubmitButton(document.querySelector('.add_schedule_form button[type=submit]')); $('#scheduleModal').modal('hide'); }"
resetSubmitButton(document.querySelector('.add_schedule_form button[type=submit]'));
$('#scheduleModal').modal('hide');
}"
hx-swap="outerHTML" hx-swap="outerHTML"
method="post" method="post"
class="add_schedule_form"> class="add_schedule_form">

View File

@ -1,10 +1,10 @@
{% load static i18n crispy_forms_tags %} {% load static i18n crispy_forms_tags %}
<!-- task Modal --> <!-- task Modal -->
<style> <style>
.completed-task { .completed-task {
text-decoration: line-through; text-decoration: line-through;
opacity: 0.7; opacity: 0.7;
} }
</style> </style>
<div class="modal fade" <div class="modal fade"
id="taskModal" id="taskModal"
@ -22,13 +22,14 @@
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form id="taskForm" action="{% url 'add_task' request.dealer.slug content_type slug %}" <form id="taskForm"
method="post" action="{% url 'add_task' request.dealer.slug content_type slug %}"
class="add_task_form" method="post"
hx-post="{% url 'add_task' request.dealer.slug content_type slug %}" class="add_task_form"
hx-target="#your-content-container" hx-post="{% url 'add_task' request.dealer.slug content_type slug %}"
hx-swap="innerHTML" hx-target="#your-content-container"
hx-boost="true"> hx-swap="innerHTML"
hx-boost="true">
{% csrf_token %} {% csrf_token %}
{{ staff_task_form|crispy }} {{ staff_task_form|crispy }}
<button type="submit" class="btn btn-phoenix-success w-100">{% trans 'Save' %}</button> <button type="submit" class="btn btn-phoenix-success w-100">{% trans 'Save' %}</button>

View File

@ -3,31 +3,31 @@
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block customCSS %} {% block customCSS %}
<style> <style>
.main-tab li:last-child { .main-tab li:last-child {
margin-left: auto; margin-left: auto;
} }
.kanban-header { .kanban-header {
position: relative; position: relative;
background-color:rgb(237, 241, 245); background-color:rgb(237, 241, 245);
font-weight: 600; font-weight: 600;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
margin-bottom: 1rem; margin-bottom: 1rem;
color: #333; color: #333;
clip-path: polygon(0 0, calc(100% - 15px) 0, 100% 50%, calc(100% - 15px) 100%, 0 100%); 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); box-shadow: 0 1px 2px rgba(0,0,0,0.1);
} }
.kanban-header::after { .kanban-header::after {
content: ""; content: "";
position: absolute; position: absolute;
right: -20px; right: -20px;
top: 0; top: 0;
width: 0; width: 0;
height: 0; height: 0;
border-top: 28px solid transparent; border-top: 28px solid transparent;
border-bottom: 28px solid transparent; border-bottom: 28px solid transparent;
border-left: 20px solid #dee2e6; border-left: 20px solid #dee2e6;
} }
</style> </style>
{% endblock customCSS %} {% endblock customCSS %}
@ -39,7 +39,6 @@
<div class="col-12 col-md-auto"> <div class="col-12 col-md-auto">
<h3 class="mb-0">{{ _("Lead Details") }}</h3> <h3 class="mb-0">{{ _("Lead Details") }}</h3>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@ -47,7 +46,6 @@
<div class="col-md-5 col-lg-5 col-xl-4"> <div class="col-md-5 col-lg-5 col-xl-4">
<div class="sticky-leads-sidebar"> <div class="sticky-leads-sidebar">
<div class="lead-details" data-breakpoint="md"> <div class="lead-details" data-breakpoint="md">
<div class="card mb-2"> <div class="card mb-2">
<div class="card-body"> <div class="card-body">
<div class="row align-items-center g-3 text-center text-xxl-start"> <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"> <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> <h5 class="fw-bolder mb-2 text-body-highlight">{{ _("Car Requested") }}</h5>
{% if lead.id_car_make.logo %} {% if lead.id_car_make.logo %}
<img src="{{ lead.id_car_make.logo.url }}" <img src="{{ lead.id_car_make.logo.url }}"
alt="Car Make Logo" alt="Car Make Logo"
class="img-fluid rounded mb-2" class="img-fluid rounded mb-2"
style="width: 60px; style="width: 60px;
height: 60px"> height: 60px">
{% endif %} {% endif %}
<p class="mb-0">{{ lead.id_car_make.get_local_name }} - {{ lead.id_car_model.get_local_name }} {{ lead.year }}</p> <p class="mb-0">{{ lead.id_car_make.get_local_name }} - {{ lead.id_car_model.get_local_name }} {{ lead.year }}</p>
</div> </div>
@ -93,16 +91,17 @@
</div> </div>
<div class="card mb-2"> <div class="card mb-2">
<div class="card-body"> <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"> <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> <h5 class="fw-bolder mb-2 text-body-highlight">{{ _("Assigned To") }}</h5>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div class="avatar avatar-tiny me-2"> <div class="avatar avatar-tiny me-2">
{% if lead.staff.logo %} {% if lead.staff.logo %}
<img class="avatar-img rounded-circle" <img class="avatar-img rounded-circle"
src="{{ lead.staff.thumbnail.url }}" src="{{ lead.staff.thumbnail.url }}"
onerror="this.src='/static/img/brand/brand-logo.png'" onerror="this.src='/static/img/brand/brand-logo.png'"
alt="Logo"> alt="Logo">
{% endif %} {% endif %}
</div> </div>
<small> <small>
@ -293,11 +292,9 @@
<div class="modal-content"> <div class="modal-content">
<form class="modal-content" <form class="modal-content"
action="{% url 'lead_transfer' request.dealer.slug lead.slug %}" action="{% url 'lead_transfer' request.dealer.slug lead.slug %}"
hx-select-oob="#assignedTo:outerHTML,#toast-container:outerHTML" hx-select-oob="#assignedTo:outerHTML,#toast-container:outerHTML"
hx-swap="none" hx-swap="none"
hx-on::after-request="{ hx-on::after-request="{ resetSubmitButton(document.querySelector('#exampleModal button[type=submit]')); $('#exampleModal').modal('hide');}"
resetSubmitButton(document.querySelector('#exampleModal button[type=submit]'));
$('#exampleModal').modal('hide');}"
method="post"> method="post">
{% csrf_token %} {% csrf_token %}
<div class="modal-header"> <div class="modal-header">
@ -488,7 +485,7 @@
data-url="{% url 'update_note' request.dealer.slug note.pk %}" data-url="{% url 'update_note' request.dealer.slug note.pk %}"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#noteModal" data-bs-target="#noteModal"
data-note-title="{{ _('Update') }}"> data-note-title="{{ _("Update") }}">
<i class='fas fa-pen-square text-primary ms-2'></i> <i class='fas fa-pen-square text-primary ms-2'></i>
{{ _("Update") }} {{ _("Update") }}
</a> </a>
@ -528,8 +525,7 @@
hx-get="{% url 'send_lead_email' request.dealer.slug lead.slug %}" hx-get="{% url 'send_lead_email' request.dealer.slug lead.slug %}"
hx-target="#emailModalBody" hx-target="#emailModalBody"
hx-select=".email-form" hx-select=".email-form"
hx-swap="innerHTML" hx-swap="innerHTML">
>
<span class="fas fa-plus me-1"></span>{{ _("Send Email") }} <span class="fas fa-plus me-1"></span>{{ _("Send Email") }}
</button> </button>
{% endif %} {% 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") }} href="{% url 'appointment:get_user_appointments' %}"> <span class="me-2 text-body align-bottom" data-feather="calendar"></span>{{ _("View in Calendar") }}
</a> </a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@ -821,62 +816,62 @@
{% include "components/note_modal.html" with content_type="lead" slug=lead.slug %} {% include "components/note_modal.html" with content_type="lead" slug=lead.slug %}
<!-- schedule Modal --> <!-- schedule Modal -->
{% include "components/schedule_modal.html" with content_type="lead" slug=lead.slug %} {% include "components/schedule_modal.html" with content_type="lead" slug=lead.slug %}
{% endblock content %} {% endblock content %}
{% block customJS %} {% block customJS %}
<script> <script>
function reset_form() { function reset_form() {
document.querySelector('#id_note').value = "" document.querySelector('#id_note').value = ""
let form = document.querySelector('.add_note_form') let form = document.querySelector('.add_note_form')
form.action = "{% url 'add_note' request.dealer.slug 'lead' lead.slug %}" form.action = "{% url 'add_note' request.dealer.slug 'lead' lead.slug %}"
} }
{% if messages %} {% if messages %}
{% for message in messages %} {% for message in messages %}
Toast.fire({ Toast.fire({
icon: "{{ message.tags }}", icon: "{{ message.tags }}",
titleText: "{{ message|safe }}" titleText: "{{ message|safe }}"
}); });
{% endfor %} {% endfor %}
{% endif %} {% endif %}
function openActionModal(leadId, currentAction, nextAction, nextActionDate) { function openActionModal(leadId, currentAction, nextAction, nextActionDate) {
const modal = new bootstrap.Modal(document.getElementById('actionTrackingModal')); const modal = new bootstrap.Modal(document.getElementById('actionTrackingModal'));
document.getElementById('actionTrackingForm').setAttribute('hx-boost', 'false'); document.getElementById('actionTrackingForm').setAttribute('hx-boost', 'false');
document.getElementById('leadId').value = leadId; document.getElementById('leadId').value = leadId;
document.getElementById('currentAction').value = currentAction; document.getElementById('currentAction').value = currentAction;
document.getElementById('nextAction').value = nextAction; document.getElementById('nextAction').value = nextAction;
document.getElementById('nextActionDate').value = nextActionDate; document.getElementById('nextActionDate').value = nextActionDate;
modal.show(); modal.show();
} }
function notify(tag, msg) { function notify(tag, msg) {
Toast.fire({ Toast.fire({
icon: tag, icon: tag,
titleText: msg titleText: msg
}); });
} }
// Close modal after successful form submission // Close modal after successful form submission
document.body.addEventListener('htmx:afterSwap', function(evt) { document.body.addEventListener('htmx:afterSwap', function(evt) {
if (evt.detail.target.id === 'main_content') { if (evt.detail.target.id === 'main_content') {
var modal = bootstrap.Modal.getInstance(document.getElementById('exampleModal')); var modal = bootstrap.Modal.getInstance(document.getElementById('exampleModal'));
if (modal) { if (modal) {
modal.hide(); modal.hide();
} }
} }
}); });
// Cleanup modal backdrop if needed // Cleanup modal backdrop if needed
document.body.addEventListener('htmx:beforeSwap', function(evt) { document.body.addEventListener('htmx:beforeSwap', function(evt) {
if (evt.detail.target.id === 'main_content') { if (evt.detail.target.id === 'main_content') {
var backdrops = document.querySelectorAll('.modal-backdrop'); var backdrops = document.querySelectorAll('.modal-backdrop');
backdrops.forEach(function(backdrop) { backdrops.forEach(function(backdrop) {
backdrop.remove(); backdrop.remove();
}); });
} }
}); });
</script> </script>
{% endblock customJS %} {% endblock customJS %}

View File

@ -1,6 +1,5 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n static crispy_forms_filters %} {% load i18n static crispy_forms_filters %}
{% block title %} {% block title %}
{% if object %} {% if object %}
{% trans 'Update Lead' %} {% trans 'Update Lead' %}
@ -8,7 +7,6 @@
{% trans 'Add New Lead' %} {% trans 'Add New Lead' %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block customcss %} {% block customcss %}
<style> <style>
.htmx-indicator{ .htmx-indicator{
@ -30,41 +28,40 @@
} }
</style> </style>
{% endblock customcss %} {% endblock customcss %}
{% block content %} {% block content %}
<main class="d-flex align-items-center justify-content-center min-vh-100 py-5"> <main class="d-flex align-items-center justify-content-center min-vh-100 py-5">
<div class="col-md-8"> <div class="col-md-8">
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp"> <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"> <div class="card-header bg-gradient py-4 border-0 rounded-top-4">
<h3 class="mb-0 fs-4 fw-bold text-center"> <h3 class="mb-0 fs-4 fw-bold text-center">
{% if object %} {% if object %}
{% trans "Update Lead" %} {% trans "Update Lead" %}
<i class="fa-solid fa-edit ms-2"></i> <i class="fa-solid fa-edit ms-2"></i>
{% else %} {% else %}
{% trans "Create New Lead" %} {% trans "Create New Lead" %}
<i class="fa-solid fa-bullhorn ms-2"></i> <i class="fa-solid fa-bullhorn ms-2"></i>
{% endif %} {% endif %}
</h3> </h3>
</div> </div>
<div class="card-body p-4 p-md-5"> <div class="card-body p-4 p-md-5">
<form class="form" method="post" enctype="multipart/form-data"> <form class="form" method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
{{ form|crispy }} {{ form|crispy }}
<hr class="my-4">
<hr class="my-4"> <div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
<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">
<button class="btn btn-phoenix-primary btn-lg me-md-2" type="submit"> <i class="fa-solid fa-floppy-disk me-1"></i>
<i class="fa-solid fa-floppy-disk me-1"></i> {% trans "Save" %}
{% trans "Save" %} </button>
</button> <a href="{% url 'lead_list' request.dealer.slug %}"
<a href="{% url 'lead_list' request.dealer.slug %}" class="btn btn-phoenix-secondary btn-lg"> class="btn btn-phoenix-secondary btn-lg">
<i class="fa-solid fa-ban me-1"></i> <i class="fa-solid fa-ban me-1"></i>
{% trans "Cancel" %} {% trans "Cancel" %}
</a> </a>
</div> </div>
</form> </form>
</div>
</div> </div>
</div> </div>
</div> </main>
</main> {% endblock %}
{% endblock %}

View File

@ -1,270 +1,263 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n static humanize %} {% load i18n static humanize %}
{% block title %} {% block title %}
{{ _("Leads") |capfirst }} {{ _("Leads") |capfirst }}
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
{% if page_obj.object_list or request.GET.q %}
{% if page_obj.object_list or request.GET.q%} <div class="row g-3 mt-4 mb-4">
<div class="row g-3 mt-4 mb-4"> <h2 class="mb-2">
<h2 class="mb-2"> {{ _("Leads") |capfirst }}
{{ _("Leads") |capfirst }} <li class="fas fa-bullhorn text-primary ms-2"></li>
<li class="fas fa-bullhorn text-primary ms-2"></li> </h2>
</h2> <!-- Action Tracking Modal -->
<!-- Action Tracking Modal --> {% comment %} {% include "crm/leads/partials/update_action.html" %} {% endcomment %}
{% 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="row g-3 justify-content-between mb-4"> <div class="d-md-flex justify-content-between">
<div class="col-auto"> {% if perms.inventory.add_lead %}
<div class="d-md-flex justify-content-between"> <div>
{% if perms.inventory.add_lead %} <a href="{% url 'lead_create' request.dealer.slug %}"
<div> class="btn btn-sm btn-phoenix-primary"><span class="fas fa-plus me-2"></span>{{ _("Add Lead") }}</a>
<a href="{% url 'lead_create' request.dealer.slug %}" </div>
class="btn btn-sm btn-phoenix-primary"><span class="fas fa-plus me-2"></span>{{ _("Add Lead") }}</a> {% endif %}
</div> </div>
{% endif %} </div>
<div class="col-auto">
<div class="d-flex">{% include 'partials/search_box.html' %}</div>
</div> </div>
</div> </div>
<div class="col-auto"> <div class="row g-3">
<div class="d-flex">{% include 'partials/search_box.html' %}</div> <div class="col-12">
</div> {% if page_obj.object_list or request.GET.q %}
</div> <div class="table-responsive scrollbar mx-n1 px-1">
<table class="table align-items-center table-flush table-hover">
<div class="row g-3"> <thead>
<div class="col-12"> <tr class="bg-body-highlight">
{% if page_obj.object_list or request.GET.q%} <th class="align-middle white-space-nowrap text-uppercase"
<div class="table-responsive scrollbar mx-n1 px-1"> scope="col"
<table class="table align-items-center table-flush table-hover"> style="width: 20%">{{ _("Lead Name") |capfirst }}</th>
<thead> <th class="align-middle white-space-nowrap text-uppercase"
<tr class="bg-body-highlight"> scope="col"
<th class="align-middle white-space-nowrap text-uppercase" style="width: 15%">
scope="col" <div class="d-inline-flex flex-center">
style="width: 20%">{{ _("Lead Name") |capfirst }}</th> <div class="d-flex align-items-center px-1 py-1 bg-success-subtle rounded me-2">
<th class="align-middle white-space-nowrap text-uppercase" <i class="text-success-dark fas fa-car"></i>
scope="col" </div>
style="width: 15%"> <span>{{ _("Car") |capfirst }}</span>
<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> </div>
<span>{{ _("Car") |capfirst }}</span> </th>
</div> <th class="align-middle white-space-nowrap text-uppercase"
</th> scope="col"
<th class="align-middle white-space-nowrap text-uppercase" style="width: 15%">
scope="col" <div class="d-inline-flex flex-center">
style="width: 15%"> <div class="d-flex align-items-center px-1 py-1 bg-success-subtle rounded me-2">
<div class="d-inline-flex flex-center"> <span class="text-success-dark" data-feather="mail"></span>
<div class="d-flex align-items-center px-1 py-1 bg-success-subtle rounded me-2"> </div>
<span class="text-success-dark" data-feather="mail"></span> <span>{{ _("email") |capfirst }}</span>
</div> </div>
<span>{{ _("email") |capfirst }}</span> </th>
</div> <th class="align-middle white-space-nowrap text-uppercase"
</th> scope="col"
<th class="align-middle white-space-nowrap text-uppercase" style="width: 15%">
scope="col" <div class="d-inline-flex flex-center">
style="width: 15%"> <div class="d-flex align-items-center px-1 py-1 bg-primary-subtle rounded me-2">
<div class="d-inline-flex flex-center"> <span class="text-primary-dark" data-feather="phone"></span>
<div class="d-flex align-items-center px-1 py-1 bg-primary-subtle rounded me-2"> </div>
<span class="text-primary-dark" data-feather="phone"></span> <div class="" dir="ltr">{{ _("Phone Number") }}</div>
</div> </div>
<div class="" dir="ltr">{{ _("Phone Number") }}</div> </th>
</div> <th class="align-middle white-space-nowrap text-uppercase"
</th> scope="col"
<th class="align-middle white-space-nowrap text-uppercase" style="width: 10%">
scope="col" <div class="d-inline-flex flex-center">
style="width: 10%"> <div class="d-flex align-items-center bg-warning-subtle rounded me-2">
<div class="d-inline-flex flex-center"> <span class="text-warning-dark" data-feather="zap"></span>
<div class="d-flex align-items-center bg-warning-subtle rounded me-2"> </div>
<span class="text-warning-dark" data-feather="zap"></span> <span>{{ _("Next Action") |capfirst }}</span>
</div> </div>
<span>{{ _("Next Action") |capfirst }}</span> </th>
</div> <th class="align-middle white-space-nowrap text-uppercase"
</th> scope="col"
<th class="align-middle white-space-nowrap text-uppercase" style="width: 15%">
scope="col" <div class="d-inline-flex flex-center">
style="width: 15%"> <div class="d-flex align-items-center px-1 py-1 bg-primary-subtle rounded me-2">
<div class="d-inline-flex flex-center"> <span class="far fa-calendar-alt"></span>
<div class="d-flex align-items-center px-1 py-1 bg-primary-subtle rounded me-2"> </div>
<span class="far fa-calendar-alt"></span> <span>{{ _("Scheduled at") }}</span>
</div> </div>
<span>{{ _("Scheduled at") }}</span> </th>
</div> <th class="align-middle white-space-nowrap text-uppercase"
</th> scope="col"
<th class="align-middle white-space-nowrap text-uppercase" style="width: 10%">
scope="col" <div class="d-inline-flex flex-center">
style="width: 10%"> <div class="d-flex align-items-center bg-success-subtle rounded me-2">
<div class="d-inline-flex flex-center"> <span class="text-success-dark" data-feather="user-check"></span>
<div class="d-flex align-items-center bg-success-subtle rounded me-2"> </div>
<span class="text-success-dark" data-feather="user-check"></span> <span>{{ _("Assigned To") |capfirst }}</span>
</div> </div>
<span>{{ _("Assigned To") |capfirst }}</span> </th>
</div> {% 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-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> <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> <span>{{ _("Opportunity")|capfirst }}</span>
</div> </div>
</th> {% endcomment %} </th> {% endcomment %}
<th class="align-middle white-space-nowrap text-uppercase" <th class="align-middle white-space-nowrap text-uppercase"
scope="col" scope="col"
style="width: 15%">{{ _("Action") }}</th> style="width: 15%">{{ _("Action") }}</th>
<th class="text-end white-space-nowrap align-middle" scope="col"></th> <th class="text-end white-space-nowrap align-middle" scope="col"></th>
</tr> </tr>
{% for lead in leads %} {% for lead in leads %}
<!-- Delete Modal --> <!-- Delete Modal -->
<div class="modal fade" <div class="modal fade"
id="deleteModal" id="deleteModal"
data-bs-backdrop="static" data-bs-backdrop="static"
data-bs-keyboard="false" data-bs-keyboard="false"
tabindex="-1" tabindex="-1"
aria-labelledby="deleteModalLabel" aria-labelledby="deleteModalLabel"
aria-hidden="true"> aria-hidden="true">
<div class="modal-dialog modal-sm"> <div class="modal-dialog modal-sm">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header justify-content-between align-items-start gap-5 px-4 pt-4 pb-3 border-0"> <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"> <h4 class="mb-0 me-2 text-danger">
{{ _("Delete") }}<i class="fas fa-exclamation-circle text-danger ms-2"></i> {{ _("Delete") }}<i class="fas fa-exclamation-circle text-danger ms-2"></i>
</h4> </h4>
<button class="btn p-0 text-body-quaternary fs-6" <button class="btn p-0 text-body-quaternary fs-6"
data-bs-dismiss="modal" data-bs-dismiss="modal"
aria-label="Close"> aria-label="Close">
<span class="fas fa-times"></span> <span class="fas fa-times"></span>
</button> </button>
</div> </div>
<div class="modal-body p-4"> <div class="modal-body p-4">
<p>{% trans "Are you sure you want to delete this lead?" %}</p> <p>{% trans "Are you sure you want to delete this lead?" %}</p>
</div> </div>
<div class="modal-footer flex justify-content-center border-top-0"> <div class="modal-footer flex justify-content-center border-top-0">
<a type="button" <a type="button"
class="btn btn-sm btn-phoenix-danger w-100" class="btn btn-sm btn-phoenix-danger w-100"
href="{% url 'lead_delete' request.dealer.slug lead.slug %}"> href="{% url 'lead_delete' request.dealer.slug lead.slug %}">
{% trans "Yes" %} {% trans "Yes" %}
</a> </a>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> <tbody>
<tbody> <tr class="hover-actions-trigger btn-reveal-trigger position-static">
<tr class="hover-actions-trigger btn-reveal-trigger position-static"> <td class="name align-middle white-space-nowrap ps-1">
<td class="name align-middle white-space-nowrap ps-1"> <div class="d-flex align-items-center">
<div class="d-flex align-items-center"> <div>
<div> <a class="fs-8 fw-bold"
<a class="fs-8 fw-bold" href="{% url 'lead_detail' request.dealer.slug lead.slug %}">{{ lead.full_name|capfirst }}</a>
href="{% url 'lead_detail' request.dealer.slug lead.slug %}">{{ lead.full_name|capfirst }}</a> <div class="d-flex align-items-center">
<div class="d-flex align-items-center"> <p class="mb-0 text-body-highlight fw-semibold fs-9 me-2"></p>
<p class="mb-0 text-body-highlight fw-semibold fs-9 me-2"></p> {% if lead.status == "new" %}
{% 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>
<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" %}
{% 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>
<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" %}
{% 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>
<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" %}
{% 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>
<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" %}
{% 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>
<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" %}
{% 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>
<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>
{% endif %} {% endif %}
{% endif %} </div>
{% 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> </div>
{% endif %} </td>
</td> <td class="align-middle white-space-nowrap fw-semibold">
</tr> <a class="text-body-highlight" href="">{{ lead.id_car_make.get_local_name }} - {{ lead.id_car_model.get_local_name }} {{ lead.year }}</a>
{% empty %} </td>
<tr> <td class="align-middle white-space-nowrap fw-semibold">
<td colspan="6" class="text-center">{% trans "No Leads found." %}</td> <a class="text-body-highlight" href="">{{ lead.email }}</a>
</tr> </td>
{% endfor %} <td class="align-middle white-space-nowrap fw-semibold">
</tbody> <a class="text-body-highlight" href="tel:{{ lead.phone_number }}">{{ lead.phone_number }}</a>
</table> </td>
</div> <td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">
{% if page_obj.paginator.num_pages > 1 %} {{ lead.next_action|upper }}
<div class="d-flex justify-content-end mt-3"> </td>
<div class="d-flex">{% include 'partials/pagination.html' %}</div> <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> </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>
{% endif %}
</div> </div>
</div> </div>
</div> {% else %}
{% else %} {% url 'lead_create' request.dealer.slug as create_lead_url %}
{% url 'lead_create' request.dealer.slug as create_lead_url %} {% include "empty-illustration-page.html" with value="lead" url=create_lead_url %}
{% include "empty-illustration-page.html" with value="lead" url=create_lead_url %} {% endif %}
{% endif %} {% endblock %}
{% endblock %}

View File

@ -5,179 +5,176 @@
{% endblock title %} {% endblock title %}
{% block customCSS %} {% block customCSS %}
<style> <style>
.kanban-column { .kanban-column {
border-radius: 8px; border-radius: 8px;
padding: 1rem; padding: 1rem;
min-height: 500px; min-height: 500px;
} }
.kanban-header { .kanban-header {
position: relative; position: relative;
font-weight: 600; font-weight: 600;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
margin-bottom: 1rem; margin-bottom: 1rem;
color: #333; color: #333;
--pointed-edge: {% if LANGUAGE_CODE == 'en' %} right {% else %} left {% endif %}; --pointed-edge: {% if LANGUAGE_CODE == 'en' %} right {% else %} left {% endif %};
clip-path: {% if LANGUAGE_CODE == 'en' %} clip-path: {% if LANGUAGE_CODE == 'en' %}
polygon(0 0, calc(100% - 15px) 0, 100% 50%, calc(100% - 15px) 100%, 0 100%) polygon(0 0, calc(100% - 15px) 0, 100% 50%, calc(100% - 15px) 100%, 0 100%)
{% else %} {% else %}
polygon(15px 0, 100% 0, 100% 100%, 15px 100%, 0 50%) polygon(15px 0, 100% 0, 100% 100%, 15px 100%, 0 50%)
{% endif %}; {% endif %};
box-shadow: 0 1px 2px rgba(0,0,0,0.1); box-shadow: 0 1px 2px rgba(0,0,0,0.1);
} }
.kanban-header::after { .kanban-header::after {
content: ""; content: "";
position: absolute; position: absolute;
right: -20px; right: -20px;
top: 0; top: 0;
width: 0; width: 0;
height: 0; height: 0;
border-top: 28px solid transparent; border-top: 28px solid transparent;
border-bottom: 28px solid transparent; border-bottom: 28px solid transparent;
border-left: 20px solid #dee2e6; border-left: 20px solid #dee2e6;
} }
.lead-card { .lead-card {
border: 1px solid #dee2e6; border: 1px solid #dee2e6;
border-radius: 8px; border-radius: 8px;
padding: 0.75rem; padding: 0.75rem;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.lead-card small { .lead-card small {
color: #6c757d; color: #6c757d;
} }
.bg-success-soft { .bg-success-soft {
background-color: rgba(17, 240, 66, 0.1) !important; background-color: rgba(17, 240, 66, 0.1) !important;
opacity: .8; opacity: .8;
} }
.bg-danger-soft { .bg-danger-soft {
background-color: rgba(230, 50, 68, 0.1) !important; background-color: rgba(230, 50, 68, 0.1) !important;
opacity: .8; opacity: .8;
} }
.bg-info-soft { .bg-info-soft {
background-color: rgba(41, 197, 245, 0.1) !important; background-color: rgba(41, 197, 245, 0.1) !important;
opacity: .8; opacity: .8;
} }
.bg-negotiation-soft { .bg-negotiation-soft {
background-color: rgba(113, 206, 206, 0.1) !important; background-color: rgba(113, 206, 206, 0.1) !important;
opacity: .8; opacity: .8;
} }
</style> </style>
{% endblock customCSS %} {% endblock customCSS %}
{% block content %} {% block content %}
{% if leads %}
{% if leads %} <div class="container-fluid my-4">
<div class="container-fluid my-4"> <div class="row justify-content-center">
<div class="row justify-content-center"> <div class="col">
<div class="col"> <div class="d-flex justify-content-between mb-3">
<div class="d-flex justify-content-between mb-3"> <h3>
<h3> {{ _("Lead Tracking") }}
{{ _("Lead Tracking") }} <li class="fas fa-bullhorn text-primary ms-2"></li>
<li class="fas fa-bullhorn text-primary ms-2"></li> </h3>
</h3> </div>
</div> <div class="row g-3">
<div class="row g-3"> <!-- New Lead -->
<!-- New Lead --> <div class="col-md">
<div class="col-md"> <div class="kanban-column bg-body">
<div class="kanban-column bg-body"> <div class="kanban-header opacity-75">
<div class="kanban-header opacity-75"> <span class="text-body">{{ _("New Leads") }} ({{ new|length }})</span>
<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> </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>
</div> <!-- Follow Ups -->
<!-- Follow Ups --> <div class="col-md">
<div class="col-md"> <div class="kanban-column bg-body">
<div class="kanban-column bg-body"> <div class="kanban-header opacity-75">
<div class="kanban-header opacity-75"> <span class="text-body">{{ _("Follow Ups") }} ({{ follow_up|length }})</span>
<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> </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>
</div> <!-- Negotiation -->
<!-- Negotiation --> <div class="col-md">
<div class="col-md"> <div class="kanban-column bg-body">
<div class="kanban-column bg-body"> <div class="kanban-header opacity-75">
<div class="kanban-header opacity-75"> <span class="text-body">{{ _("Negotiation Ups") }} ({{ follow_up|length }})</span>
<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> </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>
</div> <!-- Won -->
<!-- Won --> <div class="col-md">
<div class="col-md"> <div class="kanban-column bg-body">
<div class="kanban-column bg-body"> <div class="kanban-header bg-success-light opacity-75">
<div class="kanban-header bg-success-light opacity-75"> <span class="text-body">{{ _("Won") }} ({{ won|length }}) ({{ follow_up|length }})</span>
<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> </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>
</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>
</div> </div>
</div> {% else %}
{% else %} {% url 'lead_create' request.dealer.slug as create_lead_url %}
{% url 'lead_create' request.dealer.slug as create_lead_url %} {% include "empty-illustration-page.html" with value="lead" url=create_lead_url %}
{% include "empty-illustration-page.html" with value="lead" url=create_lead_url %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -2,22 +2,22 @@
{% load static i18n humanize %} {% load static i18n humanize %}
{% block customCSS %} {% block customCSS %}
<style> <style>
.card { .card {
box-shadow: 0 2px 4px rgba(0,0,0,0.05); box-shadow: 0 2px 4px rgba(0,0,0,0.05);
border: none; border: none;
} }
.card-header { .card-header {
background-color: #f0f2f5; background-color: #f0f2f5;
font-weight: 600; font-weight: 600;
} }
.table thead { .table thead {
background-color: #f9fafb; background-color: #f9fafb;
} }
.empty-state { .empty-state {
text-align: center; text-align: center;
padding: 50px; padding: 50px;
color: #aaa; color: #aaa;
} }
</style> </style>
{% endblock customCSS %} {% endblock customCSS %}
{% block content %} {% block content %}
@ -27,7 +27,7 @@
<h5 class="mb-0">مرحبًا</h5> <h5 class="mb-0">مرحبًا</h5>
<div> <div>
<button class="btn btn-phoenix-secondary dropdown-toggle" <button class="btn btn-phoenix-secondary dropdown-toggle"
data-bs-toggle="dropdown">الصفحة الرئيسية لـ </button> data-bs-toggle="dropdown">الصفحة الرئيسية لـ</button>
</div> </div>
</div> </div>
<!-- Main Row --> <!-- Main Row -->

View File

@ -13,14 +13,11 @@
aria-label="Close"></button> aria-label="Close"></button>
</div> </div>
<form id="actionTrackingForm" <form id="actionTrackingForm"
action="{% url 'update_lead_actions' request.dealer.slug %}" action="{% url 'update_lead_actions' request.dealer.slug %}"
hx-select-oob="#currentStage:outerHTML,#leadStatus:outerHTML,#toast-container:outerHTML" hx-select-oob="#currentStage:outerHTML,#leadStatus:outerHTML,#toast-container:outerHTML"
hx-swap="none" hx-swap="none"
hx-on::after-request="{ hx-on::after-request="{ resetSubmitButton(document.querySelector('#actionTrackingForm button[type=submit]')); $('#actionTrackingModal').modal('hide'); }"
resetSubmitButton(document.querySelector('#actionTrackingForm button[type=submit]')); method="post">
$('#actionTrackingModal').modal('hide');
}"
method="post">
<div class="modal-body"> <div class="modal-body">
{% csrf_token %} {% csrf_token %}
<input type="hidden" id="leadId" name="lead_id"> <input type="hidden" id="leadId" name="lead_id">

View File

@ -3,7 +3,9 @@
<div class="content"> <div class="content">
<h2 class="mb-5">{{ _("Notifications") }}</h2> <h2 class="mb-5">{{ _("Notifications") }}</h2>
<div class="d-flex justify-content-end mb-3"> <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> </div>
{% if notifications %} {% if notifications %}
<div class="mx-n4 mx-lg-n6 mb-5 border-bottom"> <div class="mx-n4 mx-lg-n6 mb-5 border-bottom">

View File

@ -40,7 +40,8 @@
href="{% url 'update_opportunity' request.dealer.slug opportunity.slug %}">Update Opportunity</a> href="{% url 'update_opportunity' request.dealer.slug opportunity.slug %}">Update Opportunity</a>
</li> </li>
<li> <li>
<a class="dropdown-item" type="button" <a class="dropdown-item"
type="button"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#updateStageModal">Update Stage</a> data-bs-target="#updateStageModal">Update Stage</a>
</li> </li>
@ -683,16 +684,16 @@
<form action="{% url 'add_note_to_opportunity' request.dealer.slug opportunity.slug %}" method="post"> <form action="{% url 'add_note_to_opportunity' request.dealer.slug opportunity.slug %}" method="post">
{% csrf_token %} {% csrf_token %}
<textarea class="form-control mb-3" id="notes" rows="4" name="notes" required> </textarea> <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> <button type="submit" class="btn btn-phoenix-primary mb-3">Add Note</button>
</form> </form>
{% endif %} {% endif %}
<div class="row gy-4 note-list"> <div class="row gy-4 note-list">
<div class="col-12 col-xl-auto flex-1"> <div class="col-12 col-xl-auto flex-1">
{% for note in opportunity.get_notes %} {% for note in opportunity.get_notes %}
<div class="border-2 border-dashed mb-4 pb-4 border-bottom border-translucent"> <div class="border-2 border-dashed mb-4 pb-4 border-bottom border-translucent">
<p class="mb-1 text-body-highlight">{{ note.note }}</p> <p class="mb-1 text-body-highlight">{{ note.note }}</p>
<div class="d-flex"> <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> <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> <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>
</div> </div>
@ -828,15 +829,14 @@
</button> </button>
</a> {% endcomment %} </a> {% endcomment %}
{% if opportunity.lead %} {% if opportunity.lead %}
<button class="btn btn-phoenix-primary btn-sm" <button class="btn btn-phoenix-primary btn-sm"
type="button" type="button"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#emailModal" data-bs-target="#emailModal"
hx-get="{% url 'send_lead_email' request.dealer.slug opportunity.lead.slug %}" hx-get="{% url 'send_lead_email' request.dealer.slug opportunity.lead.slug %}"
hx-target="#emailModalBody" hx-target="#emailModalBody"
hx-select=".email-form" hx-select=".email-form"
hx-swap="innerHTML" hx-swap="innerHTML">
>
<span class="fas fa-plus me-1"></span>{{ _("Send Email") }} <span class="fas fa-plus me-1"></span>{{ _("Send Email") }}
</button> </button>
{% endif %} {% endif %}
@ -858,7 +858,6 @@
</li> </li>
</ul> </ul>
</div> </div>
<div class="tab-content" id="profileTabContent"> <div class="tab-content" id="profileTabContent">
<div class="tab-pane fade show active" <div class="tab-pane fade show active"
id="tab-mail" id="tab-mail"
@ -871,7 +870,6 @@
<table class="table fs-9 mb-0"> <table class="table fs-9 mb-0">
<thead> <thead>
<tr> <tr>
<th class="sort white-space-nowrap align-middle pe-3 ps-0 text-uppercase" <th class="sort white-space-nowrap align-middle pe-3 ps-0 text-uppercase"
scope="col" scope="col"
data-sort="subject" data-sort="subject"
@ -896,14 +894,12 @@
<tbody class="list" id="all-email-table-body"> <tbody class="list" id="all-email-table-body">
{% for email in opportunity.lead.get_emails %} {% for email in opportunity.lead.get_emails %}
<tr class="hover-actions-trigger btn-reveal-trigger position-static"> <tr class="hover-actions-trigger btn-reveal-trigger position-static">
<td class="subject order align-middle white-space-nowrap py-2 ps-0"> <td class="subject order align-middle white-space-nowrap py-2 ps-0">
<a class="fw-semibold text-primary" href="#!">{{ email.subject }}</a> <a class="fw-semibold text-primary" href="#!">{{ email.subject }}</a>
<div class="fs-10 d-block">{{ email.to_email }}</div> <div class="fs-10 d-block">{{ email.to_email }}</div>
</td> </td>
<td class="sent align-middle white-space-nowrap text-start fw-bold text-body-tertiary py-2">{{ email.from_email }}</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="date align-middle white-space-nowrap text-body py-2">{{ email.created }}</td>
<td class="status align-middle fw-semibold text-end py-2"> <td class="status align-middle fw-semibold text-end py-2">
<span class="badge badge-phoenix fs-10 badge-phoenix-success">sent</span> <span class="badge badge-phoenix fs-10 badge-phoenix-success">sent</span>
</td> </td>
@ -1023,10 +1019,10 @@
<div class="tab-pane fade" <div class="tab-pane fade"
id="tab-activity" id="tab-activity"
hx-get="{% url 'opportunity_detail' request.dealer.slug opportunity.slug %}" hx-get="{% url 'opportunity_detail' request.dealer.slug opportunity.slug %}"
hx-trigger="htmx:afterRequest from:" hx-trigger="htmx:afterRequest from:"
hx-select="#tab-activity" hx-select="#tab-activity"
hx-target="this" hx-target="this"
hx-swap="outerHTML" hx-swap="outerHTML"
role="tabpanel" role="tabpanel"
aria-labelledby="activity-tab"> aria-labelledby="activity-tab">
<h2 class="mb-4">Activity</h2> <h2 class="mb-4">Activity</h2>
@ -1097,37 +1093,36 @@
</div> </div>
</div> </div>
</div> </div>
<div class="modal fade" id="updateStageModal" tabindex="-1" aria-hidden="true"> <div class="modal fade"
<div class="modal-dialog"> id="updateStageModal"
<div class="modal-content"> tabindex="-1"
<form method="post" aria-hidden="true">
action="{% url 'update_opportunity_stage' request.dealer.slug opportunity.slug %}" <div class="modal-dialog">
hx-post="{% url 'update_opportunity_stage' request.dealer.slug opportunity.slug %}" <div class="modal-content">
hx-swap="none" <form method="post"
hx-on::after-request="location.reload()"> action="{% url 'update_opportunity_stage' request.dealer.slug opportunity.slug %}"
{% csrf_token %} hx-post="{% url 'update_opportunity_stage' request.dealer.slug opportunity.slug %}"
<div class="modal-header"> hx-swap="none"
<h5 class="modal-title" id="updateStageModalLabel">{{ _("Update Opportunity Stage") }}</h5> hx-on::after-request="location.reload()">
<button class="btn btn-close p-1" {% csrf_token %}
type="button" <div class="modal-header">
data-bs-dismiss="modal" <h5 class="modal-title" id="updateStageModalLabel">{{ _("Update Opportunity Stage") }}</h5>
aria-label="Close"></button> <button class="btn btn-close p-1"
</div> type="button"
<div class="modal-body"> data-bs-dismiss="modal"
{{ stage_form|crispy }} aria-label="Close"></button>
</div> </div>
<div class="modal-footer"> <div class="modal-body">{{ stage_form|crispy }}</div>
<button class="btn btn-phoenix-primary" type="submit">{{ _("Save") }}</button> <div class="modal-footer">
<button class="btn btn-phoenix-secondary" <button class="btn btn-phoenix-primary" type="submit">{{ _("Save") }}</button>
type="button" <button class="btn btn-phoenix-secondary"
data-bs-dismiss="modal">{{ _("Cancel") }}</button> type="button"
</div> data-bs-dismiss="modal">{{ _("Cancel") }}</button>
</form> </div>
</div> </form>
</div> </div>
</div> </div>
</div>
{% include 'modal/delete_modal.html' %} {% include 'modal/delete_modal.html' %}
<!-- email Modal --> <!-- email Modal -->
{% include "components/email_modal.html" %} {% include "components/email_modal.html" %}
@ -1138,28 +1133,26 @@
<!-- schedule Modal --> <!-- schedule Modal -->
{% include "components/schedule_modal.html" with content_type="opportunity" slug=opportunity.slug %} {% include "components/schedule_modal.html" with content_type="opportunity" slug=opportunity.slug %}
{% endblock %} {% endblock %}
{% block customJS %} {% block customJS %}
<script> <script>
document.body.addEventListener('htmx:afterSwap', function(evt) {
document.body.addEventListener('htmx:afterSwap', function(evt) { if (evt.detail.target.id === 'main_content') {
if (evt.detail.target.id === 'main_content') { var modal = bootstrap.Modal.getInstance(document.getElementById('exampleModal'));
var modal = bootstrap.Modal.getInstance(document.getElementById('exampleModal')); if (modal) {
if (modal) { modal.hide();
modal.hide(); }
} }
} });
});
// Cleanup modal backdrop if needed // Cleanup modal backdrop if needed
document.body.addEventListener('htmx:beforeSwap', function(evt) { document.body.addEventListener('htmx:beforeSwap', function(evt) {
if (evt.detail.target.id === 'main_content') { if (evt.detail.target.id === 'main_content') {
var backdrops = document.querySelectorAll('.modal-backdrop'); var backdrops = document.querySelectorAll('.modal-backdrop');
backdrops.forEach(function(backdrop) { backdrops.forEach(function(backdrop) {
backdrop.remove(); backdrop.remove();
}); });
} }
}); });
</script> </script>
{% endblock customJS %} {% endblock customJS %}

View File

@ -9,201 +9,162 @@
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="container-fluid mt-4 mb-3"> <div class="container-fluid mt-4 mb-3">
<div class="row g-3 mb-4 align-items-center"> <div class="row g-3 mb-4 align-items-center">
<div class="col"> <div class="col">
<h2 class="mb-0"> <h2 class="mb-0">
{% if form.instance.pk %} {% if form.instance.pk %}
{% trans "Edit Opportunity" %} {% trans "Edit Opportunity" %}
{% else %} {% else %}
{% trans "Create New Opportunity" %} {% trans "Create New Opportunity" %}
{% endif %} {% endif %}
</h2> </h2>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<a href="{% url 'opportunity_list' request.dealer.slug %}" class="btn btn-phoenix-secondary"> <a href="{% url 'opportunity_list' request.dealer.slug %}"
<span class="fas fa-arrow-left me-2"></span>{% trans "Back to list" %} class="btn btn-phoenix-secondary">
</a> <span class="fas fa-arrow-left me-2"></span>{% trans "Back to list" %}
</div> </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> </div>
</div> <div class="row g-3">
<div class="col-lg-4"> <div class="col-lg-8">
<div class="card"> <div class="card">
<div class="card-body p-4"> <div class="card-body p-4 p-sm-5">
<h4 class="mb-3">{% trans "Opportunity Guidelines" %}</h4> <form method="post" enctype="multipart/form-data">
<ul class="nav flex-column gap-2 nav-guide"> {% csrf_token %}
<li class="nav-item"> {% if form.non_field_errors %}<div class="alert alert-danger">{{ form.non_field_errors }}</div>{% endif %}
<div class="d-flex align-items-center"> <!-- Lead Field -->
<span class="fas fa-circle text-primary fs-11 me-2"></span> <div class="mb-4">
<span class="text-body-highlight">{% trans "Probability indicates conversion chance" %}</span> <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> </div>
</li> </div>
<li class="nav-item"> <div class="col-lg-4">
<div class="d-flex align-items-center"> <div class="card">
<span class="fas fa-circle text-warning fs-11 me-2"></span> <div class="card-body p-4">
<span class="text-body-highlight">{% trans "Update stage as deal progresses" %}</span> <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> </div>
</li> </div>
<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>
</div> <script>
<script>
function updateProbabilityValue(value) { function updateProbabilityValue(value) {
const amount = document.getElementById('id_amount'); const amount = document.getElementById('id_amount');
const expectedRevenue = document.getElementById('id_expected_revenue'); const expectedRevenue = document.getElementById('id_expected_revenue');

View File

@ -75,7 +75,8 @@
height: 12px; height: 12px;
width: 12px"></span>{{ opportunity.get_stage_display }} width: 12px"></span>{{ opportunity.get_stage_display }}
</p> </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>
<div class="deals-company-agent d-flex flex-between-center"> <div class="deals-company-agent d-flex flex-between-center">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">

View File

@ -5,136 +5,137 @@
{{ _("Opportunities") }} {{ _("Opportunities") }}
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
{% if opportunities or request.GET.q %}
{% if opportunities or request.GET.q%} <div class="row g-3 mt-4">
<div class="row g-3 mt-4"> <div class="row g-3 justify-content-between mb-4">
<div class="row g-3 justify-content-between mb-4"> <div class="col-auto">
<div class="col-auto"> <div class="d-md-flex justify-content-between">
<div class="d-md-flex justify-content-between"> <h2 class="mb-3">
<h2 class="mb-3"> {{ _("Opportunities") }}
{{ _("Opportunities") }} <li class="fas fas fa-rocket text-primary ms-2"></li>
<li class="fas fas fa-rocket text-primary ms-2"></li> </h2>
</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> </div>
<div class="col-auto"> <div class="col-12">
{% if perms.inventory.add_opportunity %} <div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4">
<div class="d-flex justify-content-between"> <!-- Filter Controls -->
<a class="btn btn-phoenix-primary btn-sm" <div class="d-flex flex-column flex-lg-row align-items-start align-items-lg-center gap-3 w-100"
href="{% url 'opportunity_create' request.dealer.slug %}"> id="filter-container">
<span class="fas fa-plus me-2"></span>{{ _("Add Opportunity") }} <!-- Search Input - Wider and properly aligned -->
</a> <div class="search-box position-relative flex-grow-1 me-2"
</div> style="min-width: 200px">
{% endif %} <form class="position-relative show"
</div> id="search-form"
</div> hx-get=""
<div class="col-12"> hx-boost="false"
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4"> hx-trigger="keyup changed delay:500ms, search">
<!-- 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" <input name="q"
id="search-input" id="search-input"
class="form-control form-control-sm search-input search" class="form-control form-control-sm search-input search"
type="search" type="search"
aria-label="Search" aria-label="Search"
placeholder="{{ _("Search") }}..." placeholder="{{ _("Search") }}..."
value="{{ request.GET.q}}" /> value="{{ request.GET.q }}" />
<span class="fa fa-magnifying-glass search-box-icon"></span> <span class="fa fa-magnifying-glass search-box-icon"></span>
{% if request.GET.q %} {% if request.GET.q %}
<button type="button" <button type="button"
class="btn-close position-absolute end-0 top-50 translate-middle cursor-pointer shadow-none" class="btn-close position-absolute end-0 top-50 translate-middle cursor-pointer shadow-none"
id="clear-search" id="clear-search"
aria-label="Clear Search"></button> aria-label="Clear Search"></button>
{% endif %} {% endif %}
</form> </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> </div>
<!-- Sort Filter --> <!-- Filter Dropdowns - Aligned in a row -->
<div class="flex-grow-1"> <div class="d-flex flex-column flex-sm-row gap-3 w-100"
<select class="form-select" style="max-width: 400px">
name="sort" <!-- Stage Filter -->
hx-get="{% url 'opportunity_list' request.dealer.slug %}" <!-- Stage Filter -->
hx-trigger="change" <div class="flex-grow-1">
hx-target="#opportunities-grid" <select class="form-select"
hx-select="#opportunities-grid" name="stage"
hx-swap="outerHTML" hx-get="{% url 'opportunity_list' request.dealer.slug %}"
hx-include="#filter-container input, #filter-container select"> hx-trigger="change"
<option value="newest" hx-target="#opportunities-grid"
{% if request.GET.sort == 'newest' %}selected{% endif %}>{% trans "Newest First" %}</option> hx-select="#opportunities-grid"
<option value="highest" hx-swap="outerHTML"
{% if request.GET.sort == 'highest' %}selected{% endif %}>{% trans "Highest Value" %}</option> hx-include="#filter-container input, #filter-container select">
<option value="closing" <option value="">{% trans "All Stages" %}</option>
{% if request.GET.sort == 'closing' %}selected{% endif %}>{% trans "Earliest Close Date" %}</option> {% for value, label in stage_choices %}
</select> <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> </div>
</div> </div>
<div id="opportunities-grid" class="row g-4 px-2 px-lg-4 mt-1 mb-4">
</div> {% include 'crm/opportunities/partials/opportunity_grid.html' %}
<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> </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 %} {% 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 %} {% block customJS %}
<script> <script>
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
const searchInput = document.getElementById("search-input"); const searchInput = document.getElementById("search-input");
const clearButton = document.getElementById("clear-search"); const clearButton = document.getElementById("clear-search");
const searchForm = document.getElementById("search-form"); const searchForm = document.getElementById("search-form");
if (clearButton) { if (clearButton) {
clearButton.addEventListener("click", function() { clearButton.addEventListener("click", function() {
searchInput.value = ""; searchInput.value = "";
// This clears the search and triggers the htmx search // This clears the search and triggers the htmx search
// by submitting the form with an empty query. // by submitting the form with an empty query.
searchForm.submit(); searchForm.submit();
});
}
}); });
} </script>
});
</script>
{% endblock %} {% endblock %}
{% endblock %} {% endblock %}

View File

@ -2,24 +2,19 @@
{% load custom_filters %} {% load custom_filters %}
{% block customCSS %} {% block customCSS %}
<style> <style>
.bg-success-soft { .bg-success-soft {
background-color: rgba(25, 135, 84, 0.1) !important; background-color: rgba(25, 135, 84, 0.1) !important;
opacity: .8; opacity: .8;
} }
.bg-danger-soft { .bg-danger-soft {
background-color: rgba(220, 53, 69, 0.1) !important; background-color: rgba(220, 53, 69, 0.1) !important;
opacity: .8; opacity: .8;
} }
</style> </style>
{% endblock customCSS %} {% endblock customCSS %}
{% for opportunity in opportunities %} {% for opportunity in opportunities %}
<div class="col-12 col-md-6 col-lg-4 col-xl-3"> <div class="col-12 col-md-6 col-lg-4 col-xl-3">
<div class="card h-100 <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 %}">
{% 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="card-body">
<div class="avatar avatar-xl me-3 mb-3"> <div class="avatar avatar-xl me-3 mb-3">
{% if opportunity.car.id_car_make.logo %} {% if opportunity.car.id_car_make.logo %}
@ -53,12 +48,7 @@
<span class="badge badge-phoenix fs-10 badge-phoenix-secondary"> <span class="badge badge-phoenix fs-10 badge-phoenix-secondary">
{% endif %} {% endif %}
{{ opportunity.stage }}</span> {{ opportunity.stage }}</span>
<span class="badge badge-phoenix fs-10 <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 %}">
{% if opportunity.get_stage_display == 'Won' %}
badge-phoenix-success
{% elif opportunity.get_stage_display == 'Lost' %}
badge-phoenix-danger
{% endif %}">
{{ opportunity.get_status_display }} {{ opportunity.get_status_display }}
</span> </span>
</div> </div>

View File

@ -3,7 +3,6 @@
{% block title %} {% block title %}
{% trans "Car Bulk Upload"|capfirst %} {% trans "Car Bulk Upload"|capfirst %}
{% endblock %} {% endblock %}
{% block customCSS %} {% block customCSS %}
<style> <style>
.color-card { .color-card {
@ -74,13 +73,13 @@
</style> </style>
{% endblock customCSS %} {% endblock customCSS %}
{% block content %} {% block content %}
<div class="container mt-4"> <div class="container mt-4">
<h2> <h2>
Upload Cars CSV <i class="fa-solid fa-file-csv text-primary"></i> Upload Cars CSV <i class="fa-solid fa-file-csv text-primary"></i>
</h2> </h2>
<div class="d-flex justify-content-end"> <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 <i class="fa-solid fa-file-csv me-2"></i>Download Sample CSV
</a> </a>
</div> </div>
@ -182,7 +181,8 @@
<div class="form-text">{{ _("CSV should include columns: vin") }}</div> <div class="form-text">{{ _("CSV should include columns: vin") }}</div>
</div> </div>
<button type="submit" class="btn btn-phoenix-primary mb-2">Upload</button> <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> </form>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,7 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n static %} {% load i18n static %}
{% load crispy_forms_filters %} {% load crispy_forms_filters %}
{% block title %} {% block title %}
{% if object %} {% if object %}
{% trans 'Update Customer' %} {% trans 'Update Customer' %}
@ -9,52 +8,53 @@
{% trans 'Add New Customer' %} {% trans 'Add New Customer' %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<main class="d-flex align-items-center justify-content-center min-vh-100 py-5 "> <main class="d-flex align-items-center justify-content-center min-vh-100 py-5 ">
<div class="col-md-8"> <div class="col-md-8">
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp"> <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"> <div class="card-header bg-gradient py-4 border-0 rounded-top-4">
<h3 class="mb-0 fs-4 fw-bold text-center"> <h3 class="mb-0 fs-4 fw-bold text-center">
{% if object %} {% if object %}
{% trans "Update Customer" %} {% trans "Update Customer" %}
<i class="fa-solid fa-user-edit ms-2"></i> <i class="fa-solid fa-user-edit ms-2"></i>
{% else %} {% else %}
{% trans "Add New Customer" %} {% trans "Add New Customer" %}
<i class="fa-solid fa-user-plus ms-2"></i> <i class="fa-solid fa-user-plus ms-2"></i>
{% endif %} {% endif %}
</h3> </h3>
</div> </div>
<div class="card-body p-4 p-md-5"> <div class="card-body p-4 p-md-5">
<form method="post" class="form" enctype="multipart/form-data" novalidate> <form method="post" class="form" enctype="multipart/form-data" novalidate>
{% csrf_token %} {% csrf_token %}
{{ form|crispy }} {{ form|crispy }}
{% if form.errors %}
{% if form.errors %} <div class="alert alert-danger mt-4" role="alert">
<div class="alert alert-danger mt-4" role="alert"> <h4 class="alert-heading small">{% trans "Please correct the following errors:" %}</h4>
<h4 class="alert-heading small">{% trans "Please correct the following errors:" %}</h4> <ul class="mb-0">
<ul class="mb-0"> {% for field, errors in form.errors.items %}
{% for field, errors in form.errors.items %} <li>
<li><strong>{{ field|capfirst }}:</strong> {% for error in errors %}{{ error }}{% endfor %}</li> <strong>{{ field|capfirst }}:</strong>
{% endfor %} {% for error in errors %}{{ error }}{% endfor %}
</ul> </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> </div>
{% endif %} </form>
</div>
<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>
</div> </div>
</div> </div>
</div> </main>
</main> {% endblock %}
{% endblock %}

View File

@ -6,176 +6,175 @@
{% endblock title %} {% endblock title %}
{% block vendors %}<a class="nav-link active">{{ _("Customers") |capfirst }}</a>{% endblock %} {% block vendors %}<a class="nav-link active">{{ _("Customers") |capfirst }}</a>{% endblock %}
{% block content %} {% block content %}
{% if customers or request.GET.q %} {% if customers or request.GET.q %}
<div class="row g-3 mt-4"> <div class="row g-3 mt-4">
<h2 class="mb-2"> <h2 class="mb-2">
{{ _("Customers") |capfirst }} {{ _("Customers") |capfirst }}
<li class="fas fa-people-group text-primary ms-2"></li> <li class="fas fa-people-group text-primary ms-2"></li>
</h2> </h2>
<div class="row g-3 justify-content-between mb-4"> <div class="row g-3 justify-content-between mb-4">
<div class="col-auto"> <div class="col-auto">
<div class="d-md-flex justify-content-between"> <div class="d-md-flex justify-content-between">
{% if perms.inventory.add_customer %} {% if perms.inventory.add_customer %}
<div> <div>
<a href="{% url 'customer_create' request.dealer.slug %}" <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> class="btn btn-sm btn-phoenix-primary me-4"><span class="fas fa-plus me-2"></span>{{ _("Add Customer") }}</a>
</div> </div>
{% endif %} {% endif %}
</div>
</div>
<div class="col-auto">
<div class="d-flex">{% include 'partials/search_box.html' %}</div>
</div> </div>
</div> </div>
<div class="col-auto"> {% if page_obj.object_list or request.GET.q %}
<div class="d-flex">{% include 'partials/search_box.html' %}</div> <div class="table-responsive scrollbar transition">
</div> <table class="table align-items-center table-flush table-hover">
</div> <thead>
{% if page_obj.object_list or request.GET.q%} <tr class="bg-body-highlight">
<div class="table-responsive scrollbar transition"> <th></th>
<table class="table align-items-center table-flush table-hover"> <th class="sort white-space-nowrap align-middle text-uppercase ps-0"
<thead> scope="col"
<tr class="bg-body-highlight"> data-sort="name"
<th></th> style="width:25%">{{ _("Name") |capfirst }}</th>
<th class="sort white-space-nowrap align-middle text-uppercase ps-0" <th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent"
scope="col" scope="col"
data-sort="name" data-sort="email"
style="width:25%">{{ _("Name") |capfirst }}</th> style="width:15%">
<th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent" <div class="d-inline-flex flex-center">
scope="col" <div class="d-flex align-items-center px-1 py-1 bg-success-subtle rounded me-2">
data-sort="email" <span class="text-success-dark" data-feather="mail"></span>
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>
</div> </div>
</td> <span>{{ _("email") |capfirst }}</span>
{% endif %} </div>
<td class="email align-middle white-space-nowrap fw-semibold ps-4 border-end border-translucent"> </th>
<a class="text-body-highlight" href="">{{ customer.email }}</a> <th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent"
</td> scope="col"
<td class="phone align-middle white-space-nowrap fw-semibold ps-4 border-end border-translucent"> data-sort="phone"
<a class="text-body-highlight" href="tel:{{ customer.phone }}">{{ customer.phone_number }}</a> style="width:15%;
</td> min-width: 180px">
<td class="contact align-middle white-space-nowrap ps-4 border-end border-translucent fw-semibold text-body-highlight"> <div class="d-inline-flex flex-center">
{{ customer.national_id }} <div class="d-flex align-items-center px-1 py-1 bg-primary-subtle rounded me-2">
</td> <span class="text-primary-dark" data-feather="phone"></span>
<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"> </div>
{{ customer.address }} <span>{{ _("Phone Number") }}</span>
</td> </div>
<td class="date align-middle white-space-nowrap text-body-tertiary text-opacity-85 ps-4 text-body-tertiary"> </th>
{% if customer.active %} <th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent"
<span class="badge badge-phoenix badge-phoenix-success"><i class="fas fa-check"></i> {{ customer.active }}</span> scope="col"
{% else %} data-sort="contact"
<span class="badge badge-phoenix badge-phoenix-danger"><i class="fas fa-times"></i> {{ customer.active }}</span> style="width:15%">
{% endif %} <div class="d-inline-flex flex-center">
</td> <div class="d-flex align-items-center px-1 py-1 bg-info-subtle rounded me-2">
<td class="date align-middle white-space-nowrap text-body-tertiary text-opacity-85 ps-4 text-body-tertiary"> <span class="text-info-dark" data-feather="user"></span>
{{ customer.created|date }} </div>
</td> <span>{{ _("National ID") |capfirst }}</span>
<td class="align-middle white-space-nowrap text-end pe-0 ps-4"> </div>
{% if perms.inventory.change_customer %} </th>
<a href="{% url 'customer_update' request.dealer.slug customer.slug %}" <th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent"
class="btn btn-sm btn-phoenix-primary me-2" scope="col"
data-url="{% url 'customer_update' request.dealer.slug customer.slug %}"> data-sort="company"
<i class="fas fa-pen"></i> style="width:15%">
</a> <div class="d-inline-flex flex-center">
{% endif %} <div class="d-flex align-items-center px-1 py-1 bg-warning-subtle rounded me-2">
{% if perms.inventory.delete_customer %} <span class="text-warning-dark" data-feather="home"></span>
<button class="btn btn-phoenix-danger btn-sm delete-btn" </div>
data-url="{% url 'customer_delete' request.dealer.slug customer.slug %}" <span>{{ _("Address") |capfirst }}</span>
data-message="{{ _("Are you sure you want to delete this customer") }}" </div>
data-bs-toggle="modal" </th>
data-bs-target="#deleteModal"> <th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent"
<i class="fas fa-trash"></i> scope="col"
</button> data-sort="company"
{% endif %} style="width:15%">
</td> <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> </tr>
{% empty %} </thead>
<tr> <tbody class="list" id="lead-tables-body">
<td colspan="6" class="text-center">{% trans "No Customers found." %}</td> {% for customer in customers %}
</tr> <!-- Delete Modal -->
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
{% endfor %} <td></td>
</tbody> {% 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 %} {% endif %}
</table> {% include 'modal/delete_modal.html' %}
{% if page_obj.paginator.num_pages > 1 %} {% else %}
<div class="d-flex justify-content-end mt-3"> {% url "customer_create" request.dealer.slug as create_customer_url %}
<div class="d-flex">{% include 'partials/pagination.html' %}</div> {% include "empty-illustration-page.html" with value="customer" url=create_customer_url %}
</div>
{% endif %} {% 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 %} {% endblock %}

View File

@ -6,11 +6,12 @@
{% block content %} {% block content %}
{% include 'modal/delete_modal.html' %} {% include 'modal/delete_modal.html' %}
{% include 'components/note_modal.html' with content_type="customer" slug=customer.slug %} {% include 'components/note_modal.html' with content_type="customer" slug=customer.slug %}
<div class="mt-4"> <div class="mt-4">
<div class="row align-items-center justify-content-between g-3 mb-4"> <div class="row align-items-center justify-content-between g-3 mb-4">
<div class="col-auto"> <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>
<div class="col-auto d-flex gap-2"> <div class="col-auto d-flex gap-2">
{% if perms.inventory.change_customer %} {% if perms.inventory.change_customer %}
@ -30,7 +31,6 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="row g-4 mb-4"> <div class="row g-4 mb-4">
<div class="col-12 col-lg-4"> <div class="col-12 col-lg-4">
<div class="card h-100 shadow-sm"> <div class="card h-100 shadow-sm">
@ -39,7 +39,9 @@
<div class="col-12 col-sm-auto mb-sm-2"> <div class="col-12 col-sm-auto mb-sm-2">
<div class="avatar avatar-5xl"> <div class="avatar avatar-5xl">
{% if customer.image %} {% 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 %} {% else %}
<div class="avatar-text rounded-circle bg-secondary text-white border border-2 border-primary"> <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> <span class="fs-4">{{ customer.full_name|first|default:"?" }}</span>
@ -65,7 +67,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-12 col-lg-8"> <div class="col-12 col-lg-8">
<div class="card h-100 shadow-sm"> <div class="card h-100 shadow-sm">
<div class="card-body p-4"> <div class="card-body p-4">
@ -79,18 +80,19 @@
</li> </li>
<li class="mb-2"> <li class="mb-2">
<strong class="text-body-secondary d-block">{% trans 'Email' %}:</strong> <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>
<li class="mb-0"> <li class="mb-0">
<strong class="text-body-secondary d-block">{% trans 'Phone Number' %}:</strong> <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> </li>
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="row g-4 mb-4"> <div class="row g-4 mb-4">
<div class="col-12"> <div class="col-12">
<div class="card shadow-sm"> <div class="card shadow-sm">
@ -116,16 +118,16 @@
</thead> </thead>
<tbody> <tbody>
{% for note in notes %} {% for note in notes %}
<tr class="align-middle"> <tr class="align-middle">
<td class="text-body-secondary">{{ note.note|default_if_none:""|linebreaksbr }}</td> <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> <td class="text-body-secondary text-nowrap">{{ note.created|date:"d M Y" }}</td>
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="4" class="text-center text-body-secondary"> <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.' %} <i class="fas fa-info-circle me-2"></i>{% trans 'No notes found for this customer.' %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
@ -134,7 +136,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="row g-4 mb-3"> <div class="row g-4 mb-3">
<div class="col-12"> <div class="col-12">
<div class="card shadow-sm"> <div class="card shadow-sm">
@ -142,48 +143,82 @@
<h5 class="card-title mb-3">{% trans 'Sales History' %}</h5> <h5 class="card-title mb-3">{% trans 'Sales History' %}</h5>
<ul class="nav nav-tabs" id="myTab" role="tablist"> <ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item me-6" role="presentation"> <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>
<li class="nav-item me-6" role="presentation"> <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>
<li class="nav-item" role="presentation"> <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> </li>
</ul> </ul>
<div class="tab-content pt-3" id="myTabContent"> <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 %} {% for lead in leads %}
<div class="d-flex align-items-center mb-2"> <div class="d-flex align-items-center mb-2">
<i class="fas fa-handshake me-2 text-primary"></i> <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> </div>
{% empty %} {% empty %}
<p class="text-body-secondary">{% trans 'No leads found for this customer.' %}</p> <p class="text-body-secondary">{% trans 'No leads found for this customer.' %}</p>
{% endfor %} {% endfor %}
</div> </div>
<div class="tab-pane fade"
<div class="tab-pane fade" id="opportunities-tab-pane" role="tabpanel" aria-labelledby="opportunities-tab" tabindex="0"> id="opportunities-tab-pane"
role="tabpanel"
aria-labelledby="opportunities-tab"
tabindex="0">
{% for lead in leads %} {% for lead in leads %}
{% if lead.opportunity %} {% if lead.opportunity %}
<div class="d-flex align-items-center mb-2"> <div class="d-flex align-items-center mb-2">
<i class="fas fa-chart-line me-2 text-success"></i> <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> </div>
{% endif %} {% endif %}
{% empty %} {% empty %}
<p class="text-body-secondary">{% trans 'No opportunities found for this customer.' %}</p> <p class="text-body-secondary">{% trans 'No opportunities found for this customer.' %}</p>
{% endfor %} {% endfor %}
</div> </div>
<div class="tab-pane fade"
<div class="tab-pane fade" id="estimates-tab-pane" role="tabpanel" aria-labelledby="estimates-tab" tabindex="0"> id="estimates-tab-pane"
role="tabpanel"
aria-labelledby="estimates-tab"
tabindex="0">
{% for estimate in estimates %} {% for estimate in estimates %}
<div class="card mb-3 shadow-sm"> <div class="card mb-3 shadow-sm">
<div class="card-body p-3"> <div class="card-body p-3">
<div class="d-flex align-items-center justify-content-between mb-3"> <div class="d-flex align-items-center justify-content-between mb-3">
<h6 class="mb-0"> <h6 class="mb-0">
<i class="fas fa-file-invoice me-2 text-info"></i> <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> </h6>
<span class="badge bg-success">{{ estimate.created|date:"d M Y" }}</span> <span class="badge bg-success">{{ estimate.created|date:"d M Y" }}</span>
</div> </div>
@ -197,16 +232,22 @@
{% for invoice in estimate.invoicemodel_set.all %} {% for invoice in estimate.invoicemodel_set.all %}
<li class="mb-2"> <li class="mb-2">
<i class="fas fa-receipt me-2 {% if invoice.is_paid %}text-success{% else %}text-warning{% endif %}"></i> <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> <a href="{% url 'invoice_detail' request.dealer.slug request.entity.slug invoice.pk %}"
<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> 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> </li>
{% endfor %} {% endfor %}
{% for item in estimate.itemtransactionmodel_set.all %} {% for item in estimate.itemtransactionmodel_set.all %}
<li> <li>
<i class="fas fa-car me-2 text-primary"></i> <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}}&nbsp;&vert;&nbsp;{{item.item_model.car.id_car_make.name}}&nbsp;&vert;&nbsp;{{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 }}&nbsp;&vert;&nbsp;{{ item.item_model.car.id_car_make.name }}&nbsp;&vert;&nbsp;{{ item.item_model.car.id_car_model.name }}</a>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
@ -221,37 +262,32 @@
</div> </div>
</div> </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> <script>
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
const noteModal = document.getElementById("noteModal"); const noteModal = document.getElementById("noteModal");
const modalTitle = document.getElementById("noteModalLabel"); const modalTitle = document.getElementById("noteModalLabel");
const modalBody = noteModal.querySelector(".modal-body"); const modalBody = noteModal.querySelector(".modal-body");
noteModal.addEventListener("", function (event) { noteModal.addEventListener("", function (event) {
const button = event.relatedTarget; const button = event.relatedTarget;
const url = button.getAttribute("data-url"); const url = button.getAttribute("data-url");
const title = button.getAttribute("data-note-title"); const title = button.getAttribute("data-note-title");
fetch(url) fetch(url)
.then((response) => response.text()) .then((response) => response.text())
.then((html) => { .then((html) => {
modalBody.innerHTML = html; modalBody.innerHTML = html;
modalTitle.innerHTML = title; modalTitle.innerHTML = title;
}) })
.catch((error) => { .catch((error) => {
modalBody.innerHTML = '<p class="text-danger">{% trans 'Error loading form. Please try again later' %}.</p>'; modalBody.innerHTML = '<p class="text-danger">{% trans 'Error loading form. Please try again later' %}.</p>';
console.error("Error loading form:", error); console.error("Error loading form:", error);
}); });
}); });
}); });
</script> </script>
{% endblock %}
{% endblock %}

View File

@ -1,124 +1,126 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n %} {% load i18n %}
{% load tenhal_tag %} {% load tenhal_tag %}
{% block content %} {% block content %}
<div class="main-content flex-grow-1 container-fluid mt-4 mb-3"> <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"> <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"> <h2 class="h3 fw-bold mb-3 mb-md-0">
{% trans "Aging Inventory" %} {% trans "Aging Inventory" %}
<i class="fas fa-box-open text-danger ms-2"></i> <i class="fas fa-box-open text-danger ms-2"></i>
</h2> </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> <h4 class="text-muted mb-3 ">
<p class="text-muted mb-0">{% trans "Cars in inventory for more than 60 days." %}</p> {% trans "Aging Inventory Total" %} :: <span class=" text-danger">{{ total_aging_inventory_value|default:0.00 }}<span class="icon-saudi_riyal"></span></span>
</h4>
</div> <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"> <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"> <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> <label for="make-filter"
<select class="form-select" name="make" id="make-filter"> class="form-label mb-0 small text-uppercase fw-bold">{% trans "Make:" %}</label>
<option value="">{% trans "All" %}</option> <select class="form-select" name="make" id="make-filter">
{% for make in all_makes %} <option value="">{% trans "All" %}</option>
<option value="{{ make }}" {% if make == selected_make %}selected{% endif %}>{{ make }}</option> {% for make in all_makes %}
{% endfor %} <option value="{{ make }}" {% if make == selected_make %}selected{% endif %}>{{ make }}</option>
</select> {% endfor %}
</div> </select>
</div>
<div class="col-sm-6 col-md-2 me-2"> <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> <label for="model-filter"
<select class="form-select" name="model" id="model-filter"> class="form-label mb-0 small text-uppercase fw-bold">{% trans "Model:" %}</label>
<option value="">{% trans "All" %}</option> <select class="form-select" name="model" id="model-filter">
{% for model in all_models %} <option value="">{% trans "All" %}</option>
<option value="{{ model }}" {% if model == selected_model %}selected{% endif %}>{{ model }}</option> {% for model in all_models %}
{% endfor %} <option value="{{ model }}"
</select> {% if model == selected_model %}selected{% endif %}>{{ model }}</option>
</div> {% endfor %}
</select>
<div class="col-sm-6 col-md-2 me-2"> </div>
<label for="series-filter" class="form-label mb-0 small text-uppercase fw-bold">{% trans "Series:" %}</label> <div class="col-sm-6 col-md-2 me-2">
<select class="form-select" name="series" id="series-filter"> <label for="series-filter"
<option value="">{% trans "All" %}</option> class="form-label mb-0 small text-uppercase fw-bold">{% trans "Series:" %}</label>
{% for series in all_series %} <select class="form-select" name="series" id="series-filter">
<option value="{{ series }}" {% if series == selected_series %}selected{% endif %}>{{ series }}</option> <option value="">{% trans "All" %}</option>
{% endfor %} {% for series in all_series %}
</select> <option value="{{ series }}"
</div> {% if series == selected_series %}selected{% endif %}>{{ series }}</option>
{% endfor %}
<div class="col-sm-6 col-md-2 me-2"> </select>
<label for="year-filter" class="form-label mb-0 small text-uppercase fw-bold">{% trans "Year:" %}</label> </div>
<select class="form-select" name="year" id="year-filter"> <div class="col-sm-6 col-md-2 me-2">
<option value="">{% trans "All" %}</option> <label for="year-filter"
{% for year in all_years %} class="form-label mb-0 small text-uppercase fw-bold">{% trans "Year:" %}</label>
<option value="{{ year }}" {% if year|stringformat:"s" == selected_year %}selected{% endif %}>{{ year }}</option> <select class="form-select" name="year" id="year-filter">
{% endfor %} <option value="">{% trans "All" %}</option>
</select> {% for year in all_years %}
</div> <option value="{{ year }}"
{% if year|stringformat:"s" == selected_year %}selected{% endif %}>{{ year }}</option>
<div class="col-sm-6 col-md-2 me-2"> {% endfor %}
<label for="stock-type-filter" class="form-label mb-0 small text-uppercase fw-bold">{% trans "Stock Type:" %}</label> </select>
<select class="form-select" name="stock_type" id="stock-type-filter"> </div>
<option value="">{% trans "All" %}</option> <div class="col-sm-6 col-md-2 me-2">
{% for stock_type in all_stock_types %} <label for="stock-type-filter"
<option value="{{ stock_type }}" {% if stock_type == selected_stock_type %}selected{% endif %}>{{ stock_type|title }}</option> class="form-label mb-0 small text-uppercase fw-bold">{% trans "Stock Type:" %}</label>
{% endfor %} <select class="form-select" name="stock_type" id="stock-type-filter">
</select> <option value="">{% trans "All" %}</option>
</div> {% for stock_type in all_stock_types %}
<option value="{{ stock_type }}"
<div class="col-auto"> {% if stock_type == selected_stock_type %}selected{% endif %}>
<button type="submit" class="btn btn-primary mt-4">{% trans "Filter" %}</button> {{ stock_type|title }}
</div> </option>
</form> {% endfor %}
</select>
{% if is_paginated %} </div>
<div class="d-flex justify-content-between mb-4"> <div class="col-auto">
<span class="text-muted">{% trans "Page" %} {{ page_obj.number }} {% trans "of" %} {{ page_obj.paginator.num_pages }}</span> <button type="submit" class="btn btn-primary mt-4">{% trans "Filter" %}</button>
<span class="text-muted">{% trans "Total Aging Cars:" %} {{ page_obj.paginator.count }}</span> </div>
</div> </form>
{% endif %} {% if is_paginated %}
<div class="d-flex justify-content-between mb-4">
{% if cars %} <span class="text-muted">{% trans "Page" %} {{ page_obj.number }} {% trans "of" %} {{ page_obj.paginator.num_pages }}</span>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4"> <span class="text-muted">{% trans "Total Aging Cars:" %} {{ page_obj.paginator.count }}</span>
{% for car in cars %} </div>
<div class="col"> {% endif %}
<div class="card h-100 shadow-sm border-0"> {% if cars %}
<div class="card-body d-flex flex-column"> <div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
<h5 class="card-title text-danger fw-bold"> {% for car in cars %}
<img src="{{car.logo}}" width="40" height="40" class="">&nbsp;&nbsp;{{ car.id_car_make.name }}&nbsp;&nbsp;{{ car.id_car_model.name }}&nbsp;&nbsp;{{ car.id_car_serie.name }}&nbsp;&nbsp;{{ car.year}} <div class="col">
</h5> <div class="card h-100 shadow-sm border-0">
<p class="card-text text-muted mb-2"> <div class="card-body d-flex flex-column">
<strong>{% trans "VIN:" %}</strong> {{ car.vin }} <h5 class="card-title text-danger fw-bold">
</p> <img src="{{ car.logo }}" width="40" height="40" class="">
<p class="card-text mb-2"> &nbsp;&nbsp;{{ car.id_car_make.name }}&nbsp;&nbsp;{{ car.id_car_model.name }}&nbsp;&nbsp;{{ car.id_car_serie.name }}&nbsp;&nbsp;{{ car.year }}
<strong>{% trans "Age:" %}</strong> </h5>
<span class="badge bg-danger">{{ car.age_in_days }} {% trans "days" %}</span> <p class="card-text text-muted mb-2">
</p> <strong>{% trans "VIN:" %}</strong> {{ car.vin }}
<p class="card-text mb-2"> </p>
<strong>{% trans "Acquisition Date:" %}</strong> {{ car.receiving_date|date:"F j, Y" }} <p class="card-text mb-2">
</p> <strong>{% trans "Age:" %}</strong>
<div class="mt-auto pt-3 border-top"> <span class="badge bg-danger">{{ car.age_in_days }} {% trans "days" %}</span>
<a href="{% url 'car_detail' request.dealer.slug car.slug %}" class="btn btn-outline-primary btn-sm w-100"> </p>
{% trans "View Details" %} <p class="card-text mb-2">
</a> <strong>{% trans "Acquisition Date:" %}</strong> {{ car.receiving_date|date:"F j, Y" }}
</div> </p>
</div> <div class="mt-auto pt-3 border-top">
</div> <a href="{% url 'car_detail' request.dealer.slug car.slug %}"
</div> class="btn btn-outline-primary btn-sm w-100">{% trans "View Details" %}</a>
{% endfor %} </div>
</div> </div>
{% else %} </div>
<div class="alert alert-success d-flex align-items-center" role="alert"> </div>
<i class="fas fa-check-circle me-2"></i> {% endfor %}
<div> </div>
{% trans "Excellent! There are no cars in the aging inventory at the moment." %} {% else %}
</div> <div class="alert alert-success d-flex align-items-center" role="alert">
</div> <i class="fas fa-check-circle me-2"></i>
{% endif %} <div>{% trans "Excellent! There are no cars in the aging inventory at the moment." %}</div>
<div class="d-flex justify-content-end mt-3"> </div>
<div class="d-flex"> {% endif %}
{% if is_paginated %} <div class="d-flex justify-content-end mt-3">
{% include 'partials/pagination.html' %} <div class="d-flex">
{% endif %} {% if is_paginated %}
</div> {% include 'partials/pagination.html' %}
</div> {% endif %}
</div> </div>
{% endblock content %} </div>
</div>
{% endblock content %}

View File

@ -1,109 +1,112 @@
{% load i18n %} {% load i18n %}
{% if request.is_dealer or request.is_manager or request.is_accountant %} {% if request.is_dealer or request.is_manager or request.is_accountant %}
<h3 class="fw-bold mb-3"> <h3 class="fw-bold mb-3">
{% blocktrans with start_date=start_date|date:"F j, Y" end_date=end_date|date:"F j, Y" %} {% 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 }}) Monthly Performance Trends ({{ start_date }} - {{ end_date }})
{% endblocktrans %} {% endblocktrans %}
</h3> </h3>
<div class="row g-4 mb-5">
<div class="row g-4 mb-5"> <div class="col-12">
<div class="col-12"> <div class="card h-100 shadow-sm border-0">
<div class="card h-100 shadow-sm border-0"> <div class="card-header bg-white border-bottom-0">
<div class="card-header bg-white border-bottom-0"> <h5 class="fw-bold mb-0 text-dark">{% trans "Monthly Revenue & Profit" %}</h5>
<h5 class="fw-bold mb-0 text-dark">{% trans "Monthly Revenue & Profit" %}</h5> </div>
</div> <div class="card-body" style="height: 400px;">
<div class="card-body" style="height: 400px;"> <canvas id="revenueProfitChart"></canvas>
<canvas id="revenueProfitChart"></canvas> </div>
</div> </div>
</div> </div>
</div> <div class="col-12">
<div class="col-12"> <div class="card h-100 shadow-sm border-0">
<div class="card h-100 shadow-sm border-0"> <div class="card-header bg-white border-bottom-0">
<div class="card-header bg-white border-bottom-0"> <h5 class="fw-bold mb-0 text-dark">{% trans "Monthly Cars Sold" %}</h5>
<h5 class="fw-bold mb-0 text-dark">{% trans "Monthly Cars Sold" %}</h5> </div>
</div> <div class="card-body d-flex align-items-center justify-content-center"
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;"> style="height: 400px">
<canvas id="CarsSoldByMonthChart"></canvas> <canvas id="CarsSoldByMonthChart"></canvas>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="row g-4 mb-5">
<div class="row g-4 mb-5"> <div class="col-lg-6 col-12">
<div class="col-lg-6 col-12"> <div class="card h-100 shadow-sm border-0">
<div class="card h-100 shadow-sm border-0"> <div class="card-header bg-white border-bottom-0">
<div class="card-header bg-white border-bottom-0"> <h5 class="fw-bold mb-0 text-dark">{% trans "Sales by Make" %}</h5>
<h5 class="fw-bold mb-0 text-dark">{% trans "Sales by Make" %}</h5> </div>
</div> <div class="card-body d-flex align-items-center justify-content-center"
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;"> style="height: 400px">
<canvas id="salesByBrandChart"></canvas> <canvas id="salesByBrandChart"></canvas>
</div> </div>
</div> </div>
</div> </div>
<div class="col-lg-6 col-12">
<div class="col-lg-6 col-12"> <div class="card h-100 shadow-sm border-0">
<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">
<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>
<h5 class="fw-bold mb-0 text-dark">{% trans "Models Sold" %}</h5> <form method="GET" class="d-flex align-items-center">
<form method="GET" class="d-flex align-items-center"> <div class="form-group d-flex align-items-center me-2">
<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>
<label for="carMakeSelectSales" class="form-label mb-0 me-2">{% trans "Select Make:" %}</label> <select id="carMakeSelectSales" class="form-select" name="make_sold">
<select id="carMakeSelectSales" class="form-select" name="make_sold"> <option value="">{% trans "All Makes" %}</option>
<option value="">{% trans "All Makes" %}</option> {% for make_sold in all_makes_sold %}
{% for make_sold in all_makes_sold %} <option value="{{ make_sold }}"
<option value="{{ make_sold }}" {% if make_sold == selected_make_sales %}selected{% endif %}>{{ make_sold }}</option> {% if make_sold == selected_make_sales %}selected{% endif %}>
{% endfor %} {{ make_sold }}
</select> </option>
</div> {% endfor %}
<button type="submit" class="btn btn-primary">{% trans "Go" %}</button> </select>
<input type="hidden" name="start_date" value="{{ start_date|date:'Y-m-d' }}"> </div>
<input type="hidden" name="end_date" value="{{ end_date|date:'Y-m-d' }}"> <button type="submit" class="btn btn-primary">{% trans "Go" %}</button>
</form> <input type="hidden" name="start_date" value="{{ start_date|date:'Y-m-d' }}">
</div> <input type="hidden" name="end_date" value="{{ end_date|date:'Y-m-d' }}">
<div class="card-body" style="height: 400px;"> </form>
<canvas id="salesChartByModel"></canvas> </div>
</div> <div class="card-body" style="height: 400px;">
</div> <canvas id="salesChartByModel"></canvas>
</div> </div>
</div> </div>
{% endif %} </div>
</div>
{% if request.is_dealer or request.is_manager or request.is_inventory %} {% endif %}
<h3 class="fw-bold mb-3">{% trans "Inventory Trends" %}</h3> {% 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="row g-4 mb-5">
<div class="col-lg-6 col-12"> <div class="col-lg-6 col-12">
<div class="card h-100 shadow-sm border-0"> <div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0"> <div class="card-header bg-white border-bottom-0">
<h5 class="fw-bold mb-0 text-dark">{% trans "Inventory by Make" %}</h5> <h5 class="fw-bold mb-0 text-dark">{% trans "Inventory by Make" %}</h5>
</div> </div>
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;"> <div class="card-body d-flex align-items-center justify-content-center"
<canvas id="inventoryByMakeChart"></canvas> style="height: 400px">
</div> <canvas id="inventoryByMakeChart"></canvas>
</div> </div>
</div> </div>
</div>
<div class="col-lg-6 col-12"> <div class="col-lg-6 col-12">
<div class="card h-100 shadow-sm border-0"> <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"> <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> <h5 class="fw-bold mb-0 text-dark">{% trans "Models in Inventory" %}</h5>
<form method="GET" class="d-flex align-items-center"> <form method="GET" class="d-flex align-items-center">
<div class="form-group d-flex align-items-center me-2"> <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> <label for="carMakeSelectInventory" class="form-label mb-0 me-2">{% trans "Select Make:" %}</label>
<select id="carMakeSelectInventory" class="form-select" name="make_inventory"> <select id="carMakeSelectInventory" class="form-select" name="make_inventory">
<option value="">{% trans "All Makes" %}</option> <option value="">{% trans "All Makes" %}</option>
{% for make_inv in all_makes_inventory %} {% for make_inv in all_makes_inventory %}
<option value="{{ make_inv }}" {% if make_inv == selected_make_inventory %}selected{% endif %}>{{ make_inv }}</option> <option value="{{ make_inv }}"
{% endfor %} {% if make_inv == selected_make_inventory %}selected{% endif %}>
</select> {{ make_inv }}
</div> </option>
<button type="submit" class="btn btn-primary">{% trans "Go" %}</button> {% endfor %}
</form> </select>
</div> </div>
<div class="card-body" style="height: 400px;"> <button type="submit" class="btn btn-primary">{% trans "Go" %}</button>
<canvas id="inventoryByModelChart"></canvas> </form>
</div> </div>
</div> <div class="card-body" style="height: 400px;">
</div> <canvas id="inventoryByModelChart"></canvas>
</div> </div>
{% endif %} </div>
</div>
</div>
{% endif %}

View File

@ -1,266 +1,312 @@
{% load i18n %} {% load i18n %}
{% if request.is_dealer or request.is_manager or request.is_accountant %} {% if request.is_dealer or request.is_manager or request.is_accountant %}
<h3 class="fw-bold mb-3"> <h3 class="fw-bold mb-3">
{% blocktrans with start_date=start_date|date:"F j, Y" end_date=end_date|date:"F j, Y" %} {% blocktrans with start_date=start_date|date:"F j, Y" end_date=end_date|date:"F j, Y" %}
Sales KPIs ({{ start_date }} - {{ end_date }}) Sales KPIs ({{ start_date }} - {{ end_date }})
{% endblocktrans %} {% endblocktrans %}
</h3> </h3>
<div class="row g-4 mb-5"> <div class="row g-4 mb-5">
<div class="col-sm-6 col-md-4 col-lg-3"> <div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0"> <div class="card h-100 shadow-sm border-0">
<div class="card-body p-4"> <div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Cars Sold" %}</p> <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> <h4 class="fw-bolder text-primary mb-3">{{ total_cars_sold }}</h4>
</div> </div>
</div> </div>
</div> </div>
<div class="col-sm-6 col-md-4 col-lg-3"> <div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0"> <div class="card h-100 shadow-sm border-0">
<div class="card-body p-4"> <div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Revenue from Cars" %}</p> <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> <h4 class="fw-bolder text-primary mb-3">
</div> {{ total_revenue_from_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</div> </h4>
</div> </div>
<div class="col-sm-6 col-md-4 col-lg-3"> </div>
<div class="card h-100 shadow-sm border-0"> </div>
<div class="card-body p-4"> <div class="col-sm-6 col-md-4 col-lg-3">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Net Profit from Cars" %}</p> <div class="card h-100 shadow-sm border-0">
<h4 class="fw-bolder text-success mb-3">{{ net_profit_from_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4> <div class="card-body p-4">
</div> <p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Net Profit from Cars" %}</p>
</div> <h4 class="fw-bolder text-success mb-3">
</div> {{ net_profit_from_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
<div class="col-sm-6 col-md-4 col-lg-3"> </h4>
<div class="card h-100 shadow-sm border-0"> </div>
<div class="card-body p-4"> </div>
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Discount on Cars" %}</p> </div>
<h4 class="fw-bolder text-primary mb-3">{{ total_discount_on_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4> <div class="col-sm-6 col-md-4 col-lg-3">
</div> <div class="card h-100 shadow-sm border-0">
</div> <div class="card-body p-4">
</div> <p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Discount on Cars" %}</p>
<div class="col-sm-6 col-md-4 col-lg-3"> <h4 class="fw-bolder text-primary mb-3">
<div class="card h-100 shadow-sm border-0"> {{ total_discount_on_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
<div class="card-body p-4"> </h4>
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Cost of Cars Sold" %}</p> </div>
<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> <div class="col-sm-6 col-md-4 col-lg-3">
</div> <div class="card h-100 shadow-sm border-0">
<div class="col-sm-6 col-md-4 col-lg-3"> <div class="card-body p-4">
<div class="card h-100 shadow-sm border-0"> <p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Cost of Cars Sold" %}</p>
<div class="card-body p-4"> <h4 class="fw-bolder text-primary mb-3">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total VAT from Cars" %}</p> {{ total_cost_of_cars_sold|floatformat:2 }}<span class="icon-saudi_riyal"></span>
<h4 class="fw-bolder text-primary mb-3">{{ total_vat_collected_from_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4> </h4>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<h4 class="fw-bold my-4">{% trans "Sales of New Cars" %}</h4> <div class="card-body p-4">
<div class="row g-4 mb-5"> <p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total VAT from Cars" %}</p>
<div class="col-sm-6 col-md-4 col-lg-3"> <h4 class="fw-bolder text-primary mb-3">
<div class="card h-100 shadow-sm border-0"> {{ total_vat_collected_from_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
<div class="card-body p-4"> </h4>
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Sold" %}</p> </div>
<h4 class="fw-bolder text-primary mb-3">{{ total_new_cars_sold }}</h4> </div>
</div> </div>
</div> </div>
</div> <h4 class="fw-bold my-4">{% trans "Sales of New Cars" %}</h4>
<div class="col-sm-6 col-md-4 col-lg-3"> <div class="row g-4 mb-5">
<div class="card h-100 shadow-sm border-0"> <div class="col-sm-6 col-md-4 col-lg-3">
<div class="card-body p-4"> <div class="card h-100 shadow-sm border-0">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Revenue" %}</p> <div class="card-body p-4">
<h4 class="fw-bolder text-primary mb-3">{{ total_revenue_from_new_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4> <p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Sold" %}</p>
</div> <h4 class="fw-bolder text-primary mb-3">{{ total_new_cars_sold }}</h4>
</div> </div>
</div> </div>
<div class="col-sm-6 col-md-4 col-lg-3"> </div>
<div class="card h-100 shadow-sm border-0"> <div class="col-sm-6 col-md-4 col-lg-3">
<div class="card-body p-4"> <div class="card h-100 shadow-sm border-0">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Net Profit" %}</p> <div class="card-body p-4">
<h4 class="fw-bolder text-success mb-3">{{ net_profit_from_new_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4> <p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Revenue" %}</p>
</div> <h4 class="fw-bolder text-primary mb-3">
</div> {{ total_revenue_from_new_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</div> </h4>
<div class="col-sm-6 col-md-4 col-lg-3"> </div>
<div class="card h-100 shadow-sm border-0"> </div>
<div class="card-body p-4"> </div>
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars VAT" %}</p> <div class="col-sm-6 col-md-4 col-lg-3">
<h4 class="fw-bolder text-primary mb-3">{{ total_vat_collected_from_new_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4> <div class="card h-100 shadow-sm border-0">
</div> <div class="card-body p-4">
</div> <p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Net Profit" %}</p>
</div> <h4 class="fw-bolder text-success mb-3">
<div class="col-sm-6 col-md-4 col-lg-3"> {{ net_profit_from_new_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
<div class="card h-100 shadow-sm border-0"> </h4>
<div class="card-body p-4"> </div>
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Cost" %}</p> </div>
<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 class="col-sm-6 col-md-4 col-lg-3">
</div> <div class="card h-100 shadow-sm border-0">
</div> <div class="card-body p-4">
</div> <p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars VAT" %}</p>
<h4 class="fw-bolder text-primary mb-3">
<h4 class="fw-bold my-4">{% trans "Sales of Used Cars" %}</h4> {{ total_vat_collected_from_new_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
<div class="row g-4 mb-5"> </h4>
<div class="col-sm-6 col-md-4 col-lg-3"> </div>
<div class="card h-100 shadow-sm border-0"> </div>
<div class="card-body p-4"> </div>
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Sold" %}</p> <div class="col-sm-6 col-md-4 col-lg-3">
<h4 class="fw-bolder text-primary mb-3">{{ total_used_cars_sold }}</h4> <div class="card h-100 shadow-sm border-0">
</div> <div class="card-body p-4">
</div> <p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Cost" %}</p>
</div> <h4 class="fw-bolder text-primary mb-3">
<div class="col-sm-6 col-md-4 col-lg-3"> {{ total_cost_of_new_cars_sold|floatformat:2 }}<span class="icon-saudi_riyal"></span>
<div class="card h-100 shadow-sm border-0"> </h4>
<div class="card-body p-4"> </div>
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Revenue" %}</p> </div>
<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> <h4 class="fw-bold my-4">{% trans "Sales of Used Cars" %}</h4>
</div> <div class="row g-4 mb-5">
<div class="col-sm-6 col-md-4 col-lg-3"> <div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0"> <div class="card h-100 shadow-sm border-0">
<div class="card-body p-4"> <div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Net Profit" %}</p> <p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Sold" %}</p>
<h4 class="fw-bolder text-success mb-3">{{ net_profit_from_used_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4> <h4 class="fw-bolder text-primary mb-3">{{ total_used_cars_sold }}</h4>
</div> </div>
</div> </div>
</div> </div>
<div class="col-sm-6 col-md-4 col-lg-3"> <div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0"> <div class="card h-100 shadow-sm border-0">
<div class="card-body p-4"> <div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars VAT" %}</p> <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_vat_collected_from_used_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4> <h4 class="fw-bolder text-primary mb-3">
</div> {{ total_revenue_from_used_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</div> </h4>
</div> </div>
<div class="col-sm-6 col-md-4 col-lg-3"> </div>
<div class="card h-100 shadow-sm border-0"> </div>
<div class="card-body p-4"> <div class="col-sm-6 col-md-4 col-lg-3">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Cost" %}</p> <div class="card h-100 shadow-sm border-0">
<h4 class="fw-bolder text-primary mb-3">{{ total_cost_of_used_cars_sold|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4> <div class="card-body p-4">
</div> <p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Net Profit" %}</p>
</div> <h4 class="fw-bolder text-success mb-3">
</div> {{ net_profit_from_used_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</div> </h4>
{% endif %} </div>
</div>
{% if request.is_dealer or request.is_manager or request.is_inventory %} </div>
<h3 class="fw-bold mb-3">{% trans "Inventory KPIs" %}</h3> <div class="col-sm-6 col-md-4 col-lg-3">
<div class="row g-4 mb-5"> <div class="card h-100 shadow-sm border-0">
<div class="col-sm-6 col-md-4 col-lg-3"> <div class="card-body p-4">
<div class="card h-100 shadow-sm border-0"> <p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars VAT" %}</p>
<div class="card-body p-4"> <h4 class="fw-bolder text-primary mb-3">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Cars in Inventory" %}</p> {{ total_vat_collected_from_used_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
<h4 class="fw-bolder text-primary mb-3">{{ total_cars_in_inventory }}</h4> </h4>
</div> </div>
</div> </div>
</div> </div>
<div class="col-sm-6 col-md-4 col-lg-3"> <div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0"> <div class="card h-100 shadow-sm border-0">
<div class="card-body p-4"> <div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Inventory Value" %}</p> <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_inventory_value|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4> <h4 class="fw-bolder text-primary mb-3">
</div> {{ total_cost_of_used_cars_sold|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</div> </h4>
</div> </div>
<div class="col-sm-6 col-md-4 col-lg-3"> </div>
<div class="card h-100 shadow-sm border-0"> </div>
<div class="card-body p-4"> </div>
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars in Inventory" %}</p> {% endif %}
<h4 class="fw-bolder text-primary mb-3">{{ total_new_cars_in_inventory }}</h4> {% if request.is_dealer or request.is_manager or request.is_inventory %}
</div> <h3 class="fw-bold mb-3">{% trans "Inventory KPIs" %}</h3>
</div> <div class="row g-4 mb-5">
</div> <div class="col-sm-6 col-md-4 col-lg-3">
<div class="col-sm-6 col-md-4 col-lg-3"> <div class="card h-100 shadow-sm border-0">
<div class="card h-100 shadow-sm border-0"> <div class="card-body p-4">
<div class="card-body p-4"> <p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Cars in Inventory" %}</p>
<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_cars_in_inventory }}</h4>
<h4 class="fw-bolder text-primary mb-3">{{ total_used_cars_in_inventory }}</h4> </div>
</div> </div>
</div> </div>
</div> <div class="col-sm-6 col-md-4 col-lg-3">
<div class="col-sm-6 col-md-4 col-lg-3"> <div class="card h-100 shadow-sm border-0">
<div class="card h-100 shadow-sm border-0"> <div class="card-body p-4">
<div class="card-body p-4"> <p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Inventory Value" %}</p>
<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">
<h4 class="fw-bolder text-primary mb-3">{{ new_car_value|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4> {{ total_inventory_value|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</div> </h4>
</div> </div>
</div> </div>
<div class="col-sm-6 col-md-4 col-lg-3"> </div>
<div class="card h-100 shadow-sm border-0"> <div class="col-sm-6 col-md-4 col-lg-3">
<div class="card-body p-4"> <div class="card h-100 shadow-sm border-0">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Inventory Value" %}</p> <div class="card-body p-4">
<h4 class="fw-bolder text-primary mb-3">{{ used_car_value|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4> <p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars in Inventory" %}</p>
</div> <h4 class="fw-bolder text-primary mb-3">{{ total_new_cars_in_inventory }}</h4>
</div> </div>
</div> </div>
<div class="col-sm-6 col-md-4 col-lg-3"> </div>
<div class="card h-100 shadow-sm border-0"> <div class="col-sm-6 col-md-4 col-lg-3">
<div class="card-body p-4"> <div class="card h-100 shadow-sm border-0">
<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> <div class="card-body p-4">
<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> <p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars in Inventory" %}</p>
</div> <h4 class="fw-bolder text-primary mb-3">{{ total_used_cars_in_inventory }}</h4>
</div> </div>
</div> </div>
</div> </div>
{% endif %} <div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
{% if request.is_dealer or request.is_manager or request.is_accountant %} <div class="card-body p-4">
<h3 class="fw-bold mb-3"> <p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Inventory Value" %}</p>
{% blocktrans with start_date=start_date|date:"F j, Y" end_date=end_date|date:"F j, Y" %} <h4 class="fw-bolder text-primary mb-3">
Financial Health KPIs ({{ start_date }} - {{ end_date }}) {{ new_car_value|floatformat:2 }}<span class="icon-saudi_riyal"></span>
{% endblocktrans %} </h4>
</h3> </div>
<div class="row g-4 mb-5"> </div>
<div class="col-sm-6 col-md-4 col-lg-3"> </div>
<div class="card h-100 shadow-sm border-0"> <div class="col-sm-6 col-md-4 col-lg-3">
<div class="card-body p-4"> <div class="card h-100 shadow-sm border-0">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Revenue from Services" %}</p> <div class="card-body p-4">
<h4 class="fw-bolder text-info mb-3">{{ total_revenue_from_services|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4> <p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Inventory Value" %}</p>
</div> <h4 class="fw-bolder text-primary mb-3">
</div> {{ used_car_value|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</div> </h4>
<div class="col-sm-6 col-md-4 col-lg-3"> </div>
<div class="card h-100 shadow-sm border-0"> </div>
<div class="card-body p-4"> </div>
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total VAT from Services" %}</p> <div class="col-sm-6 col-md-4 col-lg-3">
<h4 class="fw-bolder text-primary mb-3">{{ total_vat_collected_from_services|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4> <div class="card h-100 shadow-sm border-0">
</div> <div class="card-body p-4">
</div> <p class="text-uppercase text-danger fw-bold small mb-1">
</div> <a class="text-danger"
<div class="col-sm-6 col-md-4 col-lg-3"> href="{% url 'aging_inventory_list' request.dealer.slug %}">{% trans "Aging Inventory (> 60 days)" %}</a>
<div class="card h-100 shadow-sm border-0"> </p>
<div class="card-body p-4"> <h4 class="fw-bolder text-danger mb-3">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Revenue Generated" %}</p> <a class="text-danger"
<h4 class="fw-bolder text-success mb-3">{{ total_revenue_generated|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4> href="{% url 'aging_inventory_list' request.dealer.slug %}">{{ aging_inventory_count }}</a>
</div> </h4>
</div> </div>
</div> </div>
<div class="col-sm-6 col-md-4 col-lg-3"> </div>
<div class="card h-100 shadow-sm border-0"> </div>
<div class="card-body p-4"> {% endif %}
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total VAT Collected" %}</p> {% if request.is_dealer or request.is_manager or request.is_accountant %}
<h4 class="fw-bolder text-primary mb-3">{{ total_vat_collected|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4> <h3 class="fw-bold mb-3">
</div> {% blocktrans with start_date=start_date|date:"F j, Y" end_date=end_date|date:"F j, Y" %}
</div> Financial Health KPIs ({{ start_date }} - {{ end_date }})
</div> {% endblocktrans %}
<div class="col-sm-6 col-md-4 col-lg-3"> </h3>
<div class="card h-100 shadow-sm border-0"> <div class="row g-4 mb-5">
<div class="card-body p-4"> <div class="col-sm-6 col-md-4 col-lg-3">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Expenses" %}</p> <div class="card h-100 shadow-sm border-0">
<h4 class="fw-bolder text-danger mb-3">{{ total_expenses|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4> <div class="card-body p-4">
</div> <p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Revenue from Services" %}</p>
</div> <h4 class="fw-bolder text-info mb-3">
</div> {{ total_revenue_from_services|floatformat:2 }}<span class="icon-saudi_riyal"></span>
<div class="col-sm-6 col-md-4 col-lg-3"> </h4>
<div class="card h-100 shadow-sm border-0"> </div>
<div class="card-body p-4"> </div>
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Gross Profit" %}</p> </div>
<h4 class="fw-bolder text-success mb-3">{{ gross_profit|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4> <div class="col-sm-6 col-md-4 col-lg-3">
</div> <div class="card h-100 shadow-sm border-0">
</div> <div class="card-body p-4">
</div> <p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total VAT from Services" %}</p>
</div> <h4 class="fw-bolder text-primary mb-3">
{% endif %} {{ 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 %}

View File

@ -1,429 +1,427 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n %} {% load i18n %}
{% load tenhal_tag %} {% load tenhal_tag %}
{% block title %}
{% block title %} {% trans "Dealership Dashboard"|capfirst %}
{% trans "Dealership Dashboard"|capfirst %} {% endblock title %}
{% endblock title %} {% block content %}
<div class="main-content flex-grow-1 container-fluid mt-4 mb-3">
{% block content %} <div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 pb-3 border-bottom">
<div class="main-content flex-grow-1 container-fluid mt-4 mb-3"> <h2 class="h3 fw-bold mb-3 mb-md-0">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 pb-3 border-bottom"> {% if request.is_dealer %}
<h2 class="h3 fw-bold mb-3 mb-md-0"> {% trans "Business Health Dashboard" %}
{% if request.is_dealer %} {% elif request.is_manger %}
{% trans "Business Health Dashboard" %} {% trans "Manager Dashboard" %}
{% elif request.is_manger %} {% elif request.is_inventory %}
{% trans "Manager Dashboard" %} {% trans "Inventory Dashboard" %}
{% elif request.is_inventory %} {% else %}
{% trans "Inventory Dashboard" %} {% trans "Accountant Dashboard" %}
{% else %} {% endif %}
{% trans "Accountant Dashboard" %} <i class="fas fa-chart-area text-primary ms-2"></i>
{% endif %} </h2>
<i class="fas fa-chart-area text-primary ms-2"></i> <form method="GET" class="date-filter-form">
</h2> <div class="row g-3">
<form method="GET" class="date-filter-form"> <div class="col-12 col-md-4">
<div class="row g-3"> <label for="start-date" class="form-label">{% trans "Start Date" %}</label>
<div class="col-12 col-md-4"> <input type="date"
<label for="start-date" class="form-label">{% trans "Start Date" %}</label> class="form-control"
<input type="date" class="form-control" id="start-date" name="start_date" id="start-date"
value="{{ start_date|date:'Y-m-d' }}" required> name="start_date"
</div> value="{{ start_date|date:'Y-m-d' }}"
<div class="col-12 col-md-4"> required>
<label for="end-date" class="form-label">{% trans "End Date" %}</label> </div>
<input type="date" class="form-control" id="end-date" name="end_date" <div class="col-12 col-md-4">
value="{{ end_date|date:'Y-m-d' }}" required> <label for="end-date" class="form-label">{% trans "End Date" %}</label>
</div> <input type="date"
<div class="col-12 col-md-4 d-flex align-items-end"> class="form-control"
<button type="submit" class="btn btn-primary w-100">{% trans "Apply Filter" %}</button> id="end-date"
</div> name="end_date"
</div> value="{{ end_date|date:'Y-m-d' }}"
<input type="hidden" name="make_sold" value="{{ selected_make_sales }}"> required>
</form> </div>
</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 class="row g-4 mb-5"> </div>
{% include 'dashboards/financial_data_cards.html' %} </div>
</div> <input type="hidden" name="make_sold" value="{{ selected_make_sales }}">
</form>
<div class="row g-4 mb-5"> </div>
{% include 'dashboards/chart.html' %} <div class="row g-4 mb-5">{% include 'dashboards/financial_data_cards.html' %}</div>
</div> <div class="row g-4 mb-5">{% include 'dashboards/chart.html' %}</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 %}
{% endblock content %} {% block customJS %}
<script>
{% block customJS %} // Define a color palette that aligns with the Phoenix template
<script> const primaryColor = '#7249b6';
// Define a color palette that aligns with the Phoenix template const secondaryColor = '#8193a6';
const primaryColor = '#7249b6'; const successColor = '#00d074';
const secondaryColor = '#8193a6'; const dangerColor = '#e63757';
const successColor = '#00d074'; const chartColors = [
const dangerColor = '#e63757'; '#7249b6', '#00d074', '#e63757', '#17a2b8', '#ffc107',
const chartColors = [ '#8193a6', '#28a745', '#6c757d', '#fd7e14', '#dc3545',
'#7249b6', '#00d074', '#e63757', '#17a2b8', '#ffc107', '#20c997', '#6f42c1', '#e83e8c', '#6610f2', '#007bff',
'#8193a6', '#28a745', '#6c757d', '#fd7e14', '#dc3545', '#495057'
'#20c997', '#6f42c1', '#e83e8c', '#6610f2', '#007bff', ];
'#495057'
]; // Pass translated strings from Django to JavaScript
const translatedStrings = {
// Pass translated strings from Django to JavaScript monthlyCarsSoldLabel: "{% trans 'Total Cars Sold' %}",
const translatedStrings = { monthlyRevenueLabel: "{% trans 'Monthly Revenue' %}",
monthlyCarsSoldLabel: "{% trans 'Total Cars Sold' %}", monthlyNetProfitLabel: "{% trans 'Monthly Net Profit' %}",
monthlyRevenueLabel: "{% trans 'Monthly Revenue' %}", salesByMakeLabel: "{% trans 'Car Count by Make' %}",
monthlyNetProfitLabel: "{% trans 'Monthly Net Profit' %}", salesByModelPrefix: "{% trans 'Cars Sold for' %}",
salesByMakeLabel: "{% trans 'Car Count by Make' %}", inventoryByMakeLabel: "{% trans 'Car Count by Make' %}",
salesByModelPrefix: "{% trans 'Cars Sold for' %}", inventoryByModelLabel: "{% trans 'Cars in Inventory' %}",
inventoryByMakeLabel: "{% trans 'Car Count by Make' %}", jan: "{% trans 'Jan' %}",
inventoryByModelLabel: "{% trans 'Cars in Inventory' %}", feb: "{% trans 'Feb' %}",
jan: "{% trans 'Jan' %}", mar: "{% trans 'Mar' %}",
feb: "{% trans 'Feb' %}", apr: "{% trans 'Apr' %}",
mar: "{% trans 'Mar' %}", may: "{% trans 'May' %}",
apr: "{% trans 'Apr' %}", jun: "{% trans 'Jun' %}",
may: "{% trans 'May' %}", jul: "{% trans 'Jul' %}",
jun: "{% trans 'Jun' %}", aug: "{% trans 'Aug' %}",
jul: "{% trans 'Jul' %}", sep: "{% trans 'Sep' %}",
aug: "{% trans 'Aug' %}", oct: "{% trans 'Oct' %}",
sep: "{% trans 'Sep' %}", nov: "{% trans 'Nov' %}",
oct: "{% trans 'Oct' %}", dec: "{% trans 'Dec' %}",
nov: "{% trans 'Nov' %}", cars: "{% trans 'cars' %}"
dec: "{% trans 'Dec' %}", };
cars: "{% trans 'cars' %}"
};
// Monthly Cars Sold (Bar Chart)
const ctx1 = document.getElementById('CarsSoldByMonthChart').getContext('2d');
// Monthly Cars Sold (Bar Chart) new Chart(ctx1, {
const ctx1 = document.getElementById('CarsSoldByMonthChart').getContext('2d'); type: 'bar',
new Chart(ctx1, { data: {
type: 'bar', labels: [
data: { translatedStrings.jan, translatedStrings.feb, translatedStrings.mar, translatedStrings.apr,
labels: [ translatedStrings.may, translatedStrings.jun, translatedStrings.jul, translatedStrings.aug,
translatedStrings.jan, translatedStrings.feb, translatedStrings.mar, translatedStrings.apr, translatedStrings.sep, translatedStrings.oct, translatedStrings.nov, translatedStrings.dec
translatedStrings.may, translatedStrings.jun, translatedStrings.jul, translatedStrings.aug, ],
translatedStrings.sep, translatedStrings.oct, translatedStrings.nov, translatedStrings.dec datasets: [{
], label: translatedStrings.monthlyCarsSoldLabel,
datasets: [{ data: {{ monthly_cars_sold_json|safe }},
label: translatedStrings.monthlyCarsSoldLabel, backgroundColor: primaryColor,
data: {{ monthly_cars_sold_json|safe }}, borderColor: primaryColor,
backgroundColor: primaryColor, borderWidth: 1
borderColor: primaryColor, }]
borderWidth: 1 },
}] options: {
}, responsive: true,
options: { maintainAspectRatio: false,
responsive: true, plugins: {
maintainAspectRatio: false, legend: { display: false }
plugins: { },
legend: { display: false } scales: {
}, y: {
scales: { beginAtZero: true,
y: { grid: { color: 'rgba(0, 0, 0, 0.05)' },
beginAtZero: true, ticks: {
grid: { color: 'rgba(0, 0, 0, 0.05)' }, color: secondaryColor,
ticks: { callback: function(value) {
color: secondaryColor, if (Number.isInteger(value)) {
callback: function(value) { return value;
if (Number.isInteger(value)) { }
return value; }
} }
} },
} x: {
}, grid: { display: false },
x: { ticks: { color: secondaryColor }
grid: { display: false }, }
ticks: { color: secondaryColor } }
} }
} });
}
}); // Monthly Revenue & Profit (Line Chart)
const ctx2 = document.getElementById('revenueProfitChart').getContext('2d');
// Monthly Revenue & Profit (Line Chart) new Chart(ctx2, {
const ctx2 = document.getElementById('revenueProfitChart').getContext('2d'); type: 'line',
new Chart(ctx2, { data: {
type: 'line', labels: [
data: { translatedStrings.jan, translatedStrings.feb, translatedStrings.mar, translatedStrings.apr,
labels: [ translatedStrings.may, translatedStrings.jun, translatedStrings.jul, translatedStrings.aug,
translatedStrings.jan, translatedStrings.feb, translatedStrings.mar, translatedStrings.apr, translatedStrings.sep, translatedStrings.oct, translatedStrings.nov, translatedStrings.dec
translatedStrings.may, translatedStrings.jun, translatedStrings.jul, translatedStrings.aug, ],
translatedStrings.sep, translatedStrings.oct, translatedStrings.nov, translatedStrings.dec datasets: [
], {
datasets: [ label: translatedStrings.monthlyRevenueLabel,
{ data: {{ monthly_revenue_json|safe }},
label: translatedStrings.monthlyRevenueLabel, borderColor: primaryColor,
data: {{ monthly_revenue_json|safe }}, backgroundColor: 'rgba(114, 73, 182, 0.1)', // Using primaryColor with transparency
borderColor: primaryColor, tension: 0.4,
backgroundColor: 'rgba(114, 73, 182, 0.1)', // Using primaryColor with transparency fill: true,
tension: 0.4, pointBackgroundColor: primaryColor,
fill: true, pointRadius: 5,
pointBackgroundColor: primaryColor, pointHoverRadius: 8
pointRadius: 5, },
pointHoverRadius: 8 {
}, label: translatedStrings.monthlyNetProfitLabel,
{ data: {{ monthly_net_profit_json|safe }},
label: translatedStrings.monthlyNetProfitLabel, borderColor: successColor,
data: {{ monthly_net_profit_json|safe }}, backgroundColor: 'rgba(0, 208, 116, 0.1)', // Using successColor with transparency
borderColor: successColor, tension: 0.4,
backgroundColor: 'rgba(0, 208, 116, 0.1)', // Using successColor with transparency fill: true,
tension: 0.4, pointBackgroundColor: successColor,
fill: true, pointRadius: 5,
pointBackgroundColor: successColor, pointHoverRadius: 8
pointRadius: 5, }
pointHoverRadius: 8 ]
} },
] options: {
}, responsive: true,
options: { maintainAspectRatio: false,
responsive: true, plugins: {
maintainAspectRatio: false, legend: {
plugins: { display: true,
legend: { labels: { color: '#495057', boxWidth: 20 }
display: true, },
labels: { color: '#495057', boxWidth: 20 } tooltip: {
}, backgroundColor: 'rgba(33, 37, 41, 0.9)',
tooltip: { titleColor: 'white',
backgroundColor: 'rgba(33, 37, 41, 0.9)', bodyColor: 'white',
titleColor: 'white', padding: 10,
bodyColor: 'white', callbacks: {
padding: 10, label: function(context) {
callbacks: { let label = context.dataset.label || '';
label: function(context) { if (label) {
let label = context.dataset.label || ''; label += ': ';
if (label) { }
label += ': '; if (context.parsed.y !== null) {
} label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'SAR' }).format(context.parsed.y);
if (context.parsed.y !== null) { }
label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'SAR' }).format(context.parsed.y); return label;
} }
return label; }
} }
} },
} scales: {
}, x: {
scales: { grid: { color: 'rgba(0, 0, 0, 0.05)' },
x: { ticks: { color: secondaryColor },
grid: { color: 'rgba(0, 0, 0, 0.05)' }, border: { color: secondaryColor }
ticks: { color: secondaryColor }, },
border: { color: secondaryColor } y: {
}, grid: { color: 'rgba(0, 0, 0, 0.05)' },
y: { ticks: { color: secondaryColor },
grid: { color: 'rgba(0, 0, 0, 0.05)' }, border: { color: secondaryColor }
ticks: { color: secondaryColor }, }
border: { color: secondaryColor } }
} }
} });
}
}); // Sales by Make (Pie Chart)
function getChartColors(count) {
// Sales by Make (Pie Chart) const colors = [];
function getChartColors(count) { for (let i = 0; i < count; i++) {
const colors = []; colors.push(chartColors[i % chartColors.length]);
for (let i = 0; i < count; i++) { }
colors.push(chartColors[i % chartColors.length]); return colors;
} }
return colors;
} const ctx3 = document.getElementById('salesByBrandChart').getContext('2d');
new Chart(ctx3, {
const ctx3 = document.getElementById('salesByBrandChart').getContext('2d'); type: 'pie',
new Chart(ctx3, { data: {
type: 'pie', labels: {{ sales_by_make_labels_json|safe }},
data: { datasets: [{
labels: {{ sales_by_make_labels_json|safe }}, label: translatedStrings.salesByMakeLabel,
datasets: [{ data: {{ sales_by_make_counts_json|safe }},
label: translatedStrings.salesByMakeLabel, backgroundColor: getChartColors({{ sales_by_make_counts_json|safe }}.length),
data: {{ sales_by_make_counts_json|safe }}, hoverOffset: 15,
backgroundColor: getChartColors({{ sales_by_make_counts_json|safe }}.length), }]
hoverOffset: 15, },
}] options: {
}, responsive: true,
options: { maintainAspectRatio: false,
responsive: true, plugins: {
maintainAspectRatio: false, legend: {
plugins: { position: 'right',
legend: { labels: { color: '#343a40', font: { size: 14 } }
position: 'right', },
labels: { color: '#343a40', font: { size: 14 } } tooltip: {
}, backgroundColor: 'rgba(33, 37, 41, 0.9)',
tooltip: { titleColor: '#fff',
backgroundColor: 'rgba(33, 37, 41, 0.9)', bodyColor: '#fff',
titleColor: '#fff', callbacks: {
bodyColor: '#fff', label: function(context) {
callbacks: { const label = context.label || '';
label: function(context) { const value = context.parsed || 0;
const label = context.label || ''; const total = context.dataset.data.reduce((a, b) => a + b, 0);
const value = context.parsed || 0; const percentage = ((value / total) * 100).toFixed(2);
const total = context.dataset.data.reduce((a, b) => a + b, 0); return `${label}: ${value} ${translatedStrings.cars} (${percentage}%)`;
const percentage = ((value / total) * 100).toFixed(2); }
return `${label}: ${value} ${translatedStrings.cars} (${percentage}%)`; }
} }
} }
} }
} });
}
}); // -----------------------------------------------------------
// 4. Sales by Model (Bar Chart)
// ----------------------------------------------------------- // -----------------------------------------------------------
// 4. Sales by Model (Bar Chart) const salesDataByModel = JSON.parse('{{ sales_data_by_model_json|safe }}');
// ----------------------------------------------------------- const canvasElementSales = document.getElementById('salesChartByModel');
const salesDataByModel = JSON.parse('{{ sales_data_by_model_json|safe }}'); let chartInstanceSales = null;
const canvasElementSales = document.getElementById('salesChartByModel');
let chartInstanceSales = null; if (salesDataByModel.length > 0) {
const labels = salesDataByModel.map(item => item.id_car_model__name);
if (salesDataByModel.length > 0) { const counts = salesDataByModel.map(item => item.count);
const labels = salesDataByModel.map(item => item.id_car_model__name); const backgroundColor = labels.map((_, index) => getChartColors(labels.length)[index]);
const counts = salesDataByModel.map(item => item.count);
const backgroundColor = labels.map((_, index) => getChartColors(labels.length)[index]); chartInstanceSales = new Chart(canvasElementSales, {
type: 'bar',
chartInstanceSales = new Chart(canvasElementSales, { data: {
type: 'bar', labels: labels,
data: { datasets: [{
labels: labels, label: `${translatedStrings.salesByModelPrefix} {{ selected_make_sales }}`,
datasets: [{ data: counts,
label: `${translatedStrings.salesByModelPrefix} {{ selected_make_sales }}`, backgroundColor: backgroundColor,
data: counts, borderColor: backgroundColor,
backgroundColor: backgroundColor, borderWidth: 1
borderColor: backgroundColor, }]
borderWidth: 1 },
}] options: {
}, responsive: true,
options: { maintainAspectRatio: false,
responsive: true, scales: {
maintainAspectRatio: false, y: {
scales: { beginAtZero: true,
y: { ticks: {
beginAtZero: true, callback: function(value) {
ticks: { if (Number.isInteger(value)) {
callback: function(value) { return value;
if (Number.isInteger(value)) { }
return value; }
} }
} }
} },
} plugins: {
}, tooltip: {
plugins: { callbacks: {
tooltip: { label: function(context) {
callbacks: { let label = context.dataset.label || '';
label: function(context) { if (label) {
let label = context.dataset.label || ''; label += ': ';
if (label) { }
label += ': '; label += Math.round(context.parsed.y);
} return label;
label += Math.round(context.parsed.y); }
return label; }
} }
} }
} }
} });
} }
});
} // -----------------------------------------------------------
// 5. Inventory by Make (Pie Chart)
// ----------------------------------------------------------- // -----------------------------------------------------------
// 5. Inventory by Make (Pie Chart) const ctxInventoryMake = document.getElementById('inventoryByMakeChart').getContext('2d');
// ----------------------------------------------------------- new Chart(ctxInventoryMake, {
const ctxInventoryMake = document.getElementById('inventoryByMakeChart').getContext('2d'); type: 'pie',
new Chart(ctxInventoryMake, { data: {
type: 'pie', labels: {{ inventory_by_make_labels_json|safe }},
data: { datasets: [{
labels: {{ inventory_by_make_labels_json|safe }}, label: translatedStrings.inventoryByMakeLabel,
datasets: [{ data: {{ inventory_by_make_counts_json|safe }},
label: translatedStrings.inventoryByMakeLabel, backgroundColor: getChartColors({{ inventory_by_make_counts_json|safe }}.length),
data: {{ inventory_by_make_counts_json|safe }}, hoverOffset: 15,
backgroundColor: getChartColors({{ inventory_by_make_counts_json|safe }}.length), }]
hoverOffset: 15, },
}] options: {
}, responsive: true,
options: { maintainAspectRatio: false,
responsive: true, plugins: {
maintainAspectRatio: false, legend: {
plugins: { position: 'right',
legend: { labels: { color: '#343a40', font: { size: 14 } }
position: 'right', },
labels: { color: '#343a40', font: { size: 14 } } tooltip: {
}, backgroundColor: 'rgba(33, 37, 41, 0.9)',
tooltip: { titleColor: '#fff',
backgroundColor: 'rgba(33, 37, 41, 0.9)', bodyColor: '#fff',
titleColor: '#fff', callbacks: {
bodyColor: '#fff', label: function(context) {
callbacks: { const label = context.label || '';
label: function(context) { const value = context.parsed || 0;
const label = context.label || ''; const total = context.dataset.data.reduce((a, b) => a + b, 0);
const value = context.parsed || 0; const percentage = ((value / total) * 100).toFixed(2);
const total = context.dataset.data.reduce((a, b) => a + b, 0); return `${label}: ${value} ${translatedStrings.cars} (${percentage}%)`;
const percentage = ((value / total) * 100).toFixed(2); }
return `${label}: ${value} ${translatedStrings.cars} (${percentage}%)`; }
} }
} }
} }
} });
}
}); // -----------------------------------------------------------
// 6. Inventory by Model (Bar Chart)
// ----------------------------------------------------------- // -----------------------------------------------------------
// 6. Inventory by Model (Bar Chart) const inventoryDataByModel = JSON.parse('{{ inventory_data_by_model_json|safe }}');
// ----------------------------------------------------------- const canvasInventoryModel = document.getElementById('inventoryByModelChart');
const inventoryDataByModel = JSON.parse('{{ inventory_data_by_model_json|safe }}'); const messageInventoryModel = document.getElementById('inventoryByModelMessage');
const canvasInventoryModel = document.getElementById('inventoryByModelChart');
const messageInventoryModel = document.getElementById('inventoryByModelMessage'); if (inventoryDataByModel.length > 0) {
canvasInventoryModel.style.display = 'block';
if (inventoryDataByModel.length > 0) { if (messageInventoryModel) {
canvasInventoryModel.style.display = 'block'; messageInventoryModel.style.display = 'none';
if (messageInventoryModel) { }
messageInventoryModel.style.display = 'none';
} const labels = inventoryDataByModel.map(item => item.id_car_model__name);
const counts = inventoryDataByModel.map(item => item.count);
const labels = inventoryDataByModel.map(item => item.id_car_model__name); const backgroundColor = getChartColors(labels.length);
const counts = inventoryDataByModel.map(item => item.count);
const backgroundColor = getChartColors(labels.length); new Chart(canvasInventoryModel, {
type: 'bar',
new Chart(canvasInventoryModel, { data: {
type: 'bar', labels: labels,
data: { datasets: [{
labels: labels, label: translatedStrings.inventoryByModelLabel,
datasets: [{ data: counts,
label: translatedStrings.inventoryByModelLabel, backgroundColor: backgroundColor,
data: counts, borderColor: backgroundColor,
backgroundColor: backgroundColor, borderWidth: 1
borderColor: backgroundColor, }]
borderWidth: 1 },
}] options: {
}, responsive: true,
options: { maintainAspectRatio: false,
responsive: true, scales: {
maintainAspectRatio: false, y: {
scales: { beginAtZero: true,
y: { ticks: {
beginAtZero: true, callback: function(value) {
ticks: { if (Number.isInteger(value)) {
callback: function(value) { return value;
if (Number.isInteger(value)) { }
return value; }
} }
} }
} },
} plugins: {
}, tooltip: {
plugins: { callbacks: {
tooltip: { label: function(context) {
callbacks: { let label = context.dataset.label || '';
label: function(context) { if (label) {
let label = context.dataset.label || ''; label += ': ';
if (label) { }
label += ': '; label += Math.round(context.parsed.y);
} return label;
label += Math.round(context.parsed.y); }
return label; }
} }
} }
} }
} });
} } else {
}); canvasInventoryModel.style.display = 'none';
} else { if (messageInventoryModel) {
canvasInventoryModel.style.display = 'none'; messageInventoryModel.style.display = 'flex';
if (messageInventoryModel) { }
messageInventoryModel.style.display = 'flex'; }
} </script>
} {% endblock %}
</script>
{% endblock %}

View File

@ -1,367 +1,371 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
<div class="main-content flex-grow-1 container-fluid mt-4 mb-3">
<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">
<div class="d-flex justify-content-between align-items-center mb-5 pb-3 border-bottom"> <h2 class="h3 fw-bold mb-0">
<h2 class="h3 fw-bold mb-0">Manager Dashboard<i class="fas fa-chart-area text-primary ms-2"></i></h2> Manager Dashboard<i class="fas fa-chart-area text-primary ms-2"></i>
<div class="dropdown"> </h2>
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false"> <div class="dropdown">
Last 30 Days <button class="btn btn-outline-secondary dropdown-toggle"
</button> type="button"
<ul class="dropdown-menu dropdown-menu-end shadow"> data-bs-toggle="dropdown"
<li><a class="dropdown-item" href="#">Today</a></li> aria-expanded="false">Last 30 Days</button>
<li><a class="dropdown-item" href="#">Last 7 Days</a></li> <ul class="dropdown-menu dropdown-menu-end shadow">
<li><a class="dropdown-item" href="#">Last 90 Days</a></li> <li>
</ul> <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> <div class="row g-4 mb-5">
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="row g-4 mb-5"> <div class="card h-100 shadow-sm border-0">
<div class="col-sm-6 col-md-4 col-lg-3"> <div class="card-body d-flex flex-column justify-content-between p-4">
<div class="card h-100 shadow-sm border-0"> <div>
<div class="card-body d-flex flex-column justify-content-between p-4"> <p class="text-uppercase text-muted fw-bold small mb-1">Total Revenue</p>
<div> <h4 class="fw-bolder text-primary mb-3">$1.25M</h4>
<p class="text-uppercase text-muted fw-bold small mb-1">Total Revenue</p> </div>
<h4 class="fw-bolder text-primary mb-3">$1.25M</h4> <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> </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> </div>
<div class="col-sm-6 col-md-4 col-lg-3"> <div class="row g-4 mb-5">
<div class="card h-100 shadow-sm border-0"> <div class="col-lg-8">
<div class="card-body d-flex flex-column justify-content-between p-4"> <div class="card h-100 shadow-sm border-0">
<div> <div class="card-header bg-white border-bottom-0">
<p class="text-uppercase text-muted fw-bold small mb-1">Net Profit</p> <h5 class="fw-bold mb-0 text-dark">Monthly Revenue & Profit</h5>
<h4 class="fw-bolder text-success mb-3">$1.25M</h4> </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>
<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> </div>
<div class="col-sm-6 col-md-4 col-lg-3"> <div class="row g-4">
<div class="card h-100 shadow-sm border-0"> <div class="col-lg-6">
<div class="card-body d-flex flex-column justify-content-between p-4"> <div class="card h-100 shadow-sm border-0">
<div> <div class="card-header bg-white border-bottom-0">
<p class="text-uppercase text-muted fw-bold small mb-1">Total Expense</p> <h5 class="fw-bold mb-0 text-dark">Sales by Make</h5>
<h4 class="fw-bolder text-danger mb-3">$1.25M</h4> </div>
<div class="card-body d-flex align-items-center justify-content-center"
style="height: 400px">
<canvas id="salesByBrandChart"></canvas>
</div> </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>
</div> <div class="col-lg-6">
<div class="col-sm-6 col-md-4 col-lg-3"> <div class="card h-100 shadow-sm border-0">
<div class="card h-100 shadow-sm border-0"> <div class="card-header bg-white border-bottom-0">
<div class="card-body d-flex flex-column justify-content-between p-4"> <h5 class="fw-bold mb-0 text-dark">Top Salesperson Performance</h5>
<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> </div>
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start"> <div class="card-body" style="height: 400px;">
+5 units from last month <canvas id="salespersonChart"></canvas>
</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> </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> </div>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<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>
{% endblock content %} {% endblock content %}
{% block customJS %} {% block customJS %}
<script> <script>
// Define a color palette that aligns with the Phoenix template // Define a color palette that aligns with the Phoenix template
const primaryColor = '#7249b6'; // A vibrant purple const primaryColor = '#7249b6'; // A vibrant purple
const secondaryColor = '#8193a6'; // A muted gray/blue const secondaryColor = '#8193a6'; // A muted gray/blue
const successColor = '#00d074'; // A bright green const successColor = '#00d074'; // A bright green
const warningColor = '#ffc107'; // A strong yellow const warningColor = '#ffc107'; // A strong yellow
const dangerColor = '#e63757'; // A deep red const dangerColor = '#e63757'; // A deep red
const chartColors = ['#00d27a', '#7249b6', '#32b9ff', '#e63757', '#ffc107']; const chartColors = ['#00d27a', '#7249b6', '#32b9ff', '#e63757', '#ffc107'];
// Monthly Cars Sold (Bar Chart) // Monthly Cars Sold (Bar Chart)
const ctx1 = document.getElementById('CarsSoldByMonthChart').getContext('2d'); const ctx1 = document.getElementById('CarsSoldByMonthChart').getContext('2d');
new Chart(ctx1, { new Chart(ctx1, {
type: 'bar', type: 'bar',
data: { data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
datasets: [{ datasets: [{
label: 'Total Cars Sold', label: 'Total Cars Sold',
data: [2, 3, 10, 4, 30, 12, 8, 9, 20, 12, 15, 35], data: [2, 3, 10, 4, 30, 12, 8, 9, 20, 12, 15, 35],
backgroundColor: primaryColor, backgroundColor: primaryColor,
borderColor: primaryColor, borderColor: primaryColor,
borderWidth: 1 borderWidth: 1
}] }]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
}, },
scales: { options: {
y: { responsive: true,
beginAtZero: true, maintainAspectRatio: false,
grid: { color: 'rgba(0, 0, 0, 0.05)' }, plugins: {
ticks: { color: secondaryColor } legend: { display: false }
}, },
x: { scales: {
grid: { display: false }, y: {
ticks: { color: secondaryColor } 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) // Monthly Revenue & Profit (Line Chart)
const ctx2 = document.getElementById('revenueProfitChart').getContext('2d'); const ctx2 = document.getElementById('revenueProfitChart').getContext('2d');
new Chart(ctx2, { new Chart(ctx2, {
type: 'line', type: 'line',
data: { data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
datasets: [ datasets: [
{ {
label: 'Monthly Revenue', label: 'Monthly Revenue',
data: [120000, 150000, 130000, 180000, 200000, 175000, 190000, 220000, 210000, 250000, 240000, 280000], data: [120000, 150000, 130000, 180000, 200000, 175000, 190000, 220000, 210000, 250000, 240000, 280000],
borderColor: primaryColor, borderColor: primaryColor,
backgroundColor: 'rgba(114, 73, 182, 0.1)', // Using primaryColor with transparency backgroundColor: 'rgba(114, 73, 182, 0.1)', // Using primaryColor with transparency
tension: 0.4, tension: 0.4,
fill: true, fill: true,
pointBackgroundColor: primaryColor, pointBackgroundColor: primaryColor,
pointRadius: 5, pointRadius: 5,
pointHoverRadius: 8 pointHoverRadius: 8
}, },
{ {
label: 'Monthly Net Profit', label: 'Monthly Net Profit',
data: [25000, 35000, 28000, 40000, 45000, 38000, 42000, 50000, 48000, 55000, 52000, 60000], data: [25000, 35000, 28000, 40000, 45000, 38000, 42000, 50000, 48000, 55000, 52000, 60000],
borderColor: successColor, borderColor: successColor,
backgroundColor: 'rgba(0, 208, 116, 0.1)', // Using successColor with transparency backgroundColor: 'rgba(0, 208, 116, 0.1)', // Using successColor with transparency
tension: 0.4, tension: 0.4,
fill: true, fill: true,
pointBackgroundColor: successColor, pointBackgroundColor: successColor,
pointRadius: 5, pointRadius: 5,
pointHoverRadius: 8 pointHoverRadius: 8
} }
] ]
}, },
options: { options: {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
plugins: { plugins: {
legend: { legend: {
display: true, display: true,
labels: { color: '#495057', boxWidth: 20 } labels: { color: '#495057', boxWidth: 20 }
}, },
tooltip: { tooltip: {
backgroundColor: 'rgba(33, 37, 41, 0.9)', backgroundColor: 'rgba(33, 37, 41, 0.9)',
titleColor: 'white', titleColor: 'white',
bodyColor: 'white', bodyColor: 'white',
padding: 10, padding: 10,
callbacks: { callbacks: {
label: function(context) { label: function(context) {
let label = context.dataset.label || ''; let label = context.dataset.label || '';
if (label) { if (label) {
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: { scales: {
grid: { color: 'rgba(0, 0, 0, 0.05)' }, x: {
ticks: { color: secondaryColor }, grid: { color: 'rgba(0, 0, 0, 0.05)' },
border: { color: secondaryColor } 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) // Sales by Make (Pie Chart)
const ctx3 = document.getElementById('salesByBrandChart').getContext('2d'); const ctx3 = document.getElementById('salesByBrandChart').getContext('2d');
new Chart(ctx3, { new Chart(ctx3, {
type: 'pie', type: 'pie',
data: { data: {
labels: ['Toyota', 'Ford', 'Honda', 'BMW', 'Other'], labels: ['Toyota', 'Ford', 'Honda', 'BMW', 'Other'],
datasets: [{ datasets: [{
label: 'Car Count by Make', label: 'Car Count by Make',
data: [45, 30, 25, 15, 10], data: [45, 30, 25, 15, 10],
backgroundColor: chartColors, backgroundColor: chartColors,
hoverOffset: 15, hoverOffset: 15,
}] }]
}, },
options: { options: {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
plugins: { plugins: {
legend: { legend: {
position: 'right', position: 'right',
labels: { color: '#343a40', font: { size: 14 } } labels: { color: '#343a40', font: { size: 14 } }
}, },
tooltip: { tooltip: {
backgroundColor: 'rgba(33, 37, 41, 0.9)', backgroundColor: 'rgba(33, 37, 41, 0.9)',
titleColor: '#fff', titleColor: '#fff',
bodyColor: '#fff', bodyColor: '#fff',
callbacks: { callbacks: {
label: function(context) { label: function(context) {
const label = context.label || ''; const label = context.label || '';
const value = context.parsed || 0; const value = context.parsed || 0;
const total = context.dataset.data.reduce((a, b) => a + b, 0); const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = ((value / total) * 100).toFixed(2); const percentage = ((value / total) * 100).toFixed(2);
return `${label}: ${value} cars (${percentage}%)`; return `${label}: ${value} cars (${percentage}%)`;
}
} }
} }
} }
} }
} });
});
// Salesperson Performance (Bar Chart) // Salesperson Performance (Bar Chart)
const ctx_salesperson = document.getElementById('salespersonChart').getContext('2d'); const ctx_salesperson = document.getElementById('salespersonChart').getContext('2d');
new Chart(ctx_salesperson, { new Chart(ctx_salesperson, {
type: 'bar', type: 'bar',
data: { data: {
labels: ['John Doe', 'Jane Smith', 'Peter Jones', 'Mary Brown'], labels: ['John Doe', 'Jane Smith', 'Peter Jones', 'Mary Brown'],
datasets: [{ datasets: [{
label: 'Cars Sold', label: 'Cars Sold',
data: [15, 22, 18, 25], data: [15, 22, 18, 25],
backgroundColor: chartColors, backgroundColor: chartColors,
borderWidth: 1 borderWidth: 1
}] }]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
title: { display: true, text: 'Top Salesperson Performance', font: { size: 16 } }
}, },
scales: { options: {
x: { responsive: true,
title: { display: true, text: 'Salesperson Name', color: secondaryColor }, maintainAspectRatio: false,
ticks: { color: secondaryColor } plugins: {
legend: { display: false },
title: { display: true, text: 'Top Salesperson Performance', font: { size: 16 } }
}, },
y: { scales: {
beginAtZero: true, x: {
title: { display: true, text: 'Number of Cars Sold', color: secondaryColor }, title: { display: true, text: 'Salesperson Name', color: secondaryColor },
ticks: { color: secondaryColor } ticks: { color: secondaryColor }
},
y: {
beginAtZero: true,
title: { display: true, text: 'Number of Cars Sold', color: secondaryColor },
ticks: { color: secondaryColor }
}
} }
} }
} });
}); </script>
</script> {% endblock %}
{% endblock %}

View File

@ -1,269 +1,275 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n %} {% load i18n %}
{% block content %}
{% 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">
<div class="main-content flex-grow-1 container-fluid mt-4 mb-3"> <h2 class="h3 fw-bold mb-3 mb-md-0">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 pb-3 border-bottom"> {% trans "Sales Dashboard" %} <i class="fas fa-chart-area text-primary ms-2"></i>
<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> </h2>
<form method="GET" class="date-filter-form"> <form method="GET" class="date-filter-form">
<div class="row g-3"> <div class="row g-3">
<div class="col-12 col-md-4"> <div class="col-12 col-md-4">
<label for="start-date" class="form-label">{% trans "Start Date" %}</label> <label for="start-date" class="form-label">{% trans "Start Date" %}</label>
<input type="date" class="form-control" id="start-date" name="start_date" <input type="date"
value="{{ start_date|date:'Y-m-d' }}" required> class="form-control"
</div> id="start-date"
<div class="col-12 col-md-4"> name="start_date"
<label for="end-date" class="form-label">{% trans "End Date" %}</label> value="{{ start_date|date:'Y-m-d' }}"
<input type="date" class="form-control" id="end-date" name="end_date" required>
value="{{ end_date|date:'Y-m-d' }}" required> </div>
</div> <div class="col-12 col-md-4">
<div class="col-12 col-md-4 d-flex align-items-end"> <label for="end-date" class="form-label">{% trans "End Date" %}</label>
<button type="submit" class="btn btn-primary w-100">{% trans "Apply Filter" %}</button> <input type="date"
</div> class="form-control"
</div> id="end-date"
</form> name="end_date"
</div> value="{{ end_date|date:'Y-m-d' }}"
required>
<div class="row g-4 mb-5"> </div>
<h3 class="fw-bold mb-3">{% trans "Inventory KPIs" %}</h3> <div class="col-12 col-md-4 d-flex align-items-end">
<div class="col-sm-6 col-md-4 col-lg-3"> <button type="submit" class="btn btn-primary w-100">{% trans "Apply Filter" %}</button>
<div class="card h-100 shadow-sm border-0"> </div>
<div class="card-body p-4"> </div>
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Cars in Inventory" %}</p> </form>
<h4 class="fw-bolder text-primary mb-3">{{ total_cars_in_inventory }}</h4> </div>
</div> <div class="row g-4 mb-5">
</div> <h3 class="fw-bold mb-3">{% trans "Inventory KPIs" %}</h3>
</div> <div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="col-sm-6 col-md-4 col-lg-3"> <div class="card-body p-4">
<div class="card h-100 shadow-sm border-0"> <p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Cars in Inventory" %}</p>
<div class="card-body p-4"> <h4 class="fw-bolder text-primary mb-3">{{ total_cars_in_inventory }}</h4>
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars in Inventory" %}</p> </div>
<h4 class="fw-bolder text-primary mb-3">{{ total_new_cars_in_inventory }}</h4> </div>
</div> </div>
</div> <div class="col-sm-6 col-md-4 col-lg-3">
</div> <div class="card h-100 shadow-sm border-0">
<div class="col-sm-6 col-md-4 col-lg-3"> <div class="card-body p-4">
<div class="card h-100 shadow-sm border-0"> <p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars in Inventory" %}</p>
<div class="card-body p-4"> <h4 class="fw-bolder text-primary mb-3">{{ total_new_cars_in_inventory }}</h4>
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars in Inventory" %}</p> </div>
<h4 class="fw-bolder text-primary mb-3">{{ total_used_cars_in_inventory }}</h4> </div>
</div> </div>
</div> <div class="col-sm-6 col-md-4 col-lg-3">
</div> <div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<div class="col-sm-6 col-md-4 col-lg-3"> <p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars in Inventory" %}</p>
<div class="card h-100 shadow-sm border-0"> <h4 class="fw-bolder text-primary mb-3">{{ total_used_cars_in_inventory }}</h4>
<div class="card-body p-4"> </div>
<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> </div>
<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 class="col-sm-6 col-md-4 col-lg-3">
</div> <div class="card h-100 shadow-sm border-0">
</div> <div class="card-body p-4">
</div> <p class="text-uppercase text-danger fw-bold small mb-1">
<a class="text-danger"
<div class="row g-4 mb-5"> href="{% url 'aging_inventory_list' request.dealer.slug %}">{% trans "Aging Inventory (> 60 days)" %}</a>
<div class="col-md-6"> </p>
<div class="card h-100 shadow-sm border-0"> <h4 class="fw-bolder text-danger mb-3">
<div class="card-header bg-white border-bottom-0"> <a class="text-danger"
<h5 class="fw-bold mb-0 text-dark">{% trans "Top Lead Sources" %}</h5> href="{% url 'aging_inventory_list' request.dealer.slug %}">{{ aging_inventory_count }}</a>
</div> </h4>
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;"> </div>
<canvas id="leadSourcesChart"></canvas> </div>
</div> </div>
</div> </div>
</div> <div class="row g-4 mb-5">
<div class="col-md-6">
<div class="col-md-6"> <div class="card h-100 shadow-sm border-0">
<div class="card h-100 shadow-sm border-0"> <div class="card-header bg-white border-bottom-0">
<div class="card-header bg-white border-bottom-0"> <h5 class="fw-bold mb-0 text-dark">{% trans "Top Lead Sources" %}</h5>
<h5 class="fw-bold mb-0 text-dark">{% trans "Lead Conversion Funnel" %}</h5> </div>
</div> <div class="card-body d-flex align-items-center justify-content-center"
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;"> style="height: 400px">
<canvas id="leadFunnelChart"></canvas> <canvas id="leadSourcesChart"></canvas>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6">
</div> <div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0">
</div> <h5 class="fw-bold mb-0 text-dark">{% trans "Lead Conversion Funnel" %}</h5>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <div class="card-body d-flex align-items-center justify-content-center"
{% endblock content %} style="height: 400px">
<canvas id="leadFunnelChart"></canvas>
</div>
{% block customJS%} </div>
<script> </div>
// Define your color palette at the top </div>
const primaryColor = '#7249b6'; // A vibrant purple </div>
const secondaryColor = '#8193a6'; // A muted gray/blue <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
const successColor = '#00d074'; // A bright green {% endblock content %}
const dangerColor = '#e63757'; // A deep red {% block customJS %}
const infoColor = '#17a2b8'; // Correcting the missing variable <script>
const warningColor = '#ffc107'; // Add other colors if needed // Define your color palette at the top
const primaryColor = '#7249b6'; // A vibrant purple
const chartColors = [ const secondaryColor = '#8193a6'; // A muted gray/blue
'#7249b6', '#00d074', '#e63757', '#17a2b8', '#ffc107', const successColor = '#00d074'; // A bright green
'#8193a6', '#28a745', '#6c757d', '#fd7e14', '#dc3545', const dangerColor = '#e63757'; // A deep red
'#20c997', '#6f42c1', '#e83e8c', '#6610f2', '#007bff', const infoColor = '#17a2b8'; // Correcting the missing variable
'#495057' const warningColor = '#ffc107'; // Add other colors if needed
];
const chartColors = [
// Pass translated strings from Django to JavaScript '#7249b6', '#00d074', '#e63757', '#17a2b8', '#ffc107',
const translatedStrings = { '#8193a6', '#28a745', '#6c757d', '#fd7e14', '#dc3545',
numberOfLeads: "{% trans 'Number of Leads' %}", '#20c997', '#6f42c1', '#e83e8c', '#6610f2', '#007bff',
leads: "{% trans 'Leads' %}", '#495057'
numberOfOpportunities: "{% trans 'Number of Opportunities' %}" ];
};
// Pass translated strings from Django to JavaScript
// Get the canvas and message elements const translatedStrings = {
const ctx_leadSources = document.getElementById('leadSourcesChart').getContext('2d'); numberOfLeads: "{% trans 'Number of Leads' %}",
const leadSourcesMessage = document.getElementById('leadSourcesMessage'); leads: "{% trans 'Leads' %}",
numberOfOpportunities: "{% trans 'Number of Opportunities' %}"
// Parse the JSON data from Django };
const leadSourcesLabels = JSON.parse('{{ lead_sources_labels_json|safe }}');
const leadSourcesCounts = JSON.parse('{{ lead_sources_counts_json|safe }}'); // Get the canvas and message elements
const ctx_leadSources = document.getElementById('leadSourcesChart').getContext('2d');
// Check if there is any data to display const leadSourcesMessage = document.getElementById('leadSourcesMessage');
if (leadSourcesCounts.length > 0) {
// Show the chart and hide the message // Parse the JSON data from Django
ctx_leadSources.canvas.style.display = 'block'; const leadSourcesLabels = JSON.parse('{{ lead_sources_labels_json|safe }}');
if (leadSourcesMessage) { const leadSourcesCounts = JSON.parse('{{ lead_sources_counts_json|safe }}');
leadSourcesMessage.style.display = 'none';
} // Check if there is any data to display
if (leadSourcesCounts.length > 0) {
new Chart(ctx_leadSources, { // Show the chart and hide the message
type: 'bar', ctx_leadSources.canvas.style.display = 'block';
data: { if (leadSourcesMessage) {
labels: leadSourcesLabels, leadSourcesMessage.style.display = 'none';
datasets: [{ }
label: translatedStrings.numberOfLeads,
data: leadSourcesCounts, new Chart(ctx_leadSources, {
backgroundColor: infoColor, type: 'bar',
borderColor: infoColor, data: {
borderWidth: 1 labels: leadSourcesLabels,
}] datasets: [{
}, label: translatedStrings.numberOfLeads,
options: { data: leadSourcesCounts,
indexAxis: 'y', backgroundColor: infoColor,
responsive: true, borderColor: infoColor,
maintainAspectRatio: false, borderWidth: 1
plugins: { }]
legend: { display: false }, },
title: { display: false }, options: {
tooltip: { indexAxis: 'y',
backgroundColor: 'rgba(33, 37, 41, 0.9)', responsive: true,
titleColor: '#fff', maintainAspectRatio: false,
bodyColor: '#fff', plugins: {
callbacks: { legend: { display: false },
label: function(context) { title: { display: false },
return `${translatedStrings.leads}: ${context.parsed.x}`; tooltip: {
} backgroundColor: 'rgba(33, 37, 41, 0.9)',
} titleColor: '#fff',
} bodyColor: '#fff',
}, callbacks: {
scales: { label: function(context) {
x: { return `${translatedStrings.leads}: ${context.parsed.x}`;
beginAtZero: true, }
title: { display: true, text: translatedStrings.numberOfLeads, color: secondaryColor }, }
ticks: { }
color: secondaryColor, },
callback: function(value) { scales: {
if (Number.isInteger(value)) { x: {
return value; beginAtZero: true,
} title: { display: true, text: translatedStrings.numberOfLeads, color: secondaryColor },
} ticks: {
}, color: secondaryColor,
grid: { color: 'rgba(0, 0, 0, 0.05)' } callback: function(value) {
}, if (Number.isInteger(value)) {
y: { return value;
grid: { display: false }, }
ticks: { color: secondaryColor } }
} },
} grid: { color: 'rgba(0, 0, 0, 0.05)' }
} },
}); y: {
} else { grid: { display: false },
// Hide the chart and show the message ticks: { color: secondaryColor }
ctx_leadSources.canvas.style.display = 'none'; }
if (leadSourcesMessage) { }
leadSourcesMessage.style.display = 'flex'; }
} });
} } else {
// Hide the chart and show the message
// Lead Conversion Funnel (Horizontal Bar Chart) ctx_leadSources.canvas.style.display = 'none';
const ctx_funnel = document.getElementById('leadFunnelChart').getContext('2d'); if (leadSourcesMessage) {
const leadFunnelMessage = document.getElementById('leadFunnelMessage'); leadSourcesMessage.style.display = 'flex';
}
// Parse the dynamic data from Django }
const opportunityStagesLabels = JSON.parse('{{ opportunity_stage_labels_json|safe }}');
const opportunityStagesCounts = JSON.parse('{{ opportunity_stage_counts_json|safe }}'); // Lead Conversion Funnel (Horizontal Bar Chart)
const ctx_funnel = document.getElementById('leadFunnelChart').getContext('2d');
if (opportunityStagesCounts.length > 0) { const leadFunnelMessage = document.getElementById('leadFunnelMessage');
// Show the chart and hide the message
ctx_funnel.canvas.style.display = 'block'; // Parse the dynamic data from Django
if (leadFunnelMessage) { const opportunityStagesLabels = JSON.parse('{{ opportunity_stage_labels_json|safe }}');
leadFunnelMessage.style.display = 'none'; const opportunityStagesCounts = JSON.parse('{{ opportunity_stage_counts_json|safe }}');
}
if (opportunityStagesCounts.length > 0) {
// Get a subset of colors based on the number of data points // Show the chart and hide the message
const backgroundColors = chartColors.slice(0, opportunityStagesCounts.length); ctx_funnel.canvas.style.display = 'block';
if (leadFunnelMessage) {
new Chart(ctx_funnel, { leadFunnelMessage.style.display = 'none';
type: 'bar', }
data: {
labels: opportunityStagesLabels, // Get a subset of colors based on the number of data points
datasets: [{ const backgroundColors = chartColors.slice(0, opportunityStagesCounts.length);
label: translatedStrings.numberOfOpportunities,
data: opportunityStagesCounts, new Chart(ctx_funnel, {
// Use the new backgroundColors array type: 'bar',
backgroundColor: backgroundColors, data: {
// Set borders to match the fill color labels: opportunityStagesLabels,
borderColor: backgroundColors, datasets: [{
borderWidth: 1 label: translatedStrings.numberOfOpportunities,
}] data: opportunityStagesCounts,
}, // Use the new backgroundColors array
options: { backgroundColor: backgroundColors,
indexAxis: 'y', // Set borders to match the fill color
responsive: true, borderColor: backgroundColors,
maintainAspectRatio: false, borderWidth: 1
plugins: { }]
legend: { display: false }, },
title: { display: false }, options: {
tooltip: { indexAxis: 'y',
backgroundColor: 'rgba(33, 37, 41, 0.9)', responsive: true,
titleColor: '#fff', maintainAspectRatio: false,
bodyColor: '#fff', plugins: {
callbacks: { legend: { display: false },
label: function(context) { title: { display: false },
const totalOpportunities = opportunityStagesCounts[0] || 0; tooltip: {
const currentOpportunities = context.parsed.x; backgroundColor: 'rgba(33, 37, 41, 0.9)',
const percentage = totalOpportunities > 0 ? ((currentOpportunities / totalOpportunities) * 100).toFixed(1) : 0; titleColor: '#fff',
return `${translatedStrings.leads}: ${currentOpportunities} (${percentage}%)`; bodyColor: '#fff',
} callbacks: {
} label: function(context) {
} const totalOpportunities = opportunityStagesCounts[0] || 0;
}, const currentOpportunities = context.parsed.x;
scales: { const percentage = totalOpportunities > 0 ? ((currentOpportunities / totalOpportunities) * 100).toFixed(1) : 0;
x: { return `${translatedStrings.leads}: ${currentOpportunities} (${percentage}%)`;
beginAtZero: true, }
display: false }
}, }
y: { },
grid: { display: false }, scales: {
ticks: { color: secondaryColor } x: {
} beginAtZero: true,
} display: false
} },
}); y: {
} else { grid: { display: false },
// Hide the chart and show the message ticks: { color: secondaryColor }
ctx_funnel.canvas.style.display = 'none'; }
if (leadFunnelMessage) { }
leadFunnelMessage.style.display = 'flex'; }
} });
} } else {
</script> // Hide the chart and show the message
{% endblock %} ctx_funnel.canvas.style.display = 'none';
if (leadFunnelMessage) {
leadFunnelMessage.style.display = 'flex';
}
}
</script>
{% endblock %}

View File

@ -1,93 +1,94 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n static %} {% load i18n static %}
{% block title %} {% block title %}
{% trans 'Activity' %}{% endblock %} {% trans 'Activity' %}
{% block content %} {% endblock %}
<div class="row"> {% block content %}
<div class="ol-auto pt-5 pb-9"> <div class="row">
<div class="row-sm"> <div class="ol-auto pt-5 pb-9">
<div class="row d-flex-center"> <div class="row-sm">
<div class="col-8"> <div class="row d-flex-center">
<div class="tab-content" id="myTabContent"> <div class="col-8">
<div class="tab-pane fade active show" <div class="tab-content" id="myTabContent">
id="tab-activity" <div class="tab-pane fade active show"
role="tabpanel" id="tab-activity"
aria-labelledby="activity-tab"> role="tabpanel"
<h3 class="mb-4">{{ _("Activity") }}</h3> aria-labelledby="activity-tab">
<div class="border-bottom py-4"> <h3 class="mb-4">{{ _("Activity") }}</h3>
{% for log in logs %} <div class="border-bottom py-4">
<div class="d-flex"> {% for log in logs %}
<div class="d-flex bg-primary-subtle rounded-circle flex-center me-3 bg-primary-subtle" <div class="d-flex">
style="width: 25px; <div class="d-flex bg-primary-subtle rounded-circle flex-center me-3 bg-primary-subtle"
height: 25px"> style="width: 25px;
<span class="fa-solid text-primary-dark fs-9 fa-clipboard text-primary-dark"></span> height: 25px">
</div> <span class="fa-solid text-primary-dark fs-9 fa-clipboard text-primary-dark"></span>
<div class="flex-1"> </div>
<div class="d-flex justify-content-between flex-column flex-xl-row mb-2 mb-sm-0"> <div class="flex-1">
<div class="flex-1 me-2"> <div class="d-flex justify-content-between flex-column flex-xl-row mb-2 mb-sm-0">
<h5 class="text-body-highlight lh-sm">{{ log.user }}</h5> <div class="flex-1 me-2">
</div> <h5 class="text-body-highlight lh-sm">{{ log.user }}</h5>
<div class="fs-9"> </div>
<span class="fa-regular fa-calendar-days text-primary me-2"></span><span class="fw-semibold">{{ log.timestamp }}</span> <div class="fs-9">
</div> <span class="fa-regular fa-calendar-days text-primary me-2"></span><span class="fw-semibold">{{ log.timestamp }}</span>
</div> </div>
<p class="fs-9 mb-0">{{ log.action }}</p>
</div> </div>
<p class="fs-9 mb-0">{{ log.action }}</p>
</div> </div>
</div> </div>
<div class="border-bottom border-translucent py-4">{% endfor %}</div> </div>
</div> <div class="border-bottom border-translucent py-4">{% endfor %}</div>
</div> </div>
</div> </div>
{% if is_paginated %}
<nav aria-label="Page navigation">
<ul class="pagination mb-0">
{% if page_obj.has_previous %}
<li class="page-item py-0">
<a class="page-link"
href="?page={{ page_obj.previous_page_number }}"
aria-label="Previous">
<span aria-hidden="true"><span class="fas fa-chevron-left"></span></span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true"><span class="fas fa-chevron-left"></span></span>
</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link"
href="?page={{ page_obj.next_page_number }}"
aria-label="Next">
<span aria-hidden="true"><span class="fas fa-chevron-right"></span></span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true"><span class="fas fa-chevron-right"></span></span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div> </div>
{% if is_paginated %}
<nav aria-label="Page navigation">
<ul class="pagination mb-0">
{% if page_obj.has_previous %}
<li class="page-item py-0">
<a class="page-link"
href="?page={{ page_obj.previous_page_number }}"
aria-label="Previous">
<span aria-hidden="true"><span class="fas fa-chevron-left"></span></span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true"><span class="fas fa-chevron-left"></span></span>
</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link"
href="?page={{ page_obj.next_page_number }}"
aria-label="Next">
<span aria-hidden="true"><span class="fas fa-chevron-right"></span></span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true"><span class="fas fa-chevron-right"></span></span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
{% endblock %} </div>
{% endblock %}

View File

@ -1,9 +1,10 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n static %} {% load i18n static %}
{% block title %} {% block title %}
{% trans 'Car Makes' %}{% endblock %} {% trans 'Car Makes' %}
{% block content %} {% endblock %}
<style> {% block content %}
<style>
/* Your existing CSS styles here */ /* Your existing CSS styles here */
.car-makes-grid { .car-makes-grid {
display: grid; display: grid;
@ -55,37 +56,36 @@
background: #f8f9fa; background: #f8f9fa;
border-radius: 8px; border-radius: 8px;
} }
</style> </style>
<h2 class="text-center text-primary">{{ _("Select Car Makes You Sell") }}</h2> <h2 class="text-center text-primary">{{ _("Select Car Makes You Sell") }}</h2>
<form method="post" class="mb-3" <form method="post"
action="{% url 'assign_car_makes' request.dealer.slug %}"> class="mb-3"
{% csrf_token %} action="{% url 'assign_car_makes' request.dealer.slug %}">
<div class="car-makes-grid"> {% csrf_token %}
{% for car_make in form.fields.car_makes.queryset %} <div class="car-makes-grid">
<label class="car-make-option"> {% for car_make in form.fields.car_makes.queryset %}
<input type="checkbox" <label class="car-make-option">
name="car_makes" <input type="checkbox"
value="{{ car_make.pk }}" name="car_makes"
{% if car_make.pk in form.initial.car_makes or car_make.pk|stringformat:"s" in form.car_makes.value %} value="{{ car_make.pk }}"
checked {% if car_make.pk in form.initial.car_makes or car_make.pk|stringformat:"s" in form.car_makes.value %} checked {% endif %}>
{% endif %}> <div class="car-make-image-container">
<div class="car-make-image-container"> {% if car_make.logo and car_make.logo.url %}
{% if car_make.logo and car_make.logo.url %} <img src="{{ car_make.logo.url }}"
<img src="{{ car_make.logo.url }}" alt="{{ car_make.name }}"
alt="{{ car_make.name }}" class="car-make-image">
class="car-make-image"> {% else %}
{% else %} <div class="logo-placeholder">{{ car_make.name }}</div>
<div class="logo-placeholder">{{ car_make.name }}</div> {% endif %}
{% endif %} </div>
</div> <div class="car-make-name">{{ car_make.get_local_name }}</div>
<div class="car-make-name">{{ car_make.get_local_name }}</div> </label>
</label> {% endfor %}
{% endfor %} </div>
</div> <div class="d-grid gap-2">
<div class="d-grid gap-2"> <button class="btn btn-outline-primary btn-lg" type="submit">
<button class="btn btn-outline-primary btn-lg" type="submit"> <i class="fa fa-save me-2"></i>{{ _("Save") }}
<i class="fa fa-save me-2"></i>{{ _("Save") }} </button>
</button> </div>
</div> </form>
</form> {% endblock %}
{% endblock %}

View File

@ -3,64 +3,92 @@
{% block title %} {% block title %}
{% trans 'Profile' %} {% trans 'Profile' %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="container-fluid mb-3"> <div class="container-fluid mb-3">
<div class="row align-items-center justify-content-between g-3 mb-4"> <div class="row align-items-center justify-content-between g-3 mb-4">
<div class="col-auto"> <div class="col-auto">
<h2 class="mb-0">{% trans 'Profile' %}</h2> <h2 class="mb-0">{% trans 'Profile' %}</h2>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<div class="dropdown"> <div class="dropdown">
<button class="btn btn-phoenix-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false"> <button class="btn btn-phoenix-primary dropdown-toggle"
<span class="fas fa-cog me-2"></span>{{ _("Manage Profile") }} type="button"
</button> data-bs-toggle="dropdown"
<ul class="dropdown-menu dropdown-menu-end"> aria-expanded="false">
<li><a class="dropdown-item" href="{% url 'dealer_update' dealer.slug %}"><span class="fas fa-edit me-2"></span>{{ _("Edit Profile") }}</a></li> <span class="fas fa-cog me-2"></span>{{ _("Manage Profile") }}
<li><a class="dropdown-item" href="{% url 'billing_info' %}"><span class="fas fa-credit-card me-2"></span>{{ _("Billing Information") }}</a></li> </button>
<li><a class="dropdown-item" href="{% url 'order_list' %}"><span class="fas fa-clipboard-list me-2"></span>{{ _("Plans History") }}</a></li> <ul class="dropdown-menu dropdown-menu-end">
<li><hr class="dropdown-divider"></li> <li>
<li><a class="dropdown-item text-danger" href="{% url 'account_change_password' %}"><span class="fas fa-key me-2"></span>{{ _("Change Password") }}</a></li> <a class="dropdown-item" href="{% url 'dealer_update' dealer.slug %}"><span class="fas fa-edit me-2"></span>{{ _("Edit Profile") }}</a>
</ul> </li>
<li>
<a class="dropdown-item" href="{% url 'billing_info' %}"><span class="fas fa-credit-card me-2"></span>{{ _("Billing Information") }}</a>
</li>
<li>
<a class="dropdown-item" href="{% url 'order_list' %}"><span class="fas fa-clipboard-list me-2"></span>{{ _("Plans History") }}</a>
</li>
<li>
<hr class="dropdown-divider">
</li>
<li>
<a class="dropdown-item text-danger"
href="{% url 'account_change_password' %}"><span class="fas fa-key me-2"></span>{{ _("Change Password") }}</a>
</li>
</ul>
</div>
</div> </div>
</div> </div>
</div> <div class="row g-3 mb-4">
<div class="col-12">
<div class="row g-3 mb-4"> <div class="card shadow-sm h-100">
<div class="col-12"> <div class="card-body">
<div class="card shadow-sm h-100"> <div class="d-flex flex-column flex-sm-row align-items-sm-center g-3 g-sm-5 text-center text-sm-start">
<div class="card-body"> <div class="col-12 col-sm-auto mb-3 mb-sm-0">
<div class="d-flex flex-column flex-sm-row align-items-sm-center g-3 g-sm-5 text-center text-sm-start"> <input class="d-none" id="avatarFile" type="file" />
<div class="col-12 col-sm-auto mb-3 mb-sm-0"> <label class="cursor-pointer avatar avatar-5xl border rounded-circle shadow-sm"
<input class="d-none" id="avatarFile" type="file" /> for="avatarFile">
<label class="cursor-pointer avatar avatar-5xl border rounded-circle shadow-sm" for="avatarFile"> {% if dealer.logo %}
{% if dealer.logo %} <img src="{{ dealer.logo.url }}"
<img src="{{ dealer.logo.url }}" alt="{{ dealer.get_local_name }}" class="rounded-circle" style="max-width: 150px" /> alt="{{ dealer.get_local_name }}"
{% else %} class="rounded-circle"
<img src="{% static 'images/logos/logo.png' %}" alt="{{ dealer.get_local_name }}" class="rounded-circle" style="max-width: 150px" /> style="max-width: 150px" />
{% endif %} {% else %}
</label> <img src="{% static 'images/logos/logo.png' %}"
</div> alt="{{ dealer.get_local_name }}"
class="rounded-circle"
<div class="flex-1 col-12 col-sm ms-2"> style="max-width: 150px" />
<h3>{{ dealer.get_local_name }}</h3> {% endif %}
<p class="text-body-secondary mb-1">{% trans 'Joined' %} {{ dealer.joined_at|timesince }} {% trans 'ago' %}</p> </label>
<span class="badge bg-primary-subtle text-primary">{% trans 'Last login' %}: {{ dealer.user.last_login|date:"D M d, Y H:i" }}</span>
</div>
<div class="col-12 col-sm-auto d-flex align-items-center justify-content-around flex-wrap mt-3 mt-sm-0">
<div class="text-center mx-3 mb-2 mb-sm-0">
<h6 class="mb-2 text-body-secondary">{% trans 'Total users'|capfirst %}</h6>
<h4 class="fs-7 text-body-highlight mb-2">{{ dealer.staff_count }} / {{ allowed_users }}</h4>
<div class="progress" style="height: 5px; width: 100px;">
<div class="progress-bar bg-success" role="progressbar" style="width: {{ dealer.staff_count|get_percentage:allowed_users }}%;" aria-valuenow="{{ dealer.staff_count|get_percentage:allowed_users }}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div> </div>
<div class="text-center mx-3 mb-2 mb-sm-0"> <div class="flex-1 col-12 col-sm ms-2">
<h6 class="mb-2 text-body-secondary">{% trans 'Total cars'|capfirst %}</h6> <h3>{{ dealer.get_local_name }}</h3>
<h4 class="fs-7 text-body-highlight mb-2">{{ cars_count }} / {{ allowed_cars }}</h4> <p class="text-body-secondary mb-1">{% trans 'Joined' %} {{ dealer.joined_at|timesince }} {% trans 'ago' %}</p>
<div class="progress" style="height: 5px; width: 100px;"> <span class="badge bg-primary-subtle text-primary">{% trans 'Last login' %}: {{ dealer.user.last_login|date:"D M d, Y H:i" }}</span>
<div class="progress-bar bg-info" role="progressbar" style="width: {{ cars_count|get_percentage:allowed_cars }}%;" aria-valuenow="{{ cars_count|get_percentage:allowed_cars }}" aria-valuemin="0" aria-valuemax="100"></div> </div>
<div class="col-12 col-sm-auto d-flex align-items-center justify-content-around flex-wrap mt-3 mt-sm-0">
<div class="text-center mx-3 mb-2 mb-sm-0">
<h6 class="mb-2 text-body-secondary">{% trans 'Total users'|capfirst %}</h6>
<h4 class="fs-7 text-body-highlight mb-2">{{ dealer.staff_count }} / {{ allowed_users }}</h4>
<div class="progress" style="height: 5px; width: 100px;">
<div class="progress-bar bg-success"
role="progressbar"
style="width: {{ dealer.staff_count|get_percentage:allowed_users }}%"
aria-valuenow="{{ dealer.staff_count|get_percentage:allowed_users }}"
aria-valuemin="0"
aria-valuemax="100"></div>
</div>
</div>
<div class="text-center mx-3 mb-2 mb-sm-0">
<h6 class="mb-2 text-body-secondary">{% trans 'Total cars'|capfirst %}</h6>
<h4 class="fs-7 text-body-highlight mb-2">{{ cars_count }} / {{ allowed_cars }}</h4>
<div class="progress" style="height: 5px; width: 100px;">
<div class="progress-bar bg-info"
role="progressbar"
style="width: {{ cars_count|get_percentage:allowed_cars }}%"
aria-valuenow="{{ cars_count|get_percentage:allowed_cars }}"
aria-valuemin="0"
aria-valuemax="100"></div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -68,64 +96,89 @@
</div> </div>
</div> </div>
</div> </div>
</div> <div class="row g-3">
<div class="col-12">
<div class="row g-3"> <div class="card shadow-sm">
<div class="col-12"> <div class="card-body">
<div class="card shadow-sm"> <ul class="nav nav-tabs nav-justified" id="profileTabs" role="tablist">
<div class="card-body"> <li class="nav-item" role="presentation">
<ul class="nav nav-tabs nav-justified" id="profileTabs" role="tablist"> <button class="nav-link active"
<li class="nav-item" role="presentation"> id="subscription-tab"
<button class="nav-link active" id="subscription-tab" data-bs-toggle="tab" data-bs-target="#subscription-pane" type="button" role="tab" aria-controls="subscription-pane" aria-selected="true"><span class="fas fa-star me-2"></span>{{ _("Plan & Subscription") }}</button> data-bs-toggle="tab"
</li> data-bs-target="#subscription-pane"
<li class="nav-item" role="presentation"> type="button"
<button class="nav-link" id="contact-tab" data-bs-toggle="tab" data-bs-target="#contact-pane" type="button" role="tab" aria-controls="contact-pane" aria-selected="false"><span class="fas fa-info-circle me-2"></span>{{ _("Company Details") }}</button> role="tab"
</li> aria-controls="subscription-pane"
<li class="nav-item" role="presentation"> aria-selected="true">
<button class="nav-link" id="makes-tab" data-bs-toggle="tab" data-bs-target="#makes-pane" type="button" role="tab" aria-controls="makes-pane" aria-selected="false"><span class="fas fa-car me-2"></span>{{ _("Car Brands") }}</button> <span class="fas fa-star me-2"></span>{{ _("Plan & Subscription") }}
</li> </button>
</ul> </li>
<div class="tab-content pt-4" id="profileTabsContent"> <li class="nav-item" role="presentation">
<div class="tab-pane fade show active" id="subscription-pane" role="tabpanel" aria-labelledby="subscription-tab"> <button class="nav-link"
<div class="row g-3"> id="contact-tab"
<div class="col-12 col-lg-6"> data-bs-toggle="tab"
<div class="card h-100 shadow-sm"> data-bs-target="#contact-pane"
<div class="card-body"> type="button"
<div class="d-flex align-items-center justify-content-between mb-3"> role="tab"
<h3 class="mb-0">{{ dealer.user.userplan.plan|capfirst }}</h3> aria-controls="contact-pane"
{% if dealer.user.userplan and not dealer.user.userplan.is_expired %} aria-selected="false">
<span class="badge bg-success-subtle text-success">{{ _("Active") }}</span> <span class="fas fa-info-circle me-2"></span>{{ _("Company Details") }}
{% elif dealer.user.userplan and dealer.user.userplan.is_expired %} </button>
<span class="badge bg-danger-subtle text-danger">{{ _("Expired") }}</span> </li>
{% else %} <li class="nav-item" role="presentation">
<span class="badge bg-warning-subtle text-warning">{{ _("No Active Plan") }}</span> <button class="nav-link"
{% endif %} id="makes-tab"
</div> data-bs-toggle="tab"
<p class="fs-9 text-body-secondary"> data-bs-target="#makes-pane"
{% if dealer.user.userplan and not dealer.user.userplan.is_expired %} type="button"
{% trans 'Active until' %}: {{ dealer.user.userplan.expire }} &nbsp; <small>{% trans 'Days left' %}: {{ dealer.user.userplan.days_left }}</small> role="tab"
{% else %} aria-controls="makes-pane"
{% trans 'Please subscribe or renew your plan to continue using our services.' %} aria-selected="false">
{% endif %} <span class="fas fa-car me-2"></span>{{ _("Car Brands") }}
</p> </button>
</li>
<div class="d-flex align-items-end mb-3"> </ul>
<h4 class="fw-bolder me-1"> <div class="tab-content pt-4" id="profileTabsContent">
{{ dealer.user.userplan.plan.planpricing_set.first.price }} <span class="icon-saudi_riyal"></span> <div class="tab-pane fade show active"
</h4> id="subscription-pane"
<h5 class="fs-9 fw-normal text-body-tertiary ms-1">{{ _("Per month") }}</h5> role="tabpanel"
</div> aria-labelledby="subscription-tab">
<div class="row g-3">
<ul class="list-unstyled mb-4"> <div class="col-12 col-lg-6">
{% for line in dealer.user.userplan.plan.description|splitlines %} <div class="card h-100 shadow-sm">
<li class="d-flex align-items-center mb-1"> <div class="card-body">
<span class="uil uil-check-circle text-success me-2"></span> <div class="d-flex align-items-center justify-content-between mb-3">
<span class="text-body-secondary">{{ line }}</span> <h3 class="mb-0">{{ dealer.user.userplan.plan|capfirst }}</h3>
</li> {% if dealer.user.userplan and not dealer.user.userplan.is_expired %}
{% endfor %} <span class="badge bg-success-subtle text-success">{{ _("Active") }}</span>
</ul> {% elif dealer.user.userplan and dealer.user.userplan.is_expired %}
<span class="badge bg-danger-subtle text-danger">{{ _("Expired") }}</span>
{% comment %} <div class="d-flex justify-content-end gap-2"> {% else %}
<span class="badge bg-warning-subtle text-warning">{{ _("No Active Plan") }}</span>
{% endif %}
</div>
<p class="fs-9 text-body-secondary">
{% if dealer.user.userplan and not dealer.user.userplan.is_expired %}
{% trans 'Active until' %}: {{ dealer.user.userplan.expire }} &nbsp; <small>{% trans 'Days left' %}: {{ dealer.user.userplan.days_left }}</small>
{% else %}
{% trans 'Please subscribe or renew your plan to continue using our services.' %}
{% endif %}
</p>
<div class="d-flex align-items-end mb-3">
<h4 class="fw-bolder me-1">
{{ dealer.user.userplan.plan.planpricing_set.first.price }} <span class="icon-saudi_riyal"></span>
</h4>
<h5 class="fs-9 fw-normal text-body-tertiary ms-1">{{ _("Per month") }}</h5>
</div>
<ul class="list-unstyled mb-4">
{% for line in dealer.user.userplan.plan.description|splitlines %}
<li class="d-flex align-items-center mb-1">
<span class="uil uil-check-circle text-success me-2"></span>
<span class="text-body-secondary">{{ line }}</span>
</li>
{% endfor %}
</ul>
{% comment %} <div class="d-flex justify-content-end gap-2">
{% if dealer.user.userplan.is_expired %} {% if dealer.user.userplan.is_expired %}
<a href="{% url 'pricing_page' request.dealer.slug %}" class="btn btn-warning"><span class="fas fa-redo-alt me-2"></span>{{ _("Renew") }}</a> <a href="{% url 'pricing_page' request.dealer.slug %}" class="btn btn-warning"><span class="fas fa-redo-alt me-2"></span>{{ _("Renew") }}</a>
{% endif %} {% endif %}
@ -135,114 +188,141 @@
{% if not dealer.user.userplan %} {% if not dealer.user.userplan %}
<a href="{% url 'pricing_page' request.dealer.slug %}" class="btn btn-success"><span class="fas fa-cart-plus me-2"></span>{{ _("Subscribe Now") }}</a> <a href="{% url 'pricing_page' request.dealer.slug %}" class="btn btn-success"><span class="fas fa-cart-plus me-2"></span>{{ _("Subscribe Now") }}</a>
{% endif %} {% endif %}
</div> {% endcomment %} </div> {% endcomment %}
<div class="d-flex justify-content-end gap-2"> <div class="d-flex justify-content-end gap-2">
{% if not dealer.user.userplan %} {% if not dealer.user.userplan %}
<a href="{% url 'pricing_page' request.dealer.slug %}" class="btn btn-outline-primary"><span class="fas fa-cart-plus me-2"></span>{{ _("Subscribe Now") }}</a> <a href="{% url 'pricing_page' request.dealer.slug %}"
class="btn btn-outline-primary"><span class="fas fa-cart-plus me-2"></span>{{ _("Subscribe Now") }}</a>
{% elif dealer.user.userplan.is_expired %} {% elif dealer.user.userplan.is_expired %}
<a href="{% url 'pricing_page' request.dealer.slug %}" class="btn btn-outline-warning"><span class="fas fa-redo-alt me-2"></span>{{ _("Renew") }}</a> <a href="{% url 'pricing_page' request.dealer.slug %}"
class="btn btn-outline-warning"><span class="fas fa-redo-alt me-2"></span>{{ _("Renew") }}</a>
{% elif dealer.user.userplan.plan.name != "Enterprise" %} {% elif dealer.user.userplan.plan.name != "Enterprise" %}
<a href="{% url 'pricing_page' request.dealer.slug %}" class="btn btn-outline-primary"><span class="fas fa-rocket me-2"></span>{{ _("Upgrade Plan") }}</a> <a href="{% url 'pricing_page' request.dealer.slug %}"
class="btn btn-outline-primary"><span class="fas fa-rocket me-2"></span>{{ _("Upgrade Plan") }}</a>
{% endif %} {% endif %}
</div>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="col-12 col-lg-6">
<div class="col-12 col-lg-6"> <div class="card h-100 shadow-sm">
<div class="card h-100 shadow-sm"> <div class="card-body d-flex flex-column justify-content-center">
<div class="card-body d-flex flex-column justify-content-center"> <div class="d-flex justify-content-between mb-4">
<div class="d-flex justify-content-between mb-4"> <h5 class="mb-0 text-body-highlight">{{ _("Manage Users & Cars") }}</h5>
<h5 class="mb-0 text-body-highlight">{{ _("Manage Users & Cars") }}</h5> </div>
<div class="mb-4">
<h6 class="text-body-secondary">{{ _("Total users") }}</h6>
<div class="progress" style="height: 10px;">
<div class="progress-bar bg-success"
role="progressbar"
style="width: {{ dealer.staff_count|get_percentage:allowed_users }}%"
aria-valuenow="{{ dealer.staff_count|get_percentage:allowed_users }}"
aria-valuemin="0"
aria-valuemax="100"></div>
</div>
<div class="d-flex justify-content-between text-body-secondary fs-9 mt-2">
<span>{{ _("Used") }}: {{ dealer.staff_count }}</span>
<span>{{ _("Limit") }}: {{ allowed_users }}</span>
</div>
</div>
<div class="mb-4">
<h6 class="text-body-secondary">{{ _("Total cars") }}</h6>
<div class="progress" style="height: 10px;">
<div class="progress-bar bg-info"
role="progressbar"
style="width: {{ cars_count|get_percentage:allowed_cars }}%"
aria-valuenow="{{ cars_count|get_percentage:allowed_cars }}"
aria-valuemin="0"
aria-valuemax="100"></div>
</div>
<div class="d-flex justify-content-between text-body-secondary fs-9 mt-2">
<span>{{ _("Used") }}: {{ cars_count }}</span>
<span>{{ _("Limit") }}: {{ allowed_cars }}</span>
</div>
</div>
<small class="text-body-secondary mt-auto">{{ _("Contact support to increase your limits") }}</small>
</div> </div>
<div class="mb-4">
<h6 class="text-body-secondary">{{ _("Total users") }}</h6>
<div class="progress" style="height: 10px;">
<div class="progress-bar bg-success" role="progressbar" style="width: {{ dealer.staff_count|get_percentage:allowed_users }}%;" aria-valuenow="{{ dealer.staff_count|get_percentage:allowed_users }}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<div class="d-flex justify-content-between text-body-secondary fs-9 mt-2">
<span>{{ _("Used") }}: {{ dealer.staff_count }}</span>
<span>{{ _("Limit") }}: {{ allowed_users }}</span>
</div>
</div>
<div class="mb-4">
<h6 class="text-body-secondary">{{ _("Total cars") }}</h6>
<div class="progress" style="height: 10px;">
<div class="progress-bar bg-info" role="progressbar" style="width: {{ cars_count|get_percentage:allowed_cars }}%;" aria-valuenow="{{ cars_count|get_percentage:allowed_cars }}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<div class="d-flex justify-content-between text-body-secondary fs-9 mt-2">
<span>{{ _("Used") }}: {{ cars_count }}</span>
<span>{{ _("Limit") }}: {{ allowed_cars }}</span>
</div>
</div>
<small class="text-body-secondary mt-auto">{{ _("Contact support to increase your limits") }}</small>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="tab-pane fade"
id="contact-pane"
<div class="tab-pane fade" id="contact-pane" role="tabpanel" aria-labelledby="contact-tab"> role="tabpanel"
<div class="row g-3"> aria-labelledby="contact-tab">
<div class="col-12 col-lg-6"> <div class="row g-3">
<div class="card h-100 shadow-sm"> <div class="col-12 col-lg-6">
<div class="card-body"> <div class="card h-100 shadow-sm">
<h5 class="mb-3">{% trans 'Contact Information' %}</h5> <div class="card-body">
<div class="d-flex align-items-center mb-3"> <h5 class="mb-3">{% trans 'Contact Information' %}</h5>
<span class="fas fa-location-dot me-3 text-primary"></span> <div class="d-flex align-items-center mb-3">
<div> <span class="fas fa-location-dot me-3 text-primary"></span>
<h6 class="mb-0">{% trans 'Address' %}</h6> <div>
<p class="mb-0 text-body-secondary">{{ dealer.address }}</p> <h6 class="mb-0">{% trans 'Address' %}</h6>
<p class="mb-0 text-body-secondary">{{ dealer.address }}</p>
</div>
</div> </div>
</div> <div class="d-flex align-items-center mb-3">
<div class="d-flex align-items-center mb-3"> <span class="fas fa-envelope me-3 text-info"></span>
<span class="fas fa-envelope me-3 text-info"></span> <div>
<div> <h6 class="mb-0">{% trans 'Email' %}</h6>
<h6 class="mb-0">{% trans 'Email' %}</h6> <p class="mb-0 text-body-secondary">{{ dealer.user.email }}</p>
<p class="mb-0 text-body-secondary">{{ dealer.user.email }}</p> </div>
</div> </div>
</div> <div class="d-flex align-items-center">
<div class="d-flex align-items-center"> <span class="fas fa-phone me-3 text-success"></span>
<span class="fas fa-phone me-3 text-success"></span> <div>
<div> <h6 class="mb-0">{% trans 'Phone' %}</h6>
<h6 class="mb-0">{% trans 'Phone' %}</h6> <p class="mb-0 text-body-secondary" dir="ltr">{{ dealer.phone_number }}</p>
<p class="mb-0 text-body-secondary" dir="ltr">{{ dealer.phone_number }}</p> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="col-12 col-lg-6">
<div class="col-12 col-lg-6"> <div class="card h-100 shadow-sm">
<div class="card h-100 shadow-sm"> <div class="card-body">
<div class="card-body"> <h5 class="mb-3">{{ _("VAT Information") }}</h5>
<h5 class="mb-3">{{ _("VAT Information") }}</h5> <form action="{% url 'dealer_vat_rate_update' request.dealer.slug %}"
<form action="{% url 'dealer_vat_rate_update' request.dealer.slug %}" method="post"> method="post">
{% csrf_token %} {% csrf_token %}
{{ vatform|crispy }} {{ vatform|crispy }}
<button class="btn btn-phoenix-primary mt-3" type="submit"><i class="fa-solid fa-pen-to-square me-1"></i>{% trans 'Update VAT' %}</button> <button class="btn btn-phoenix-primary mt-3" type="submit">
</form> <i class="fa-solid fa-pen-to-square me-1"></i>{% trans 'Update VAT' %}
</button>
</form>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="tab-pane fade"
id="makes-pane"
<div class="tab-pane fade" id="makes-pane" role="tabpanel" aria-labelledby="makes-tab"> role="tabpanel"
<div class="card h-100 shadow-sm"> aria-labelledby="makes-tab">
<div class="card-body"> <div class="card h-100 shadow-sm">
<h5 class="mb-4">{{ _("Makes you are selling") }}</h5> <div class="card-body">
<div class="d-flex flex-wrap gap-3 mb-4"> <h5 class="mb-4">{{ _("Makes you are selling") }}</h5>
{% for make in car_makes %} <div class="d-flex flex-wrap gap-3 mb-4">
<div class="text-center p-2 border rounded-3"> {% for make in car_makes %}
{% if make.logo %} <div class="text-center p-2 border rounded-3">
<img src="{{ make.logo.url }}" alt="{{ make.get_local_name }}" class="rounded" style="height: 48px; width: auto; background-color:white;" /> {% if make.logo %}
{% endif %} <img src="{{ make.logo.url }}"
<p class="fs-8 text-body-secondary mt-1 mb-0">{{ make.get_local_name }}</p> alt="{{ make.get_local_name }}"
</div> class="rounded"
{% empty %} style="height: 48px;
<p class="text-body-secondary">{{ _("No car makes selected.") }}</p> width: auto;
{% endfor %} background-color:white" />
{% endif %}
<p class="fs-8 text-body-secondary mt-1 mb-0">{{ make.get_local_name }}</p>
</div>
{% empty %}
<p class="text-body-secondary">{{ _("No car makes selected.") }}</p>
{% endfor %}
</div>
<a class="btn btn-phoenix-warning"
href="{% url 'assign_car_makes' request.dealer.slug %}"><span class="fas fa-plus me-2"></span>{{ _("Select Makes") }}</a>
</div> </div>
<a class="btn btn-phoenix-warning" href="{% url 'assign_car_makes' request.dealer.slug %}"><span class="fas fa-plus me-2"></span>{{ _("Select Makes") }}</a>
</div> </div>
</div> </div>
</div> </div>
@ -251,5 +331,4 @@
</div> </div>
</div> </div>
</div> </div>
</div> {% endblock %}
{% endblock %}

View File

@ -5,34 +5,38 @@
{{ _("Update Dealer Information") }} {{ _("Update Dealer Information") }}
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
<main class="d-flex align-items-center justify-content-center min-vh-80 py-5"> <main class="d-flex align-items-center justify-content-center min-vh-80 py-5">
<div class="col-md-8"> <div class="col-md-8">
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp"> <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"> <div class="card-header bg-gradient py-4 border-0 rounded-top-4">
<h3 class="mb-0 fs-4 fw-bold text-center"> <h3 class="mb-0 fs-4 fw-bold text-center">
{{ _("Update Dealer Information") }} {{ _("Update Dealer Information") }}
<i class="fas fa-car ms-2"></i> <i class="fas fa-car ms-2"></i>
</h3> </h3>
</div> </div>
<div class="card-body p-4 p-md-5"> <div class="card-body p-4 p-md-5">
<form hx-boost="false" method="post" enctype="multipart/form-data" class="needs-validation" novalidate> <form hx-boost="false"
{% csrf_token %} method="post"
{{ form|crispy }} enctype="multipart/form-data"
<hr class="my-4"> class="needs-validation"
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3"> novalidate>
<button class="btn btn-phoenix-primary btn-lg me-md-2" type="submit"> {% csrf_token %}
<i class="fa-solid fa-floppy-disk me-1"></i> {{ form|crispy }}
{{ _("Save") }} <hr class="my-4">
</button> <div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
<a href="{% url 'dealer_detail' request.dealer.slug %}" <button class="btn btn-phoenix-primary btn-lg me-md-2" type="submit">
class="btn btn-phoenix-secondary btn-lg"> <i class="fa-solid fa-floppy-disk me-1"></i>
<i class="fa-solid fa-ban me-1"></i> {{ _("Save") }}
{% trans "Cancel" %} </button>
</a> <a href="{% url 'dealer_detail' request.dealer.slug %}"
</div> class="btn btn-phoenix-secondary btn-lg">
</form> <i class="fa-solid fa-ban me-1"></i>
{% trans "Cancel" %}
</a>
</div>
</form>
</div>
</div> </div>
</div> </div>
</div> </main>
</main> {% endblock %}
{% endblock %}

View File

@ -170,10 +170,7 @@
valign="top" valign="top"
style="font-family: Open Sans, Helvetica, Arial, sans-serif; style="font-family: Open Sans, Helvetica, Arial, sans-serif;
padding-bottom: 30px"> padding-bottom: 30px">
<p style="color: #ffffff; <p style="color: #ffffff; font-size: 14px; line-height: 24px; margin: 0">{% trans 'Thank you for choosing us.' %}</p>
font-size: 14px;
line-height: 24px;
margin: 0">{% trans 'Thank you for choosing us.' %}</p>
</td> </td>
</tr> </tr>
</table> </table>

View File

@ -1,18 +1,18 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<body style="font-family: 'Segoe UI', Tahoma, sans-serif; direction: rtl;"> <body style="font-family: 'Segoe UI', Tahoma, sans-serif; direction: rtl;">
<p>مرحباً {{ user.get_full_name }}،</p> <p>مرحباً {{ user.get_full_name }}،</p>
<p>
<p> اشتراكك في <strong>{{ plan.name }}</strong> سينتهي خلال
اشتراكك في <strong>{{ plan.name }}</strong> سينتهي خلال {{ days_until_expire }} يوم في {{ expiration_date|date:"j F Y" }}.
{{ days_until_expire }} يوم في {{ expiration_date|date:"j F Y" }}. </p>
</p> <p>
<a href="{{ RENEWAL_URL }}">جدد اشتراكك الآن</a> لمواصلة الخدمة.
<p> </p>
<a href="{{ RENEWAL_URL }}">جدد اشتراكك الآن</a> لمواصلة الخدمة. <p>
</p> مع أطيب التحيات،
<br>
<p>مع أطيب التحيات،<br> فريق تنحل
فريق تنحل</p> </p>
</body> </body>
</html> </html>

View File

@ -1,14 +1,18 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<body style="font-family: Arial, sans-serif; direction: {{ direction }};"> <body style="font-family: Arial, sans-serif; direction: {{ direction }};">
<p>Hello {{ user.get_full_name }},</p> <p>Hello {{ user.get_full_name }},</p>
<p>
<p>Your <strong>{{ plan.name }}</strong> subscription will expire Your <strong>{{ plan.name }}</strong> subscription will expire
in {{ days_until_expire }} days on {{ expiration_date|date:"F j, Y" }}.</p> in {{ days_until_expire }} days on {{ expiration_date|date:"F j, Y" }}.
</p>
<p><a href="{{ RENEWAL_URL }}">Renew now</a> to continue service.</p> <p>
<a href="{{ RENEWAL_URL }}">Renew now</a> to continue service.
<p>Best regards,<br> </p>
The Team at Tenhal</p> <p>
</body> Best regards,
</html> <br>
The Team at Tenhal
</p>
</body>
</html>

View File

@ -1,34 +1,42 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<style> <style>
body { font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 0; } body { font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 0; }
.container { max-width: 600px; margin: 20px auto; background-color: #ffffff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); } .container { max-width: 600px; margin: 20px auto; background-color: #ffffff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); }
h2 { color: #333333; } h2 { color: #333333; }
p { color: #555555; line-height: 1.6; } p { color: #555555; line-height: 1.6; }
.footer { text-align: center; margin-top: 20px; font-size: 0.8em; color: #888888; } .footer { text-align: center; margin-top: 20px; font-size: 0.8em; color: #888888; }
.highlight { font-weight: bold; color: #007bff; } .highlight { font-weight: bold; color: #007bff; }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<h2>Hello {{ user_name }},</h2> <h2>Hello {{ user_name }},</h2>
<p>{% trans "This is a friendly reminder for your upcoming schedule" %}:</p> <p>{% trans "This is a friendly reminder for your upcoming schedule" %}:</p>
<p> <p>
<span class="highlight">{% trans "Purpose" %}:</span> {{ schedule_purpose }}<br> <span class="highlight">{% trans "Purpose" %}:</span> {{ schedule_purpose }}
<span class="highlight">{% trans "Scheduled At" %}:</span> {{ scheduled_at }}<br> <br>
<span class="highlight">{% trans "Type" %}:</span> {{ schedule_type }}<br> <span class="highlight">{% trans "Scheduled At" %}:</span> {{ scheduled_at }}
{% if customer_name != 'N/A' %}<span class="highlight">{% trans "Customer" %}:</span> {{ customer_name }}<br>{% endif %} <br>
{% if notes %}<span class="highlight">{% trans "Notes" %}:</span> {{ notes }}<br>{% endif %} <span class="highlight">{% trans "Type" %}:</span> {{ schedule_type }}
</p> <br>
<p>{% trans "Please be prepared for your schedule" %}.</p> {% if customer_name != 'N/A' %}
<p>{% trans "Thank you" %}!</p> <span class="highlight">{% trans "Customer" %}:</span> {{ customer_name }}
<p class="fs-4">{% trans "The team at Tenhal" %}.</p> <br>
<div class="footer"> {% endif %}
<p>{% trans "This is an automated reminder. Please do not reply to this email." %}</p> {% if notes %}
<span class="highlight">{% trans "Notes" %}:</span> {{ notes }}
<br>
{% endif %}
</p>
<p>{% trans "Please be prepared for your schedule" %}.</p>
<p>{% trans "Thank you" %}!</p>
<p class="fs-4">{% trans "The team at Tenhal" %}.</p>
<div class="footer">
<p>{% trans "This is an automated reminder. Please do not reply to this email." %}</p>
</div>
</div> </div>
</body>
</div> </html>
</body>
</html>

View File

@ -1,70 +1,62 @@
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
<style>
<style>
.empty-state-container {
.empty-state-container { background-color: #ffffff;
background-color: #ffffff; padding: 50px;
padding: 50px; border-radius: 5px; /* Rounded corners */
border-radius: 5px; /* Rounded corners */ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08); /* Subtle shadow */
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08); /* Subtle shadow */ text-align: center;
text-align: center; max-width: 70rem; /* Max width for content - made wider */
max-width: 70rem; /* Max width for content - made wider */ width: 90%; /* Fluid width */
width: 90%; /* Fluid width */ margin: 0px auto; /* Added margin-top and auto for horizontal centering */
margin: 0px auto; /* Added margin-top and auto for horizontal centering */ max-height: 80vh; /* Added min-height to control the height */
max-height: 80vh; /* Added min-height to control the height */ display: flex; /* Use flexbox for vertical centering of content */
display: flex; /* Use flexbox for vertical centering of content */ flex-direction: column; /* Stack children vertically */
flex-direction: column; /* Stack children vertically */ justify-content: center; /* Center content vertically */
justify-content: center; /* Center content vertically */ align-items: center; /* Center content horizontally */
align-items: center; /* Center content horizontally */ }
} .empty-state-image {
.empty-state-image { max-width: 50%; /* Responsive image size */
max-width: 50%; /* Responsive image size */ height: auto%;
height: auto%;
border-radius: 10px; /* Rounded corners for image */
border-radius: 10px; /* Rounded corners for image */ }
} .empty-state-title {
.empty-state-title { color: #343a40; /* Dark text for title */
color: #343a40; /* Dark text for title */ font-weight: 600;
font-weight: 600; margin-bottom: 15px;
margin-bottom: 15px; }
} .empty-state-text {
.empty-state-text { color: #6c757d; /* Muted text for description */
color: #6c757d; /* Muted text for description */ margin-bottom: 30px;
margin-bottom: 30px; line-height: 1.6;
line-height: 1.6; }
} /* No specific styles for .btn-add-new or .message-box are needed here as per previous updates */
/* No specific styles for .btn-add-new or .message-box are needed here as per previous updates */ </style>
</style> <div class="empty-state-container">
<!-- Empty State Illustration -->
{% if image %}
<div class="empty-state-container"> {% static image as final_image_path %}
<!-- Empty State Illustration --> {% else %}
{% static 'images/no_content/no_item.jpg' as final_image_path %}
{% if image %} {% endif %}
{% static image as final_image_path %} <p class="sm">
{% else %} <img src="{{ final_image_path }}"
{% static 'images/no_content/no_item.jpg' as final_image_path %} alt="No-empty-state-image"
{% endif %} class="empty-state-image">
<p class="sm"> <p>
<img src="{{ final_image_path }}" alt="No-empty-state-image" class="empty-state-image"> <!-- Title -->
<p> <h3 class="empty-state-title">{% blocktrans %}No {{ value}} Yet{% endblocktrans %}</h3>
<!-- Description -->
<!-- Title --> <p class="empty-state-text">
<h3 class="empty-state-title"> {% blocktrans %}It looks like you haven't added any {{ value }} to your account.
{% blocktrans %}No {{ value}} Yet{% endblocktrans %} Click the button below to get started and add your first {{ value }}!{% endblocktrans %}
</h3> </p>
<!-- Call to Action Button -->
<!-- Description --> <a class="btn btn-lg btn-primary" href="{{ url }}">
<p class="empty-state-text"> <i class="fa fa-plus me-2"></i>
{% blocktrans %}It looks like you haven't added any {{ value }} to your account. {% blocktrans %}Create New {{value}}{% endblocktrans %}
Click the button below to get started and add your first {{ value }}!{% endblocktrans %} </a>
</p> </div>
<!-- Call to Action Button -->
<a class="btn btn-lg btn-primary" href="{{ url }}">
<i class="fa fa-plus me-2"></i>
{% blocktrans %}Create New {{value}}{% endblocktrans %}
</a>
</div>

View File

@ -61,19 +61,19 @@
rel="stylesheet" rel="stylesheet"
id="user-style-default"> id="user-style-default">
<script> <script>
var phoenixIsRTL = window.config.config.phoenixIsRTL; var phoenixIsRTL = window.config.config.phoenixIsRTL;
if (phoenixIsRTL) { if (phoenixIsRTL) {
var linkDefault = document.getElementById('style-default'); var linkDefault = document.getElementById('style-default');
var userLinkDefault = document.getElementById('user-style-default'); var userLinkDefault = document.getElementById('user-style-default');
linkDefault.setAttribute('disabled', true); linkDefault.setAttribute('disabled', true);
userLinkDefault.setAttribute('disabled', true); userLinkDefault.setAttribute('disabled', true);
document.querySelector('html').setAttribute('dir', 'rtl'); document.querySelector('html').setAttribute('dir', 'rtl');
} else { } else {
var linkRTL = document.getElementById('style-rtl'); var linkRTL = document.getElementById('style-rtl');
var userLinkRTL = document.getElementById('user-style-rtl'); var userLinkRTL = document.getElementById('user-style-rtl');
linkRTL.setAttribute('disabled', true); linkRTL.setAttribute('disabled', true);
userLinkRTL.setAttribute('disabled', true); userLinkRTL.setAttribute('disabled', true);
} }
</script> </script>
</head> </head>
<body> <body>
@ -115,17 +115,17 @@
</div> </div>
</div> </div>
<script> <script>
var navbarTopStyle = window.config.config.phoenixNavbarTopStyle; var navbarTopStyle = window.config.config.phoenixNavbarTopStyle;
var navbarTop = document.querySelector('.navbar-top'); var navbarTop = document.querySelector('.navbar-top');
if (navbarTopStyle === 'darker') { if (navbarTopStyle === 'darker') {
navbarTop.setAttribute('data-navbar-appearance', 'darker'); navbarTop.setAttribute('data-navbar-appearance', 'darker');
} }
var navbarVerticalStyle = window.config.config.phoenixNavbarVerticalStyle; var navbarVerticalStyle = window.config.config.phoenixNavbarVerticalStyle;
var navbarVertical = document.querySelector('.navbar-vertical'); var navbarVertical = document.querySelector('.navbar-vertical');
if (navbarVertical && navbarVerticalStyle === 'darker') { if (navbarVertical && navbarVerticalStyle === 'darker') {
navbarVertical.setAttribute('data-navbar-appearance', 'darker'); navbarVertical.setAttribute('data-navbar-appearance', 'darker');
} }
</script> </script>
<div class="support-chat-row"> <div class="support-chat-row">
<div class="row-fluid support-chat"> <div class="row-fluid support-chat">

View File

@ -61,19 +61,19 @@
rel="stylesheet" rel="stylesheet"
id="user-style-default"> id="user-style-default">
<script> <script>
var phoenixIsRTL = window.config.config.phoenixIsRTL; var phoenixIsRTL = window.config.config.phoenixIsRTL;
if (phoenixIsRTL) { if (phoenixIsRTL) {
var linkDefault = document.getElementById('style-default'); var linkDefault = document.getElementById('style-default');
var userLinkDefault = document.getElementById('user-style-default'); var userLinkDefault = document.getElementById('user-style-default');
linkDefault.setAttribute('disabled', true); linkDefault.setAttribute('disabled', true);
userLinkDefault.setAttribute('disabled', true); userLinkDefault.setAttribute('disabled', true);
document.querySelector('html').setAttribute('dir', 'rtl'); document.querySelector('html').setAttribute('dir', 'rtl');
} else { } else {
var linkRTL = document.getElementById('style-rtl'); var linkRTL = document.getElementById('style-rtl');
var userLinkRTL = document.getElementById('user-style-rtl'); var userLinkRTL = document.getElementById('user-style-rtl');
linkRTL.setAttribute('disabled', true); linkRTL.setAttribute('disabled', true);
userLinkRTL.setAttribute('disabled', true); userLinkRTL.setAttribute('disabled', true);
} }
</script> </script>
</head> </head>
<body> <body>
@ -111,17 +111,17 @@
</div> </div>
</div> </div>
<script> <script>
var navbarTopStyle = window.config.config.phoenixNavbarTopStyle; var navbarTopStyle = window.config.config.phoenixNavbarTopStyle;
var navbarTop = document.querySelector('.navbar-top'); var navbarTop = document.querySelector('.navbar-top');
if (navbarTopStyle === 'darker') { if (navbarTopStyle === 'darker') {
navbarTop.setAttribute('data-navbar-appearance', 'darker'); navbarTop.setAttribute('data-navbar-appearance', 'darker');
} }
var navbarVerticalStyle = window.config.config.phoenixNavbarVerticalStyle; var navbarVerticalStyle = window.config.config.phoenixNavbarVerticalStyle;
var navbarVertical = document.querySelector('.navbar-vertical'); var navbarVertical = document.querySelector('.navbar-vertical');
if (navbarVertical && navbarVerticalStyle === 'darker') { if (navbarVertical && navbarVerticalStyle === 'darker') {
navbarVertical.setAttribute('data-navbar-appearance', 'darker'); navbarVertical.setAttribute('data-navbar-appearance', 'darker');
} }
</script> </script>
<div class="support-chat-row"> <div class="support-chat-row">
<div class="row-fluid support-chat"> <div class="row-fluid support-chat">

View File

@ -15,7 +15,6 @@
</div> </div>
</div> </div>
</footer> {% endcomment %} </footer> {% endcomment %}
{% comment %} <footer class="footer position-absolute fs-9 bg-info-subtle"> {% comment %} <footer class="footer position-absolute fs-9 bg-info-subtle">
<div class="row g-0 justify-content-between align-items-center h-100"> <div class="row g-0 justify-content-between align-items-center h-100">
<div class="col-12 col-sm-auto text-center text-warning"> <div class="col-12 col-sm-auto text-center text-warning">
@ -32,7 +31,6 @@
</div> </div>
</div> </div>
</footer> {% endcomment %} </footer> {% endcomment %}
{% comment %} <footer class="footer position-absolute fs-9 bg-white text-secondary"> {% comment %} <footer class="footer position-absolute fs-9 bg-white text-secondary">
<div class="row g-0 justify-content-between align-items-center h-100"> <div class="row g-0 justify-content-between align-items-center h-100">
<div class="col-12 col-sm-auto text-center"> <div class="col-12 col-sm-auto text-center">
@ -51,73 +49,64 @@
</div> </div>
</div> </div>
</footer> {% endcomment %} </footer> {% endcomment %}
<style>
.improved-footer {
<style>
.improved-footer {
/* Kept `position-absolute` and adjusted padding */ /* Kept `position-absolute` and adjusted padding */
position: absolute; position: absolute;
bottom: 0; bottom: 0;
width: 90%; width: 90%;
padding: 1.5rem; padding: 1.5rem;
border-top: 1px solid rgba(255, 255, 255, 0.05);
color: var(--text-color);
}
.improved-footer .text-body {
color: var(--text-color) !important;
font-weight: 400;
}
.improved-footer .fw-bold {
font-weight: 600 !important;
color: var(--link-color);
}
.improved-footer a {
color: var(--link-color) !important;
text-decoration: none;
transition: color 0.3s ease-in-out, transform 0.3s ease-in-out;
}
.improved-footer a:hover {
color: #d1d5db !important; /* A slightly softer white on hover */
transform: translateY(-2px);
}
.improved-footer .fas.fa-registered {
font-size: 0.8rem;
color: var(--text-color);
opacity: 0.6;
}
</style>
border-top: 1px solid rgba(255, 255, 255, 0.05);
<footer class="improved-footer"> color: var(--text-color);
<div class="container"> }
<div class="row g-0 justify-content-between align-items-center h-100">
<div class="col-12 col-sm-auto text-center"> .improved-footer .text-body {
<span class="text-body"> © 2025 All rights reserved</span> color: var(--text-color) !important;
<span class="fw-bold">Haikal</span>&nbsp;|&nbsp;<span class="fw-bold">هيكل</span> font-weight: 400;
</div> }
<div class="col-12 col-sm-auto text-center">
<span class="text-body">Powered by </span> .improved-footer .fw-bold {
<span> font-weight: 600 !important;
<a class="mx-1 text-secondary" href="https://tenhal.sa"> color: var(--link-color);
<span>TENHAL</span>&nbsp;|&nbsp;<span>تنحل</span> }
</a>
</span> .improved-footer a {
<span class="fas fa-registered fw-light"></span> color: var(--link-color) !important;
</div> text-decoration: none;
transition: color 0.3s ease-in-out, transform 0.3s ease-in-out;
}
.improved-footer a:hover {
color: #d1d5db !important; /* A slightly softer white on hover */
transform: translateY(-2px);
}
.improved-footer .fas.fa-registered {
font-size: 0.8rem;
color: var(--text-color);
opacity: 0.6;
}
</style>
<footer class="improved-footer">
<div class="container">
<div class="row g-0 justify-content-between align-items-center h-100">
<div class="col-12 col-sm-auto text-center">
<span class="text-body">© 2025 All rights reserved</span>
<span class="fw-bold">Haikal</span>&nbsp;|&nbsp;<span class="fw-bold">هيكل</span>
</div>
<div class="col-12 col-sm-auto text-center">
<span class="text-body">Powered by</span>
<span>
<a class="mx-1 text-secondary" href="https://tenhal.sa">
<span>TENHAL</span>&nbsp;|&nbsp;<span>تنحل</span>
</a>
</span>
<span class="fas fa-registered fw-light"></span>
</div> </div>
</div> </div>
</footer> </div>
</footer>

View File

@ -10,49 +10,51 @@
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<main class="d-flex align-items-center justify-content-center min-vh-80 py-5"> <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="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 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"> <div class="card-header bg-gradient py-4 border-0 rounded-top-4">
<h3 class="mb-0 fs-4 fw-bold text-center"> <h3 class="mb-0 fs-4 fw-bold text-center">
{% if object %} {% if object %}
{% trans "Update Group" %} {% trans "Update Group" %}
<i class="fa-solid fa-user-group ms-2"></i> <i class="fa-solid fa-user-group ms-2"></i>
{% else %} {% else %}
{% trans "Create Group" %} {% trans "Create Group" %}
<i class="fa-solid fa-user-plus ms-2"></i> <i class="fa-solid fa-user-plus ms-2"></i>
{% endif %} {% endif %}
</h3> </h3>
</div> </div>
<div class="card-body p-4 p-md-5"> <div class="card-body p-4 p-md-5">
<form method="post" class="needs-validation" novalidate> <form method="post" class="needs-validation" novalidate>
{% csrf_token %} {% csrf_token %}
{{ redirect_field }} {{ redirect_field }}
{{ form|crispy }} {{ form|crispy }}
{% if form.errors %}
{% if form.errors %} <div class="alert alert-danger mt-4" role="alert">
<div class="alert alert-danger mt-4" role="alert"> <h4 class="alert-heading small">{% trans "Please correct the following errors:" %}</h4>
<h4 class="alert-heading small">{% trans "Please correct the following errors:" %}</h4> <ul class="mb-0">
<ul class="mb-0"> {% for field, errors in form.errors.items %}
{% for field, errors in form.errors.items %} <li>
<li><strong>{{ field|capfirst }}:</strong> {% for error in errors %}{{ error }}{% endfor %}</li> <strong>{{ field|capfirst }}:</strong>
{% endfor %} {% for error in errors %}{{ error }}{% endfor %}
</ul> </li>
{% endfor %}
</ul>
</div>
{% endif %}
<hr class="my-4">
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
<button class="btn btn-phoenix-primary btn-lg md-me-2" type="submit">
<i class="fa-solid fa-floppy-disk me-1"></i>{% trans "Save" %}
</button>
<a href="{% url 'group_list' request.dealer.slug %}"
class="btn btn-phoenix-secondary btn-lg">
<i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}
</a>
</div> </div>
{% endif %} </form>
</div>
<hr class="my-4">
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
<button class="btn btn-phoenix-primary btn-lg md-me-2" type="submit">
<i class="fa-solid fa-floppy-disk me-1"></i>{% trans "Save" %}
</button>
<a href="{% url 'group_list' request.dealer.slug %}" class="btn btn-phoenix-secondary btn-lg">
<i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}
</a>
</div>
</form>
</div> </div>
</div> </div>
</div> </main>
</main> {% endblock %}
{% endblock %}

View File

@ -6,70 +6,73 @@
{% trans "Groups" %} {% trans "Groups" %}
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
<main class="py-5"> <main class="py-5">
<div class="container"> <div class="container">
{% if groups or request.GET.q %} {% if groups or request.GET.q %}
<div class="card border-0 rounded-4 animate__animated animate__fadeInUp"> <div class="card border-0 rounded-4 animate__animated animate__fadeInUp">
<div class="card-header border-bottom d-flex flex-column flex-md-row justify-content-between align-items-md-center p-4"> <div class="card-header border-bottom d-flex flex-column flex-md-row justify-content-between align-items-md-center p-4">
<h5 class="card-title mb-2 mb-md-0 me-md-4 fw-bold"> <i class="fa-solid fa-user-group fs-3 me-1 text-primary "></i>{% trans "Groups" %}</h5> <h5 class="card-title mb-2 mb-md-0 me-md-4 fw-bold">
<div class="d-flex gap-2"> <i class="fa-solid fa-user-group fs-3 me-1 text-primary "></i>{% trans "Groups" %}
<a href="{% url 'group_create' request.dealer.slug %}" </h5>
class="btn btn-phoenix-primary"> <div class="d-flex gap-2">
<i class="fa-solid fa-user-group fs-9 me-1"></i> <a href="{% url 'group_create' request.dealer.slug %}"
<span class="fas fa-plus me-2"></span>{% trans "Add Group" %} class="btn btn-phoenix-primary">
</a> <i class="fa-solid fa-user-group fs-9 me-1"></i>
<a href="{% url 'user_list' request.dealer.slug %}" <span class="fas fa-plus me-2"></span>{% trans "Add Group" %}
class="btn btn-phoenix-secondary"> </a>
<span class="fas fas fa-arrow-left me-2"></span>{% trans "Back to Staffs" %} <a href="{% url 'user_list' request.dealer.slug %}"
</a> class="btn btn-phoenix-secondary">
</div> <span class="fas fas fa-arrow-left me-2"></span>{% trans "Back to Staffs" %}
</div> </a>
<div class="card-body p-0">
<div class="table-responsive scrollbar mx-n1 px-1 mt-3">
<table class="table align-items-center table-hover mb-0">
<thead>
<tr class="bg-light">
<th scope="col" class="text-secondary text-uppercase fw-bold ps-4">{% trans 'name'|capfirst %}</th>
<th scope="col" class="text-secondary text-uppercase fw-bold">{% trans 'total Users'|capfirst %}</th>
<th scope="col" class="text-secondary text-uppercase fw-bold">{% trans 'total permission'|capfirst %}</th>
<th scope="col" class="text-secondary text-uppercase fw-bold text-end pe-4">{% trans 'actions'|capfirst %}</th>
</tr>
</thead>
<tbody>
{% for group in groups %}
<tr>
<td class="align-middle white-space-nowrap ps-4">{{ group.name }}</td>
<td class="align-middle white-space-nowrap">
<i class="fa-solid fa-users me-1"></i> {{ group.users.count }}
</td>
<td class="align-middle white-space-nowrap">
<i class="fa-solid fa-unlock me-1"></i> {{ group.permissions.count }}
</td>
<td class="align-middle white-space-nowrap text-end pe-4">
<a class="btn btn-phoenix-secondary btn-sm"
href="{% url 'group_detail' request.dealer.slug group.id %}">
<i class="fa-solid fa-eye me-1"></i>
{% trans 'view'|capfirst %}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% if page_obj.paginator.num_pages > 1 %}
<div class="card-footer bg-light border-top">
<div class="d-flex justify-content-end">
{% include 'partials/pagination.html' %}
</div> </div>
</div> </div>
{% endif %} <div class="card-body p-0">
</div> <div class="table-responsive scrollbar mx-n1 px-1 mt-3">
{% else %} <table class="table align-items-center table-hover mb-0">
{% url "group_create" request.dealer.slug as create_group_url %} <thead>
{% include "empty-illustration-page.html" with value="group" url=create_group_url %} <tr class="bg-light">
{% endif %} <th scope="col" class="text-secondary text-uppercase fw-bold ps-4">{% trans 'name'|capfirst %}</th>
</div> <th scope="col" class="text-secondary text-uppercase fw-bold">{% trans 'total Users'|capfirst %}</th>
</main> <th scope="col" class="text-secondary text-uppercase fw-bold">{% trans 'total permission'|capfirst %}</th>
{% endblock %} <th scope="col"
class="text-secondary text-uppercase fw-bold text-end pe-4">
{% trans 'actions'|capfirst %}
</th>
</tr>
</thead>
<tbody>
{% for group in groups %}
<tr>
<td class="align-middle white-space-nowrap ps-4">{{ group.name }}</td>
<td class="align-middle white-space-nowrap">
<i class="fa-solid fa-users me-1"></i> {{ group.users.count }}
</td>
<td class="align-middle white-space-nowrap">
<i class="fa-solid fa-unlock me-1"></i> {{ group.permissions.count }}
</td>
<td class="align-middle white-space-nowrap text-end pe-4">
<a class="btn btn-phoenix-secondary btn-sm"
href="{% url 'group_detail' request.dealer.slug group.id %}">
<i class="fa-solid fa-eye me-1"></i>
{% trans 'view'|capfirst %}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% if page_obj.paginator.num_pages > 1 %}
<div class="card-footer bg-light border-top">
<div class="d-flex justify-content-end">{% include 'partials/pagination.html' %}</div>
</div>
{% endif %}
</div>
{% else %}
{% url "group_create" request.dealer.slug as create_group_url %}
{% include "empty-illustration-page.html" with value="group" url=create_group_url %}
{% endif %}
</div>
</main>
{% endblock %}

View File

@ -21,144 +21,138 @@
</div> </div>
</div> </div>
</div> </div>
<div> <div>
<form method="post" novalidate> <form method="post" novalidate>
{% csrf_token %} {% csrf_token %}
<div class="row mb-4">
<div class="row mb-4"> <div class="col-md-6">
<div class="col-md-6"> <div class="input-group">
<div class="input-group"> <span class="input-group-text"><i class="fas fa-search"></i></span>
<span class="input-group-text"><i class="fas fa-search"></i></span> <input type="text"
<input type="text" class="form-control" id="permissionSearch" class="form-control"
placeholder="{% trans 'Search permissions...' %}"> id="permissionSearch"
</div> placeholder="{% trans 'Search permissions...' %}">
</div>
<div class="col-md-6">
<div class="alert alert-info py-2 mb-0">
<i class="fas fa-info-circle me-2"></i>
{% trans "Checked items are currently assigned permissions" %}
</div>
</div>
</div>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4" id="permissionsGrid">
{% for app_label, models in grouped_permissions.items %}
<div class="col"> {# This div opens for each app_label #}
<div class="card h-100 border-{% if app_label in group_permission_apps %}primary{% else %}light{% endif %}">
<div class="card-header bg-{% if app_label in group_permission_apps %}primary text-white{% else %}light{% endif %}">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
<i class="fas fa-{% if app_label in group_permission_apps %}check-circle{% else %}cube{% endif %} me-2"></i>
{{ app_label|capfirst }}
</h5>
<span class="badge bg-{% if app_label in group_permission_apps %}light text-primary{% else %}secondary{% endif %}">
{{ models|length }} {% trans "categories" %}
</span>
</div>
</div> </div>
<div class="card-body"> </div>
<div class="accordion" id="accordion-{{ app_label|slugify }}"> <div class="col-md-6">
{% for model, perms in models.items %} <div class="alert alert-info py-2 mb-0">
<div class="accordion-item border-0 mb-2"> <i class="fas fa-info-circle me-2"></i>
<h6 class="accordion-header" id="heading-{{ app_label|slugify }}-{{ model|slugify }}"> {% trans "Checked items are currently assigned permissions" %}
<button class="accordion-button collapsed bg-white shadow-none py-2" </div>
type="button" </div>
data-bs-toggle="collapse" </div>
data-bs-target="#collapse-{{ app_label|slugify }}-{{ model|slugify }}" <div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4"
aria-expanded="false" id="permissionsGrid">
aria-controls="collapse-{{ app_label|slugify }}-{{ model|slugify }}"> {% for app_label, models in grouped_permissions.items %}
<i class="fas fa-{% if model == 'Custom' %}star{% else %}table{% endif %} me-2"></i> <div class="col">
{{ model|capfirst }} {# This div opens for each app_label #}
<span class="badge bg-{% if model in group_permission_models %}primary{% else %}secondary{% endif %} rounded-pill ms-2"> <div class="card h-100 border-{% if app_label in group_permission_apps %}primary{% else %}light{% endif %}">
{# This is where you might need the custom filter 'count_checked' #} <div class="card-header bg-{% if app_label in group_permission_apps %}primary text-white{% else %}light{% endif %}">
{{ perms|length }} / {{ perms|count_checked:group_permission_ids }} <div class="d-flex justify-content-between align-items-center">
</span> <h5 class="card-title mb-0">
</button> <i class="fas fa-{% if app_label in group_permission_apps %}check-circle{% else %}cube{% endif %} me-2"></i>
</h6> {{ app_label|capfirst }}
<div id="collapse-{{ app_label|slugify }}-{{ model|slugify }}" </h5>
class="accordion-collapse collapse" <span class="badge bg-{% if app_label in group_permission_apps %}light text-primary{% else %}secondary{% endif %}">
aria-labelledby="heading-{{ app_label|slugify }}-{{ model|slugify }}" {{ models|length }} {% trans "categories" %}
data-bs-parent="#accordion-{{ app_label|slugify }}"> </span>
<div class="accordion-body pt-0 px-0">
<div class="list-group list-group-flush">
{% for perm in perms %}
<label class="list-group-item d-flex gap-2 {% if perm.id in group_permission_ids %}bg-light-primary{% endif %}">
<input class="form-check-input flex-shrink-0 mt-0"
type="checkbox"
name="permissions"
value="{{ perm.id }}"
id="perm_{{ perm.id }}"
{% if perm.id in group_permission_ids %}checked{% endif %}>
<span>
<span class="d-block fw-bold">{{ perm.name|capfirst }}</span>
<small class="d-block text-muted">{{ perm.codename }}</small>
{% if model == 'Custom' %}
<span class="badge bg-info mt-1">
<i class="fas fa-star me-1"></i>{% trans "Custom" %}
</span>
{% elif perm.id in group_permission_ids %}
<span class="badge bg-success mt-1">
<i class="fas fa-check me-1"></i>{% trans "Assigned" %}
</span>
{% endif %}
</span>
</label>
{% endfor %}
</div>
</div>
</div> </div>
</div> </div>
{% endfor %} <div class="card-body">
<div class="accordion" id="accordion-{{ app_label|slugify }}">
{% for model, perms in models.items %}
<div class="accordion-item border-0 mb-2">
<h6 class="accordion-header"
id="heading-{{ app_label|slugify }}-{{ model|slugify }}">
<button class="accordion-button collapsed bg-white shadow-none py-2"
type="button"
data-bs-toggle="collapse"
data-bs-target="#collapse-{{ app_label|slugify }}-{{ model|slugify }}"
aria-expanded="false"
aria-controls="collapse-{{ app_label|slugify }}-{{ model|slugify }}">
<i class="fas fa-{% if model == 'Custom' %}star{% else %}table{% endif %} me-2"></i>
{{ model|capfirst }}
<span class="badge bg-{% if model in group_permission_models %}primary{% else %}secondary{% endif %} rounded-pill ms-2">
{# This is where you might need the custom filter 'count_checked' #}
{{ perms|length }} / {{ perms|count_checked:group_permission_ids }}
</span>
</button>
</h6>
<div id="collapse-{{ app_label|slugify }}-{{ model|slugify }}"
class="accordion-collapse collapse"
aria-labelledby="heading-{{ app_label|slugify }}-{{ model|slugify }}"
data-bs-parent="#accordion-{{ app_label|slugify }}">
<div class="accordion-body pt-0 px-0">
<div class="list-group list-group-flush">
{% for perm in perms %}
<label class="list-group-item d-flex gap-2 {% if perm.id in group_permission_ids %}bg-light-primary{% endif %}">
<input class="form-check-input flex-shrink-0 mt-0"
type="checkbox"
name="permissions"
value="{{ perm.id }}"
id="perm_{{ perm.id }}"
{% if perm.id in group_permission_ids %}checked{% endif %}>
<span>
<span class="d-block fw-bold">{{ perm.name|capfirst }}</span>
<small class="d-block text-muted">{{ perm.codename }}</small>
{% if model == 'Custom' %}
<span class="badge bg-info mt-1">
<i class="fas fa-star me-1"></i>{% trans "Custom" %}
</span>
{% elif perm.id in group_permission_ids %}
<span class="badge bg-success mt-1">
<i class="fas fa-check me-1"></i>{% trans "Assigned" %}
</span>
{% endif %}
</span>
</label>
{% endfor %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="row mt-4 mb-4">
<div class="col">
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="badge bg-primary rounded-pill me-2">{{ group_permission_ids|length }} {% trans "selected" %}</span>
<span class="text-muted">{% trans "Permissions will be updated immediately" %}</span>
</div>
<div>
<button type="submit" class="btn btn-lg btn-primary me-2">
<i class="fas fa-save me-1"></i>{% trans "Save Changes" %}
</button>
</div> </div>
</div> </div>
</div> </div>
</div>
{% endfor %}
</div>
<div class="row mt-4 mb-4">
<div class="col">
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="badge bg-primary rounded-pill me-2">
{{ group_permission_ids|length }} {% trans "selected" %}
</span>
<span class="text-muted">
{% trans "Permissions will be updated immediately" %}
</span>
</div>
<div>
<button type="submit" class="btn btn-lg btn-primary me-2">
<i class="fas fa-save me-1"></i>{% trans "Save Changes" %}
</button>
</div>
</div>
</div> </div>
</div> </form>
</form> </div>
</div> <style>
.bg-light-primary {
<style> background-color: rgba(13, 110, 253, 0.1);
.bg-light-primary { }
background-color: rgba(13, 110, 253, 0.1); .list-group-item:hover {
} background-color: rgba(0, 0, 0, 0.03);
.list-group-item:hover { }
background-color: rgba(0, 0, 0, 0.03); .accordion-button:not(.collapsed) {
} box-shadow: none;
.accordion-button:not(.collapsed) { background-color: transparent;
box-shadow: none; }
background-color: transparent; .accordion-button:focus {
} box-shadow: none;
.accordion-button:focus { border-color: rgba(0,0,0,.125);
box-shadow: none; }
border-color: rgba(0,0,0,.125); </style>
} <script>
</style> document.addEventListener('DOMContentLoaded', function() {
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize all accordions // Initialize all accordions
document.querySelectorAll('.accordion-button').forEach(button => { document.querySelectorAll('.accordion-button').forEach(button => {
button.addEventListener('click', function() { button.addEventListener('click', function() {

View File

@ -71,20 +71,12 @@
<div class="d-flex gap-3"> <div class="d-flex gap-3">
<span id="clearChatBtn" <span id="clearChatBtn"
class="translate-middle-y cursor-pointer" class="translate-middle-y cursor-pointer"
title="{% if LANGUAGE_CODE == 'ar' %} title="{% if LANGUAGE_CODE == 'ar' %} مسح المحادثة {% else %} Clear Chat {% endif %}">
مسح المحادثة
{% else %}
Clear Chat
{% endif %}">
<i class="fas fa-trash-alt text-danger"></i> <i class="fas fa-trash-alt text-danger"></i>
</span> </span>
<span id="exportChatBtn" <span id="exportChatBtn"
class="translate-middle-y cursor-pointer" class="translate-middle-y cursor-pointer"
title="{% if LANGUAGE_CODE == 'ar' %} title="{% if LANGUAGE_CODE == 'ar' %} تصدير المحادثة {% else %} Export Chat {% endif %}">
تصدير المحادثة
{% else %}
Export Chat
{% endif %}">
<i class="fas fa-download text-success"></i> <i class="fas fa-download text-success"></i>
</span> </span>
</div> </div>

File diff suppressed because it is too large Load Diff

View File

@ -3,11 +3,7 @@
{% block content %} {% block content %}
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<div id="dashboard-content" <div id="dashboard-content"
hx-get="{% if request.is_sales and not request.is_manager %} hx-get="{% if request.is_sales and not request.is_manager %} {% url 'sales_dashboard' request.dealer.slug %} {% else %} {% url 'general_dashboard' request.dealer.slug %} {% endif %}"
{% url 'sales_dashboard' request.dealer.slug %}
{% else %}
{% url 'general_dashboard' request.dealer.slug %}
{% endif %}"
hx-trigger="load" hx-trigger="load"
hx-target="#dashboard-content" hx-target="#dashboard-content"
hx-swap="innerHTML"> hx-swap="innerHTML">
@ -17,6 +13,4 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -1,67 +1,69 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n %} {% load i18n %}
{%block title%} {%trans 'Add Colors'%} {% endblock%} {% block title %}
{% block content %} {% trans 'Add Colors' %} {% endblock %}
<div class="row mt-4 mb-3"> {% block content %}
<h3 class="text-center">{% trans "Add Colors" %}</h3> <div class="row mt-4 mb-3">
<p class="text-center"> <h3 class="text-center">{% trans "Add Colors" %}</h3>
{% trans "Select exterior and interior colors for" %} {{ car.id_car_make.get_local_name }} {{ car.id_car_model.get_local_name }} <p class="text-center">
</p> {% trans "Select exterior and interior colors for" %} {{ car.id_car_make.get_local_name }} {{ car.id_car_model.get_local_name }}
<form method="post"> </p>
{% csrf_token %} <form method="post">
<!-- Exterior Colors --> {% csrf_token %}
<div class="row g-4"> <!-- Exterior Colors -->
<p class="fs-5 mb-0">{% trans 'Exterior Colors' %}</p> <div class="row g-4">
{% for color in form.fields.exterior.queryset %} <p class="fs-5 mb-0">{% trans 'Exterior Colors' %}</p>
<div class="col-lg-4 col-xl-2"> {% for color in form.fields.exterior.queryset %}
<div class="card rounded shadow-sm color-card"> <div class="col-lg-4 col-xl-2">
<label class="color-option"> <div class="card rounded shadow-sm color-card">
<input class="color-radio" <label class="color-option">
type="radio" <input class="color-radio"
name="exterior" type="radio"
value="{{ color.id }}" {% if color.id == form.instance.exterior.id %}checked{% endif %}> name="exterior"
value="{{ color.id }}"
<div class="card-body color-display" {% if color.id == form.instance.exterior.id %}checked{% endif %}>
style="background-color: rgb({{ color.rgb }})"> <div class="card-body color-display"
<div class=""> style="background-color: rgb({{ color.rgb }})">
<small>{{ color.get_local_name }}</small> <div class="">
<small>{{ color.get_local_name }}</small>
</div>
</div> </div>
</div> </label>
</label> </div>
</div> </div>
</div> {% endfor %}
{% endfor %} <!-- Interior Colors -->
<!-- Interior Colors --> <p class="fs-5 mt-3 mb-0">{% trans 'Interior Colors' %}</p>
<p class="fs-5 mt-3 mb-0">{% trans 'Interior Colors' %}</p> {% for color in form.fields.interior.queryset %}
{% for color in form.fields.interior.queryset %} <div class="col-lg-4 col-xl-2">
<div class="col-lg-4 col-xl-2"> <div class="card rounded shadow-sm color-card">
<div class="card rounded shadow-sm color-card"> <label class="color-option">
<label class="color-option"> <input class="color-radio"
<input class="color-radio" type="radio"
type="radio" name="interior"
name="interior" value="{{ color.id }}"
value="{{ color.id }}" {% if color.id == form.instance.interior.id %}checked{% endif %}> {% if color.id == form.instance.interior.id %}checked{% endif %}>
<div class="card-body color-display" <div class="card-body color-display"
style="background-color: rgb({{ color.rgb }})"> style="background-color: rgb({{ color.rgb }})">
<div class=""> <div class="">
<small>{{ color.get_local_name }}</small> <small>{{ color.get_local_name }}</small>
</div>
</div> </div>
</div> </label>
</label> </div>
</div> </div>
</div> {% endfor %}
{% endfor %} </div>
</div> <div class="d-flex justify-content-center mt-4">
<button class="btn btn-lg btn-phoenix-primary me-2" type="submit">
<div class="d-flex justify-content-center mt-4"> <i class="fa-solid fa-floppy-disk me-1"></i>{{ _("Save") }}
<button class="btn btn-lg btn-phoenix-primary me-2" type="submit"> </button>
<i class="fa-solid fa-floppy-disk me-1"></i>{{ _("Save") }} <a href="{% url 'car_detail' request.dealer.slug car.slug %}"
</button> class="btn btn-lg btn-phoenix-secondary"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
<a href="{% url 'car_detail' request.dealer.slug car.slug %}" class="btn btn-lg btn-phoenix-secondary"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a> </div>
</div> </form>
</form> </div>
</div> <style>
<style>
.color-card { .color-card {
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
@ -93,7 +95,7 @@
} }
.color-radio:focus + .color-display { .color-radio:focus + .color-display {
border: 3px solid rgb(44, 229, 44); border: 3px solid rgb(44, 229, 44);
box-shadow: 0 0 10px rgba(44, 123, 229, 0.5); box-shadow: 0 0 10px rgba(44, 123, 229, 0.5);
} }

View File

@ -1,41 +1,34 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n %} {% load i18n %}
{% block title %}Delete Car{% endblock %} {% block title %}Delete Car{% endblock %}
{% block content %} {% block content %}
<main class="d-flex align-items-center justify-content-center min-vh-50 py-5"> <main class="d-flex align-items-center justify-content-center min-vh-50 py-5">
<div class="col-md-6 "> <div class="col-md-6 ">
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp"> <div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
<div class="card-body p-4 p-md-5 text-center bg-gradient">
<div class="card-body p-4 p-md-5 text-center bg-gradient"> <div class="mb-4">
<div class="mb-4"> <i class="fa-solid fa-triangle-exclamation text-danger"
<i class="fa-solid fa-triangle-exclamation text-danger" style="font-size: 2rem;"></i> style="font-size: 2rem"></i>
</div>
<h1 class="card-title fw-bold mb-3 fs-4">{% trans 'Confirm Deletion' %}</h1>
<p class="fs-7 mb-4">{% trans "Are you absolutely sure you want to delete the car" %}</p>
<p class="fs-6 mb-4">
"<strong class="">{{ car }}</strong>"?
</p>
<p class="fs-7 mb-4">{% trans "This action is permanent and cannot be undone." %}</p>
<form method="post"
class="d-grid gap-3 d-sm-flex justify-content-sm-center">
{% csrf_token %}
<button type="submit" class="btn btn-phoenix-danger btn-lg px-5">
<i class="fa-solid fa-trash-can me-2"></i>{% trans 'Confirm Delete' %}
</button>
<a href="{% url 'car_list' request.dealer.slug %}"
class="btn btn-phoenix-secondary btn-lg px-5">
<i class="fa-solid fa-ban me-2"></i>{% trans 'Cancel' %}
</a>
</form>
</div> </div>
<h1 class="card-title fw-bold mb-3 fs-4">{% trans 'Confirm Deletion' %}</h1>
<p class="fs-7 mb-4">
{% trans "Are you absolutely sure you want to delete the car" %}
</p>
<p class="fs-6 mb-4">
"<strong class="">{{ car }}</strong>"?
</p>
<p class="fs-7 mb-4">
{% trans "This action is permanent and cannot be undone." %}
</p>
<form method="post" class="d-grid gap-3 d-sm-flex justify-content-sm-center">
{% csrf_token %}
<button type="submit" class="btn btn-phoenix-danger btn-lg px-5">
<i class="fa-solid fa-trash-can me-2"></i>{% trans 'Confirm Delete' %}
</button>
<a href="{% url 'car_list' request.dealer.slug %}" class="btn btn-phoenix-secondary btn-lg px-5">
<i class="fa-solid fa-ban me-2"></i>{% trans 'Cancel' %}
</a>
</form>
</div> </div>
</div> </div>
</div> </main>
</main> {% endblock %}
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More