Compare commits

..

No commits in common. "2bbcae2e7a79d6b6fb16d6c8f67e1c2cfc843561" and "1b5d0fbf7d06f60893e1d43027d3cf04725bdfe3" have entirely different histories.

199 changed files with 13126 additions and 13795 deletions

View File

@ -10,11 +10,9 @@ 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()
@ -32,17 +30,11 @@ 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(
"http": AuthMiddlewareStack( URLRouter([
URLRouter( path("sse/notifications/", NotificationSSEApp()),
[ 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,16 +146,9 @@ class StaffForm(forms.ModelForm):
) )
class Meta: class Meta:
model = Staff model = Staff
fields = [ fields = ["first_name","last_name", "arabic_name", "phone_number", "address", "logo", "group"]
"first_name",
"last_name",
"arabic_name",
"phone_number",
"address",
"logo",
"group",
]
# Dealer Form # Dealer Form
@ -446,15 +439,13 @@ 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( raise forms.ValidationError({"cost_price": "Cost price should not be greater than marked price"})
{"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):
@ -1177,7 +1168,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
@ -1298,7 +1289,6 @@ 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.
@ -1315,13 +1305,17 @@ 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
@ -1639,7 +1633,8 @@ 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.journalentrymodeldjango_ledger.purchaseordermodel", "django_ledger.journalentrymodel"
"django_ledger.purchaseordermodel",
] ]
permissions = cache.get( permissions = cache.get(
@ -2143,115 +2138,91 @@ 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( widget=forms.PasswordInput(attrs={'class': 'form-control', 'placeholder': 'New Password'})
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( widget=forms.PasswordInput(attrs={'class': 'form-control', 'placeholder': 'Confirm New Password'})
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( self.fields['model'].queryset = CarModel.objects.filter(id_car_make_id=make_id)
id_car_make_id=make_id
)
if model_id: if model_id:
self.fields["serie"].queryset = CarSerie.objects.filter( self.fields['serie'].queryset = CarSerie.objects.filter(id_car_model_id=model_id)
id_car_model_id=model_id
)
if serie_id: if serie_id:
self.fields["trim"].queryset = CarTrim.objects.filter( self.fields['trim'].queryset = CarTrim.objects.filter(id_car_serie_id=serie_id)
id_car_serie_id=serie_id
)
class RecallCreateForm(forms.ModelForm): class RecallCreateForm(forms.ModelForm):
class Meta: class Meta:
model = Recall model = Recall
fields = [ fields = ['title', 'description', 'make', 'model', 'serie', 'trim', 'year_from', 'year_to']
"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 = [("resolved", "Resolved"), ("closed", "Closed")] self.fields['status'].choices = [
('resolved', 'Resolved'),
('closed', 'Closed')
]

View File

@ -1,10 +1,9 @@
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"]
@ -18,8 +17,7 @@ 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,12 +8,11 @@ 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"
@ -34,30 +33,27 @@ 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( message = render_to_string('emails/invoice_past_due_reminder.txt', {
"emails/invoice_past_due_reminder.txt", 'customer_name': inv.customer.customer_name,
{ 'invoice_number': inv.invoice_number,
"customer_name": inv.customer.customer_name, 'amount_due': inv.amount_due,
"invoice_number": inv.invoice_number, 'days_past_due': inv.due_in_days(),
"amount_due": inv.amount_due, 'SITE_NAME': settings.SITE_NAME
"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,
@ -69,24 +65,21 @@ 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( message = render_to_string('emails/invoice_past_due.txt', {
"emails/invoice_past_due.txt", 'customer_name': inv.customer.customer_name,
{ 'invoice_number': inv.invoice_number,
"customer_name": inv.customer.customer_name, 'amount_due': inv.amount_due,
"invoice_number": inv.invoice_number, 'days_past_due': (today - inv.date_due).days,
"amount_due": inv.amount_due, 'SITE_NAME': settings.SITE_NAME
"days_past_due": (today - inv.date_due).days, })
"SITE_NAME": settings.SITE_NAME,
},
)
# send notification to accountatnt # send notification to accountatnt
recipients = ( recipients = (
@ -97,28 +90,24 @@ 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={ kwargs={"dealer_slug": dealer.slug, "entity_slug": dealer.entity.slug, "pk": inv.pk},
"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,
@ -142,4 +131,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,11 +2,9 @@ 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 = ""
@ -16,43 +14,27 @@ 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 = ( car = i.get_itemtxs_data()[0].filter(
i.get_itemtxs_data()[0] item_model__car__vin=car_data['vin']
.filter(item_model__car__vin=car_data["vin"]) ).first().item_model.car
.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( net_add_price = Decimal(data['total_additionals']) # per car or split however you want
data["total_additionals"] vat_amount = Decimal(data['total_vat_amount']) * qty # prorate if multi-qty
) # 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( 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)
"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")
@ -94,4 +76,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,11 +2,8 @@ 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 = ""
@ -28,4 +25,4 @@ class Command(BaseCommand):
) )
order.complete_order() order.complete_order()
print(user.userplan) print(user.userplan)

View File

@ -11,7 +11,6 @@ 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"
@ -31,18 +30,17 @@ 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, expire=target_date active=True,
).select_related("user", "plan") expire=target_date
).select_related('user', 'plan')
self.stdout.write( self.stdout.write(f"Queuing {days}-day reminders for {expiring_plans.count()} plans")
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
@ -52,13 +50,14 @@ 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, expire__lt=timezone.now().date() active=True,
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")
@ -67,6 +66,7 @@ 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, status=Order.STATUS.NEW created__lt=cutoff,
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,10 +5,5 @@ 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( async_task(func="inventory.tasks.test_task",dealer=instance,hook="inventory.hooks.print_results")
func="inventory.tasks.test_task",
dealer=instance,
hook="inventory.hooks.print_results",
)

View File

@ -3,24 +3,21 @@ 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( parser.add_argument('--count', type=int, default=1, help='Number of dealers to seed')
"--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):
@ -46,16 +43,7 @@ class Command(BaseCommand):
"address": f"Street {n}, Riyadh", "address": f"Street {n}, Riyadh",
} }
dealer = create_user_dealer( dealer = create_user_dealer(payload['email'], payload['password'], payload['name'], payload['arabic_name'], payload['phone_number'], payload['crn'], payload['vrn'], payload['address'])
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)
@ -73,7 +61,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.
""" """
@ -84,13 +72,14 @@ 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, defaults={"plan": plan, "active": True} user=user,
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",
@ -125,5 +114,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,31 +4,11 @@ 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 ( from inventory.models import AdditionalServices, Car, CarColors, CarFinance, CarMake, CustomGroup, Customer, Dealer, ExteriorColors, InteriorColors, Lead, UnitOfMeasure,Vendor,Staff
AdditionalServices, from django_ledger.models import PurchaseOrderModel,ItemTransactionModel,ItemModel,EntityModel
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
@ -36,7 +16,6 @@ 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"
@ -52,6 +31,7 @@ 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)")
@ -63,32 +43,20 @@ 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( e.create_purchase_order(po_title=f"Test PO {random.randint(1,9999)}-{i}")
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( 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}")
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
@ -97,9 +65,7 @@ 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( user = User.objects.create_user(username=email, email=email, password=password)
username=email, email=email, password=password
)
user.is_staff = True user.is_staff = True
user.save() user.save()
@ -108,24 +74,17 @@ 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( staff = Staff.objects.create(dealer=dealer,user=user,name=name,arabic_name=name,phone_number=fake.phone_number(),active=True)
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 = [
@ -144,20 +103,18 @@ 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( model = make.carmodel_set.filter(name__contains=result["model"]).first()
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,
@ -171,12 +128,9 @@ 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, car=car, cost_price=cp, selling_price=0,marked_price=cp+random.randint(2000, 7000)
cost_price=cp,
selling_price=0,
marked_price=cp + random.randint(2000, 7000),
) )
CarColors.objects.create( CarColors.objects.create(
car=car, car=car,
@ -187,8 +141,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,
@ -207,7 +161,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",
@ -242,11 +196,12 @@ 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):
for i in range(random.randint(1, 60)): def _create_random_lead(self,dealer):
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()
@ -269,7 +224,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,
@ -288,4 +243,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,22 +152,11 @@ 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/", "/ar/signup/", "/en/signup/", "/ar/login/", "/en/login/",
"/en/signup/", "/ar/logout/", "/en/logout/", "/en/ledger/", "/ar/ledger/",
"/ar/login/", "/en/notifications/", "/ar/notifications/", "/en/appointment/",
"/en/login/", "/ar/appointment/", "/en/feature/recall/","/ar/feature/recall/",
"/ar/logout/", "/ar/help_center/", "/en/help_center/",
"/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,14 +42,12 @@ 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
@ -231,9 +229,7 @@ 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 = models.ImageField(_("logo"), upload_to="car_make", blank=True, null=True,default="user-logo.jpg")
_("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)
@ -593,7 +589,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
@ -608,7 +604,9 @@ 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 Decimal(self.price * vat.rate) return (
Decimal(self.price * vat.rate)
)
class Meta: class Meta:
verbose_name = _("Additional Services") verbose_name = _("Additional Services")
@ -685,13 +683,10 @@ 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, max_digits=14, decimal_places=2, verbose_name=_("Cost Price"),default=Decimal("0.00")
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,
@ -715,7 +710,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")
) )
@ -778,7 +773,6 @@ 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()
@ -793,15 +787,9 @@ class Car(Base):
) )
except Exception: except Exception:
return False return False
@property @property
def invoice(self): def invoice(self):
return ( return self.item_model.invoicemodel_set.first if self.item_model.invoicemodel_set.first() else None
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()
@ -885,54 +873,39 @@ 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( return sum([Decimal((x.price)*(vat.rate)) for x in self.additional_services.filter(taxable=True)])
[
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 { return {"services": [[x,((x.price)*(vat.rate) if x.taxable else 0)] for x in self.additional_services.all()],
"services": [ "total_":self.get_additional_services_amount_,
[x, ((x.price) * (vat.rate) if x.taxable else 0)] "total":self.get_additional_services_amount,
for x in self.additional_services.all() "services_vat":self.get_additional_services_vat}
],
"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):
@ -940,32 +913,29 @@ 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( return Decimal(self.final_price_plus_vat) + Decimal(self.get_additional_services()['total_']) #total services with vat and car_sell price with vat
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:
@ -991,6 +961,10 @@ 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",
@ -1027,7 +1001,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")
@ -1337,11 +1311,7 @@ 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( phone_number = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator()])
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")
) )
@ -1395,7 +1365,6 @@ 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:
@ -1446,7 +1415,6 @@ 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")
@ -1461,17 +1429,15 @@ 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, on_delete=models.CASCADE, related_name="staff") user = models.OneToOneField(
User, on_delete=models.CASCADE, related_name="staff"
)
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="staff") 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( phone_number = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator()])
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")
) )
@ -1479,11 +1445,7 @@ 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", upload_to="logos/staff", blank=True, null=True, verbose_name=_("Image"),default="default-image/user.jpg"
blank=True,
null=True,
verbose_name=_("Image"),
default="default-image/user.jpg",
) )
thumbnail = ImageSpecField( thumbnail = ImageSpecField(
source="logo", source="logo",
@ -1518,7 +1480,6 @@ 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
@ -1583,7 +1544,8 @@ class Staff(models.Model):
permissions = [] permissions = []
constraints = [ constraints = [
models.UniqueConstraint( models.UniqueConstraint(
fields=["dealer", "user"], name="unique_staff_email_per_dealer" fields=['dealer', 'user'],
name='unique_staff_email_per_dealer'
) )
] ]
@ -1686,11 +1648,7 @@ 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, User, on_delete=models.CASCADE, related_name="customer_profile", null=True, blank=True
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")
@ -1718,11 +1676,7 @@ 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/", upload_to="customers/", blank=True, null=True, verbose_name=_("Image"),default="default-image/user-jpg"
blank=True,
null=True,
verbose_name=_("Image"),
default="default-image/user-jpg",
) )
thumbnail = ImageSpecField( thumbnail = ImageSpecField(
source="image", source="image",
@ -1754,7 +1708,8 @@ class Customer(models.Model):
class Meta: class Meta:
constraints = [ constraints = [
models.UniqueConstraint( models.UniqueConstraint(
fields=["dealer", "email"], name="unique_customer_email_per_dealer" fields=['dealer', 'email'],
name='unique_customer_email_per_dealer'
) )
] ]
verbose_name = _("Customer") verbose_name = _("Customer")
@ -1824,13 +1779,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
@ -1867,11 +1822,7 @@ 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, User, on_delete=models.CASCADE, related_name="organization_profile", null=True, blank=True
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"))
@ -1880,20 +1831,12 @@ 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( phone_number = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator()])
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", upload_to="logos", blank=True, null=True, verbose_name=_("Logo"),default="default-image/user.jpg"
blank=True,
null=True,
verbose_name=_("Logo"),
default="default-image/user.jpg",
) )
thumbnail = ImageSpecField( thumbnail = ImageSpecField(
source="logo", source="logo",
@ -2022,11 +1965,7 @@ 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( phone_number = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator()])
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")
@ -2046,11 +1985,7 @@ 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( phone_number = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator()])
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")
) )
@ -2240,9 +2175,8 @@ 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:
@ -2312,7 +2246,6 @@ 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():
@ -2322,7 +2255,6 @@ 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")
@ -2741,19 +2673,11 @@ 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( phone_number = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator()])
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", upload_to="logos/vendors", blank=True, null=True, verbose_name=_("Logo"),default="default-image/user.jpg"
blank=True,
null=True,
verbose_name=_("Logo"),
default="default-image/user.jpg",
) )
thumbnail = ImageSpecField( thumbnail = ImageSpecField(
source="logo", source="logo",
@ -3301,6 +3225,7 @@ class CustomGroup(models.Model):
"activity", "activity",
"payment", "payment",
"vendor", "vendor",
], ],
other_perms=[ other_perms=[
"view_car", "view_car",
@ -3311,7 +3236,8 @@ 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(
@ -3607,7 +3533,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 []
@ -3620,13 +3546,11 @@ 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( ).union(cls.objects.filter(
cls.objects.filter( dealer=dealer,
dealer=dealer, content_type=ContentType.objects.get_for_model(EstimateModel),
content_type=ContentType.objects.get_for_model(EstimateModel), related_content_type=ContentType.objects.get_for_model(User),
related_content_type=ContentType.objects.get_for_model(User), ))
)
)
else: else:
qs = cls.objects.filter( qs = cls.objects.filter(
dealer=dealer, dealer=dealer,
@ -3635,17 +3559,7 @@ 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( 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()])
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
@ -3658,7 +3572,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 []
@ -3671,13 +3585,11 @@ 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( ).union(cls.objects.filter(
cls.objects.filter( dealer=dealer,
dealer=dealer, content_type=content_type,
content_type=content_type, related_content_type=ContentType.objects.get_for_model(User),
related_content_type=ContentType.objects.get_for_model(User), ))
)
)
else: else:
qs = cls.objects.filter( qs = cls.objects.filter(
dealer=dealer, dealer=dealer,
@ -3696,16 +3608,32 @@ 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, models.DO_NOTHING, verbose_name=_("Make"), null=True, blank=True CarMake,
models.DO_NOTHING,
verbose_name=_("Make"),
null=True,
blank=True
) )
model = models.ForeignKey( model = models.ForeignKey(
CarModel, models.DO_NOTHING, verbose_name=_("Model"), null=True, blank=True CarModel,
models.DO_NOTHING,
verbose_name=_("Model"),
null=True,
blank=True
) )
serie = models.ForeignKey( serie = models.ForeignKey(
CarSerie, models.DO_NOTHING, verbose_name=_("Series"), null=True, blank=True CarSerie,
models.DO_NOTHING,
verbose_name=_("Series"),
null=True,
blank=True
) )
trim = models.ForeignKey( trim = models.ForeignKey(
CarTrim, models.DO_NOTHING, verbose_name=_("Trim"), null=True, blank=True CarTrim,
models.DO_NOTHING,
verbose_name=_("Trim"),
null=True,
blank=True
) )
year_from = models.IntegerField(verbose_name=_("From Year"), null=True, blank=True) year_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)
@ -3715,7 +3643,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:
@ -3725,16 +3653,11 @@ 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 = models.ForeignKey(Recall, on_delete=models.CASCADE, related_name='notifications')
Recall, on_delete=models.CASCADE, related_name="notifications" dealer = models.ForeignKey("Dealer", on_delete=models.CASCADE, related_name='recall_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")
@ -3743,30 +3666,27 @@ 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( priority = models.CharField(max_length=20, choices=PRIORITY_CHOICES, default='medium')
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)
@ -3777,7 +3697,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
@ -3809,11 +3729,9 @@ class Ticket(models.Model):
class CarImage(models.Model): class CarImage(models.Model):
car = models.OneToOneField( car = models.OneToOneField('Car', on_delete=models.CASCADE, related_name='generated_image')
"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)
@ -3834,5 +3752,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,7 +87,6 @@ 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()
@ -96,24 +95,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, id__gt=last_id, is_read=False user=user,
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":
@ -144,17 +143,15 @@ 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",
"type": "http.response.body", "body": b":keep-alive\n\n",
"body": b":keep-alive\n\n", "more_body": True
"more_body": True, })
}
)
# await asyncio.sleep(3) # await asyncio.sleep(3)
@ -164,18 +161,16 @@ 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",
"type": "http.response.start", "status": 200,
"status": 200, "headers": [
"headers": [ (b"content-type", b"text/event-stream"),
(b"content-type", b"text/event-stream"), (b"cache-control", b"no-cache"),
(b"cache-control", b"no-cache"), (b"connection", b"keep-alive"),
(b"connection", b"keep-alive"), (b"x-accel-buffering", b"no"),
(b"x-accel-buffering", b"no"), ]
], })
}
)
async def _send_notification(self, send, notification): async def _send_notification(self, send, notification):
try: try:
@ -184,25 +179,27 @@ 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",
"type": "http.response.body", "body": event_str.encode("utf-8"),
"body": event_str.encode("utf-8"), "more_body": True
"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",
"type": "http.response.start", "status": status,
"status": status, "headers": [(b"content-type", b"text/plain")]
"headers": [(b"content-type", b"text/plain")], })
} await send({
) "type": "http.response.body",
await send({"type": "http.response.body", "body": body}) "body": body
})
async def _close_connection(self, send): async def _close_connection(self, send):
await send({"type": "http.response.body", "body": b""}) await send({
"type": "http.response.body",
"body": b""
})

View File

@ -20,12 +20,7 @@ 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 ( from django_ledger.models import ItemTransactionModel,InvoiceModel,LedgerModel,EntityModel
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 (
@ -40,28 +35,17 @@ 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 ( from django_ledger.models import PurchaseOrderModel, EstimateModel, BillModel, ChartOfAccountModel
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 ( from django_ledger.forms.invoice import (BaseInvoiceModelUpdateForm, InvoiceModelCreateForEstimateForm,
BaseInvoiceModelUpdateForm, get_invoice_itemtxs_formset_class,
InvoiceModelCreateForEstimateForm, DraftInvoiceModelUpdateForm, InReviewInvoiceModelUpdateForm,
get_invoice_itemtxs_formset_class, ApprovedInvoiceModelUpdateForm, PaidInvoiceModelUpdateForm,
DraftInvoiceModelUpdateForm, AccruedAndApprovedInvoiceModelUpdateForm, InvoiceModelCreateForm)
InReviewInvoiceModelUpdateForm,
ApprovedInvoiceModelUpdateForm,
PaidInvoiceModelUpdateForm,
AccruedAndApprovedInvoiceModelUpdateForm,
InvoiceModelCreateForm,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -87,11 +71,7 @@ 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"] = [ context["po_ready_to_fulfill"] = [item for item in po_model.get_itemtxs_data()[0] if item.po_item_status == 'received']
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)
@ -796,12 +776,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
@ -822,137 +802,115 @@ 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( messages.add_message(self.request,
self.request, messages.ERROR,
messages.ERROR, f'Warning! This invoice is locked. Must unlock before making any changes.',
f"Warning! This invoice is locked. Must unlock before making any changes.", extra_tags='is-danger')
extra_tags="is-danger",
)
if ledger_model.locked: if ledger_model.locked:
messages.add_message( messages.add_message(self.request,
self.request, messages.ERROR,
messages.ERROR, f'Warning! This Invoice is Locked. Must unlock before making any changes.',
f"Warning! This Invoice is Locked. Must unlock before making any changes.", extra_tags='is-danger')
extra_tags="is-danger",
)
if not ledger_model.is_posted(): if not ledger_model.is_posted():
messages.add_message( messages.add_message(self.request,
self.request, messages.INFO,
messages.INFO, f'This Invoice has not been posted. Must post to see ledger changes.',
f"This Invoice has not been posted. Must post to see ledger changes.", extra_tags='is-info')
extra_tags="is-info",
)
if not itemtxs_formset: if not itemtxs_formset:
itemtxs_qs = invoice_model.itemtransactionmodel_set.all().select_related( itemtxs_qs = invoice_model.itemtransactionmodel_set.all().select_related('item_model')
"item_model" itemtxs_qs, itemtxs_agg = invoice_model.get_itemtxs_data(queryset=itemtxs_qs)
) invoice_itemtxs_formset_class = get_invoice_itemtxs_formset_class(invoice_model)
itemtxs_qs, itemtxs_agg = invoice_model.get_itemtxs_data(
queryset=itemtxs_qs
)
invoice_itemtxs_formset_class = get_invoice_itemtxs_formset_class(
invoice_model
)
itemtxs_formset = invoice_itemtxs_formset_class( itemtxs_formset = invoice_itemtxs_formset_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,
invoice_model=invoice_model, invoice_model=invoice_model,
queryset=itemtxs_qs, queryset=itemtxs_qs
) )
else: else:
itemtxs_qs, itemtxs_agg = invoice_model.get_itemtxs_data( itemtxs_qs, itemtxs_agg = invoice_model.get_itemtxs_data(queryset=itemtxs_formset.queryset)
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( return reverse('invoice_detail',
"invoice_detail", kwargs={
kwargs={ 'dealer_slug': self.request.dealer.slug,
"dealer_slug": self.request.dealer.slug, 'entity_slug': entity_slug,
"entity_slug": entity_slug, 'pk': invoice_pk
"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 = ( self.queryset = InvoiceModel.objects.for_entity(
InvoiceModel.objects.for_entity( entity_slug=self.kwargs['entity_slug'],
entity_slug=self.kwargs["entity_slug"], user_model=self.request.user user_model=self.request.user
) ).select_related('customer', 'ledger').order_by('-created')
.select_related("customer", "ledger") return super().get_queryset().prefetch_related('itemtransactionmodel_set')
.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( messages.add_message(self.request,
self.request, messages.SUCCESS,
messages.SUCCESS, f'Invoice {self.object.invoice_number} successfully updated.',
f"Invoice {self.object.invoice_number} successfully updated.", extra_tags='is-success')
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( redirect_to=reverse('invoice_update',
"invoice_update", kwargs={
kwargs={ 'dealer_slug': request.dealer.slug,
"dealer_slug": request.dealer.slug, 'entity_slug': entity_slug,
"entity_slug": entity_slug, 'pk': invoice_pk
"pk": invoice_pk, })
},
)
) )
return super(InvoiceModelUpdateView, self).get(request, *args, **kwargs) return super(InvoiceModelUpdateView, self).get(request, *args, **kwargs)
@ -964,22 +922,18 @@ 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_itemtxs_formset_class = get_invoice_itemtxs_formset_class(invoice_model)
invoice_model itemtxs_formset = invoice_itemtxs_formset_class(request.POST,
) user_model=self.request.dealer.user,
itemtxs_formset = invoice_itemtxs_formset_class( invoice_model=invoice_model,
request.POST, entity_slug=entity_slug)
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)
@ -987,12 +941,8 @@ 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( entity_qs = EntityModel.objects.for_user(user_model=self.request.dealer.user)
user_model=self.request.dealer.user entity_model: EntityModel = get_object_or_404(entity_qs, slug__exact=entity_slug)
)
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
@ -1003,72 +953,53 @@ 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=[ update_fields=['amount_due',
"amount_due", 'amount_receivable',
"amount_receivable", 'amount_unearned',
"amount_unearned", 'amount_earned',
"amount_earned", 'updated']
"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( messages.add_message(request,
request, message=f'Items for Invoice {invoice_model.invoice_number} saved.',
message=f"Items for Invoice {invoice_model.invoice_number} saved.", level=messages.SUCCESS,
level=messages.SUCCESS, extra_tags='is-success')
extra_tags="is-success",
)
return HttpResponseRedirect( return HttpResponseRedirect(
redirect_to=reverse( redirect_to=reverse('django_ledger:invoice-update',
"django_ledger:invoice-update", kwargs={
kwargs={ 'entity_slug': entity_slug,
"entity_slug": entity_slug, 'invoice_pk': invoice_pk
"invoice_pk": invoice_pk, })
},
)
) )
# if not valid, return formset with errors... # if not valid, return formset with errors...
return self.render_to_response( return self.render_to_response(context=self.get_context_data(itemtxs_formset=itemtxs_formset))
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(
LoginRequiredMixin, PermissionRequiredMixin class ChartOfAccountModelModelBaseViewMixIn(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( self.queryset = entity_model.chartofaccountmodel_set.all().order_by('-updated')
"-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( return reverse('coa-list', kwargs={'dealer_slug': self.request.dealer.slug,
"coa-list", 'entity_slug': self.request.entity.slug})
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):
@ -1079,116 +1010,84 @@ 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"] = ( context['page_title'] = 'Inactive Chart of Account List' if self.inactive else 'Chart of Accounts List'
"Inactive Chart of Account List" context['header_title'] = 'Inactive Chart of Account List' if self.inactive else 'Chart of Accounts 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, **self.get_form_kwargs() entity_model=self.request.entity,
**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"] = ( context['header_subtitle'] = f'New Chart of Accounts: {self.request.entity.name}'
f"New Chart of Accounts: {self.request.entity.name}" context['header_subtitle_icon'] = 'gravity-ui:hierarchy'
)
context["header_subtitle_icon"] = "gravity-ui:hierarchy"
return context return context
def get_success_url(self): def get_success_url(self):
return reverse( return reverse('coa-list', kwargs={'dealer_slug': self.request.dealer.slug,
"coa-list", 'entity_slug': self.request.entity.slug})
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"] = ( context['page_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}'
)
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( return reverse('coa-list', kwargs={'dealer_slug': self.request.dealer.slug,
"coa-list", 'entity_slug': self.request.entity.slug})
kwargs={
"dealer_slug": self.request.dealer.slug,
"entity_slug": self.request.entity.slug,
},
)
class CharOfAccountModelActionView( class CharOfAccountModelActionView(ChartOfAccountModelModelBaseViewMixIn,
ChartOfAccountModelModelBaseViewMixIn, RedirectView, SingleObjectMixin RedirectView,
): 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( response = super(CharOfAccountModelActionView, self).get(request, *args, **kwargs)
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( messages.add_message(request, level=messages.SUCCESS, extra_tags='is-success',
request, message=_('Successfully updated {} Default Chart of Account to '.format(
level=messages.SUCCESS, request.entity.name) +
extra_tags="is-success", '{}'.format(coa_model.name)))
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( messages.add_message(request,
request, message=e.message, level=messages.ERROR, extra_tags="is-danger" message=e.message,
) level=messages.ERROR,
extra_tags='is-danger')
return response return response

View File

@ -28,7 +28,6 @@ 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
@ -178,11 +177,7 @@ 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( async_task(func="inventory.tasks.create_coa_accounts",dealer=instance,hook="inventory.hooks.check_create_coa_accounts")
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)
@ -278,14 +273,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()
@ -1044,11 +1039,7 @@ 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={ kwargs={"dealer_slug": dealer.slug,"entity_slug":instance.entity.slug, "pk": instance.pk},
"dealer_slug": dealer.slug,
"entity_slug": instance.entity.slug,
"pk": instance.pk,
},
), ),
), ),
) )
@ -1095,10 +1086,7 @@ 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={ kwargs={"dealer_slug": instance.dealer.slug, "pk": instance.estimate.pk},
"dealer_slug": instance.dealer.slug,
"pk": instance.estimate.pk,
},
), ),
), ),
) )
@ -1143,7 +1131,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},
), )
), ),
) )
@ -1200,11 +1188,7 @@ 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={ kwargs={"dealer_slug": dealer.slug, "entity_slug": dealer.entity.slug, "bill_pk": instance.pk},
"dealer_slug": dealer.slug,
"entity_slug": dealer.entity.slug,
"bill_pk": instance.pk,
},
), ),
), ),
) )
@ -1240,6 +1224,7 @@ 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:
@ -1264,23 +1249,20 @@ 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={ kwargs={"dealer_slug": instance.dealer.slug, "ticket_id": instance.pk},
"dealer_slug": instance.dealer.slug, ),
"ticket_id": instance.pk,
},
), ),
), )
)
@receiver(post_save, sender=models.CarColors) @receiver(post_save, sender=models.CarColors)
@ -1291,31 +1273,30 @@ 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_image, created = models.CarImage.objects.get_or_create(car=car, defaults={'image_hash': car.get_hash})
car=car, defaults={"image_hash": car.get_hash}
)
# Check for existing image with same hash # Check for existing image with same hash
existing = ( existing = models.CarImage.objects.filter(
models.CarImage.objects.filter( image_hash=car_image.image_hash,
image_hash=car_image.image_hash, image__isnull=False 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(existing.image.name, existing.image.file, save=True) car_image.image.save(
existing.image.name,
existing.image.file,
save=True
)
logger.info(f"Reused image for car {car.vin}") 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,19 +17,11 @@ 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 ( from inventory.models import DealerSettings, Dealer,Schedule,Notification,CarReservation,CarStatusChoices,CarImage
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)
@ -60,7 +52,6 @@ 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")
@ -71,6 +62,9 @@ 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)
@ -806,6 +800,8 @@ 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:
@ -813,49 +809,41 @@ 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 = ( subject_en = f"Your {plan.name} subscription expires in {days_until_expire} days"
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',
f"emails/expiration_reminder_{user_language}.txt", 'emails/expiration_reminder.txt'
"emails/expiration_reminder.txt", ], context)
],
context,
)
html_content = render_to_string( html_content = render_to_string([
[ f'emails/expiration_reminder_{user_language}.html',
f"emails/expiration_reminder_{user_language}.html", 'emails/expiration_reminder.html'
"emails/expiration_reminder.html", ], context)
],
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()
@ -865,7 +853,6 @@ 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:
@ -874,6 +861,7 @@ 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.
@ -883,67 +871,46 @@ 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 [ if not schedule.scheduled_by.email or schedule.status in ["completed", "canceled"]:
"completed", logger.error(f"Skipping email for Schedule ID {schedule_id}: No email or schedule status is {schedule.status}.")
"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( ).format(scheduled_type=schedule.scheduled_type, url=reverse("schedule_calendar", kwargs={"dealer_slug": schedule.dealer.slug})),)
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( 'scheduled_at': schedule.scheduled_at.astimezone(timezone.get_current_timezone()).strftime('%Y-%m-%d %H:%M %Z'), # Format with timezone
timezone.get_current_timezone() 'schedule_type': schedule.scheduled_type,
).strftime("%Y-%m-%d %H:%M %Z"), # Format with timezone 'customer_name': schedule.customer.customer_name if schedule.customer else 'N/A',
"schedule_type": schedule.scheduled_type, 'notes': schedule.notes,
"customer_name": schedule.customer.customer_name 'user_name': schedule.scheduled_by.get_full_name() or schedule.scheduled_by.email,
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( logger.info(f"Successfully sent reminder email for Schedule ID: {schedule_id} to {user_email}")
f"Successfully sent reminder email for Schedule ID: {schedule_id} to {user_email}"
)
except Schedule.DoesNotExist: except Schedule.DoesNotExist:
logger.info( logger.info(f"Schedule with ID {schedule_id} does not exist. Cannot send reminder.")
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):
""" """
@ -951,14 +918,9 @@ 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( logger.info(f"Email task for Schedule ID {task.args[0]} completed successfully. Result: {task.result}")
f"Email task for Schedule ID {task.args[0]} completed successfully. Result: {task.result}"
)
else: else:
logger.error( logger.error(f"Email task for Schedule ID {task.args[0]} failed. Error: {task.result}")
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:
@ -969,9 +931,8 @@ 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):
@ -979,25 +940,22 @@ 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" 'message': 'Image generated' if result.get('success') else 'Generation failed'
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,7 +25,6 @@ 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:
@ -687,12 +686,11 @@ 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,18 +40,13 @@ 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/<slug:dealer_slug>/general/", #dashboards for manager, dealer, inventory and accounatant
views.general_dashboard, path("dashboards/<slug:dealer_slug>/general/", views.general_dashboard,name="general_dashboard"),
name="general_dashboard", #dashboard for sales
), 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,
@ -782,11 +777,7 @@ urlpatterns = [
views.EstimateDetailView.as_view(), views.EstimateDetailView.as_view(),
name="estimate_detail", name="estimate_detail",
), ),
path( path('<slug:dealer_slug>/sales/estimates/print/<uuid:pk>/', views.EstimatePrintView.as_view(), name='estimate_print'),
"<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,
@ -943,6 +934,7 @@ 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/",
@ -1101,47 +1093,32 @@ urlpatterns = [
name="entity-ic-date", name="entity-ic-date",
), ),
# Chart of Accounts... # Chart of Accounts...
path( path('<slug:dealer_slug>/chart-of-accounts/<slug:entity_slug>/list/',
"<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/',
path( views.ChartOfAccountModelListView.as_view(inactive=True),
"<slug:dealer_slug>/chart-of-accounts/<slug:entity_slug>/list/inactive/", name='coa-list-inactive'),
views.ChartOfAccountModelListView.as_view(inactive=True), path('<slug:dealer_slug>/<slug:entity_slug>/create/',
name="coa-list-inactive", views.ChartOfAccountModelCreateView.as_view(),
), name='coa-create'),
path( path('<slug:dealer_slug>/<slug:entity_slug>/detail/<slug:coa_slug>/',
"<slug:dealer_slug>/<slug:entity_slug>/create/", views.ChartOfAccountModelListView.as_view(),
views.ChartOfAccountModelCreateView.as_view(), name='coa-detail'),
name="coa-create", path('<slug:dealer_slug>/<slug:entity_slug>/update/<slug:coa_slug>/',
), views.ChartOfAccountModelUpdateView.as_view(),
path( name='coa-update'),
"<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( path('<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-default/',
"<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-default/", views.CharOfAccountModelActionView.as_view(action_name='mark_as_default'),
views.CharOfAccountModelActionView.as_view(action_name="mark_as_default"), name='coa-action-mark-as-default'),
name="coa-action-mark-as-default", path('<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-active/',
), views.CharOfAccountModelActionView.as_view(action_name='mark_as_active'),
path( name='coa-action-mark-as-active'),
"<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/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_active"), views.CharOfAccountModelActionView.as_view(action_name='mark_as_inactive'),
name="coa-action-mark-as-active", name='coa-action-mark-as-inactive'),
),
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(
@ -1317,74 +1294,40 @@ 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( path('purchase-report/<slug:dealer_slug>/csv/', views.purchase_report_csv_export, name='purchase-report-csv-export'),
"purchase-report/<slug:dealer_slug>/csv/",
views.purchase_report_csv_export, path(
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( path('car-sale-report/<slug:dealer_slug>/csv/', views.car_sale_report_csv_export, name='car-sale-report-csv-export'),
"car-sale-report/<slug:dealer_slug>/csv/",
views.car_sale_report_csv_export, path('feature/recall/', views.RecallListView.as_view(), name='recall_list'),
name="car-sale-report-csv-export", path('feature/recall/filter/', views.RecallFilterView, name='recall_filter'),
), path('feature/recall/<int:pk>/view/', views.RecallDetailView.as_view(), name='recall_detail'),
path("feature/recall/", views.RecallListView.as_view(), name="recall_list"), path('feature/recall/create/', views.RecallCreateView.as_view(), name='recall_create'),
path("feature/recall/filter/", views.RecallFilterView, name="recall_filter"), path('feature/recall/success/', views.RecallSuccessView.as_view(), name='recall_success'),
path(
"feature/recall/<int:pk>/view/", path('<slug:dealer_slug>/schedules/calendar/', views.schedule_calendar, name='schedule_calendar'),
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( path('<slug:dealer_slug>/staff/<slug:slug>detail/', views.StaffDetailView.as_view(), name='staff_detail'),
"<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( path('<slug:dealer_slug>/help_center/tickets/', views.ticket_list, name='ticket_list'),
"<slug:dealer_slug>/help_center/tickets/", views.ticket_list, name="ticket_list" path('help_center/tickets/<slug:dealer_slug>/create/', views.create_ticket, name='create_ticket'),
), path('<slug:dealer_slug>/help_center/tickets/<int:ticket_id>/', views.ticket_detail, name='ticket_detail'),
path( path('help_center/tickets/<int:ticket_id>/update/', views.ticket_update, name='ticket_update'),
"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,12 +73,15 @@ 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(f"HTTP error fetching JWT token from {url}: ", exc_info=True) logger.error(
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
@ -166,7 +169,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):
@ -233,10 +236,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,
@ -254,7 +257,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}")
@ -1035,25 +1038,22 @@ 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 [ return [x for item in self.item_transactions
x for x in item.item_model.car.additional_services
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,23 +1091,21 @@ 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 "grand_total": sell_price + (sell_price * self.vat_rate) + car.total_additional_services,
+ (sell_price * self.vat_rate) "additional_services": car.additional_services,# self._get_nested_value(
+ car.total_additional_services, #item, self.ADDITIONAL_SERVICES_KEY
"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) for item in self.item_transactions Decimal(item.item_model.car.marked_price)
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)
@ -1115,15 +1113,13 @@ 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 "grand_total":total_price_discounted + total_vat_amount + total_additionals,
+ total_vat_amount
+ total_additionals,
} }
def get_finance_data(self): def get_finance_data(self):
@ -1135,9 +1131,7 @@ 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( "total_price_before_discount": round(totals["total_price_before_discount"], 2),
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),
@ -1146,8 +1140,6 @@ 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.
@ -1193,25 +1185,22 @@ 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 [ return [x for item in self.item_transactions
x for x in item.item_model.car.additional_services
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
@ -1226,17 +1215,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,
@ -1249,23 +1238,21 @@ 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 "grand_total": sell_price + (sell_price * self.vat_rate) + car.total_additional_services,
+ (sell_price * self.vat_rate) "additional_services": car.additional_services,# self._get_nested_value(
+ car.total_additional_services, #item, self.ADDITIONAL_SERVICES_KEY
"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) for item in self.item_transactions Decimal(item.item_model.car.marked_price)
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)
@ -1273,15 +1260,13 @@ 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 "grand_total":total_price_discounted + total_vat_amount + total_additionals,
+ total_vat_amount
+ total_additionals,
} }
def get_finance_data(self): def get_finance_data(self):
@ -1293,9 +1278,7 @@ 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( "total_price_before_discount": round(totals["total_price_before_discount"], 2),
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),
@ -1305,60 +1288,58 @@ 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
def get_finance_data(estimate, dealer): extra_info = models.ExtraInfo.objects.get(
vat = models.VatRate.objects.filter(dealer=dealer, is_active=True).first() dealer=dealer,
item = estimate.get_itemtxs_data()[0].first() content_type=ContentType.objects.get_for_model(EstimateModel),
car = item.item_model.car object_id=estimate.pk,
if isinstance(estimate, InvoiceModel) and hasattr(estimate, "ce_model"): )
estimate = estimate.ce_model discount = extra_info.data.get("discount", 0)
discount = Decimal(discount)
extra_info = models.ExtraInfo.objects.get( additional_services = car.get_additional_services()
dealer=dealer, discounted_price=(Decimal(car.marked_price) - discount)
content_type=ContentType.objects.get_for_model(EstimateModel), vat_amount = discounted_price * vat.rate
object_id=estimate.pk, total_services_vat=sum([x[1] for x in additional_services.get("services")])
) total_vat=vat_amount+total_services_vat
discount = extra_info.data.get("discount", 0) return {
discount = Decimal(discount) "car": car,
"discounted_price": discounted_price or 0,
additional_services = car.get_additional_services() "price_before_discount": car.marked_price,
discounted_price = Decimal(car.marked_price) - discount "vat_amount": vat_amount,
vat_amount = discounted_price * vat.rate "vat_rate": vat.rate,
total_services_vat = sum([x[1] for x in additional_services.get("services")]) "discount_amount": discount,
total_vat = vat_amount + total_services_vat "additional_services": additional_services,
return { "final_price": discounted_price + vat_amount,
"car": car, "total_services_vat":total_services_vat,
"discounted_price": discounted_price or 0, "total_vat":total_vat,
"price_before_discount": car.marked_price, "grand_total": discounted_price + total_vat + additional_services.get("total")
"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.
@ -1573,6 +1554,7 @@ 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):
""" """
@ -1584,7 +1566,6 @@ 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:
@ -1593,39 +1574,15 @@ 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 = ( cash_acc = entity.get_default_coa_accounts().filter(role_default=True, role=roles.ASSET_CA_CASH).first()
entity.get_default_coa_accounts() ar_acc = entity.get_default_coa_accounts().filter(role_default=True, role=roles.ASSET_CA_RECEIVABLES).first()
.filter(role_default=True, role=roles.ASSET_CA_CASH) vat_acc = entity.get_default_coa_accounts().filter(role_default=True, role=roles.LIABILITY_CL_TAXES_PAYABLE).first()
.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()
ar_acc = ( cogs_acc = entity.get_default_coa_accounts().filter(role_default=True, role=roles.COGS).first()
entity.get_default_coa_accounts() inv_acc = entity.get_default_coa_accounts().filter(role_default=True, role=roles.ASSET_CA_INVENTORY).first()
.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(
@ -1633,12 +1590,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
@ -1649,15 +1606,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)
@ -1673,8 +1630,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
@ -1682,8 +1639,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:
@ -1692,15 +1649,16 @@ 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)"
) )
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@ -1711,7 +1669,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
@ -1719,12 +1677,15 @@ 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, account=inv_acc, amount=cost_total, tx_type="credit" journal_entry=je_cogs,
account=inv_acc,
amount=cost_total,
tx_type='credit'
) )
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# 2C. Update car state flags inside the same transaction # 2C. Update car state flags inside the same transaction
@ -1732,12 +1693,10 @@ 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,
@ -1828,29 +1787,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):
@ -1898,7 +1857,6 @@ 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(
@ -1985,6 +1943,7 @@ 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)
@ -2380,7 +2339,6 @@ 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(
@ -2413,16 +2371,17 @@ 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 = ( existing = models.CarImage.objects.filter(
models.CarImage.objects.filter( image_hash=car_image.image_hash,
image_hash=car_image.image_hash, image__isnull=False image__isnull=False
) ).exclude(car=car).first()
.exclude(car=car)
.first()
)
if existing: if existing:
car_image.image.save(existing.image.name, existing.image.file, save=True) car_image.image.save(
existing.image.name,
existing.image.file,
save=True
)
return car_image.image.url 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
@ -2435,7 +2394,6 @@ 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)
@ -2456,7 +2414,6 @@ 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"""
@ -2479,7 +2436,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")
@ -2487,17 +2444,16 @@ 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", data=login_data f"{self.BASE_URL}/login",
data=login_data
) )
if login_response.status_code != 200: if login_response.status_code != 200:
raise Exception( raise Exception(f"Login failed with status {login_response.status_code}")
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
@ -2516,38 +2472,39 @@ 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}", timeout=160 f"{self.BASE_URL}{image_url}",
timeout=160
) )
image_response.raise_for_status() image_response.raise_for_status()
@ -2563,11 +2520,9 @@ 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
@ -2584,31 +2539,29 @@ 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( logger.info(f"Resized image from {len(image_data)} to {len(resized_data)} bytes")
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:
@ -2616,7 +2569,6 @@ 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
@ -2625,10 +2577,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}")
@ -2637,10 +2589,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
@ -2654,21 +2606,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,15 +2,13 @@ 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,10 +98,9 @@ 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( if not CarEquipment.objects.filter(id_car_equipment=item["id_car_equipment"]).exists():
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(
@ -109,7 +108,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,175 +1,177 @@
{% 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" <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
rel="stylesheet"> <style>
<style> :root {
:root { --dark-bg: #121212;
--dark-bg: #121212; --main-color: #ff3864;
--main-color: #ff3864; --secondary-color: #e6e6e6;
--secondary-color: #e6e6e6; }
} body, html {
body, html { height: 100%;
height: 100%; margin: 0;
margin: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: var(--dark-bg);
background-color: var(--dark-bg); color: var(--secondary-color);
color: var(--secondary-color); overflow: hidden; /* Hide overflow for the particles */
overflow: hidden; /* Hide overflow for the particles */ }
} .center-content {
.center-content { height: 100%;
height: 100%; display: flex;
display: flex; align-items: center;
align-items: center; justify-content: center;
justify-content: center; text-align: center;
text-align: center; flex-direction: column;
flex-direction: column; z-index: 2; /* Ensure content is on top of particles */
z-index: 2; /* Ensure content is on top of particles */ position: relative;
position: relative; }
} .glitch {
.glitch { font-size: 10rem;
font-size: 10rem; font-weight: bold;
font-weight: bold; color: var(--main-color);
color: var(--main-color); position: relative;
position: relative; animation: glitch-animation 2.5s infinite;
animation: glitch-animation 2.5s infinite; }
} @keyframes glitch-animation {
@keyframes glitch-animation { 0% { text-shadow: 2px 2px var(--secondary-color); }
0% { text-shadow: 2px 2px var(--secondary-color); } 20% { text-shadow: -2px -2px var(--secondary-color); }
20% { text-shadow: -2px -2px var(--secondary-color); } 40% { text-shadow: 4px 4px var(--main-color); }
40% { text-shadow: 4px 4px var(--main-color); } 60% { text-shadow: -4px -4px var(--main-color); }
60% { text-shadow: -4px -4px var(--main-color); } 80% { text-shadow: 6px 6px var(--secondary-color); }
80% { text-shadow: 6px 6px var(--secondary-color); } 100% { text-shadow: -6px -6px var(--secondary-color); }
100% { text-shadow: -6px -6px var(--secondary-color); } }
} .main-message {
.main-message { font-size: 2.5rem;
font-size: 2.5rem; margin-top: -20px;
margin-top: -20px; letter-spacing: 5px;
letter-spacing: 5px; text-transform: uppercase;
text-transform: uppercase; }
} .sub-message {
.sub-message { font-size: 1.2rem;
font-size: 1.2rem; color: #8c8c8c;
color: #8c8c8c; margin-top: 10px;
margin-top: 10px; }
} .home-button {
.home-button { margin-top: 30px;
margin-top: 30px; padding: 12px 30px;
padding: 12px 30px; background-color: var(--main-color);
background-color: var(--main-color); border: none;
border: none; color: #fff;
color: #fff; border-radius: 5px;
border-radius: 5px; font-size: 1rem;
font-size: 1rem; text-decoration: none;
text-decoration: none; transition: background-color 0.3s, transform 0.3s;
transition: background-color 0.3s, transform 0.3s; }
} .home-button:hover {
.home-button:hover { background-color: #d12e52;
background-color: #d12e52; transform: scale(1.05);
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 class="center-content container-fluid"> <div id="particles-js"></div>
<h1 class="glitch">403</h1>
<h2 class="main-message">{% trans "Access Forbidden" %}</h2> <div class="center-content container-fluid">
<p class="sub-message">{% trans "You do not have permission to view this page." %}</p> <h1 class="glitch">403</h1>
<p class="sub-message fs-2">{% trans "Powered By Tenhal, Riyadh Saudi Arabia" %}</p> <h2 class="main-message">{% trans "Access Forbidden" %}</h2>
<a href="{% url 'home' %}" class="home-button">{% trans "Go Home" %}</a> <p class="sub-message">{% trans "You do not have permission to view this page."%}</p>
</div> <p class="sub-message fs-2">{% trans "Powered By Tenhal, Riyadh Saudi Arabia"%}</p>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> <a href="{% url 'home' %}" class="home-button">{% trans "Go Home" %}</a>
<script src="https://cdn.jsdelivr.net/npm/particles.js@2.0.0/particles.min.js"></script> </div>
<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,
"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, "enable": true,
"distance": 150, "value_area": 800
"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": { "color": {
"detect_on": "canvas", "value": "#ff3864"
"events": { },
"onhover": { "shape": {
"enable": true, "type": "circle",
"mode": "grab" },
}, "opacity": {
"onclick": { "value": 0.5,
"enable": true, "random": false,
"mode": "push" "anim": {
}, "enable": false
"resize": true
},
"modes": {
"grab": {
"distance": 140,
"line_linked": {
"opacity": 1
}
},
"push": {
"particles_nb": 4
}
} }
}, },
"retina_detect": true "size": {
}); "value": 3,
</script> "random": true,
</body> },
</html> "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": {
"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,6 +6,7 @@
{% 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"">
@ -77,7 +78,9 @@
</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,7 +282,9 @@
</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 %}
@ -291,97 +293,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) { function validateEmail(email) {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return emailRegex.test(email) && email !== ''; return emailRegex.test(email) && email !== '';
} }
function validateform2(name,arabic_name,phone_number) { function validateform2(name,arabic_name,phone_number) {
if (name === '' || arabic_name === '' || phone_number === '' || phone_number.length < 10 || !phone_number.startsWith('056')) { if (name === '' || arabic_name === '' || phone_number === '' || phone_number.length < 10 || !phone_number.startsWith('056')) {
return false; return false;
} }
return true return true
} }
function validate_sa_phone_number(phone_number) { function validate_sa_phone_number(phone_number) {
const phone_numberRegex = /^056[0-9]{7}$/; const phone_numberRegex = /^056[0-9]{7}$/;
return phone_numberRegex.test(phone_number) && phone_numberRegex !== ''; return phone_numberRegex.test(phone_number) && phone_numberRegex !== '';
} }
function getAllFormData() { function getAllFormData() {
const forms = document.querySelectorAll('.needs-validation'); const forms = document.querySelectorAll('.needs-validation');
const formData = {}; const formData = {};
forms.forEach(form => { forms.forEach(form => {
const fields = form.querySelectorAll('input,textarea,select'); const fields = form.querySelectorAll('input,textarea,select');
fields.forEach(field => { fields.forEach(field => {
formData[field.name] = field.value; formData[field.name] = field.value;
}); });
}); });
return formData; return formData;
} }
function showLoading() { function showLoading() {
Swal.fire({ Swal.fire({
title: "{% trans 'Please Wait' %}", title: "{% trans 'Please Wait' %}",
text: "{% trans 'Loading' %}...", text: "{% trans 'Loading' %}...",
allowOutsideClick: false, allowOutsideClick: false,
didOpen: () => { didOpen: () => {
Swal.showLoading(); Swal.showLoading();
}
});
} }
});
}
function hideLoading() { function hideLoading() {
Swal.close(); Swal.close();
} }
function notify(tag,msg){ function notify(tag,msg){
Swal.fire({ Swal.fire({
icon: tag, icon: tag,
titleText: msg 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 getCookie(name) { }
let cookieValue = null; return cookieValue;
if (document.cookie && document.cookie !== "") { }
const cookies = document.cookie.split(";"); async function sendFormData() {
for (let cookie of cookies) { const formData = getAllFormData();
cookie = cookie.trim(); const url = "{% url 'account_signup' %}";
if (cookie.substring(0, name.length + 1) === name + "=") { const csrftoken = getCookie('csrftoken');
cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); try {
break; showLoading();
} const response = await fetch(url, {
} method: 'POST',
} headers: {
return cookieValue; 'X-CSRFToken': '{{csrf_token}}',
} 'Content-Type': 'application/json',
async function sendFormData() { },
const formData = getAllFormData(); body: JSON.stringify(formData),
const url = "{% url 'account_signup' %}"; });
const csrftoken = getCookie('csrftoken'); hideLoading();
try { const data = await response.json();
showLoading(); if (response.ok) {
const response = await fetch(url, { notify("success","Account created successfully");
method: 'POST', setTimeout(() => {
headers: { window.location.href = "{% url 'account_login' %}";
'X-CSRFToken': '{{csrf_token}}', }, 1000);
'Content-Type': 'application/json', } else {
}, notify("error",data.error);
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>
<hr class="my-4"> <div class="col-12 mt-4">
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3"> <h4 class="mb-4 text-center">{% trans 'Default Bill Accounts' %}</h4>
<button class="btn btn-phoenix-primary btn-lg" type="submit"> {{ form.bill_cash_account|as_crispy_field }}
<i class="fa-solid fa-pen-to-square me-1"></i> {{ form.bill_prepaid_account|as_crispy_field }}
{% trans 'Update' %} {{ form.bill_unearned_account|as_crispy_field }}
</button>
</div> </div>
</form> </div>
</div> <hr class="my-4">
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
<button class="btn btn-phoenix-primary btn-lg" type="submit">
<i class="fa-solid fa-pen-to-square me-1"></i>
{% trans 'Update' %}
</button>
</div>
</form>
</div> </div>
</div> </div>
</main> </div>
{% endblock %} </main>
{% 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,33 +1,29 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n %} {% load i18n %}
{% block title %} {% block title %}
{% trans 'Admin Management' %} {% trans 'Admin Management' %} {% endblock %}
{% endblock %} {% block content %}
{% block content %} <h3 class="my-4">{% trans "Admin Management" %}<li class="fa fa-user-cog ms-2 text-primary"></li></h3>
<h3 class="my-4"> <div class="row row-cols-1 row-cols-sm-2 row-cols-md-4 g-4 mt-10">
{% trans "Admin Management" %} <div class="col">
<li class="fa fa-user-cog ms-2 text-primary"></li> <a href="{% url 'user_management' request.dealer.slug %}">
</h3> <div class="card h-100">
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-4 g-4 mt-10"> <div class="card-header text-center">
<div class="col"> <h5 class="card-title">{{ _("User Management") }}</h5>
<a href="{% url 'user_management' request.dealer.slug %}"> <span class="me-2"><i class="fas fa-user fa-2x"></i></span>
<div class="card h-100"> </div>
<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>
</div> </a>
</a> </div>
</div> <div class="col">
<div class="col"> <a href="{% url 'audit_log_dashboard' request.dealer.slug %}">
<a href="{% url 'audit_log_dashboard' request.dealer.slug %}"> <div class="card h-100">
<div class="card h-100"> <div class="card-header text-center">
<div class="card-header text-center"> <h5 class="card-title">{{ _("Audit Log Dashboard") }}</h5>
<h5 class="card-title">{{ _("Audit Log Dashboard") }}</h5> <span class="me-2"><i class="fas fa-user fa-2x"></i></span>
<span class="me-2"><i class="fas fa-user fa-2x"></i></span> </div>
</div> </div>
</div> </a>
</a> </div>
</div> </div>
</div> {% endblock content %}
{% endblock content %}

View File

@ -32,7 +32,15 @@
<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 {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}" <div class="alert alert-dismissible
{% if message.tags %}
alert-
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
danger
{% else %}
{{ message.tags }}
{% endif %}
{% endif %}"
role="alert">{{ message }}</div> role="alert">{{ message }}</div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}

View File

@ -78,7 +78,15 @@
<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 {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}" <div class="alert alert-dismissible
{% if message.tags %}
alert-
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
danger
{% else %}
{{ message.tags }}
{% endif %}
{% endif %}"
role="alert">{{ message }}</div> role="alert">{{ message }}</div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}

View File

@ -65,7 +65,15 @@
<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 {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}" <div class="alert alert-dismissible
{% if message.tags %}
alert-
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
danger
{% else %}
{{ message.tags }}
{% endif %}
{% endif %}"
role="alert">{{ message }}</div> role="alert">{{ message }}</div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}

View File

@ -25,7 +25,15 @@
<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 {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}" <div class="alert alert-dismissible
{% if message.tags %}
alert-
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
danger
{% else %}
{{ message.tags }}
{% endif %}
{% endif %}"
role="alert">{{ message }}</div> role="alert">{{ message }}</div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}

View File

@ -11,9 +11,22 @@
method="post" method="post"
action="" action=""
id="workingHoursForm" id="workingHoursForm"
data-action="{% if working_hours_instance %} update {% else %} create {% endif %}" data-action="{% if working_hours_instance %}
data-working-hours-id=" {% if working_hours_instance %} {{ working_hours_instance.id }} {% else %} 0 {% endif %}" update
data-staff-user-id="{% if staff_user_id %} {{ staff_user_id }} {% else %} 0 {% endif %}"> {% else %}
create
{% endif %}"
data-working-hours-id="
{% if working_hours_instance %}
{{ working_hours_instance.id }}
{% else %}
0
{% endif %}"
data-staff-user-id="{% if staff_user_id %}
{{ staff_user_id }}
{% else %}
0
{% endif %}">
{% csrf_token %} {% 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">
@ -81,7 +94,15 @@
<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 {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}" <div class="alert alert-dismissible
{% if message.tags %}
alert-
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
danger
{% else %}
{{ message.tags }}
{% endif %}
{% endif %}"
role="alert">{{ message }}</div> role="alert">{{ message }}</div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}

View File

@ -23,7 +23,15 @@
<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 {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}" <div class="alert alert-dismissible
{% if message.tags %}
alert-
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
danger
{% else %}
{{ message.tags }}
{% endif %}
{% endif %}"
role="alert">{{ message }}</div> role="alert">{{ message }}</div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}

View File

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

View File

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

View File

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

View File

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

View File

@ -40,7 +40,15 @@
<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 {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}" <div class="alert alert-dismissible
{% if message.tags %}
alert-
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
danger
{% else %}
{{ message.tags }}
{% endif %}
{% endif %}"
role="alert">{{ message }}</div> role="alert">{{ message }}</div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}

View File

@ -3,7 +3,11 @@
<!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' %} rtl {% else %} ltr {% endif %}" dir="{% if LANGUAGE_CODE == 'ar' %}
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,7 +2,11 @@
<!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' %} dir="rtl" {% else %} dir="ltr" {% endif %} {% if LANGUAGE_CODE == 'ar' %}
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">
@ -50,14 +54,8 @@
<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' %}" <link href="{% static 'css/theme-rtl.min.css' %}" type="text/css" rel="stylesheet" id="style-rtl">
type="text/css" <link href="{% static 'css/user-rtl.min.css' %}" type="text/css" rel="stylesheet" id="user-style-rtl">
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"
@ -68,8 +66,11 @@
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>
@ -84,25 +85,19 @@
{% 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" <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>
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 %} {% block content %}{% endblock 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 %}
@ -131,7 +126,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 %}
@ -161,57 +156,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) { document.addEventListener('htmx:afterRequest', function(evt) {
if(evt.detail.xhr.status == 403){ if(evt.detail.xhr.status == 403){
/* Notify the user of a 404 Not Found response */ /* Notify the user of a 404 Not Found response */
notify("error", "You do not have permission to view this page"); notify("error", "You do not have permission to view this page");
} }
if(evt.detail.xhr.status == 404){ if(evt.detail.xhr.status == 404){
/* Notify the user of a 404 Not Found response */ /* Notify the user of a 404 Not Found response */
return alert("Error: Could Not Find Resource"); return alert("Error: Could Not Find Resource");
} }
if (evt.detail.successful != true) { if (evt.detail.successful != true) {
console.log(evt.detail.xhr.statusText) console.log(evt.detail.xhr.statusText)
/* Notify of an unexpected error, & print error to console */ /* Notify of an unexpected error, & print error to console */
notify("error", `Unexpected Error ,${evt.detail.xhr.statusText}`); notify("error", `Unexpected Error ,${evt.detail.xhr.statusText}`);
} }
}); });
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();
}); });
} }
}); });
// 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') {
document.querySelectorAll('.modal').forEach(function(m) { document.querySelectorAll('.modal').forEach(function(m) {
var modal = bootstrap.Modal.getInstance(m); var modal = bootstrap.Modal.getInstance(m);
if (modal) { if (modal) {
modal.hide(); 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 }}" <form action="{{ form_action_url }}" method="post" id="djl-bill-model-create-form-id" class="needs-validation" novalidate>
method="post" {% csrf_token %}
id="djl-bill-model-create-form-id" {% if po_model %}
class="needs-validation" <div class="text-center mb-4">
novalidate> <h3 class="h5">{% trans 'Bill for' %} {{ po_model.po_number }}</h3>
{% csrf_token %} <p class="text-muted mb-3">{% trans 'Bill for' %} {{ po_model.po_title }}</p>
{% if po_model %} <div class="d-flex flex-column gap-2">
<div class="text-center mb-4"> {% for itemtxs in po_itemtxs_qs %}
<h3 class="h5">{% trans 'Bill for' %} {{ po_model.po_number }}</h3> <span class="badge bg-secondary">{{ itemtxs }}</span>
<p class="text-muted mb-3">{% trans 'Bill for' %} {{ po_model.po_title }}</p> {% endfor %}
<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>
</form> {% endif %}
</div> <div class="mb-4">
{{ form|crispy }}
</div>
<hr class="my-4">
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
<button class="btn btn-phoenix-primary btn-lg me-md-2" type="submit">
<i class="fa-solid fa-floppy-disk me-1"></i>
{{ _("Save") }}
</button>
<a href="{% url 'bill_list' request.dealer.slug%}" class="btn btn-phoenix-secondary btn-lg">
<i class="fa-solid fa-ban me-1"></i>
{% trans "Cancel" %}
</a>
</div>
</form>
</div> </div>
</div> </div>
</main> </div>
{% endblock %} </main>
{% endblock %}

View File

@ -4,160 +4,180 @@
{% 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> </div>
{% endif %} {% if bill.is_configured %}
</div>
</div> <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">
<div class="card-header pb-0"> <h6 class="text-uppercase text-xs text-muted mb-2">
<div class="d-flex align-items-center mb-2"> {% trans 'Cash Account' %}:
<i class="fas fa-receipt me-3 text-primary"></i> {% if bill.cash_account %}
<h5 class="mb-0">{% trans 'Bill Items' %}</h5> <a href="{% url 'account_detail' request.dealer.slug bill.cash_account.coa_model.pk bill.cash_account.uuid %}"
</div> class="text-decoration-none ms-1">{{ bill.cash_account.code }}</a>
</div> {% else %}
<div class="card-body px-0 pt-0 pb-2"> {{ bill.cash_account.code }}
<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 %}
{% endif %} </h6>
</td> <h4 class="mb-0" id="djl-bill-detail-amount-paid">
</tr> {% currency_symbol %}{{ bill.get_amount_cash | absolute | currency_format }}
{% endfor %} </h4>
</tbody>
<tfoot> </div>
<tr> {% if bill.accrue %}
<td colspan="3"></td> <div class="col-12 col-md-3">
<td class="text-end">
<strong>{% trans 'Total' %}</strong> <h6 class="text-uppercase text-xs text-muted mb-2">
</td> {% trans 'Prepaid Account' %}:
<td class="text-end"> {% if bill.prepaid_account %}
<strong>{% currency_symbol %}{{ total_amount__sum | currency_format }}</strong> <a href="{% url 'account_detail' request.dealer.slug bill.prepaid_account.coa_model.pk bill.prepaid_account.uuid %}"
</td> class="text-decoration-none ms-1">
<td></td> {{ bill.prepaid_account.code }}
</tr> </a>
</tfoot> {% else %}
</table> {{ bill.prepaid_account.code }}
</div> {% 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>
</div> </div>
<div class="col-12">
<div class="card mb-3 shadow-sm">
<div class="card-header pb-0">
<div class="d-flex align-items-center mb-2">
<i class="fas fa-receipt me-3 text-primary"></i>
<h5 class="mb-0">{% trans 'Bill Items' %}</h5>
</div>
</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="col-12">
<div class="card mb-3 shadow-sm"> <div class="card mb-3 shadow-sm">
<div class="card-header pb-0"> <div class="card-header pb-0">
<div class="d-flex align-items-center mb-2"> <div class="d-flex align-items-center mb-2">
<i class="fas fa-exchange-alt me-3 text-primary"></i> <i class="fas fa-exchange-alt me-3 text-primary"></i>
<h5 class="mb-0">{% trans 'Bill Transactions' %}</h5> <h5 class="mb-0">{% trans 'Bill Transactions' %}</h5>
</div> </div>
</div> </div>
<div class="card-body px-0 pt-0 pb-2 table-responsive">{% transactions_table bill %}</div> <div class="card-body px-0 pt-0 pb-2 table-responsive">{% transactions_table bill %}</div>
</div> </div>
</div> </div>
<div> <div>
{% include "bill/includes/mark_as.html" %} {% include "bill/includes/mark_as.html" %}
{% endblock %} {% endblock %}

View File

@ -14,8 +14,7 @@
<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 %}" <form action="{% url 'bill-update' dealer_slug=request.dealer.slug entity_slug=view.kwargs.entity_slug bill_pk=bill_model.uuid %}" method="post">
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,6 +4,7 @@
{% 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">
@ -50,16 +51,15 @@
</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" <a hx-boost="true" href="{% url 'django_ledger:bill-update' entity_slug=entity_slug bill_pk=bill.uuid %}"
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 }}')" <button onclick="djLedger.toggleModal('{{ bill.get_html_id }}')" class="btn btn-sm btn-phoenix-info">{% trans 'Mark as Paid' %}</button>
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,11 +202,10 @@
<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" <a hx-boost="true" href="{% url 'bill-update' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill.uuid %}">
href="{% url 'bill-update' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill.uuid %}"> <button class="btn btn-phoenix-primary">
<button class="btn btn-phoenix-primary"> <i class="fas fa-edit me-2"></i>{% trans 'Update' %}
<i class="fas fa-edit me-2"></i>{% trans 'Update' %}
</button> </button>
</a> </a>
{% endif %} {% endif %}
@ -220,6 +219,7 @@
{% 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,40 +227,41 @@
{% 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 Approved', '{% url 'bill-action-mark-as-approved' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Approved')"> onclick="showPOModal('Mark as Paid', '{% url 'bill-action-mark-as-paid' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Paid')">
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Approved' %} <i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Paid' %}
</button> </button>
{% endif %} {% endif %}
{% endif %} <!-- Void Button -->
<!-- Mark as Paid --> {% if bill.can_void %}
{% if bill.can_pay %} <button class="btn btn-phoenix-danger"
<button class="btn btn-phoenix-success" 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')">
onclick="showPOModal('Mark as Paid', '{% url 'bill-action-mark-as-paid' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Paid')"> <i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Void' %}
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Paid' %} </button>
</button> {% endif %}
{% endif %} <!-- Cancel Button -->
<!-- Void Button --> {% if bill.can_cancel %}
{% if bill.can_void %} <button class="btn btn-phoenix-danger"
<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')">
onclick="showPOModal('Mark as Void', '{% url 'bill-action-mark-as-void' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Void')"> <i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Canceled' %}
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Void' %} </button>
</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 %}
<!-- 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,142 +2,137 @@
{% 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" <form id="bill-update-form" action="{% url 'bill-update-items' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill_pk %}"
action="{% url 'bill-update-items' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill_pk %}" method="post">
method="post"> {% else %}
{% else %} <form id="bill-update-form" hx-trigger="load delay:300ms" hx-swap="outerHTML" hx-target="#bill-update-form" hx-select="#bill-update-form" hx-post="{% url 'bill-update-items' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill_pk %}"
<form id="bill-update-form" method="post">
hx-trigger="load delay:300ms" {% endif %}
hx-swap="outerHTML" <div class="container-fluid py-4">
hx-target="#bill-update-form" <!-- Page Header -->
hx-select="#bill-update-form" <div class="row mb-4">
hx-post="{% url 'bill-update-items' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill_pk %}" <div class="col-12">
method="post"> <h2 class="text-primary mb-0 d-flex align-items-center">
{% endif %} <i class="fas fa-receipt me-2"></i>
<div class="container-fluid py-4"> {% trans 'Bill Items' %}
<!-- Page Header --> </h2>
<div class="row mb-4"> <hr class="my-3">
<div class="col-12">
<h2 class="text-primary mb-0 d-flex align-items-center">
<i class="fas fa-receipt me-2"></i>
{% trans 'Bill Items' %}
</h2>
<hr class="my-3">
</div>
</div> </div>
<!-- Form Content --> </div>
<div class="row"> <!-- Form Content -->
<div class="col-12"> <div class="row">
{% csrf_token %} <div class="col-12">
{{ item_formset.non_form_errors }} {% csrf_token %}
{{ item_formset.management_form }} {{ item_formset.non_form_errors }}
<!-- Card Container --> {{ item_formset.management_form }}
<div class="card shadow-sm"> <!-- Card Container -->
<div class="card-body p-0"> <div class="card shadow-sm">
<!-- Responsive Table --> <div class="card-body p-0">
<div class="table-responsive"> <!-- Responsive Table -->
<table class="table table-hover align-middle mb-0"> <div class="table-responsive">
<thead class="table-light"> <table class="table table-hover align-middle mb-0">
<tr> <thead class="table-light">
<th class="text-uppercase text-xxs font-weight-bolder">{% trans 'Item' %}</th> <tr>
<th class="text-uppercase text-xxs font-weight-bolder text-center">{% trans 'PO Qty' %}</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 Amount' %}</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 'Quantity' %}</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 'Unit Cost' %}</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' %}</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-end">{% trans 'Total' %}</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 'Delete' %}</th> <th class="text-uppercase text-xxs font-weight-bolder text-end">{% trans 'Total' %}</th>
</tr> <th class="text-uppercase text-xxs font-weight-bolder text-center">{% trans 'Delete' %}</th>
</thead> </tr>
<tbody> </thead>
{% for f in item_formset %} <tbody>
<tr class="align-middle"> {% for f in item_formset %}
<!-- Item Column --> <tr class="align-middle">
<td> <!-- Item Column -->
<div class="d-flex flex-column ms-2"> <td>
{% for hidden_field in f.hidden_fields %}{{ hidden_field }}{% endfor %} <div class="d-flex flex-column ms-2">
{{ f.item_model|add_class:"form-control" }} {% for hidden_field in f.hidden_fields %}{{ hidden_field }}{% endfor %}
{% if f.errors %}<span class="text-danger text-xs">{{ f.errors }}</span>{% endif %} {{ 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> </div>
</td> {% endif %}
<!-- PO Quantity -->
<td class="text-center">
<span class="text-muted text-xs">
{% if f.instance.po_quantity %}{{ f.instance.po_quantity }}{% endif %}
</span>
</td>
<!-- PO Amount -->
<td class="text-center">
{% if f.instance.po_total_amount %}
<div class="d-flex flex-column">
<span class="text-xs font-weight-bold">{% currency_symbol %}{{ f.instance.po_total_amount | currency_format }}</span>
<a class="btn btn-sm btn-phoenix-info mt-1"
href="{% url 'purchase_order_detail' dealer_slug entity_slug f.instance.po_model_id %}">
{% trans 'View PO' %}
</a>
</div>
{% endif %}
</td>
<!-- Quantity -->
<td class="text-center">
<div class="input-group input-group-sm w-100">{{ f.quantity|add_class:"form-control" }}</div>
</td>
<!-- Unit Cost -->
<td class="text-center">
<div class="input-group input-group-sm w-100">{{ f.unit_cost|add_class:"form-control" }}</div>
</td>
<!-- Entity Unit -->
<td class="text-center">{{ f.entity_unit|add_class:"form-control" }}</td>
<!-- Total Amount -->
<td class="text-end">
<span class="text-xs font-weight-bold">
<span>{% currency_symbol %}</span>{{ f.instance.total_amount | currency_format }}
</span>
</td>
<!-- Delete Checkbox -->
<td class="text-center">
{% if item_formset.can_delete %}<div class="form-check d-flex justify-content-center">{{ f.DELETE }}</div>{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
<!-- Footer Total -->
<tfoot class="total-row">
<tr>
<td colspan="5"></td>
<td class="text-end">
<strong>{% trans 'Total' %}</strong>
</td> </td>
<td class="text-end"> <!-- Quantity -->
<strong>{% currency_symbol %}{{ total_amount__sum | currency_format }}</strong> <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> </td>
<td></td>
</tr> </tr>
</tfoot> {% endfor %}
</table> </tbody>
</div> <!-- 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>
<!-- Action Buttons --> </div>
<div class="row mt-4"> <!-- Action Buttons -->
<div class="col-12"> <div class="row mt-4">
<div class="d-flex justify-content-start gap-2"> <div class="col-12">
{% if not item_formset.has_po %} <div class="d-flex justify-content-start gap-2">
<a href="{% url 'django_ledger:product-create' entity_slug=entity_slug %}" {% if not item_formset.has_po %}
class="btn btn-phoenix-primary"> <a href="{% url 'django_ledger:product-create' entity_slug=entity_slug %}"
<i class="fas fa-plus me-1"></i> class="btn btn-phoenix-primary">
{% trans 'New Item' %} <i class="fas fa-plus me-1"></i>
</a> {% trans 'New Item' %}
{% endif %} </a>
<button type="submit" class="btn btn-phoenix-primary"> {% endif %}
<i class="fas fa-save me-1"></i> <button type="submit" class="btn btn-phoenix-primary">
{% trans 'Save Changes' %} <i class="fas fa-save me-1"></i>
</button> {% trans 'Save Changes' %}
</div> </button>
</div> </div>
</div> </div>
</div> </div>
</form> </div>
</form>

View File

@ -2,52 +2,58 @@
{% 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" <form method="post" id="{{ form.get_form_id }}" class="needs-validation" novalidate>
id="{{ form.get_form_id }}" {% csrf_token %}
class="needs-validation"
novalidate> {# Bootstrap form rendering #}
{% csrf_token %} <div class="mb-3">
{# Bootstrap form rendering #} {{ form.name.label_tag }}
<div class="mb-3"> {{ form.name|add_class:"form-control" }}
{{ form.name.label_tag }} {% if form.name.help_text %}
{{ form.name|add_class:"form-control" }} <small class="form-text text-muted">{{ form.name.help_text }}</small>
{% if form.name.help_text %}<small class="form-text text-muted">{{ form.name.help_text }}</small>{% endif %} {% endif %}
{% for error in form.name.errors %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %} {% for error in form.name.errors %}
</div> <div class="invalid-feedback d-block">{{ error }}</div>
<div class="mb-3"> {% endfor %}
{{ form.description.label_tag }} </div>
{{ form.description|add_class:"form-control" }} <div class="mb-3">
{% if form.description.help_text %} {{ form.description.label_tag }}
<small class="form-text text-muted">{{ form.description.help_text }}</small> {{ form.description|add_class:"form-control" }}
{% endif %} {% if form.description.help_text %}
{% for error in form.description.errors %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %} <small class="form-text text-muted">{{ form.description.help_text }}</small>
</div> {% endif %}
<hr class="my-4"> {% for error in form.description.errors %}
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3"> <div class="invalid-feedback d-block">{{ error }}</div>
<button type="submit" class="btn btn-phoenix-primary btn-lg me-md-2"> {% endfor %}
<i class="fa-solid fa-plus me-1"></i> </div>
{% trans 'Create' %}
</button> <hr class="my-4">
<a href="{% url 'coa-list' request.dealer.slug request.entity.slug %}" <div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
class="btn btn-phoenix-secondary btn-lg"> <button type="submit" class="btn btn-phoenix-primary btn-lg me-md-2">
<i class="fa-solid fa-ban me-1"></i> <i class="fa-solid fa-plus me-1"></i>
{% trans 'Cancel' %} {% trans 'Create' %}
</a> </button>
</div> <a href="{% url 'coa-list' request.dealer.slug request.entity.slug %}"
</form> class="btn btn-phoenix-secondary btn-lg">
</div> <i class="fa-solid fa-ban me-1"></i>
{% trans 'Cancel' %}
</a>
</div>
</form>
</div> </div>
</div> </div>
</main> </div>
{% endblock %} </main>
{% endblock %}

View File

@ -2,6 +2,7 @@
{% 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">
@ -9,19 +10,17 @@
<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 %}" <a href="{% url 'coa-create' request.dealer.slug request.entity.slug %}" class="btn btn-phoenix-primary">
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" <a class="btn btn-phoenix-warning mb-4" href="{% url 'coa-list-inactive' request.dealer.slug request.entity.slug %}">
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" <a class="btn btn-phoenix-warning mb-4" href="{% url 'coa-list' request.dealer.slug request.entity.slug %}">
href="{% url 'coa-list' request.dealer.slug request.entity.slug %}">
{% trans 'Show Active' %} {% trans 'Show Active' %}
</a> </a>
{% endif %} {% endif %}
@ -29,9 +28,11 @@
</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">{% include 'chart_of_accounts/includes/coa_card.html' with coa_model=coa_model %}</div> <div class="col">
{% include 'chart_of_accounts/includes/coa_card.html' with coa_model=coa_model %}
</div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -2,20 +2,23 @@
{% 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 %}" <form action="{% url 'coa-update' request.dealer.slug request.entity.slug coa_model.slug %}" id="{{ form.form_id }}" method="post">
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 %}<small class="form-text text-muted">{{ form.name.help_text }}</small>{% endif %} {% if form.name.help_text %}
{% for error in form.name.errors %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %} <small class="form-text text-muted">{{ form.name.help_text }}</small>
{% endif %}
{% for error in form.name.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
</div> </div>
<div class="mb-3"> <div class="mb-3">
{{ form.description.label_tag }} {{ form.description.label_tag }}
@ -23,13 +26,19 @@
{% 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 %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %} {% for error in form.description.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
</div> </div>
<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">{% trans 'Update' %}</button> <button class="btn btn-phoenix-primary" type="submit">
{% 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>
@ -37,4 +46,4 @@
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,6 +1,7 @@
{% 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">
@ -18,16 +19,17 @@
{% 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">
@ -44,22 +46,27 @@
<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">
@ -77,37 +84,36 @@
</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 %}" <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">
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 %}"
class="btn btn-sm btn-phoenix-success fw-bold flex-grow-1 flex-sm-grow-0"> <a href="{% url 'account_list' request.dealer.slug coa_model.pk %}" class="btn btn-sm btn-phoenix-success fw-bold flex-grow-1 flex-sm-grow-0">
<i class="fas fa-book me-1"></i> {% trans 'Accounts' %} <i class="fas fa-book me-1"></i> {% trans 'Accounts' %}
</a> </a>
<a href="{% url 'account_create' request.dealer.slug coa_model.pk %}"
class="btn btn-sm btn-phoenix-info fw-bold flex-grow-1 flex-sm-grow-0"> <a href="{% url 'account_create' request.dealer.slug coa_model.pk %}" class="btn btn-sm btn-phoenix-info fw-bold flex-grow-1 flex-sm-grow-0">
<i class="fas fa-plus-circle me-1"></i> {% trans 'Add Account' %} <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 %}" <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">
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 %}" <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">
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 %}" <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">
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,22 +1,16 @@
{% load i18n crispy_forms_tags %} {% load i18n crispy_forms_tags %}
<div class="modal fade" <div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true">
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" <button class="btn p-0 text-body-quaternary fs-6" data-bs-dismiss="modal" aria-label="Close">
data-bs-dismiss="modal" <span class="fas fa-times"></span>
aria-label="Close"> </button>
<span class="fas fa-times"></span> </div>
</button> <div id="emailModalBody" class="modal-body">
</div> <h1>hi</h1>
<div id="emailModalBody" class="modal-body">
<h1>hi</h1>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>

View File

@ -15,11 +15,13 @@
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form id="noteForm" <form id="noteForm" action="{% url 'add_note' request.dealer.slug content_type slug %}"
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="{ resetSubmitButton(document.querySelector('.add_note_form button[type=submit]')); $('#noteModal').modal('hide'); }" hx-on::after-request="{
resetSubmitButton(document.querySelector('.add_note_form button[type=submit]'));
$('#noteModal').modal('hide');
}"
hx-swap="outerHTML" hx-swap="outerHTML"
method="post" method="post"
class="add_note_form"> class="add_note_form">
@ -32,7 +34,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,11 +15,13 @@
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form id="scheduleForm" <form id="scheduleForm" action="{% url 'schedule_event' request.dealer.slug content_type slug %}"
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="{ resetSubmitButton(document.querySelector('.add_schedule_form button[type=submit]')); $('#scheduleModal').modal('hide'); }" hx-on::after-request="{
resetSubmitButton(document.querySelector('.add_schedule_form button[type=submit]'));
$('#scheduleModal').modal('hide');
}"
hx-swap="outerHTML" 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,14 +22,13 @@
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form id="taskForm" <form id="taskForm" action="{% url 'add_task' request.dealer.slug content_type slug %}"
action="{% url 'add_task' request.dealer.slug content_type slug %}" method="post"
method="post" class="add_task_form"
class="add_task_form" hx-post="{% url 'add_task' request.dealer.slug content_type slug %}"
hx-post="{% url 'add_task' request.dealer.slug content_type slug %}" hx-target="#your-content-container"
hx-target="#your-content-container" hx-swap="innerHTML"
hx-swap="innerHTML" hx-boost="true">
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,6 +39,7 @@
<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>
@ -46,6 +47,7 @@
<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">
@ -78,11 +80,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>
@ -91,17 +93,16 @@
</div> </div>
<div class="card mb-2"> <div class="card mb-2">
<div class="card-body"> <div class="card-body">
<div id="assignedTo" <div id="assignedTo" class="row align-items-center g-3 text-center text-xxl-start">
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>
@ -292,9 +293,11 @@
<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="{ resetSubmitButton(document.querySelector('#exampleModal button[type=submit]')); $('#exampleModal').modal('hide');}" hx-on::after-request="{
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">
@ -485,7 +488,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>
@ -525,7 +528,8 @@
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 %}
@ -803,6 +807,7 @@
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>
@ -816,62 +821,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,5 +1,6 @@
{% 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' %}
@ -7,6 +8,7 @@
{% trans 'Add New Lead' %} {% trans 'Add New Lead' %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block customcss %} {% block customcss %}
<style> <style>
.htmx-indicator{ .htmx-indicator{
@ -28,40 +30,41 @@
} }
</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">
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3"> <hr class="my-4">
<button class="btn btn-phoenix-primary btn-lg me-md-2" type="submit"> <div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
<i class="fa-solid fa-floppy-disk me-1"></i> <button class="btn btn-phoenix-primary btn-lg me-md-2" type="submit">
{% trans "Save" %} <i class="fa-solid fa-floppy-disk me-1"></i>
</button> {% trans "Save" %}
<a href="{% url 'lead_list' request.dealer.slug %}" </button>
class="btn btn-phoenix-secondary btn-lg"> <a href="{% url 'lead_list' request.dealer.slug %}" 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>
</main> </div>
{% endblock %} </main>
{% endblock %}

View File

@ -1,263 +1,270 @@
{% 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 %}
<div class="row g-3 mt-4 mb-4"> {% if page_obj.object_list or request.GET.q%}
<h2 class="mb-2"> <div class="row g-3 mt-4 mb-4">
{{ _("Leads") |capfirst }} <h2 class="mb-2">
<li class="fas fa-bullhorn text-primary ms-2"></li> {{ _("Leads") |capfirst }}
</h2> <li class="fas fa-bullhorn text-primary ms-2"></li>
<!-- Action Tracking Modal --> </h2>
{% comment %} {% include "crm/leads/partials/update_action.html" %} {% endcomment %} <!-- Action Tracking Modal -->
<div class="row g-3 justify-content-between mb-4"> {% comment %} {% include "crm/leads/partials/update_action.html" %} {% endcomment %}
<div class="col-auto">
<div class="d-md-flex justify-content-between"> <div class="row g-3 justify-content-between mb-4">
{% if perms.inventory.add_lead %} <div class="col-auto">
<div> <div class="d-md-flex justify-content-between">
<a href="{% url 'lead_create' request.dealer.slug %}" {% if perms.inventory.add_lead %}
class="btn btn-sm btn-phoenix-primary"><span class="fas fa-plus me-2"></span>{{ _("Add Lead") }}</a> <div>
</div> <a href="{% url 'lead_create' request.dealer.slug %}"
{% endif %} class="btn btn-sm btn-phoenix-primary"><span class="fas fa-plus me-2"></span>{{ _("Add Lead") }}</a>
</div> </div>
</div> {% endif %}
<div class="col-auto">
<div class="d-flex">{% include 'partials/search_box.html' %}</div>
</div> </div>
</div> </div>
<div class="row g-3"> <div class="col-auto">
<div class="col-12"> <div class="d-flex">{% include 'partials/search_box.html' %}</div>
{% if page_obj.object_list or request.GET.q %} </div>
<div class="table-responsive scrollbar mx-n1 px-1"> </div>
<table class="table align-items-center table-flush table-hover">
<thead> <div class="row g-3">
<tr class="bg-body-highlight"> <div class="col-12">
<th class="align-middle white-space-nowrap text-uppercase" {% if page_obj.object_list or request.GET.q%}
scope="col" <div class="table-responsive scrollbar mx-n1 px-1">
style="width: 20%">{{ _("Lead Name") |capfirst }}</th> <table class="table align-items-center table-flush table-hover">
<th class="align-middle white-space-nowrap text-uppercase" <thead>
scope="col" <tr class="bg-body-highlight">
style="width: 15%"> <th class="align-middle white-space-nowrap text-uppercase"
<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"> style="width: 20%">{{ _("Lead Name") |capfirst }}</th>
<i class="text-success-dark fas fa-car"></i> <th class="align-middle white-space-nowrap text-uppercase"
</div> scope="col"
<span>{{ _("Car") |capfirst }}</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">
<i class="text-success-dark fas fa-car"></i>
</div> </div>
</th> <span>{{ _("Car") |capfirst }}</span>
<th class="align-middle white-space-nowrap text-uppercase" </div>
scope="col" </th>
style="width: 15%"> <th class="align-middle white-space-nowrap text-uppercase"
<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"> style="width: 15%">
<span class="text-success-dark" data-feather="mail"></span> <div class="d-inline-flex flex-center">
</div> <div class="d-flex align-items-center px-1 py-1 bg-success-subtle rounded me-2">
<span>{{ _("email") |capfirst }}</span> <span class="text-success-dark" data-feather="mail"></span>
</div> </div>
</th> <span>{{ _("email") |capfirst }}</span>
<th class="align-middle white-space-nowrap text-uppercase" </div>
scope="col" </th>
style="width: 15%"> <th class="align-middle white-space-nowrap text-uppercase"
<div class="d-inline-flex flex-center"> scope="col"
<div class="d-flex align-items-center px-1 py-1 bg-primary-subtle rounded me-2"> style="width: 15%">
<span class="text-primary-dark" data-feather="phone"></span> <div class="d-inline-flex flex-center">
</div> <div class="d-flex align-items-center px-1 py-1 bg-primary-subtle rounded me-2">
<div class="" dir="ltr">{{ _("Phone Number") }}</div> <span class="text-primary-dark" data-feather="phone"></span>
</div> </div>
</th> <div class="" dir="ltr">{{ _("Phone Number") }}</div>
<th class="align-middle white-space-nowrap text-uppercase" </div>
scope="col" </th>
style="width: 10%"> <th class="align-middle white-space-nowrap text-uppercase"
<div class="d-inline-flex flex-center"> scope="col"
<div class="d-flex align-items-center bg-warning-subtle rounded me-2"> style="width: 10%">
<span class="text-warning-dark" data-feather="zap"></span> <div class="d-inline-flex flex-center">
</div> <div class="d-flex align-items-center bg-warning-subtle rounded me-2">
<span>{{ _("Next Action") |capfirst }}</span> <span class="text-warning-dark" data-feather="zap"></span>
</div> </div>
</th> <span>{{ _("Next Action") |capfirst }}</span>
<th class="align-middle white-space-nowrap text-uppercase" </div>
scope="col" </th>
style="width: 15%"> <th class="align-middle white-space-nowrap text-uppercase"
<div class="d-inline-flex flex-center"> scope="col"
<div class="d-flex align-items-center px-1 py-1 bg-primary-subtle rounded me-2"> style="width: 15%">
<span class="far fa-calendar-alt"></span> <div class="d-inline-flex flex-center">
</div> <div class="d-flex align-items-center px-1 py-1 bg-primary-subtle rounded me-2">
<span>{{ _("Scheduled at") }}</span> <span class="far fa-calendar-alt"></span>
</div> </div>
</th> <span>{{ _("Scheduled at") }}</span>
<th class="align-middle white-space-nowrap text-uppercase" </div>
scope="col" </th>
style="width: 10%"> <th class="align-middle white-space-nowrap text-uppercase"
<div class="d-inline-flex flex-center"> scope="col"
<div class="d-flex align-items-center bg-success-subtle rounded me-2"> style="width: 10%">
<span class="text-success-dark" data-feather="user-check"></span> <div class="d-inline-flex flex-center">
</div> <div class="d-flex align-items-center bg-success-subtle rounded me-2">
<span>{{ _("Assigned To") |capfirst }}</span> <span class="text-success-dark" data-feather="user-check"></span>
</div> </div>
</th> <span>{{ _("Assigned To") |capfirst }}</span>
{% comment %} <th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 10%;"> </div>
</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>
<tbody> </div>
<tr class="hover-actions-trigger btn-reveal-trigger position-static"> <tbody>
<td class="name align-middle white-space-nowrap ps-1"> <tr class="hover-actions-trigger btn-reveal-trigger position-static">
<div class="d-flex align-items-center"> <td class="name align-middle white-space-nowrap ps-1">
<div> <div class="d-flex align-items-center">
<a class="fs-8 fw-bold" <div>
href="{% url 'lead_detail' request.dealer.slug lead.slug %}">{{ lead.full_name|capfirst }}</a> <a class="fs-8 fw-bold"
<div class="d-flex align-items-center"> href="{% url 'lead_detail' request.dealer.slug lead.slug %}">{{ lead.full_name|capfirst }}</a>
<p class="mb-0 text-body-highlight fw-semibold fs-9 me-2"></p> <div class="d-flex align-items-center">
{% if lead.status == "new" %} <p class="mb-0 text-body-highlight fw-semibold fs-9 me-2"></p>
<span class="badge badge-phoenix badge-phoenix-primary"><span class="badge-label">{{ _("New") }}</span><span class="fa fa-bell ms-1"></span></span> {% if lead.status == "new" %}
{% elif lead.status == "pending" %} <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-warning"><span class="badge-label">{{ _("Pending") }}</span><span class="fa fa-clock-o ms-1"></span></span> {% elif lead.status == "pending" %}
{% elif lead.status == "in_progress" %} <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-info"><span class="badge-label">{{ _("In Progress") }}</span><span class="fa fa-wrench ms-1"></span></span> {% elif lead.status == "in_progress" %}
{% elif lead.status == "qualified" %} <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-success"><span class="badge-label">{{ _("Qualified") }}</span><span class="fa fa-check ms-1"></span></span> {% elif lead.status == "qualified" %}
{% elif lead.status == "contacted" %} <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-info"><span class="badge-label">{{ _("Contacted") }}</span><span class="fa fa-times ms-1"></span></span> {% elif lead.status == "contacted" %}
{% elif lead.status == "canceled" %} <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-danger"><span class="badge-label">{{ _("Canceled") }}</span><span class="fa fa-times ms-1"></span></span> {% elif lead.status == "canceled" %}
{% endif %} <span class="badge badge-phoenix badge-phoenix-danger"><span class="badge-label">{{ _("Canceled") }}</span><span class="fa fa-times ms-1"></span></span>
</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 %} {% endif %}
</div> </div>
<small>
{% if lead.staff == request.staff %}
{{ _("Me") }}
{% elif LANGUAGE_CODE == "en" %}
{{ lead.staff.fullname|capfirst }}
{% else %}
{{ lead.staff.arabic_name }}
{% endif %}
</small>
</div> </div>
</td> </div>
<td class="align-middle white-space-nowrap text-end"> </td>
{% if user == lead.staff.user or request.is_dealer %} <td class="align-middle white-space-nowrap fw-semibold">
<div class="btn-reveal-trigger position-static"> <a class="text-body-highlight" href="">{{ lead.id_car_make.get_local_name }} - {{ lead.id_car_model.get_local_name }} {{ lead.year }}</a>
<button class="btn btn-sm dropdown-toggle dropdown-caret-none transition-none btn-reveal fs-10" </td>
type="button" <td class="align-middle white-space-nowrap fw-semibold">
data-bs-toggle="dropdown" <a class="text-body-highlight" href="">{{ lead.email }}</a>
data-boundary="window" </td>
aria-haspopup="true" <td class="align-middle white-space-nowrap fw-semibold">
aria-expanded="false" <a class="text-body-highlight" href="tel:{{ lead.phone_number }}">{{ lead.phone_number }}</a>
data-bs-reference="parent"> </td>
<span class="fas fa-ellipsis-h fs-10"></span> <td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">
</button> {{ lead.next_action|upper }}
<div class="dropdown-menu dropdown-menu-end py-2"> </td>
{% if perms.inventory.change_lead %} <td class="align-middle white-space-nowrap fw-semibold">{{ lead.next_action_date|upper }}</td>
<a href="{% url 'lead_update' request.dealer.slug lead.slug %}" <td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">
class="dropdown-item text-success-dark">{% trans "Edit" %}</a> <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.change_lead %}{% endif %} {% endif %}
{% if not lead.opportunity %} {% if perms.inventory.delete_lead %}
{% if perms.inventory.add_opportunity %} <div class="dropdown-divider"></div>
<a href="{% url 'lead_opportunity_create' request.dealer.slug lead.slug %}" <button class="dropdown-item text-danger"
class="dropdown-item text-success-dark">{% trans "Convert to Opportunity" %}</a> data-bs-toggle="modal"
{% endif %} data-bs-target="#deleteModal">
{% endif %} {% trans "Delete" %}
{% if perms.inventory.delete_lead %} </button>
<div class="dropdown-divider"></div> {% endif %}
<button class="dropdown-item text-danger"
data-bs-toggle="modal"
data-bs-target="#deleteModal">
{% trans "Delete" %}
</button>
{% endif %}
</div>
</div> </div>
{% endif %} </div>
</td> {% endif %}
</tr> </td>
{% empty %} </tr>
<tr> {% empty %}
<td colspan="6" class="text-center">{% trans "No Leads found." %}</td> <tr>
</tr> <td colspan="6" class="text-center">{% trans "No Leads found." %}</td>
{% endfor %} </tr>
</tbody> {% endfor %}
</table> </tbody>
</table>
</div>
{% if page_obj.paginator.num_pages > 1 %}
<div class="d-flex justify-content-end mt-3">
<div class="d-flex">{% include 'partials/pagination.html' %}</div>
</div> </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>
{% else %} </div>
{% url 'lead_create' request.dealer.slug as create_lead_url %} {% else %}
{% include "empty-illustration-page.html" with value="lead" url=create_lead_url %} {% url 'lead_create' request.dealer.slug as create_lead_url %}
{% endif %} {% include "empty-illustration-page.html" with value="lead" url=create_lead_url %}
{% endblock %} {% endif %}
{% endblock %}

View File

@ -5,176 +5,179 @@
{% 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 %}
<div class="container-fluid my-4"> {% if leads %}
<div class="row justify-content-center"> <div class="container-fluid my-4">
<div class="col"> <div class="row justify-content-center">
<div class="d-flex justify-content-between mb-3"> <div class="col">
<h3> <div class="d-flex justify-content-between mb-3">
{{ _("Lead Tracking") }} <h3>
<li class="fas fa-bullhorn text-primary ms-2"></li> {{ _("Lead Tracking") }}
</h3> <li class="fas fa-bullhorn text-primary ms-2"></li>
</div> </h3>
<div class="row g-3"> </div>
<!-- New Lead --> <div class="row g-3">
<div class="col-md"> <!-- New Lead -->
<div class="kanban-column bg-body"> <div class="col-md">
<div class="kanban-header opacity-75"> <div class="kanban-column bg-body">
<span class="text-body">{{ _("New Leads") }} ({{ new|length }})</span> <div class="kanban-header opacity-75">
</div> <span class="text-body">{{ _("New Leads") }} ({{ new|length }})</span>
{% for lead in new %}
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
<div class="lead-card">
<strong>{{ lead.full_name|capfirst }}</strong>
<br>
<small>{{ lead.email }}</small>
<br>
<small>{{ lead.phone_number }}</small>
</div>
</a>
{% endfor %}
</div>
</div>
<!-- Follow Ups -->
<div class="col-md">
<div class="kanban-column bg-body">
<div class="kanban-header opacity-75">
<span class="text-body">{{ _("Follow Ups") }} ({{ follow_up|length }})</span>
</div>
{% for lead in follow_up %}
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
<div class="lead-card">
<strong>{{ lead.full_name|capfirst }}</strong>
<br>
<small>{{ lead.email }}</small>
<br>
<small>{{ lead.phone_number }}</small>
</div>
</a>
{% endfor %}
</div>
</div>
<!-- Negotiation -->
<div class="col-md">
<div class="kanban-column bg-body">
<div class="kanban-header opacity-75">
<span class="text-body">{{ _("Negotiation Ups") }} ({{ follow_up|length }})</span>
</div>
{% for lead in negotiation %}
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
<div class="lead-card">
<strong>{{ lead.full_name|capfirst }}</strong>
<br>
<small>{{ lead.email }}</small>
<br>
<small>{{ lead.phone_number }}</small>
</div>
</a>
{% endfor %}
</div>
</div>
<!-- Won -->
<div class="col-md">
<div class="kanban-column bg-body">
<div class="kanban-header bg-success-light opacity-75">
<span class="text-body">{{ _("Won") }} ({{ won|length }}) ({{ follow_up|length }})</span>
</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 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> </div>
<!-- Follow Ups -->
<div class="col-md">
<div class="kanban-column bg-body">
<div class="kanban-header opacity-75">
<span class="text-body">{{ _("Follow Ups") }} ({{ follow_up|length }})</span>
</div>
{% for lead in follow_up %}
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
<div class="lead-card">
<strong>{{ lead.full_name|capfirst }}</strong>
<br>
<small>{{ lead.email }}</small>
<br>
<small>{{ lead.phone_number }}</small>
</div>
</a>
{% endfor %}
</div>
</div>
<!-- Negotiation -->
<div class="col-md">
<div class="kanban-column bg-body">
<div class="kanban-header opacity-75">
<span class="text-body">{{ _("Negotiation Ups") }} ({{ follow_up|length }})</span>
</div>
{% for lead in negotiation %}
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
<div class="lead-card">
<strong>{{ lead.full_name|capfirst }}</strong>
<br>
<small>{{ lead.email }}</small>
<br>
<small>{{ lead.phone_number }}</small>
</div>
</a>
{% endfor %}
</div>
</div>
<!-- Won -->
<div class="col-md">
<div class="kanban-column bg-body">
<div class="kanban-header bg-success-light opacity-75">
<span class="text-body">{{ _("Won") }} ({{ won|length }}) ({{ follow_up|length }})</span>
</div>
{% for lead in won %}
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
<div class="lead-card">
<strong>{{ lead.full_name|capfirst }}</strong>
<br>
<small>{{ lead.email }}</small>
<br>
<small>{{ lead.phone_number }}</small>
</div>
</a>
{% endfor %}
</div>
</div>
<!-- Lose -->
<div class="col-md">
<div class="kanban-column bg-body">
<div class="kanban-header bg-danger-light opacity-75">{{ _("Lost") }} ({{ lose|length }})</div>
{% for lead in lose %}
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
<div class="lead-card">
<strong>{{ lead.full_name|capfirst }}</strong>
<br>
<small>{{ lead.email }}</small>
<br>
<small>{{ lead.phone_number }}</small>
</div>
</a>
{% endfor %}
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
{% else %} </div>
{% url 'lead_create' request.dealer.slug as create_lead_url %} {% else %}
{% include "empty-illustration-page.html" with value="lead" url=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 %}
{% 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,11 +13,14 @@
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="{ resetSubmitButton(document.querySelector('#actionTrackingForm button[type=submit]')); $('#actionTrackingModal').modal('hide'); }" hx-on::after-request="{
method="post"> resetSubmitButton(document.querySelector('#actionTrackingForm button[type=submit]'));
$('#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,9 +3,7 @@
<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' %}" <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>
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,8 +40,7 @@
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" <a class="dropdown-item" type="button"
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>
@ -684,16 +683,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>
@ -829,14 +828,15 @@
</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,6 +858,7 @@
</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"
@ -870,6 +871,7 @@
<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"
@ -894,12 +896,14 @@
<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>
@ -1019,10 +1023,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>
@ -1093,36 +1097,37 @@
</div> </div>
</div> </div>
</div> </div>
<div class="modal fade" <div class="modal fade" id="updateStageModal" tabindex="-1" aria-hidden="true">
id="updateStageModal" <div class="modal-dialog">
tabindex="-1" <div class="modal-content">
aria-hidden="true"> <form method="post"
<div class="modal-dialog"> action="{% url 'update_opportunity_stage' request.dealer.slug opportunity.slug %}"
<div class="modal-content"> hx-post="{% url 'update_opportunity_stage' request.dealer.slug opportunity.slug %}"
<form method="post" hx-swap="none"
action="{% url 'update_opportunity_stage' request.dealer.slug opportunity.slug %}" hx-on::after-request="location.reload()">
hx-post="{% url 'update_opportunity_stage' request.dealer.slug opportunity.slug %}" {% csrf_token %}
hx-swap="none" <div class="modal-header">
hx-on::after-request="location.reload()"> <h5 class="modal-title" id="updateStageModalLabel">{{ _("Update Opportunity Stage") }}</h5>
{% csrf_token %} <button class="btn btn-close p-1"
<div class="modal-header"> type="button"
<h5 class="modal-title" id="updateStageModalLabel">{{ _("Update Opportunity Stage") }}</h5> data-bs-dismiss="modal"
<button class="btn btn-close p-1" aria-label="Close"></button>
type="button" </div>
data-bs-dismiss="modal" <div class="modal-body">
aria-label="Close"></button> {{ stage_form|crispy }}
</div> </div>
<div class="modal-body">{{ stage_form|crispy }}</div> <div class="modal-footer">
<div class="modal-footer"> <button class="btn btn-phoenix-primary" type="submit">{{ _("Save") }}</button>
<button class="btn btn-phoenix-primary" type="submit">{{ _("Save") }}</button> <button class="btn btn-phoenix-secondary"
<button class="btn btn-phoenix-secondary" type="button"
type="button" data-bs-dismiss="modal">{{ _("Cancel") }}</button>
data-bs-dismiss="modal">{{ _("Cancel") }}</button> </div>
</div> </form>
</form> </div>
</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" %}
@ -1133,26 +1138,28 @@
<!-- 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) {
if (evt.detail.target.id === 'main_content') { document.body.addEventListener('htmx:afterSwap', function(evt) {
var modal = bootstrap.Modal.getInstance(document.getElementById('exampleModal')); if (evt.detail.target.id === 'main_content') {
if (modal) { var modal = bootstrap.Modal.getInstance(document.getElementById('exampleModal'));
modal.hide(); if (modal) {
} 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,162 +9,201 @@
{% 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 %}" <a href="{% url 'opportunity_list' request.dealer.slug %}" class="btn btn-phoenix-secondary">
class="btn btn-phoenix-secondary"> <span class="fas fa-arrow-left me-2"></span>{% trans "Back to list" %}
<span class="fas fa-arrow-left me-2"></span>{% trans "Back to list" %} </a>
</a> </div>
</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 class="col-lg-4">
<div class="card">
<div class="card-body p-4">
<h4 class="mb-3">{% trans "Opportunity Guidelines" %}</h4>
<ul class="nav flex-column gap-2 nav-guide">
<li class="nav-item">
<div class="d-flex align-items-center">
<span class="fas fa-circle text-primary fs-11 me-2"></span>
<span class="text-body-highlight">{% trans "Probability indicates conversion chance" %}</span>
</div>
</li>
<li class="nav-item">
<div class="d-flex align-items-center">
<span class="fas fa-circle text-warning fs-11 me-2"></span>
<span class="text-body-highlight">{% trans "Update stage as deal progresses" %}</span>
</div>
</li>
<li class="nav-item">
<div class="d-flex align-items-center">
<span class="fas fa-circle text-success fs-11 me-2"></span>
<span class="text-body-highlight">{% trans "Set realistic closing dates" %}</span>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</div> </div>
<script>
<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 class="col-lg-4">
<div class="card">
<div class="card-body p-4">
<h4 class="mb-3">{% trans "Opportunity Guidelines" %}</h4>
<ul class="nav flex-column gap-2 nav-guide">
<li class="nav-item">
<div class="d-flex align-items-center">
<span class="fas fa-circle text-primary fs-11 me-2"></span>
<span class="text-body-highlight">{% trans "Probability indicates conversion chance" %}</span>
</div>
</li>
<li class="nav-item">
<div class="d-flex align-items-center">
<span class="fas fa-circle text-warning fs-11 me-2"></span>
<span class="text-body-highlight">{% trans "Update stage as deal progresses" %}</span>
</div>
</li>
<li class="nav-item">
<div class="d-flex align-items-center">
<span class="fas fa-circle text-success fs-11 me-2"></span>
<span class="text-body-highlight">{% trans "Set realistic closing dates" %}</span>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<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,8 +75,7 @@
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> <p class="ms-auto fs-9 text-body-emphasis fw-semibold mb-0 deals-revenue">{{ opportunity.car.total }}</p># TODO : check later
# 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,137 +5,136 @@
{{ _("Opportunities") }} {{ _("Opportunities") }}
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
{% if opportunities or request.GET.q %}
<div class="row g-3 mt-4"> {% if opportunities or request.GET.q%}
<div class="row g-3 justify-content-between mb-4"> <div class="row g-3 mt-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">
<h2 class="mb-3"> <div class="d-md-flex justify-content-between">
{{ _("Opportunities") }} <h2 class="mb-3">
<li class="fas fas fa-rocket text-primary ms-2"></li> {{ _("Opportunities") }}
</h2> <li class="fas fas fa-rocket text-primary ms-2"></li>
</div> </h2>
</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-12"> <div class="col-auto">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4"> {% if perms.inventory.add_opportunity %}
<!-- Filter Controls --> <div class="d-flex justify-content-between">
<div class="d-flex flex-column flex-lg-row align-items-start align-items-lg-center gap-3 w-100" <a class="btn btn-phoenix-primary btn-sm"
id="filter-container"> href="{% url 'opportunity_create' request.dealer.slug %}">
<!-- Search Input - Wider and properly aligned --> <span class="fas fa-plus me-2"></span>{{ _("Add Opportunity") }}
<div class="search-box position-relative flex-grow-1 me-2" </a>
style="min-width: 200px"> </div>
<form class="position-relative show" {% endif %}
id="search-form" </div>
hx-get="" </div>
hx-boost="false" <div class="col-12">
hx-trigger="keyup changed delay:500ms, search"> <div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4">
<!-- Filter Controls -->
<div class="d-flex flex-column flex-lg-row align-items-start align-items-lg-center gap-3 w-100"
id="filter-container">
<!-- Search Input - Wider and properly aligned -->
<div class="search-box position-relative flex-grow-1 me-2" style="min-width: 200px">
<form class="position-relative show" id="search-form"
hx-get=""
hx-boost="false"
hx-trigger="keyup changed delay:500ms, search">
<input name="q" <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>
<!-- Filter Dropdowns - Aligned in a row --> <!-- Sort Filter -->
<div class="d-flex flex-column flex-sm-row gap-3 w-100" <div class="flex-grow-1">
style="max-width: 400px"> <select class="form-select"
<!-- Stage Filter --> name="sort"
<!-- Stage Filter --> hx-get="{% url 'opportunity_list' request.dealer.slug %}"
<div class="flex-grow-1"> hx-trigger="change"
<select class="form-select" hx-target="#opportunities-grid"
name="stage" hx-select="#opportunities-grid"
hx-get="{% url 'opportunity_list' request.dealer.slug %}" hx-swap="outerHTML"
hx-trigger="change" hx-include="#filter-container input, #filter-container select">
hx-target="#opportunities-grid" <option value="newest"
hx-select="#opportunities-grid" {% if request.GET.sort == 'newest' %}selected{% endif %}>{% trans "Newest First" %}</option>
hx-swap="outerHTML" <option value="highest"
hx-include="#filter-container input, #filter-container select"> {% if request.GET.sort == 'highest' %}selected{% endif %}>{% trans "Highest Value" %}</option>
<option value="">{% trans "All Stages" %}</option> <option value="closing"
{% for value, label in stage_choices %} {% if request.GET.sort == 'closing' %}selected{% endif %}>{% trans "Earliest Close Date" %}</option>
<option value="{{ value }}" </select>
{% 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">
{% include 'crm/opportunities/partials/opportunity_grid.html' %} </div>
<div id="opportunities-grid" class="row g-4 px-2 px-lg-4 mt-1 mb-4">
{% include 'crm/opportunities/partials/opportunity_grid.html' %}
</div>
{% if page_obj.paginator.num_pages > 1 %}
<div class="d-flex justify-content-end mt-3">
<div class="d-flex">{% include 'partials/pagination.html' %}</div>
</div> </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,19 +2,24 @@
{% 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 {% if opportunity.get_stage_display == 'Closed Won' %} bg-success-soft {% elif opportunity.get_stage_display == 'Closed Lost' %} bg-danger-soft {% endif %}"> <div class="card h-100
{% if opportunity.get_stage_display == 'Closed Won' %}
bg-success-soft
{% elif opportunity.get_stage_display == 'Closed Lost' %}
bg-danger-soft
{% endif %}">
<div class="card-body"> <div class="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 %}
@ -48,7 +53,12 @@
<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 {% if opportunity.get_stage_display == 'Won' %} badge-phoenix-success {% elif opportunity.get_stage_display == 'Lost' %} badge-phoenix-danger {% endif %}"> <span class="badge badge-phoenix fs-10
{% if opportunity.get_stage_display == 'Won' %}
badge-phoenix-success
{% elif opportunity.get_stage_display == 'Lost' %}
badge-phoenix-danger
{% endif %}">
{{ opportunity.get_status_display }} {{ opportunity.get_status_display }}
</span> </span>
</div> </div>

View File

@ -3,6 +3,7 @@
{% 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 {
@ -73,13 +74,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' %}" <a href="{% static 'sample/cars_sample.csv' %}" class="btn btn-phoenix-primary mt-4">
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>
@ -181,8 +182,7 @@
<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 %}" <a href="{% url 'car_list' request.dealer.slug %}" class="btn btn-phoenix-secondary mb-2">Cancel</a>
class="btn btn-phoenix-secondary mb-2">Cancel</a>
</form> </form>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,6 +1,7 @@
{% 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' %}
@ -8,53 +9,52 @@
{% 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 %}
<div class="alert alert-danger mt-4" role="alert"> {% if form.errors %}
<h4 class="alert-heading small">{% trans "Please correct the following errors:" %}</h4> <div class="alert alert-danger mt-4" role="alert">
<ul class="mb-0"> <h4 class="alert-heading small">{% trans "Please correct the following errors:" %}</h4>
{% for field, errors in form.errors.items %} <ul class="mb-0">
<li> {% for field, errors in form.errors.items %}
<strong>{{ field|capfirst }}:</strong> <li><strong>{{ field|capfirst }}:</strong> {% for error in errors %}{{ error }}{% endfor %}</li>
{% for error in errors %}{{ error }}{% endfor %} {% endfor %}
</li> </ul>
{% 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>
</form> {% endif %}
</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>
</main> </div>
{% endblock %} </main>
{% endblock %}

View File

@ -6,175 +6,176 @@
{% 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>
{% if page_obj.object_list or request.GET.q %} <div class="col-auto">
<div class="table-responsive scrollbar transition"> <div class="d-flex">{% include 'partials/search_box.html' %}</div>
<table class="table align-items-center table-flush table-hover"> </div>
<thead> </div>
<tr class="bg-body-highlight"> {% if page_obj.object_list or request.GET.q%}
<th></th> <div class="table-responsive scrollbar transition">
<th class="sort white-space-nowrap align-middle text-uppercase ps-0" <table class="table align-items-center table-flush table-hover">
scope="col" <thead>
data-sort="name" <tr class="bg-body-highlight">
style="width:25%">{{ _("Name") |capfirst }}</th> <th></th>
<th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent" <th class="sort white-space-nowrap align-middle text-uppercase ps-0"
scope="col" scope="col"
data-sort="email" data-sort="name"
style="width:15%"> style="width:25%">{{ _("Name") |capfirst }}</th>
<div class="d-inline-flex flex-center"> <th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent"
<div class="d-flex align-items-center px-1 py-1 bg-success-subtle rounded me-2"> scope="col"
<span class="text-success-dark" data-feather="mail"></span> data-sort="email"
</div> style="width:15%">
<span>{{ _("email") |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">
<span class="text-success-dark" data-feather="mail"></span>
</div> </div>
</th> <span>{{ _("email") |capfirst }}</span>
<th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent" </div>
scope="col" </th>
data-sort="phone" <th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent"
style="width:15%; scope="col"
min-width: 180px"> data-sort="phone"
<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"> min-width: 180px">
<span class="text-primary-dark" data-feather="phone"></span> <div class="d-inline-flex flex-center">
</div> <div class="d-flex align-items-center px-1 py-1 bg-primary-subtle rounded me-2">
<span>{{ _("Phone Number") }}</span> <span class="text-primary-dark" data-feather="phone"></span>
</div> </div>
</th> <span>{{ _("Phone Number") }}</span>
<th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent" </div>
scope="col" </th>
data-sort="contact" <th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent"
style="width:15%"> scope="col"
<div class="d-inline-flex flex-center"> data-sort="contact"
<div class="d-flex align-items-center px-1 py-1 bg-info-subtle rounded me-2"> style="width:15%">
<span class="text-info-dark" data-feather="user"></span> <div class="d-inline-flex flex-center">
</div> <div class="d-flex align-items-center px-1 py-1 bg-info-subtle rounded me-2">
<span>{{ _("National ID") |capfirst }}</span> <span class="text-info-dark" data-feather="user"></span>
</div> </div>
</th> <span>{{ _("National ID") |capfirst }}</span>
<th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent" </div>
scope="col" </th>
data-sort="company" <th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent"
style="width:15%"> scope="col"
<div class="d-inline-flex flex-center"> data-sort="company"
<div class="d-flex align-items-center px-1 py-1 bg-warning-subtle rounded me-2"> style="width:15%">
<span class="text-warning-dark" data-feather="home"></span> <div class="d-inline-flex flex-center">
</div> <div class="d-flex align-items-center px-1 py-1 bg-warning-subtle rounded me-2">
<span>{{ _("Address") |capfirst }}</span> <span class="text-warning-dark" data-feather="home"></span>
</div> </div>
</th> <span>{{ _("Address") |capfirst }}</span>
<th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent" </div>
scope="col" </th>
data-sort="company" <th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent"
style="width:15%"> scope="col"
<div class="d-inline-flex flex-center"> data-sort="company"
<div class="d-flex align-items-center px-1 py-1 bg-warning-subtle rounded me-2"> style="width:15%">
<span class="text-warning-dark" data-feather="grid"></span> <div class="d-inline-flex flex-center">
</div> <div class="d-flex align-items-center px-1 py-1 bg-warning-subtle rounded me-2">
<span>{{ _("Active") |capfirst }}</span> <span class="text-warning-dark" data-feather="grid"></span>
</div> </div>
</th> <span>{{ _("Active") |capfirst }}</span>
<th class="sort align-middle ps-4 pe-5 text-uppercase" </div>
scope="col" </th>
data-sort="date" <th class="sort align-middle ps-4 pe-5 text-uppercase"
style="width:15%"> scope="col"
{{ _("Create date") }} <span class="text-warning-dark" data-feather="clock"></span> data-sort="date"
</th> style="width:15%">
<th class="sort text-end align-middle pe-0 ps-4" scope="col"></th> {{ _("Create date") }} <span class="text-warning-dark" data-feather="clock"></span>
</tr> </th>
</thead> <th class="sort text-end align-middle pe-0 ps-4" scope="col"></th>
<tbody class="list" id="lead-tables-body"> </tr>
{% for customer in customers %} </thead>
<!-- Delete Modal --> <tbody class="list" id="lead-tables-body">
<tr class="hover-actions-trigger btn-reveal-trigger position-static"> {% for customer in customers %}
<td></td> <!-- Delete Modal -->
{% if perms.inventory.view_customer %} <tr class="hover-actions-trigger btn-reveal-trigger position-static">
<td class="name align-middle white-space-nowrap ps-0"> <td></td>
<div class="d-flex align-items-center"> {% if perms.inventory.view_customer %}
<div> <td class="name align-middle white-space-nowrap ps-0">
<a class="fs-8 fw-bold" <div class="d-flex align-items-center">
href="{% url 'customer_detail' request.dealer.slug customer.slug %}">{{ customer.full_name }}</a> <div>
<div class="d-flex align-items-center"></div> <a class="fs-8 fw-bold"
</div> href="{% url 'customer_detail' request.dealer.slug customer.slug %}">{{ customer.full_name }}</a>
<div class="d-flex align-items-center"></div>
</div> </div>
</td> </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 %} {% endif %}
<td class="email align-middle white-space-nowrap fw-semibold ps-4 border-end border-translucent"> </td>
<a class="text-body-highlight" href="">{{ customer.email }}</a> <td class="date align-middle white-space-nowrap text-body-tertiary text-opacity-85 ps-4 text-body-tertiary">
</td> {{ customer.created|date }}
<td class="phone align-middle white-space-nowrap fw-semibold ps-4 border-end border-translucent"> </td>
<a class="text-body-highlight" href="tel:{{ customer.phone }}">{{ customer.phone_number }}</a> <td class="align-middle white-space-nowrap text-end pe-0 ps-4">
</td> {% if perms.inventory.change_customer %}
<td class="contact align-middle white-space-nowrap ps-4 border-end border-translucent fw-semibold text-body-highlight"> <a href="{% url 'customer_update' request.dealer.slug customer.slug %}"
{{ customer.national_id }} class="btn btn-sm btn-phoenix-primary me-2"
</td> data-url="{% url 'customer_update' request.dealer.slug customer.slug %}">
<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"> <i class="fas fa-pen"></i>
{{ customer.address }} </a>
</td> {% endif %}
<td class="date align-middle white-space-nowrap text-body-tertiary text-opacity-85 ps-4 text-body-tertiary"> {% if perms.inventory.delete_customer %}
{% if customer.active %} <button class="btn btn-phoenix-danger btn-sm delete-btn"
<span class="badge badge-phoenix badge-phoenix-success"><i class="fas fa-check"></i> {{ customer.active }}</span> data-url="{% url 'customer_delete' request.dealer.slug customer.slug %}"
{% else %} data-message="{{ _("Are you sure you want to delete this customer") }}"
<span class="badge badge-phoenix badge-phoenix-danger"><i class="fas fa-times"></i> {{ customer.active }}</span> data-bs-toggle="modal"
{% endif %} data-bs-target="#deleteModal">
</td> <i class="fas fa-trash"></i>
<td class="date align-middle white-space-nowrap text-body-tertiary text-opacity-85 ps-4 text-body-tertiary"> </button>
{{ customer.created|date }} {% endif %}
</td> </td>
<td class="align-middle white-space-nowrap text-end pe-0 ps-4"> </tr>
{% if perms.inventory.change_customer %} {% empty %}
<a href="{% url 'customer_update' request.dealer.slug customer.slug %}" <tr>
class="btn btn-sm btn-phoenix-primary me-2" <td colspan="6" class="text-center">{% trans "No Customers found." %}</td>
data-url="{% url 'customer_update' request.dealer.slug customer.slug %}"> </tr>
<i class="fas fa-pen"></i>
</a> {% endfor %}
{% endif %} </tbody>
{% 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 %}
{% include 'modal/delete_modal.html' %} </table>
{% else %} {% if page_obj.paginator.num_pages > 1 %}
{% url "customer_create" request.dealer.slug as create_customer_url %} <div class="d-flex justify-content-end mt-3">
{% include "empty-illustration-page.html" with value="customer" url=create_customer_url %} <div class="d-flex">{% include 'partials/pagination.html' %}</div>
</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,12 +6,11 @@
{% 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"> <h3 class="mb-0">{% trans 'Customer details' %}<i class="fas fa-user ms-2 text-primary"></i></h3>
{% 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 %}
@ -31,6 +30,7 @@
{% 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,9 +39,7 @@
<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" <img class="rounded-circle border border-2 border-primary" src="{{ customer.image.url }}" alt="{{ customer.full_name }}"/>
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>
@ -67,6 +65,7 @@
</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">
@ -80,19 +79,18 @@
</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:"" }}" <a href="mailto:{{ customer.email|default:"" }}" class="text-decoration-none">{{ customer.email|default:_("N/A") }}</a>
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:"" }}" <a href="tel:{{ customer.phone_number|default:"" }}" class="text-decoration-none">{{ customer.phone_number|default:_("N/A") }}</a>
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">
@ -118,16 +116,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>
@ -136,6 +134,7 @@
</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">
@ -143,82 +142,48 @@
<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" <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>
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" <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>
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" <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>
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" <div class="tab-pane fade show active" id="leads-tab-pane" role="tabpanel" aria-labelledby="leads-tab" tabindex="0">
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 %}" <a href="{% url 'lead_detail' request.dealer.slug lead.slug %}" class="fw-bold">{{ lead }}</a>
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"
id="opportunities-tab-pane" <div class="tab-pane fade" id="opportunities-tab-pane" role="tabpanel" aria-labelledby="opportunities-tab" tabindex="0">
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 %}" <a href="{% url 'opportunity_detail' request.dealer.slug lead.opportunity.slug %}" class="fw-bold">{{ lead.opportunity }}</a>
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"
id="estimates-tab-pane" <div class="tab-pane fade" id="estimates-tab-pane" role="tabpanel" aria-labelledby="estimates-tab" tabindex="0">
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 %}" <a href="{% url 'estimate_detail' request.dealer.slug estimate.pk %}" class="text-decoration-none">{{ estimate }}</a>
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>
@ -232,22 +197,16 @@
{% 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 %}" <a href="{% url 'invoice_detail' request.dealer.slug request.entity.slug invoice.pk %}" class="text-decoration-none">{{ invoice }}</a>
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>
<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>
@ -262,32 +221,37 @@
</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,126 +1,124 @@
{% 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 "> <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>
{% trans "Aging Inventory Total" %} :: <span class=" text-danger">{{ total_aging_inventory_value|default:0.00 }}<span class="icon-saudi_riyal"></span></span> <p class="text-muted mb-0">{% trans "Cars in inventory for more than 60 days." %}</p>
</h4>
<p class="text-muted mb-0">{% trans "Cars in inventory for more than 60 days." %}</p> </div>
</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" <label for="make-filter" class="form-label mb-0 small text-uppercase fw-bold">{% trans "Make:" %}</label>
class="form-label mb-0 small text-uppercase fw-bold">{% trans "Make:" %}</label> <select class="form-select" name="make" id="make-filter">
<select class="form-select" name="make" id="make-filter"> <option value="">{% trans "All" %}</option>
<option value="">{% trans "All" %}</option> {% for make in all_makes %}
{% for make in all_makes %} <option value="{{ make }}" {% if make == selected_make %}selected{% endif %}>{{ make }}</option>
<option value="{{ make }}" {% if make == selected_make %}selected{% endif %}>{{ make }}</option> {% endfor %}
{% endfor %} </select>
</select> </div>
</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" <label for="model-filter" class="form-label mb-0 small text-uppercase fw-bold">{% trans "Model:" %}</label>
class="form-label mb-0 small text-uppercase fw-bold">{% trans "Model:" %}</label> <select class="form-select" name="model" id="model-filter">
<select class="form-select" name="model" id="model-filter"> <option value="">{% trans "All" %}</option>
<option value="">{% trans "All" %}</option> {% for model in all_models %}
{% for model in all_models %} <option value="{{ model }}" {% if model == selected_model %}selected{% endif %}>{{ model }}</option>
<option value="{{ model }}" {% endfor %}
{% if model == selected_model %}selected{% endif %}>{{ model }}</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="series-filter" class="form-label mb-0 small text-uppercase fw-bold">{% trans "Series:" %}</label>
<label for="series-filter" <select class="form-select" name="series" id="series-filter">
class="form-label mb-0 small text-uppercase fw-bold">{% trans "Series:" %}</label> <option value="">{% trans "All" %}</option>
<select class="form-select" name="series" id="series-filter"> {% for series in all_series %}
<option value="">{% trans "All" %}</option> <option value="{{ series }}" {% if series == selected_series %}selected{% endif %}>{{ series }}</option>
{% for series in all_series %} {% endfor %}
<option value="{{ series }}" </select>
{% if series == selected_series %}selected{% endif %}>{{ series }}</option> </div>
{% endfor %}
</select> <div class="col-sm-6 col-md-2 me-2">
</div> <label for="year-filter" class="form-label mb-0 small text-uppercase fw-bold">{% trans "Year:" %}</label>
<div class="col-sm-6 col-md-2 me-2"> <select class="form-select" name="year" id="year-filter">
<label for="year-filter" <option value="">{% trans "All" %}</option>
class="form-label mb-0 small text-uppercase fw-bold">{% trans "Year:" %}</label> {% for year in all_years %}
<select class="form-select" name="year" id="year-filter"> <option value="{{ year }}" {% if year|stringformat:"s" == selected_year %}selected{% endif %}>{{ year }}</option>
<option value="">{% trans "All" %}</option> {% endfor %}
{% for year in all_years %} </select>
<option value="{{ year }}" </div>
{% if year|stringformat:"s" == selected_year %}selected{% endif %}>{{ year }}</option>
{% endfor %} <div class="col-sm-6 col-md-2 me-2">
</select> <label for="stock-type-filter" class="form-label mb-0 small text-uppercase fw-bold">{% trans "Stock Type:" %}</label>
</div> <select class="form-select" name="stock_type" id="stock-type-filter">
<div class="col-sm-6 col-md-2 me-2"> <option value="">{% trans "All" %}</option>
<label for="stock-type-filter" {% for stock_type in all_stock_types %}
class="form-label mb-0 small text-uppercase fw-bold">{% trans "Stock Type:" %}</label> <option value="{{ stock_type }}" {% if stock_type == selected_stock_type %}selected{% endif %}>{{ stock_type|title }}</option>
<select class="form-select" name="stock_type" id="stock-type-filter"> {% endfor %}
<option value="">{% trans "All" %}</option> </select>
{% for stock_type in all_stock_types %} </div>
<option value="{{ stock_type }}"
{% if stock_type == selected_stock_type %}selected{% endif %}> <div class="col-auto">
{{ stock_type|title }} <button type="submit" class="btn btn-primary mt-4">{% trans "Filter" %}</button>
</option> </div>
{% endfor %} </form>
</select>
</div> {% if is_paginated %}
<div class="col-auto"> <div class="d-flex justify-content-between mb-4">
<button type="submit" class="btn btn-primary mt-4">{% trans "Filter" %}</button> <span class="text-muted">{% trans "Page" %} {{ page_obj.number }} {% trans "of" %} {{ page_obj.paginator.num_pages }}</span>
</div> <span class="text-muted">{% trans "Total Aging Cars:" %} {{ page_obj.paginator.count }}</span>
</form> </div>
{% if is_paginated %} {% endif %}
<div class="d-flex justify-content-between mb-4">
<span class="text-muted">{% trans "Page" %} {{ page_obj.number }} {% trans "of" %} {{ page_obj.paginator.num_pages }}</span> {% if cars %}
<span class="text-muted">{% trans "Total Aging Cars:" %} {{ page_obj.paginator.count }}</span> <div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
</div> {% for car in cars %}
{% endif %} <div class="col">
{% if cars %} <div class="card h-100 shadow-sm border-0">
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4"> <div class="card-body d-flex flex-column">
{% for car in cars %} <h5 class="card-title text-danger fw-bold">
<div class="col"> <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="card h-100 shadow-sm border-0"> </h5>
<div class="card-body d-flex flex-column"> <p class="card-text text-muted mb-2">
<h5 class="card-title text-danger fw-bold"> <strong>{% trans "VIN:" %}</strong> {{ car.vin }}
<img src="{{ car.logo }}" width="40" height="40" class=""> </p>
&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 }} <p class="card-text mb-2">
</h5> <strong>{% trans "Age:" %}</strong>
<p class="card-text text-muted mb-2"> <span class="badge bg-danger">{{ car.age_in_days }} {% trans "days" %}</span>
<strong>{% trans "VIN:" %}</strong> {{ car.vin }} </p>
</p> <p class="card-text mb-2">
<p class="card-text mb-2"> <strong>{% trans "Acquisition Date:" %}</strong> {{ car.receiving_date|date:"F j, Y" }}
<strong>{% trans "Age:" %}</strong> </p>
<span class="badge bg-danger">{{ car.age_in_days }} {% trans "days" %}</span> <div class="mt-auto pt-3 border-top">
</p> <a href="{% url 'car_detail' request.dealer.slug car.slug %}" class="btn btn-outline-primary btn-sm w-100">
<p class="card-text mb-2"> {% trans "View Details" %}
<strong>{% trans "Acquisition Date:" %}</strong> {{ car.receiving_date|date:"F j, Y" }} </a>
</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> </div>
</div> {% endfor %}
</div> </div>
</div> {% else %}
</div> <div class="alert alert-success d-flex align-items-center" role="alert">
{% endfor %} <i class="fas fa-check-circle me-2"></i>
</div> <div>
{% else %} {% trans "Excellent! There are no cars in the aging inventory at the moment." %}
<div class="alert alert-success d-flex align-items-center" role="alert"> </div>
<i class="fas fa-check-circle me-2"></i> </div>
<div>{% trans "Excellent! There are no cars in the aging inventory at the moment." %}</div> {% endif %}
</div> <div class="d-flex justify-content-end mt-3">
{% endif %} <div class="d-flex">
<div class="d-flex justify-content-end mt-3"> {% if is_paginated %}
<div class="d-flex"> {% include 'partials/pagination.html' %}
{% if is_paginated %} {% endif %}
{% include 'partials/pagination.html' %} </div>
{% endif %} </div>
</div> </div>
</div> {% endblock content %}
</div>
{% endblock content %}

View File

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

View File

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

View File

@ -1,371 +1,367 @@
{% 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="d-flex justify-content-between align-items-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-0"> <div class="d-flex justify-content-between align-items-center mb-5 pb-3 border-bottom">
Manager Dashboard<i class="fas fa-chart-area text-primary ms-2"></i> <h2 class="h3 fw-bold mb-0">Manager Dashboard<i class="fas fa-chart-area text-primary ms-2"></i></h2>
</h2> <div class="dropdown">
<div class="dropdown"> <button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<button class="btn btn-outline-secondary dropdown-toggle" Last 30 Days
type="button" </button>
data-bs-toggle="dropdown" <ul class="dropdown-menu dropdown-menu-end shadow">
aria-expanded="false">Last 30 Days</button> <li><a class="dropdown-item" href="#">Today</a></li>
<ul class="dropdown-menu dropdown-menu-end shadow"> <li><a class="dropdown-item" href="#">Last 7 Days</a></li>
<li> <li><a class="dropdown-item" href="#">Last 90 Days</a></li>
<a class="dropdown-item" href="#">Today</a> </ul>
</li>
<li>
<a class="dropdown-item" href="#">Last 7 Days</a>
</li>
<li>
<a class="dropdown-item" href="#">Last 90 Days</a>
</li>
</ul>
</div>
</div> </div>
<div class="row g-4 mb-5"> </div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0"> <div class="row g-4 mb-5">
<div class="card-body d-flex flex-column justify-content-between p-4"> <div class="col-sm-6 col-md-4 col-lg-3">
<div> <div class="card h-100 shadow-sm border-0">
<p class="text-uppercase text-muted fw-bold small mb-1">Total Revenue</p> <div class="card-body d-flex flex-column justify-content-between p-4">
<h4 class="fw-bolder text-primary mb-3">$1.25M</h4> <div>
</div> <p class="text-uppercase text-muted fw-bold small mb-1">Total Revenue</p>
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start"> <h4 class="fw-bolder text-primary mb-3">$1.25M</h4>
+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="row g-4 mb-5"> <div class="col-sm-6 col-md-4 col-lg-3">
<div class="col-lg-8"> <div class="card h-100 shadow-sm border-0">
<div class="card h-100 shadow-sm border-0"> <div class="card-body d-flex flex-column justify-content-between p-4">
<div class="card-header bg-white border-bottom-0"> <div>
<h5 class="fw-bold mb-0 text-dark">Monthly Revenue & Profit</h5> <p class="text-uppercase text-muted fw-bold small mb-1">Net Profit</p>
</div> <h4 class="fw-bolder text-success mb-3">$1.25M</h4>
<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="row g-4"> <div class="col-sm-6 col-md-4 col-lg-3">
<div class="col-lg-6"> <div class="card h-100 shadow-sm border-0">
<div class="card h-100 shadow-sm border-0"> <div class="card-body d-flex flex-column justify-content-between p-4">
<div class="card-header bg-white border-bottom-0"> <div>
<h5 class="fw-bold mb-0 text-dark">Sales by Make</h5> <p class="text-uppercase text-muted fw-bold small mb-1">Total Expense</p>
</div> <h4 class="fw-bolder text-danger mb-3">$1.25M</h4>
<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 class="col-lg-6"> </div>
<div class="card h-100 shadow-sm border-0"> <div class="col-sm-6 col-md-4 col-lg-3">
<div class="card-header bg-white border-bottom-0"> <div class="card h-100 shadow-sm border-0">
<h5 class="fw-bold mb-0 text-dark">Top Salesperson Performance</h5> <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> </div>
<div class="card-body" style="height: 400px;"> <span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
<canvas id="salespersonChart"></canvas> +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> </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 }
}, },
options: { scales: {
responsive: true, y: {
maintainAspectRatio: false, beginAtZero: true,
plugins: { grid: { color: 'rgba(0, 0, 0, 0.05)' },
legend: { display: false } ticks: { color: secondaryColor }
}, },
scales: { x: {
y: { grid: { display: false },
beginAtZero: true, ticks: { color: secondaryColor }
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 }
}, },
scales: { y: {
x: { grid: { color: 'rgba(0, 0, 0, 0.05)' },
grid: { color: 'rgba(0, 0, 0, 0.05)' }, ticks: { color: secondaryColor },
ticks: { color: secondaryColor }, border: { 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 } }
}, },
options: { scales: {
responsive: true, x: {
maintainAspectRatio: false, title: { display: true, text: 'Salesperson Name', color: secondaryColor },
plugins: { ticks: { color: secondaryColor }
legend: { display: false },
title: { display: true, text: 'Top Salesperson Performance', font: { size: 16 } }
}, },
scales: { y: {
x: { beginAtZero: true,
title: { display: true, text: 'Salesperson Name', color: secondaryColor }, title: { display: true, text: 'Number of Cars Sold', 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> });
{% endblock %} </script>
{% endblock %}

View File

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

View File

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

View File

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

View File

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

View File

@ -5,38 +5,34 @@
{{ _("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" <form hx-boost="false" method="post" enctype="multipart/form-data" class="needs-validation" novalidate>
method="post" {% csrf_token %}
enctype="multipart/form-data" {{ form|crispy }}
class="needs-validation" <hr class="my-4">
novalidate> <div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
{% csrf_token %} <button class="btn btn-phoenix-primary btn-lg me-md-2" type="submit">
{{ form|crispy }} <i class="fa-solid fa-floppy-disk me-1"></i>
<hr class="my-4"> {{ _("Save") }}
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3"> </button>
<button class="btn btn-phoenix-primary btn-lg me-md-2" type="submit"> <a href="{% url 'dealer_detail' request.dealer.slug %}"
<i class="fa-solid fa-floppy-disk me-1"></i> class="btn btn-phoenix-secondary btn-lg">
{{ _("Save") }} <i class="fa-solid fa-ban me-1"></i>
</button> {% trans "Cancel" %}
<a href="{% url 'dealer_detail' request.dealer.slug %}" </a>
class="btn btn-phoenix-secondary btn-lg"> </div>
<i class="fa-solid fa-ban me-1"></i> </form>
{% trans "Cancel" %}
</a>
</div>
</form>
</div>
</div> </div>
</div> </div>
</main> </div>
{% endblock %} </main>
{% endblock %}

View File

@ -170,7 +170,10 @@
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; font-size: 14px; line-height: 24px; margin: 0">{% trans 'Thank you for choosing us.' %}</p> <p style="color: #ffffff;
font-size: 14px;
line-height: 24px;
margin: 0">{% trans 'Thank you for choosing us.' %}</p>
</td> </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>
اشتراكك في <strong>{{ plan.name }}</strong> سينتهي خلال <p>
{{ days_until_expire }} يوم في {{ expiration_date|date:"j F Y" }}. اشتراكك في <strong>{{ plan.name }}</strong> سينتهي خلال
</p> {{ days_until_expire }} يوم في {{ expiration_date|date:"j F Y" }}.
<p> </p>
<a href="{{ RENEWAL_URL }}">جدد اشتراكك الآن</a> لمواصلة الخدمة.
</p> <p>
<p> <a href="{{ RENEWAL_URL }}">جدد اشتراكك الآن</a> لمواصلة الخدمة.
مع أطيب التحيات، </p>
<br>
فريق تنحل <p>مع أطيب التحيات،<br>
</p> فريق تنحل</p>
</body> </body>
</html> </html>

View File

@ -1,18 +1,14 @@
<!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>
Your <strong>{{ plan.name }}</strong> subscription will expire <p>Your <strong>{{ plan.name }}</strong> subscription will expire
in {{ days_until_expire }} days on {{ expiration_date|date:"F j, Y" }}. in {{ days_until_expire }} days on {{ expiration_date|date:"F j, Y" }}.</p>
</p>
<p> <p><a href="{{ RENEWAL_URL }}">Renew now</a> to continue service.</p>
<a href="{{ RENEWAL_URL }}">Renew now</a> to continue service.
</p> <p>Best regards,<br>
<p> The Team at Tenhal</p>
Best regards, </body>
<br> </html>
The Team at Tenhal
</p>
</body>
</html>

View File

@ -1,42 +1,34 @@
<!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 }} <span class="highlight">{% trans "Purpose" %}:</span> {{ schedule_purpose }}<br>
<br> <span class="highlight">{% trans "Scheduled At" %}:</span> {{ scheduled_at }}<br>
<span class="highlight">{% trans "Scheduled At" %}:</span> {{ scheduled_at }} <span class="highlight">{% trans "Type" %}:</span> {{ schedule_type }}<br>
<br> {% if customer_name != 'N/A' %}<span class="highlight">{% trans "Customer" %}:</span> {{ customer_name }}<br>{% endif %}
<span class="highlight">{% trans "Type" %}:</span> {{ schedule_type }} {% if notes %}<span class="highlight">{% trans "Notes" %}:</span> {{ notes }}<br>{% endif %}
<br> </p>
{% if customer_name != 'N/A' %} <p>{% trans "Please be prepared for your schedule" %}.</p>
<span class="highlight">{% trans "Customer" %}:</span> {{ customer_name }} <p>{% trans "Thank you" %}!</p>
<br> <p class="fs-4">{% trans "The team at Tenhal" %}.</p>
{% endif %} <div class="footer">
{% if notes %} <p>{% trans "This is an automated reminder. Please do not reply to this email." %}</p>
<span class="highlight">{% trans "Notes" %}:</span> {{ notes }}
<br>
{% endif %}
</p>
<p>{% trans "Please be prepared for your schedule" %}.</p>
<p>{% trans "Thank you" %}!</p>
<p class="fs-4">{% trans "The team at Tenhal" %}.</p>
<div class="footer">
<p>{% trans "This is an automated reminder. Please do not reply to this email." %}</p>
</div>
</div> </div>
</body>
</html> </div>
</body>
</html>

View File

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

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,6 +15,7 @@
</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">
@ -31,6 +32,7 @@
</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">
@ -49,64 +51,73 @@
</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);
color: var(--text-color); <footer class="improved-footer">
} <div class="container">
<div class="row g-0 justify-content-between align-items-center h-100">
.improved-footer .text-body { <div class="col-12 col-sm-auto text-center">
color: var(--text-color) !important; <span class="text-body"> © 2025 All rights reserved</span>
font-weight: 400; <span class="fw-bold">Haikal</span>&nbsp;|&nbsp;<span class="fw-bold">هيكل</span>
} </div>
<div class="col-12 col-sm-auto text-center">
.improved-footer .fw-bold { <span class="text-body">Powered by </span>
font-weight: 600 !important; <span>
color: var(--link-color); <a class="mx-1 text-secondary" href="https://tenhal.sa">
} <span>TENHAL</span>&nbsp;|&nbsp;<span>تنحل</span>
</a>
.improved-footer a { </span>
color: var(--link-color) !important; <span class="fas fa-registered fw-light"></span>
text-decoration: none; </div>
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>
</div> </footer>
</footer>

View File

@ -10,51 +10,49 @@
{% 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 %}
<div class="alert alert-danger mt-4" role="alert"> {% if form.errors %}
<h4 class="alert-heading small">{% trans "Please correct the following errors:" %}</h4> <div class="alert alert-danger mt-4" role="alert">
<ul class="mb-0"> <h4 class="alert-heading small">{% trans "Please correct the following errors:" %}</h4>
{% for field, errors in form.errors.items %} <ul class="mb-0">
<li> {% for field, errors in form.errors.items %}
<strong>{{ field|capfirst }}:</strong> <li><strong>{{ field|capfirst }}:</strong> {% for error in errors %}{{ error }}{% endfor %}</li>
{% for error in errors %}{{ error }}{% endfor %} {% endfor %}
</li> </ul>
{% 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>
</form> {% endif %}
</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>
</main> </div>
{% endblock %} </main>
{% endblock %}

View File

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

View File

@ -21,138 +21,144 @@
</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="col-md-6"> <div class="row mb-4">
<div class="input-group"> <div class="col-md-6">
<span class="input-group-text"><i class="fas fa-search"></i></span> <div class="input-group">
<input type="text" <span class="input-group-text"><i class="fas fa-search"></i></span>
class="form-control" <input type="text" class="form-control" id="permissionSearch"
id="permissionSearch" placeholder="{% trans 'Search permissions...' %}">
placeholder="{% trans 'Search permissions...' %}">
</div>
</div>
<div class="col-md-6">
<div class="alert alert-info py-2 mb-0">
<i class="fas fa-info-circle me-2"></i>
{% trans "Checked items are currently assigned permissions" %}
</div>
</div> </div>
</div> </div>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4" <div class="col-md-6">
id="permissionsGrid"> <div class="alert alert-info py-2 mb-0">
{% for app_label, models in grouped_permissions.items %} <i class="fas fa-info-circle me-2"></i>
<div class="col"> {% trans "Checked items are currently assigned permissions" %}
{# This div opens for each app_label #} </div>
<div class="card h-100 border-{% if app_label in group_permission_apps %}primary{% else %}light{% endif %}"> </div>
<div class="card-header bg-{% if app_label in group_permission_apps %}primary text-white{% else %}light{% endif %}"> </div>
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0"> <div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4" id="permissionsGrid">
<i class="fas fa-{% if app_label in group_permission_apps %}check-circle{% else %}cube{% endif %} me-2"></i> {% for app_label, models in grouped_permissions.items %}
{{ app_label|capfirst }} <div class="col"> {# This div opens for each app_label #}
</h5> <div class="card h-100 border-{% if app_label in group_permission_apps %}primary{% else %}light{% endif %}">
<span class="badge bg-{% if app_label in group_permission_apps %}light text-primary{% else %}secondary{% endif %}"> <div class="card-header bg-{% if app_label in group_permission_apps %}primary text-white{% else %}light{% endif %}">
{{ models|length }} {% trans "categories" %} <div class="d-flex justify-content-between align-items-center">
</span> <h5 class="card-title mb-0">
</div> <i class="fas fa-{% if app_label in group_permission_apps %}check-circle{% else %}cube{% endif %} me-2"></i>
</div> {{ app_label|capfirst }}
<div class="card-body"> </h5>
<div class="accordion" id="accordion-{{ app_label|slugify }}"> <span class="badge bg-{% if app_label in group_permission_apps %}light text-primary{% else %}secondary{% endif %}">
{% for model, perms in models.items %} {{ models|length }} {% trans "categories" %}
<div class="accordion-item border-0 mb-2"> </span>
<h6 class="accordion-header" </div>
id="heading-{{ app_label|slugify }}-{{ model|slugify }}"> </div>
<button class="accordion-button collapsed bg-white shadow-none py-2" <div class="card-body">
type="button" <div class="accordion" id="accordion-{{ app_label|slugify }}">
data-bs-toggle="collapse" {% for model, perms in models.items %}
data-bs-target="#collapse-{{ app_label|slugify }}-{{ model|slugify }}" <div class="accordion-item border-0 mb-2">
aria-expanded="false" <h6 class="accordion-header" id="heading-{{ app_label|slugify }}-{{ model|slugify }}">
aria-controls="collapse-{{ app_label|slugify }}-{{ model|slugify }}"> <button class="accordion-button collapsed bg-white shadow-none py-2"
<i class="fas fa-{% if model == 'Custom' %}star{% else %}table{% endif %} me-2"></i> type="button"
{{ model|capfirst }} data-bs-toggle="collapse"
<span class="badge bg-{% if model in group_permission_models %}primary{% else %}secondary{% endif %} rounded-pill ms-2"> data-bs-target="#collapse-{{ app_label|slugify }}-{{ model|slugify }}"
{# This is where you might need the custom filter 'count_checked' #} aria-expanded="false"
{{ perms|length }} / {{ perms|count_checked:group_permission_ids }} 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> </span>
</button> {% elif perm.id in group_permission_ids %}
</h6> <span class="badge bg-success mt-1">
<div id="collapse-{{ app_label|slugify }}-{{ model|slugify }}" <i class="fas fa-check me-1"></i>{% trans "Assigned" %}
class="accordion-collapse collapse" </span>
aria-labelledby="heading-{{ app_label|slugify }}-{{ model|slugify }}" {% endif %}
data-bs-parent="#accordion-{{ app_label|slugify }}"> </span>
<div class="accordion-body pt-0 px-0"> </label>
<div class="list-group list-group-flush"> {% endfor %}
{% 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>
{% endfor %} </div>
</div> </div>
</div> </div>
</div> {% endfor %}
</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>
</form> </div>
</div> </form>
<style> </div>
.bg-light-primary {
background-color: rgba(13, 110, 253, 0.1); <style>
} .bg-light-primary {
.list-group-item:hover { background-color: rgba(13, 110, 253, 0.1);
background-color: rgba(0, 0, 0, 0.03); }
} .list-group-item:hover {
.accordion-button:not(.collapsed) { background-color: rgba(0, 0, 0, 0.03);
box-shadow: none; }
background-color: transparent; .accordion-button:not(.collapsed) {
} box-shadow: none;
.accordion-button:focus { background-color: transparent;
box-shadow: none; }
border-color: rgba(0,0,0,.125); .accordion-button:focus {
} box-shadow: none;
</style> border-color: rgba(0,0,0,.125);
<script> }
document.addEventListener('DOMContentLoaded', function() { </style>
<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,12 +71,20 @@
<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' %} مسح المحادثة {% else %} Clear Chat {% endif %}"> title="{% if LANGUAGE_CODE == 'ar' %}
مسح المحادثة
{% 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' %} تصدير المحادثة {% else %} Export Chat {% endif %}"> title="{% if LANGUAGE_CODE == 'ar' %}
تصدير المحادثة
{% 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,7 +3,11 @@
{% 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 %} {% url 'sales_dashboard' request.dealer.slug %} {% else %} {% url 'general_dashboard' request.dealer.slug %} {% endif %}" hx-get="{% if request.is_sales and not request.is_manager %}
{% url 'sales_dashboard' request.dealer.slug %}
{% else %}
{% url 'general_dashboard' request.dealer.slug %}
{% endif %}"
hx-trigger="load" hx-trigger="load"
hx-target="#dashboard-content" hx-target="#dashboard-content"
hx-swap="innerHTML"> hx-swap="innerHTML">
@ -13,4 +17,6 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -1,69 +1,67 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n %} {% load i18n %}
{% block title %} {%block title%} {%trans 'Add Colors'%} {% endblock%}
{% trans 'Add Colors' %} {% endblock %} {% block content %}
{% block content %} <div class="row mt-4 mb-3">
<div class="row mt-4 mb-3"> <h3 class="text-center">{% trans "Add Colors" %}</h3>
<h3 class="text-center">{% trans "Add Colors" %}</h3> <p class="text-center">
<p class="text-center"> {% trans "Select exterior and interior colors for" %} {{ car.id_car_make.get_local_name }} {{ car.id_car_model.get_local_name }}
{% trans "Select exterior and interior colors for" %} {{ car.id_car_make.get_local_name }} {{ car.id_car_model.get_local_name }} </p>
</p> <form method="post">
<form method="post"> {% csrf_token %}
{% csrf_token %} <!-- Exterior Colors -->
<!-- Exterior Colors --> <div class="row g-4">
<div class="row g-4"> <p class="fs-5 mb-0">{% trans 'Exterior Colors' %}</p>
<p class="fs-5 mb-0">{% trans 'Exterior Colors' %}</p> {% for color in form.fields.exterior.queryset %}
{% for color in form.fields.exterior.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="exterior"
name="exterior" value="{{ color.id }}" {% if color.id == form.instance.exterior.id %}checked{% endif %}>
value="{{ color.id }}"
{% if color.id == form.instance.exterior.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>
</label> </div>
</div> </label>
</div> </div>
{% endfor %} </div>
<!-- Interior Colors --> {% endfor %}
<p class="fs-5 mt-3 mb-0">{% trans 'Interior Colors' %}</p> <!-- Interior Colors -->
{% for color in form.fields.interior.queryset %} <p class="fs-5 mt-3 mb-0">{% trans 'Interior Colors' %}</p>
<div class="col-lg-4 col-xl-2"> {% for color in form.fields.interior.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="interior" type="radio"
value="{{ color.id }}" name="interior"
{% if color.id == form.instance.interior.id %}checked{% endif %}> value="{{ color.id }}" {% 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>
</label> </div>
</div> </label>
</div> </div>
{% endfor %} </div>
</div> {% endfor %}
<div class="d-flex justify-content-center mt-4"> </div>
<button class="btn btn-lg btn-phoenix-primary me-2" type="submit">
<i class="fa-solid fa-floppy-disk me-1"></i>{{ _("Save") }} <div class="d-flex justify-content-center mt-4">
</button> <button class="btn btn-lg btn-phoenix-primary me-2" type="submit">
<a href="{% url 'car_detail' request.dealer.slug car.slug %}" <i class="fa-solid fa-floppy-disk me-1"></i>{{ _("Save") }}
class="btn btn-lg btn-phoenix-secondary"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a> </button>
</div> <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>
</form> </div>
</div> </form>
<style> </div>
<style>
.color-card { .color-card {
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
@ -95,7 +93,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,34 +1,41 @@
{% 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="mb-4"> <div class="card-body p-4 p-md-5 text-center bg-gradient">
<i class="fa-solid fa-triangle-exclamation text-danger" <div class="mb-4">
style="font-size: 2rem"></i> <i class="fa-solid fa-triangle-exclamation text-danger" style="font-size: 2rem;"></i>
</div>
<h1 class="card-title fw-bold mb-3 fs-4">{% trans 'Confirm Deletion' %}</h1>
<p class="fs-7 mb-4">{% trans "Are you absolutely sure you want to delete the car" %}</p>
<p class="fs-6 mb-4">
"<strong class="">{{ car }}</strong>"?
</p>
<p class="fs-7 mb-4">{% trans "This action is permanent and cannot be undone." %}</p>
<form method="post"
class="d-grid gap-3 d-sm-flex justify-content-sm-center">
{% csrf_token %}
<button type="submit" class="btn btn-phoenix-danger btn-lg px-5">
<i class="fa-solid fa-trash-can me-2"></i>{% trans 'Confirm Delete' %}
</button>
<a href="{% url 'car_list' request.dealer.slug %}"
class="btn btn-phoenix-secondary btn-lg px-5">
<i class="fa-solid fa-ban me-2"></i>{% trans 'Cancel' %}
</a>
</form>
</div> </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>
</main> </div>
{% endblock %} </main>
{% endblock %}

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