Compare commits

...

2 Commits

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

View File

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

View File

@ -56,7 +56,7 @@ from .models import (
DealerSettings,
Tasks,
Recall,
Ticket
Ticket,
)
from django_ledger import models as ledger_models
from django.forms import (
@ -146,9 +146,16 @@ class StaffForm(forms.ModelForm):
)
class Meta:
model = Staff
fields = ["first_name","last_name", "arabic_name", "phone_number", "address", "logo", "group"]
fields = [
"first_name",
"last_name",
"arabic_name",
"phone_number",
"address",
"logo",
"group",
]
# Dealer Form
@ -439,7 +446,9 @@ class CarFinanceForm(forms.ModelForm):
marked_price = cleaned_data.get("marked_price")
if cost_price > marked_price:
raise forms.ValidationError({"cost_price": "Cost price should not be greater than marked price"})
raise forms.ValidationError(
{"cost_price": "Cost price should not be greater than marked price"}
)
return cleaned_data
@ -1289,6 +1298,7 @@ class OpportunityForm(forms.ModelForm):
if self.instance and self.instance.pk:
self.fields["probability"].initial = self.instance.probability
class OpportunityStageForm(forms.ModelForm):
"""
Represents a form for creating or editing Opportunity instances.
@ -1305,17 +1315,13 @@ class OpportunityStageForm(forms.ModelForm):
:type Meta.fields: list
"""
class Meta:
model = Opportunity
fields = [
"stage",
]
class InvoiceModelCreateForm(InvoiceModelCreateFormBase):
"""
Represents a form for creating an Invoice model that inherits from a base
@ -1633,8 +1639,7 @@ class PermissionForm(forms.ModelForm):
"django_ledger.billmodeldjango_ledger.itemmodel",
"django_ledger.invoicemodel",
"django_ledger.vendormodel",
"django_ledger.journalentrymodel"
"django_ledger.purchaseordermodel",
"django_ledger.journalentrymodeldjango_ledger.purchaseordermodel",
]
permissions = cache.get(
@ -2138,91 +2143,115 @@ class VatRateForm(forms.ModelForm):
class CustomSetPasswordForm(SetPasswordForm):
new_password1 = forms.CharField(
label="New Password",
widget=forms.PasswordInput(attrs={'class': 'form-control', 'placeholder': 'New Password'})
widget=forms.PasswordInput(
attrs={"class": "form-control", "placeholder": "New Password"}
),
)
new_password2 = forms.CharField(
label="Confirm New Password",
widget=forms.PasswordInput(attrs={'class': 'form-control', 'placeholder': 'Confirm New Password'})
widget=forms.PasswordInput(
attrs={"class": "form-control", "placeholder": "Confirm New Password"}
),
)
# forms.py
class RecallFilterForm(forms.Form):
make = forms.ModelChoiceField(
queryset=CarMake.objects.all(),
required=False,
label=_("Make"),
widget=forms.Select(attrs={'class': 'form-control'})
widget=forms.Select(attrs={"class": "form-control"}),
)
model = forms.ModelChoiceField(
queryset=CarModel.objects.none(),
required=False,
label=_("Model"),
widget=forms.Select(attrs={'class': 'form-control'})
widget=forms.Select(attrs={"class": "form-control"}),
)
serie = forms.ModelChoiceField(
queryset=CarSerie.objects.none(),
required=False,
label=_("Series"),
widget=forms.Select(attrs={'class': 'form-control'})
widget=forms.Select(attrs={"class": "form-control"}),
)
trim = forms.ModelChoiceField(
queryset=CarTrim.objects.none(),
required=False,
label=_("Trim"),
widget=forms.Select(attrs={'class': 'form-control'})
widget=forms.Select(attrs={"class": "form-control"}),
)
year_from = forms.IntegerField(
required=False,
label=_("From Year"),
widget=forms.NumberInput(attrs={"class": "form-control"}),
)
year_to = forms.IntegerField(
required=False,
label=_("To Year"),
widget=forms.NumberInput(attrs={"class": "form-control"}),
)
year_from = forms.IntegerField(required=False, label=_("From Year"),
widget=forms.NumberInput(attrs={'class': 'form-control'}))
year_to = forms.IntegerField(required=False, label=_("To Year"),
widget=forms.NumberInput(attrs={'class': 'form-control'}))
def __init__(self, *args, **kwargs):
make_id = kwargs.pop('make_id', None)
model_id = kwargs.pop('model_id', None)
serie_id = kwargs.pop('serie_id', None)
make_id = kwargs.pop("make_id", None)
model_id = kwargs.pop("model_id", None)
serie_id = kwargs.pop("serie_id", None)
super().__init__(*args, **kwargs)
if make_id:
self.fields['model'].queryset = CarModel.objects.filter(id_car_make_id=make_id)
self.fields["model"].queryset = CarModel.objects.filter(
id_car_make_id=make_id
)
if model_id:
self.fields['serie'].queryset = CarSerie.objects.filter(id_car_model_id=model_id)
self.fields["serie"].queryset = CarSerie.objects.filter(
id_car_model_id=model_id
)
if serie_id:
self.fields['trim'].queryset = CarTrim.objects.filter(id_car_serie_id=serie_id)
self.fields["trim"].queryset = CarTrim.objects.filter(
id_car_serie_id=serie_id
)
class RecallCreateForm(forms.ModelForm):
class Meta:
model = Recall
fields = ['title', 'description', 'make', 'model', 'serie', 'trim', 'year_from', 'year_to']
fields = [
"title",
"description",
"make",
"model",
"serie",
"trim",
"year_from",
"year_to",
]
widgets = {
'make': forms.Select(attrs={'class': 'form-control'}),
'model': forms.Select(attrs={'class': 'form-control'}),
'serie': forms.Select(attrs={'class': 'form-control'}),
'trim': forms.Select(attrs={'class': 'form-control'}),
'title': forms.TextInput(attrs={'class': 'form-control'}),
'description': forms.Textarea(attrs={'class': 'form-control'}),
'year_from': forms.NumberInput(attrs={'class': 'form-control'}),
'year_to': forms.NumberInput(attrs={'class': 'form-control'}),
"make": forms.Select(attrs={"class": "form-control"}),
"model": forms.Select(attrs={"class": "form-control"}),
"serie": forms.Select(attrs={"class": "form-control"}),
"trim": forms.Select(attrs={"class": "form-control"}),
"title": forms.TextInput(attrs={"class": "form-control"}),
"description": forms.Textarea(attrs={"class": "form-control"}),
"year_from": forms.NumberInput(attrs={"class": "form-control"}),
"year_to": forms.NumberInput(attrs={"class": "form-control"}),
}
class TicketForm(forms.ModelForm):
class Meta:
model = Ticket
fields = ['subject', 'description', 'priority']
fields = ["subject", "description", "priority"]
widgets = {
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 10}),
"description": forms.Textarea(attrs={"class": "form-control", "rows": 10}),
}
class TicketResolutionForm(forms.ModelForm):
class Meta:
model = Ticket
fields = ['status', 'resolution_notes']
fields = ["status", "resolution_notes"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit status choices to resolution options
self.fields['status'].choices = [
('resolved', 'Resolved'),
('closed', 'Closed')
]
self.fields["status"].choices = [("resolved", "Resolved"), ("closed", "Closed")]

View File

@ -4,6 +4,7 @@ from .utils import get_accounts_data,create_account
logger = logging.getLogger(__name__)
def check_create_coa_accounts(task):
logger.info("Checking if all accounts are created")
instance = task.kwargs["dealer"]
@ -17,6 +18,7 @@ def check_create_coa_accounts(task):
logger.info(f"Default account does not exist: {account_data['code']}")
create_account(entity, coa, account_data)
def print_results(task):
dealer = task.kwargs["dealer"]
print("HOOK: ", dealer)

View File

@ -13,6 +13,7 @@ from inventory.models import ExtraInfo,Notification,CustomGroup
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Handles invoices due date reminders"
@ -33,27 +34,30 @@ class Command(BaseCommand):
def invocie_expiration_reminders(self):
"""Queue email reminders for expiring plans"""
reminder_days = getattr(settings, 'INVOICE_PAST_DUE_REMIND', [3, 7, 14])
reminder_days = getattr(settings, "INVOICE_PAST_DUE_REMIND", [3, 7, 14])
today = timezone.now().date()
for days in reminder_days:
target_date = today + timedelta(days=days)
expiring_plans = InvoiceModel.objects.filter(
date_due=target_date
).select_related('customer','ce_model')
).select_related("customer", "ce_model")
for inv in expiring_plans:
# dealer = inv.customer.customer_set.first().dealer
subject = f"Your invoice is due in {days} days"
message = render_to_string('emails/invoice_past_due_reminder.txt', {
'customer_name': inv.customer.customer_name,
'invoice_number': inv.invoice_number,
'amount_due': inv.amount_due,
'days_past_due': inv.due_in_days(),
'SITE_NAME': settings.SITE_NAME
})
message = render_to_string(
"emails/invoice_past_due_reminder.txt",
{
"customer_name": inv.customer.customer_name,
"invoice_number": inv.invoice_number,
"amount_due": inv.amount_due,
"days_past_due": inv.due_in_days(),
"SITE_NAME": settings.SITE_NAME,
},
)
send_email(
'noreply@yourdomain.com',
"noreply@yourdomain.com",
inv.customer.email,
subject,
message,
@ -66,20 +70,23 @@ class Command(BaseCommand):
today = timezone.now().date()
expiring_plans = InvoiceModel.objects.filter(
date_due__lte=today
).select_related('customer','ce_model')
).select_related("customer", "ce_model")
# Send email
for inv in expiring_plans:
dealer = inv.customer.customer_set.first().dealer
subject = f"Your invoice is past due"
message = render_to_string('emails/invoice_past_due.txt', {
'customer_name': inv.customer.customer_name,
'invoice_number': inv.invoice_number,
'amount_due': inv.amount_due,
'days_past_due': (today - inv.date_due).days,
'SITE_NAME': settings.SITE_NAME
})
message = render_to_string(
"emails/invoice_past_due.txt",
{
"customer_name": inv.customer.customer_name,
"invoice_number": inv.invoice_number,
"amount_due": inv.amount_due,
"days_past_due": (today - inv.date_due).days,
"SITE_NAME": settings.SITE_NAME,
},
)
# send notification to accountatnt
recipients = (
@ -100,14 +107,18 @@ class Command(BaseCommand):
invoice_number=inv.invoice_number,
url=reverse(
"invoice_detail",
kwargs={"dealer_slug": dealer.slug, "entity_slug": dealer.entity.slug, "pk": inv.pk},
kwargs={
"dealer_slug": dealer.slug,
"entity_slug": dealer.entity.slug,
"pk": inv.pk,
},
),
),
)
# send email to customer
send_email(
'noreply@yourdomain.com',
"noreply@yourdomain.com",
inv.customer.email,
subject,
message,

View File

@ -5,6 +5,8 @@ from inventory.models import Car
from django_ledger.models import EntityModel, InvoiceModel, ItemModel
from inventory.utils import CarFinanceCalculator
from rich import print
class Command(BaseCommand):
help = ""
@ -18,23 +20,39 @@ class Command(BaseCommand):
calc = CarFinanceCalculator(i)
data = calc.get_finance_data()
for car_data in data['cars']:
car = i.get_itemtxs_data()[0].filter(
item_model__car__vin=car_data['vin']
).first().item_model.car
for car_data in data["cars"]:
car = (
i.get_itemtxs_data()[0]
.filter(item_model__car__vin=car_data["vin"])
.first()
.item_model.car
)
print("car", car)
qty = Decimal(car_data['quantity'])
qty = Decimal(car_data["quantity"])
print("qty", qty)
# amounts from calculator
net_car_price = Decimal(car_data['total']) # after discount
net_add_price = Decimal(data['total_additionals']) # per car or split however you want
vat_amount = Decimal(data['total_vat_amount']) * qty # prorate if multi-qty
net_car_price = Decimal(car_data["total"]) # after discount
net_add_price = Decimal(
data["total_additionals"]
) # per car or split however you want
vat_amount = Decimal(data["total_vat_amount"]) * qty # prorate if multi-qty
# grand_total = net_car_price + net_add_price + vat_amount
grand_total = Decimal(data['grand_total'])
cost_total = Decimal(car_data['cost_price']) * qty
grand_total = Decimal(data["grand_total"])
cost_total = Decimal(car_data["cost_price"]) * qty
print("net_car_price", net_car_price, "net_add_price", net_add_price, "vat_amount", vat_amount, "grand_total", grand_total, "cost_total", cost_total)
print(
"net_car_price",
net_car_price,
"net_add_price",
net_add_price,
"vat_amount",
vat_amount,
"grand_total",
grand_total,
"cost_total",
cost_total,
)
# acc_cars = e.get_coa_accounts().get(name="Inventory (Cars)")
# acc_sales = e.get_coa_accounts().get(name="Car Sales")

View File

@ -3,7 +3,10 @@ from django.contrib.auth import get_user_model
import datetime
from inventory.models import Dealer
from plans.models import Plan, Order, PlanPricing
User = get_user_model()
class Command(BaseCommand):
help = ""

View File

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

View File

@ -5,5 +5,10 @@ from django_q.tasks import async_task, result
class Command(BaseCommand):
def handle(self, *args, **kwargs):
from inventory.models import Dealer
instance = Dealer.objects.first()
async_task(func="inventory.tasks.test_task",dealer=instance,hook="inventory.hooks.print_results")
async_task(
func="inventory.tasks.test_task",
dealer=instance,
hook="inventory.hooks.print_results",
)

View File

@ -10,14 +10,17 @@ from django_q.tasks import async_task
User = get_user_model()
class Command(BaseCommand):
help = "Seed a full dealership via the real signup & downstream views"
def add_arguments(self, parser):
parser.add_argument('--count', type=int, default=1, help='Number of dealers to seed')
parser.add_argument(
"--count", type=int, default=1, help="Number of dealers to seed"
)
def handle(self, *args, **opts):
count = opts['count']
count = opts["count"]
client = Client() # lives inside management command
for n in range(6, 9):
@ -43,7 +46,16 @@ class Command(BaseCommand):
"address": f"Street {n}, Riyadh",
}
dealer = create_user_dealer(payload['email'], payload['password'], payload['name'], payload['arabic_name'], payload['phone_number'], payload['crn'], payload['vrn'], payload['address'])
dealer = create_user_dealer(
payload["email"],
payload["password"],
payload["name"],
payload["arabic_name"],
payload["phone_number"],
payload["crn"],
payload["vrn"],
payload["address"],
)
user = dealer.user
self._assign_random_plan(user)
self._services(dealer)
@ -72,8 +84,7 @@ class Command(BaseCommand):
plan = random.choice(plans)
user_plan, created = UserPlan.objects.get_or_create(
user=user,
defaults={'plan': plan, 'active': True}
user=user, defaults={"plan": plan, "active": True}
)
if created:
user_plan.initialize()
@ -114,5 +125,5 @@ class Command(BaseCommand):
price=additional_service["price"],
description=additional_service["description"],
dealer=dealer,
uom="Unit"
uom="Unit",
)

View File

@ -7,8 +7,28 @@ from django.contrib.auth import get_user_model
from plans.models import Order, PlanPricing, AbstractOrder, UserPlan, BillingInfo, Plan
from inventory.services import decodevin
from inventory.tasks import create_user_dealer
from inventory.models import AdditionalServices, Car, CarColors, CarFinance, CarMake, CustomGroup, Customer, Dealer, ExteriorColors, InteriorColors, Lead, UnitOfMeasure,Vendor,Staff
from django_ledger.models import PurchaseOrderModel,ItemTransactionModel,ItemModel,EntityModel
from inventory.models import (
AdditionalServices,
Car,
CarColors,
CarFinance,
CarMake,
CustomGroup,
Customer,
Dealer,
ExteriorColors,
InteriorColors,
Lead,
UnitOfMeasure,
Vendor,
Staff,
)
from django_ledger.models import (
PurchaseOrderModel,
ItemTransactionModel,
ItemModel,
EntityModel,
)
from django_q.tasks import async_task
from faker import Faker
from appointment.models import Appointment, AppointmentRequest, Service, StaffMember
@ -16,6 +36,7 @@ from appointment.models import Appointment, AppointmentRequest, Service, StaffMe
User = get_user_model()
fake = Faker()
class Command(BaseCommand):
help = "Seed a full dealership via the real signup & downstream views"
@ -31,7 +52,6 @@ class Command(BaseCommand):
# self._create_randome_services(dealer)
# self._create_random_lead(dealer)
# dealer = Dealer.objects.get(name="Dealer #6")
# coa_model = dealer.entity.get_default_coa()
# inventory_account = dealer.entity.get_all_accounts().get(name="Inventory (Cars)")
@ -46,7 +66,9 @@ class Command(BaseCommand):
for i in range(random.randint(1, 70)):
try:
e: EntityModel = dealer.entity
e.create_purchase_order(po_title=f"Test PO {random.randint(1,9999)}-{i}")
e.create_purchase_order(
po_title=f"Test PO {random.randint(1, 9999)}-{i}"
)
except Exception as e:
self.stderr.write(self.style.ERROR(f"Error : {e}"))
@ -56,7 +78,17 @@ class Command(BaseCommand):
name = fake.name()
n = random.randint(1, 9999)
phone = f"05678{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}"
Vendor.objects.create(dealer=dealer, name=f"{name}{n}", arabic_name=f"{name}{n}", email=f"{name}{n}@tenhal.sa", phone_number=phone,crn=f"CRN {n}", vrn=f"VRN {n}", address=f"Address {fake.address()}",contact_person=f"Contact Person {name}{n}")
Vendor.objects.create(
dealer=dealer,
name=f"{name}{n}",
arabic_name=f"{name}{n}",
email=f"{name}{n}@tenhal.sa",
phone_number=phone,
crn=f"CRN {n}",
vrn=f"VRN {n}",
address=f"Address {fake.address()}",
contact_person=f"Contact Person {name}{n}",
)
except Exception as e:
pass
@ -65,7 +97,9 @@ class Command(BaseCommand):
name = f"{fake.name()}{i}"
email = fake.email()
password = "Tenhal@123"
user = User.objects.create_user(username=email, email=email, password=password)
user = User.objects.create_user(
username=email, email=email, password=password
)
user.is_staff = True
user.save()
@ -74,7 +108,14 @@ class Command(BaseCommand):
# for service in services:
# staff_member.services_offered.add(service)
staff = Staff.objects.create(dealer=dealer,user=user,name=name,arabic_name=name,phone_number=fake.phone_number(),active=True)
staff = Staff.objects.create(
dealer=dealer,
user=user,
name=name,
arabic_name=name,
phone_number=fake.phone_number(),
active=True,
)
groups = CustomGroup.objects.filter(dealer=dealer)
random_group = random.choice(list(groups))
@ -107,7 +148,9 @@ class Command(BaseCommand):
vin = f"{vin[:-4]}{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}"
result = decodevin(vin)
make = CarMake.objects.get(name=result["maker"])
model = make.carmodel_set.filter(name__contains=result["model"]).first()
model = make.carmodel_set.filter(
name__contains=result["model"]
).first()
if not model or model == "":
model = random.choice(make.carmodel_set.all())
year = result["modelYear"]
@ -130,7 +173,10 @@ class Command(BaseCommand):
print(car)
cp = random.randint(10000, 100000)
CarFinance.objects.create(
car=car, cost_price=cp, selling_price=0,marked_price=cp+random.randint(2000, 7000)
car=car,
cost_price=cp,
selling_price=0,
marked_price=cp + random.randint(2000, 7000),
)
CarColors.objects.create(
car=car,
@ -196,10 +242,9 @@ class Command(BaseCommand):
price=additional_service["price"],
description=additional_service["description"],
dealer=dealer,
uom=uom
uom=uom,
)
def _create_random_lead(self, dealer):
for i in range(random.randint(1, 60)):
try:
@ -224,7 +269,7 @@ class Command(BaseCommand):
id_car_model=model,
source="website",
channel="website",
staff=staff
staff=staff,
)
c = Customer(
dealer=dealer,

View File

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

View File

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

View File

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

View File

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

View File

@ -28,6 +28,7 @@ from plans.models import UserPlan
from plans.signals import order_completed, activate_user_plan
from inventory.tasks import send_email
from django.conf import settings
# logging
import logging
@ -177,7 +178,11 @@ def create_ledger_entity(sender, instance, created, **kwargs):
entity.create_uom(name=u[1], unit_abbr=u[0])
# Create COA accounts, background task
async_task(func="inventory.tasks.create_coa_accounts",dealer=instance,hook="inventory.hooks.check_create_coa_accounts")
async_task(
func="inventory.tasks.create_coa_accounts",
dealer=instance,
hook="inventory.hooks.check_create_coa_accounts",
)
# async_task('inventory.tasks.check_create_coa_accounts', instance, schedule_type='O', schedule_time=timedelta(seconds=20))
# create_settings(instance.pk)
@ -1039,7 +1044,11 @@ def po_fullfilled_notification(sender, instance, created, **kwargs):
po_number=instance.po_number,
url=reverse(
"purchase_order_detail",
kwargs={"dealer_slug": dealer.slug,"entity_slug":instance.entity.slug, "pk": instance.pk},
kwargs={
"dealer_slug": dealer.slug,
"entity_slug": instance.entity.slug,
"pk": instance.pk,
},
),
),
)
@ -1086,7 +1095,10 @@ def sale_order_created_notification(sender, instance, created, **kwargs):
estimate_number=instance.estimate.estimate_number,
url=reverse(
"estimate_detail",
kwargs={"dealer_slug": instance.dealer.slug, "pk": instance.estimate.pk},
kwargs={
"dealer_slug": instance.dealer.slug,
"pk": instance.estimate.pk,
},
),
),
)
@ -1131,7 +1143,7 @@ def estimate_in_review_notification(sender, instance, created, **kwargs):
url=reverse(
"estimate_detail",
kwargs={"dealer_slug": dealer.slug, "pk": instance.pk},
)
),
),
)
@ -1188,7 +1200,11 @@ def bill_model_in_approve_notification(sender, instance, created, **kwargs):
bill_number=instance.bill_number,
url=reverse(
"bill-update",
kwargs={"dealer_slug": dealer.slug, "entity_slug": dealer.entity.slug, "bill_pk": instance.pk},
kwargs={
"dealer_slug": dealer.slug,
"entity_slug": dealer.entity.slug,
"bill_pk": instance.pk,
},
),
),
)
@ -1224,7 +1240,6 @@ def bill_model_in_approve_notification(sender, instance, created, **kwargs):
# )
@receiver(post_save, sender=models.Ticket)
def send_ticket_notification(sender, instance, created, **kwargs):
if created:
@ -1259,7 +1274,10 @@ def send_ticket_notification(sender, instance, created, **kwargs):
ticket_number=instance.pk,
url=reverse(
"ticket_detail",
kwargs={"dealer_slug": instance.dealer.slug, "ticket_id": instance.pk},
kwargs={
"dealer_slug": instance.dealer.slug,
"ticket_id": instance.pk,
},
),
),
)
@ -1273,28 +1291,29 @@ def handle_car_image(sender, instance, created, **kwargs):
try:
# Create or get car image record
car = instance.car
car_image, created = models.CarImage.objects.get_or_create(car=car, defaults={'image_hash': car.get_hash})
car_image, created = models.CarImage.objects.get_or_create(
car=car, defaults={"image_hash": car.get_hash}
)
# Check for existing image with same hash
existing = models.CarImage.objects.filter(
image_hash=car_image.image_hash,
image__isnull=False
).exclude(car=car).first()
existing = (
models.CarImage.objects.filter(
image_hash=car_image.image_hash, image__isnull=False
)
.exclude(car=car)
.first()
)
if existing:
# Copy existing image
car_image.image.save(
existing.image.name,
existing.image.file,
save=True
)
car_image.image.save(existing.image.name, existing.image.file, save=True)
logger.info(f"Reused image for car {car.vin}")
else:
# Schedule async generation
async_task(
'inventory.tasks.generate_car_image_task',
"inventory.tasks.generate_car_image_task",
car_image.id,
task_name=f"generate_car_image_{car.vin}"
task_name=f"generate_car_image_{car.vin}",
)
logger.info(f"Scheduled image generation for car {car.vin}")

View File

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

View File

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

View File

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

View File

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

View File

@ -2,13 +2,15 @@ from django.core.validators import RegexValidator
from django.utils.translation import gettext_lazy as _
import re
class SaudiPhoneNumberValidator(RegexValidator):
def __init__(self, *args, **kwargs):
super().__init__(
regex=r"^(\+9665|05)[0-9]{8}$",
message=_("Enter a valid Saudi phone number (05XXXXXXXX or +9665XXXXXXXX)"),
)
def __call__(self, value):
# Remove any whitespace, dashes, or other separators
cleaned_value = re.sub(r'[\s\-\(\)\.]', '', str(value))
cleaned_value = re.sub(r"[\s\-\(\)\.]", "", str(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", ""),
# logo=item.get("Logo", ""),
# is_sa_import=item.get("is_sa_import", False),
slug=unique_slug
slug=unique_slug,
)
# Step 2: Insert CarModel
@ -60,7 +60,7 @@ def run():
id_car_make_id=item["id_car_make"],
name=item["name"],
# arabic_name=item.get("arabic_name", ""),
slug=unique_slug
slug=unique_slug,
)
# Step 3: Insert CarSerie
@ -77,7 +77,7 @@ def run():
year_begin=item.get("year_begin"),
year_end=item.get("year_end"),
generation_name=item.get("generation_name", ""),
slug=unique_slug
slug=unique_slug,
)
# Step 4: Insert CarTrim
@ -98,9 +98,10 @@ def run():
# Step 5: Insert CarEquipment
for item in tqdm(data["car_equipment"], desc="Inserting CarEquipment"):
if not CarEquipment.objects.filter(id_car_equipment=item["id_car_equipment"]).exists():
if not CarEquipment.objects.filter(
id_car_equipment=item["id_car_equipment"]
).exists():
if CarTrim.objects.filter(id_car_trim=item["id_car_trim"]).exists():
unique_slug = generate_unique_slug(CarEquipment, item["name"])
CarEquipment.objects.create(
@ -108,7 +109,7 @@ def run():
id_car_trim_id=item["id_car_trim"],
name=item["name"],
year_begin=item.get("year"),
slug=unique_slug
slug=unique_slug,
)
# Step 6: Insert CarSpecification (Parent specifications first)

View File

@ -5,7 +5,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>403 - Access Forbidden</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet">
<style>
:root {
--dark-bg: #121212;
@ -84,9 +85,7 @@
</style>
</head>
<body>
<div id="particles-js"></div>
<div class="center-content container-fluid">
<h1 class="glitch">403</h1>
<h2 class="main-message">{% trans "Access Forbidden" %}</h2>
@ -94,7 +93,6 @@
<p class="sub-message fs-2">{% trans "Powered By Tenhal, Riyadh Saudi Arabia" %}</p>
<a href="{% url 'home' %}" class="home-button">{% trans "Go Home" %}</a>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/particles.js@2.0.0/particles.min.js"></script>
<script>

View File

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

View File

@ -282,9 +282,7 @@
</div>
</div>
</section>
{% include 'footer.html' %}
<script src="{% static 'js/phoenix.js' %}"></script>
{% endblock content %}
{% block customJS %}

View File

@ -1,9 +1,13 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}
{% trans 'Admin Management' %} {% endblock %}
{% trans 'Admin Management' %}
{% endblock %}
{% block content %}
<h3 class="my-4">{% trans "Admin Management" %}<li class="fa fa-user-cog ms-2 text-primary"></li></h3>
<h3 class="my-4">
{% trans "Admin Management" %}
<li class="fa fa-user-cog ms-2 text-primary"></li>
</h3>
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-4 g-4 mt-10">
<div class="col">
<a href="{% url 'user_management' request.dealer.slug %}">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,11 +2,7 @@
<!DOCTYPE html>
{% get_current_language as LANGUAGE_CODE %}
<html lang="{{ LANGUAGE_CODE }}"
{% if LANGUAGE_CODE == 'ar' %}
dir="rtl"
{% else %}
dir="ltr"
{% endif %}
{% if LANGUAGE_CODE == 'ar' %} dir="rtl" {% else %} dir="ltr" {% endif %}
data-bs-theme=""
data-navigation-type="default"
data-navbar-horizontal-shape="default">
@ -54,8 +50,14 @@
<link href="{% static 'css/custom.css' %}" rel="stylesheet">
{% comment %} <link rel="stylesheet" href="https://unicons.iconscout.com/release/v4.0.8/css/line.css"> {% endcomment %}
{% if LANGUAGE_CODE == 'ar' %}
<link href="{% static 'css/theme-rtl.min.css' %}" type="text/css" rel="stylesheet" id="style-rtl">
<link href="{% static 'css/user-rtl.min.css' %}" type="text/css" rel="stylesheet" id="user-style-rtl">
<link href="{% static 'css/theme-rtl.min.css' %}"
type="text/css"
rel="stylesheet"
id="style-rtl">
<link href="{% static 'css/user-rtl.min.css' %}"
type="text/css"
rel="stylesheet"
id="user-style-rtl">
{% else %}
<link href="{% static 'css/theme.min.css' %}"
type="text/css"
@ -66,11 +68,8 @@
rel="stylesheet"
id="user-style-default">
{% endif %}
<script src="{% static 'js/main.js' %}"></script>
<script src="{% static 'js/jquery.min.js' %}"></script>
{% comment %} <script src="{% static 'js/echarts.js' %}"></script> {% endcomment %}
{% comment %} {% block customCSS %}{% endblock %} {% endcomment %}
</head>
@ -87,17 +86,23 @@
<div id="spinner" class="htmx-indicator spinner-bg">
<img src="{% static 'spinner.svg' %}" width="100" height="100" alt="">
</div>
<div id="main_content" class="fade-me-in" hx-boost="false" hx-target="#main_content" hx-select="#main_content" hx-swap="outerHTML transition:true" hx-select-oob="#toast-container" hx-history-elt>
<div id="main_content"
class="fade-me-in"
hx-boost="false"
hx-target="#main_content"
hx-select="#main_content"
hx-swap="outerHTML transition:true"
hx-select-oob="#toast-container"
hx-history-elt>
{% block customCSS %}{% endblock %}
{% block content %}{% endblock content %}
{% block content %}
{% endblock content %}
{% block customJS %}{% endblock %}
{% comment %} <script src="{% static 'vendors/feather-icons/feather.min.js' %}"></script>
<script src="{% static 'vendors/fontawesome/all.min.js' %}"></script>
<script src="{% static 'vendors/popper/popper.min.js' %}"></script>
<script src="{% static 'vendors/bootstrap/bootstrap.min.js' %}"></script> {% endcomment %}
<script src="{% static 'js/phoenix.js' %}"></script>
</div>
{% block body %}
{% endblock body %}

View File

@ -6,7 +6,6 @@
{% block title %}
{{ _("Create Bill") |capfirst }}
{% endblock title %}
{% block content %}
<main class="d-flex align-items-center justify-content-center min-vh-80 py-5 ">
<div class="col-md-6">
@ -18,29 +17,30 @@
</h3>
</div>
<div class="card-body p-4 p-md-5">
<form action="{{ form_action_url }}" method="post" id="djl-bill-model-create-form-id" class="needs-validation" novalidate>
<form action="{{ form_action_url }}"
method="post"
id="djl-bill-model-create-form-id"
class="needs-validation"
novalidate>
{% csrf_token %}
{% if po_model %}
<div class="text-center mb-4">
<h3 class="h5">{% trans 'Bill for' %} {{ po_model.po_number }}</h3>
<p class="text-muted mb-3">{% trans 'Bill for' %} {{ po_model.po_title }}</p>
<div class="d-flex flex-column gap-2">
{% for itemtxs in po_itemtxs_qs %}
<span class="badge bg-secondary">{{ itemtxs }}</span>
{% endfor %}
{% for itemtxs in po_itemtxs_qs %}<span class="badge bg-secondary">{{ itemtxs }}</span>{% endfor %}
</div>
</div>
{% endif %}
<div class="mb-4">
{{ form|crispy }}
</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">
<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>

View File

@ -4,9 +4,7 @@
{% load django_ledger %}
{% load custom_filters %}
{% block title %}Bill Details{% endblock %}
{% block content %}
<div class="row mt-4">
<div class="col-12 mb-3">
<div class="card shadow-sm">
@ -14,10 +12,8 @@
{% 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 %}
@ -30,18 +26,14 @@
<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>
class="text-decoration-none ms-1">{{ bill.prepaid_account.code }}</a>
{% else %}
{{ bill.prepaid_account.code }}
{% endif %}
@ -49,17 +41,13 @@
<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>
class="text-decoration-none ms-1">{{ bill.unearned_account.code }}</a>
{% else %}
{{ bill.unearned_account.code }}
{% endif %}
@ -67,30 +55,23 @@
<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 class="col-12">
<div class="card mb-3 shadow-sm">
<div class="card-header pb-0">
@ -166,7 +147,6 @@
</div>
</div>
</div>
<div class="col-12">
<div class="card mb-3 shadow-sm">
<div class="card-header pb-0">

View File

@ -14,7 +14,8 @@
<div class="card mb-2">
<div class="card-body">
{% include 'bill/includes/card_bill.html' with dealer_slug=request.dealer.slug bill=bill_model style='bill-detail' entity_slug=view.kwargs.entity_slug %}
<form action="{% url 'bill-update' dealer_slug=request.dealer.slug entity_slug=view.kwargs.entity_slug bill_pk=bill_model.uuid %}" method="post">
<form action="{% url 'bill-update' dealer_slug=request.dealer.slug entity_slug=view.kwargs.entity_slug bill_pk=bill_model.uuid %}"
method="post">
{% csrf_token %}
<div class="mb-3">{{ form|crispy }}</div>
<button type="submit" class="btn btn-phoenix-primary mb-2 me-2">

View File

@ -4,7 +4,6 @@
{% if not create_bill %}
{% if style == 'dashboard' %}
<!-- Dashboard Style Card -->
<div class="">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3 text-primary">
@ -51,15 +50,16 @@
</div>
<!-- Modal Action -->
{% modal_action bill 'get' entity_slug %}
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="{% url 'django_ledger:bill-detail' entity_slug=entity_slug bill_pk=bill.uuid %}"
class="btn btn-sm btn-phoenix-primary me-md-2">{% trans 'View' %}</a>
{% if perms.django_ledger.change_billmodel %}
<a hx-boost="true" href="{% url 'django_ledger:bill-update' entity_slug=entity_slug bill_pk=bill.uuid %}"
<a hx-boost="true"
href="{% url 'django_ledger:bill-update' entity_slug=entity_slug bill_pk=bill.uuid %}"
class="btn btn-sm btn-phoenix-warning me-md-2">{% trans 'Update' %}</a>
{% if bill.can_pay %}
<button onclick="djLedger.toggleModal('{{ bill.get_html_id }}')" class="btn btn-sm btn-phoenix-info">{% trans 'Mark as Paid' %}</button>
<button onclick="djLedger.toggleModal('{{ bill.get_html_id }}')"
class="btn btn-sm btn-phoenix-info">{% trans 'Mark as Paid' %}</button>
{% endif %}
{% if bill.can_cancel %}
<button onclick="djLedger.toggleModal('{{ bill.get_html_id }}')"
@ -203,7 +203,8 @@
<!-- Update Button -->
{% if perms.django_ledger.change_billmodel %}
{% if "update" not in request.path %}
<a hx-boost="true" href="{% url 'bill-update' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill.uuid %}">
<a hx-boost="true"
href="{% url 'bill-update' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill.uuid %}">
<button class="btn btn-phoenix-primary">
<i class="fas fa-edit me-2"></i>{% trans 'Update' %}
</button>
@ -219,7 +220,6 @@
{% endif %}
<!-- Mark as Review -->
{% if bill.can_review %}
<button class="btn btn-phoenix-warning"
onclick="showPOModal('Mark as Review', '{% url 'bill-action-mark-as-review' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Review')">
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Review' %}
@ -261,7 +261,6 @@
</button>
{% modal_action_v2 bill bill.get_mark_as_canceled_url bill.get_mark_as_canceled_message bill.get_mark_as_canceled_html_id %}
{% endif %}
{% endif %}
</div>
</div>

View File

@ -2,12 +2,17 @@
{% load static %}
{% load django_ledger %}
{% load widget_tweaks %}
{% if bill.get_itemtxs_data.1.total_amount__sum > 0 %}
<form id="bill-update-form" action="{% url 'bill-update-items' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill_pk %}"
<form id="bill-update-form"
action="{% url 'bill-update-items' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill_pk %}"
method="post">
{% else %}
<form id="bill-update-form" hx-trigger="load delay:300ms" hx-swap="outerHTML" hx-target="#bill-update-form" hx-select="#bill-update-form" hx-post="{% url 'bill-update-items' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill_pk %}"
<form id="bill-update-form"
hx-trigger="load delay:300ms"
hx-swap="outerHTML"
hx-target="#bill-update-form"
hx-select="#bill-update-form"
hx-post="{% url 'bill-update-items' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill_pk %}"
method="post">
{% endif %}
<div class="container-fluid py-4">

View File

@ -2,7 +2,6 @@
{% load i18n static %}
{% load django_ledger %}
{% load widget_tweaks %}
{% block content %}
<main class="d-flex align-items-center justify-content-center min-vh-80 py-5">
<div class="col-12 col-sm-10 col-md-8 col-lg-6 col-xl-5">
@ -14,19 +13,17 @@
</h3>
</div>
<div class="card-body p-4 p-md-5">
<form method="post" id="{{ form.get_form_id }}" class="needs-validation" novalidate>
<form method="post"
id="{{ form.get_form_id }}"
class="needs-validation"
novalidate>
{% csrf_token %}
{# Bootstrap form rendering #}
<div class="mb-3">
{{ form.name.label_tag }}
{{ form.name|add_class:"form-control" }}
{% if form.name.help_text %}
<small class="form-text text-muted">{{ form.name.help_text }}</small>
{% endif %}
{% for error in form.name.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
{% if form.name.help_text %}<small class="form-text text-muted">{{ form.name.help_text }}</small>{% endif %}
{% for error in form.name.errors %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %}
</div>
<div class="mb-3">
{{ form.description.label_tag }}
@ -34,11 +31,8 @@
{% if form.description.help_text %}
<small class="form-text text-muted">{{ form.description.help_text }}</small>
{% endif %}
{% for error in form.description.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
{% for error in form.description.errors %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %}
</div>
<hr class="my-4">
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
<button type="submit" class="btn btn-phoenix-primary btn-lg me-md-2">

View File

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

View File

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

View File

@ -1,7 +1,6 @@
{% load django_ledger %}
{% load i18n %}
{% now "Y" as current_year %}
<div class="card shadow-sm border-0 mb-4">
<div class="card-header {% if coa_model.is_default %}bg-gray-100{% elif not coa_model.is_active %}bg-danger text-white{% endif %} py-3 d-flex align-items-center">
<div class="me-3">
@ -29,7 +28,6 @@
{% endif %}
</div>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-sm-6">
@ -46,27 +44,22 @@
<span class="text-muted ms-2">{{ coa_model.slug }}</span>
</div>
</div>
<div class="col-sm-6">
<div class="mb-2">
<span class="fw-bold"><i class="fas fa-list-alt me-1"></i> {% trans 'Total Accounts' %}:</span>
<span class="ms-2">{{ coa_model.accountmodel_total__count }}</span>
</div>
<div class="mb-2">
<span class="fw-bold text-info"><i class="fas fa-check-circle me-1"></i> {% trans 'Active Accounts' %}:</span>
<span class="ms-2">{{ coa_model.accountmodel_active__count }}</span>
</div>
<div class="mb-2">
<span class="fw-bold text-danger"><i class="fas fa-lock me-1"></i> {% trans 'Locked Accounts' %}:</span>
<span class="ms-2">{{ coa_model.accountmodel_locked__count }}</span>
</div>
</div>
</div>
<hr class="my-3">
<div class="row g-2">
<div class="col-sm-6">
<small class="text-muted d-block">
@ -84,33 +77,34 @@
</div>
</div>
</div>
<div class="card-footer bg-transparent border-top-0 pt-0 pt-sm-3">
<div class="d-flex flex-wrap gap-2">
<a href="{% url 'coa-update' request.dealer.slug request.entity.slug coa_model.slug %}" class="btn btn-sm btn-phoenix-warning fw-bold flex-grow-1 flex-sm-grow-0">
<a href="{% url 'coa-update' request.dealer.slug request.entity.slug coa_model.slug %}"
class="btn btn-sm btn-phoenix-warning fw-bold flex-grow-1 flex-sm-grow-0">
<i class="fas fa-edit me-1"></i> {% trans 'Update' %}
</a>
<a href="{% url 'account_list' request.dealer.slug coa_model.pk %}" class="btn btn-sm btn-phoenix-success fw-bold flex-grow-1 flex-sm-grow-0">
<a href="{% url 'account_list' request.dealer.slug coa_model.pk %}"
class="btn btn-sm btn-phoenix-success fw-bold flex-grow-1 flex-sm-grow-0">
<i class="fas fa-book me-1"></i> {% trans 'Accounts' %}
</a>
<a href="{% url 'account_create' request.dealer.slug coa_model.pk %}" class="btn btn-sm btn-phoenix-info fw-bold flex-grow-1 flex-sm-grow-0">
<a href="{% url 'account_create' request.dealer.slug coa_model.pk %}"
class="btn btn-sm btn-phoenix-info fw-bold flex-grow-1 flex-sm-grow-0">
<i class="fas fa-plus-circle me-1"></i> {% trans 'Add Account' %}
</a>
{% if coa_model.can_mark_as_default %}
<a href="{% url 'coa-action-mark-as-default' request.dealer.slug request.entity.slug coa_model.slug %}" class="btn btn-sm btn-phoenix-danger fw-bold flex-grow-1 flex-sm-grow-0">
<a href="{% url 'coa-action-mark-as-default' request.dealer.slug request.entity.slug coa_model.slug %}"
class="btn btn-sm btn-phoenix-danger fw-bold flex-grow-1 flex-sm-grow-0">
<i class="fas fa-star me-1"></i> {% trans 'Mark as Default' %}
</a>
{% endif %}
{% if coa_model.can_deactivate %}
<a href="{% url 'coa-action-mark-as-inactive' request.dealer.slug request.entity.slug coa_model.slug %}" class="btn btn-sm btn-phoenix-warning fw-bold flex-grow-1 flex-sm-grow-0">
<a href="{% url 'coa-action-mark-as-inactive' request.dealer.slug request.entity.slug coa_model.slug %}"
class="btn btn-sm btn-phoenix-warning fw-bold flex-grow-1 flex-sm-grow-0">
<i class="fas fa-toggle-off me-1"></i> {% trans 'Mark as Inactive' %}
</a>
{% elif coa_model.can_activate %}
<a href="{% url 'coa-action-mark-as-active' request.dealer.slug request.entity.slug coa_model.slug %}" class="btn btn-sm btn-phoenix-success fw-bold flex-grow-1 flex-sm-grow-0">
<a href="{% url 'coa-action-mark-as-active' request.dealer.slug request.entity.slug coa_model.slug %}"
class="btn btn-sm btn-phoenix-success fw-bold flex-grow-1 flex-sm-grow-0">
<i class="fas fa-toggle-on me-1"></i> {% trans 'Mark as Active' %}
</a>
{% endif %}

View File

@ -1,10 +1,16 @@
{% load i18n crispy_forms_tags %}
<div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true">
<div class="modal fade"
id="emailModal"
tabindex="-1"
aria-labelledby="emailModalLabel"
aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header justify-content-between align-items-start gap-5 px-4 pt-4 pb-3 border-0">
<h4 class="modal-title" id="emailModalLabel">{% trans 'Send Email' %}</h4>
<button class="btn p-0 text-body-quaternary fs-6" data-bs-dismiss="modal" aria-label="Close">
<button class="btn p-0 text-body-quaternary fs-6"
data-bs-dismiss="modal"
aria-label="Close">
<span class="fas fa-times"></span>
</button>
</div>

View File

@ -15,13 +15,11 @@
</button>
</div>
<div class="modal-body">
<form id="noteForm" action="{% url 'add_note' request.dealer.slug content_type slug %}"
<form id="noteForm"
action="{% url 'add_note' request.dealer.slug content_type slug %}"
hx-select="#notesTable"
hx-target="#notesTable"
hx-on::after-request="{
resetSubmitButton(document.querySelector('.add_note_form button[type=submit]'));
$('#noteModal').modal('hide');
}"
hx-on::after-request="{ resetSubmitButton(document.querySelector('.add_note_form button[type=submit]')); $('#noteModal').modal('hide'); }"
hx-swap="outerHTML"
method="post"
class="add_note_form">

View File

@ -15,13 +15,11 @@
</button>
</div>
<div class="modal-body">
<form id="scheduleForm" action="{% url 'schedule_event' request.dealer.slug content_type slug %}"
<form id="scheduleForm"
action="{% url 'schedule_event' request.dealer.slug content_type slug %}"
hx-select=".taskTable"
hx-target=".taskTable"
hx-on::after-request="{
resetSubmitButton(document.querySelector('.add_schedule_form button[type=submit]'));
$('#scheduleModal').modal('hide');
}"
hx-on::after-request="{ resetSubmitButton(document.querySelector('.add_schedule_form button[type=submit]')); $('#scheduleModal').modal('hide'); }"
hx-swap="outerHTML"
method="post"
class="add_schedule_form">

View File

@ -22,7 +22,8 @@
</button>
</div>
<div class="modal-body">
<form id="taskForm" action="{% url 'add_task' request.dealer.slug content_type slug %}"
<form id="taskForm"
action="{% url 'add_task' request.dealer.slug content_type slug %}"
method="post"
class="add_task_form"
hx-post="{% url 'add_task' request.dealer.slug content_type slug %}"

View File

@ -39,7 +39,6 @@
<div class="col-12 col-md-auto">
<h3 class="mb-0">{{ _("Lead Details") }}</h3>
</div>
</div>
</div>
</div>
@ -47,7 +46,6 @@
<div class="col-md-5 col-lg-5 col-xl-4">
<div class="sticky-leads-sidebar">
<div class="lead-details" data-breakpoint="md">
<div class="card mb-2">
<div class="card-body">
<div class="row align-items-center g-3 text-center text-xxl-start">
@ -93,7 +91,8 @@
</div>
<div class="card mb-2">
<div class="card-body">
<div id="assignedTo" class="row align-items-center g-3 text-center text-xxl-start">
<div id="assignedTo"
class="row align-items-center g-3 text-center text-xxl-start">
<div class="col-6 col-sm-auto d-flex flex-column align-items-center text-center">
<h5 class="fw-bolder mb-2 text-body-highlight">{{ _("Assigned To") }}</h5>
<div class="d-flex align-items-center">
@ -295,9 +294,7 @@
action="{% url 'lead_transfer' request.dealer.slug lead.slug %}"
hx-select-oob="#assignedTo:outerHTML,#toast-container:outerHTML"
hx-swap="none"
hx-on::after-request="{
resetSubmitButton(document.querySelector('#exampleModal button[type=submit]'));
$('#exampleModal').modal('hide');}"
hx-on::after-request="{ resetSubmitButton(document.querySelector('#exampleModal button[type=submit]')); $('#exampleModal').modal('hide');}"
method="post">
{% csrf_token %}
<div class="modal-header">
@ -488,7 +485,7 @@
data-url="{% url 'update_note' request.dealer.slug note.pk %}"
data-bs-toggle="modal"
data-bs-target="#noteModal"
data-note-title="{{ _('Update') }}">
data-note-title="{{ _("Update") }}">
<i class='fas fa-pen-square text-primary ms-2'></i>
{{ _("Update") }}
</a>
@ -528,8 +525,7 @@
hx-get="{% url 'send_lead_email' request.dealer.slug lead.slug %}"
hx-target="#emailModalBody"
hx-select=".email-form"
hx-swap="innerHTML"
>
hx-swap="innerHTML">
<span class="fas fa-plus me-1"></span>{{ _("Send Email") }}
</button>
{% endif %}
@ -807,7 +803,6 @@
href="{% url 'appointment:get_user_appointments' %}"> <span class="me-2 text-body align-bottom" data-feather="calendar"></span>{{ _("View in Calendar") }}
</a>
</div>
</div>
</div>
</div>

View File

@ -1,6 +1,5 @@
{% extends 'base.html' %}
{% load i18n static crispy_forms_filters %}
{% block title %}
{% if object %}
{% trans 'Update Lead' %}
@ -8,7 +7,6 @@
{% trans 'Add New Lead' %}
{% endif %}
{% endblock %}
{% block customcss %}
<style>
.htmx-indicator{
@ -30,7 +28,6 @@
}
</style>
{% endblock customcss %}
{% block content %}
<main class="d-flex align-items-center justify-content-center min-vh-100 py-5">
<div class="col-md-8">
@ -50,14 +47,14 @@
<form class="form" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form|crispy }}
<hr class="my-4">
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
<button class="btn btn-phoenix-primary btn-lg me-md-2" type="submit">
<i class="fa-solid fa-floppy-disk me-1"></i>
{% trans "Save" %}
</button>
<a href="{% url 'lead_list' request.dealer.slug %}" class="btn btn-phoenix-secondary btn-lg">
<a href="{% url 'lead_list' request.dealer.slug %}"
class="btn btn-phoenix-secondary btn-lg">
<i class="fa-solid fa-ban me-1"></i>
{% trans "Cancel" %}
</a>

View File

@ -1,11 +1,9 @@
{% extends 'base.html' %}
{% load i18n static humanize %}
{% block title %}
{{ _("Leads") |capfirst }}
{% endblock title %}
{% block content %}
{% if page_obj.object_list or request.GET.q %}
<div class="row g-3 mt-4 mb-4">
<h2 class="mb-2">
@ -14,7 +12,6 @@
</h2>
<!-- Action Tracking Modal -->
{% comment %} {% include "crm/leads/partials/update_action.html" %} {% endcomment %}
<div class="row g-3 justify-content-between mb-4">
<div class="col-auto">
<div class="d-md-flex justify-content-between">
@ -30,7 +27,6 @@
<div class="d-flex">{% include 'partials/search_box.html' %}</div>
</div>
</div>
<div class="row g-3">
<div class="col-12">
{% if page_obj.object_list or request.GET.q %}
@ -206,7 +202,6 @@
</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">
@ -224,8 +219,7 @@
<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 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 %}"
@ -258,7 +252,6 @@
<div class="d-flex">{% include 'partials/pagination.html' %}</div>
</div>
{% endif %}
{% endif %}
</div>
</div>

View File

@ -65,7 +65,6 @@
</style>
{% endblock customCSS %}
{% block content %}
{% if leads %}
<div class="container-fluid my-4">
<div class="row justify-content-center">
@ -170,8 +169,6 @@
{% endfor %}
</div>
</div>
</div>
</div>
</div>

View File

@ -16,10 +16,7 @@
action="{% url 'update_lead_actions' request.dealer.slug %}"
hx-select-oob="#currentStage:outerHTML,#leadStatus:outerHTML,#toast-container:outerHTML"
hx-swap="none"
hx-on::after-request="{
resetSubmitButton(document.querySelector('#actionTrackingForm button[type=submit]'));
$('#actionTrackingModal').modal('hide');
}"
hx-on::after-request="{ resetSubmitButton(document.querySelector('#actionTrackingForm button[type=submit]')); $('#actionTrackingModal').modal('hide'); }"
method="post">
<div class="modal-body">
{% csrf_token %}

View File

@ -3,7 +3,9 @@
<div class="content">
<h2 class="mb-5">{{ _("Notifications") }}</h2>
<div class="d-flex justify-content-end mb-3">
<a href="{% url 'mark_all_notifications_as_read' %}" hx-select-oob="#toast-container:outerHTML" class="btn btn-phoenix-primary"><i class="far fa-envelope fs-8 me-2"></i>{{ _("Mark all as read") }}</a>
<a href="{% url 'mark_all_notifications_as_read' %}"
hx-select-oob="#toast-container:outerHTML"
class="btn btn-phoenix-primary"><i class="far fa-envelope fs-8 me-2"></i>{{ _("Mark all as read") }}</a>
</div>
{% if notifications %}
<div class="mx-n4 mx-lg-n6 mb-5 border-bottom">

View File

@ -40,7 +40,8 @@
href="{% url 'update_opportunity' request.dealer.slug opportunity.slug %}">Update Opportunity</a>
</li>
<li>
<a class="dropdown-item" type="button"
<a class="dropdown-item"
type="button"
data-bs-toggle="modal"
data-bs-target="#updateStageModal">Update Stage</a>
</li>
@ -835,8 +836,7 @@
hx-get="{% url 'send_lead_email' request.dealer.slug opportunity.lead.slug %}"
hx-target="#emailModalBody"
hx-select=".email-form"
hx-swap="innerHTML"
>
hx-swap="innerHTML">
<span class="fas fa-plus me-1"></span>{{ _("Send Email") }}
</button>
{% endif %}
@ -858,7 +858,6 @@
</li>
</ul>
</div>
<div class="tab-content" id="profileTabContent">
<div class="tab-pane fade show active"
id="tab-mail"
@ -871,7 +870,6 @@
<table class="table fs-9 mb-0">
<thead>
<tr>
<th class="sort white-space-nowrap align-middle pe-3 ps-0 text-uppercase"
scope="col"
data-sort="subject"
@ -896,14 +894,12 @@
<tbody class="list" id="all-email-table-body">
{% for email in opportunity.lead.get_emails %}
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
<td class="subject order align-middle white-space-nowrap py-2 ps-0">
<a class="fw-semibold text-primary" href="#!">{{ email.subject }}</a>
<div class="fs-10 d-block">{{ email.to_email }}</div>
</td>
<td class="sent align-middle white-space-nowrap text-start fw-bold text-body-tertiary py-2">{{ email.from_email }}</td>
<td class="date align-middle white-space-nowrap text-body py-2">{{ email.created }}</td>
<td class="status align-middle fw-semibold text-end py-2">
<span class="badge badge-phoenix fs-10 badge-phoenix-success">sent</span>
</td>
@ -1097,7 +1093,10 @@
</div>
</div>
</div>
<div class="modal fade" id="updateStageModal" tabindex="-1" aria-hidden="true">
<div class="modal fade"
id="updateStageModal"
tabindex="-1"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post"
@ -1113,9 +1112,7 @@
data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div class="modal-body">
{{ stage_form|crispy }}
</div>
<div class="modal-body">{{ stage_form|crispy }}</div>
<div class="modal-footer">
<button class="btn btn-phoenix-primary" type="submit">{{ _("Save") }}</button>
<button class="btn btn-phoenix-secondary"
@ -1126,8 +1123,6 @@
</div>
</div>
</div>
{% include 'modal/delete_modal.html' %}
<!-- email Modal -->
{% include "components/email_modal.html" %}
@ -1138,10 +1133,8 @@
<!-- schedule Modal -->
{% include "components/schedule_modal.html" with content_type="opportunity" slug=opportunity.slug %}
{% endblock %}
{% block customJS %}
<script>
document.body.addEventListener('htmx:afterSwap', function(evt) {
if (evt.detail.target.id === 'main_content') {
var modal = bootstrap.Modal.getInstance(document.getElementById('exampleModal'));

View File

@ -21,25 +21,19 @@
</h2>
</div>
<div class="col-auto">
<a href="{% url 'opportunity_list' request.dealer.slug %}" class="btn btn-phoenix-secondary">
<a href="{% url 'opportunity_list' request.dealer.slug %}"
class="btn btn-phoenix-secondary">
<span class="fas fa-arrow-left me-2"></span>{% trans "Back to list" %}
</a>
</div>
</div>
<div class="row g-3">
<div class="col-lg-8">
<div class="card">
<div class="card-body p-4 p-sm-5">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% if form.non_field_errors %}
<div class="alert alert-danger">
{{ form.non_field_errors }}
</div>
{% endif %}
{% 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 }}">
@ -47,13 +41,8 @@
<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 }}
{% if form.lead.errors %}<div class="invalid-feedback d-block">{{ form.lead.errors }}</div>{% endif %}
</div>
{% endif %}
</div>
<!-- Car Field -->
<div class="mb-4">
<label class="form-label" for="{{ form.car.id_for_label }}">
@ -61,13 +50,8 @@
<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 }}
{% if form.car.errors %}<div class="invalid-feedback d-block">{{ form.car.errors }}</div>{% endif %}
</div>
{% endif %}
</div>
<!-- Stage Field -->
<div class="mb-4">
<label class="form-label" for="{{ form.stage.id_for_label }}">
@ -75,11 +59,7 @@
<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 %}
{% if form.stage.errors %}<div class="invalid-feedback d-block">{{ form.stage.errors }}</div>{% endif %}
</div>
<!-- Amount Field -->
<div class="mb-4">
@ -91,13 +71,8 @@
<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 }}
{% if form.amount.errors %}<div class="invalid-feedback d-block">{{ form.amount.errors }}</div>{% endif %}
</div>
{% endif %}
</div>
<!-- Probability Field -->
<div class="mb-4">
<label class="form-label" for="{{ form.probability.id_for_label }}">
@ -108,54 +83,41 @@
<input type="range"
name="{{ form.probability.name }}"
id="{{ form.probability.id_for_label }}"
min="0" max="100" step="1"
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">
<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 }}
{% if form.probability.errors %}<div class="invalid-feedback d-block">{{ form.probability.errors }}</div>{% endif %}
</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>
<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>
<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>
<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>
<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">
@ -202,7 +164,6 @@
</div>
</div>
</div>
<script>
function updateProbabilityValue(value) {
const amount = document.getElementById('id_amount');

View File

@ -75,7 +75,8 @@
height: 12px;
width: 12px"></span>{{ opportunity.get_stage_display }}
</p>
<p class="ms-auto fs-9 text-body-emphasis fw-semibold mb-0 deals-revenue">{{ opportunity.car.total }}</p># TODO : check later
<p class="ms-auto fs-9 text-body-emphasis fw-semibold mb-0 deals-revenue">{{ opportunity.car.total }}</p>
# TODO : check later
</div>
<div class="deals-company-agent d-flex flex-between-center">
<div class="d-flex align-items-center">

View File

@ -5,7 +5,6 @@
{{ _("Opportunities") }}
{% endblock title %}
{% block content %}
{% if opportunities or request.GET.q %}
<div class="row g-3 mt-4">
<div class="row g-3 justify-content-between mb-4">
@ -34,12 +33,13 @@
<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"
<div class="search-box position-relative flex-grow-1 me-2"
style="min-width: 200px">
<form class="position-relative show"
id="search-form"
hx-get=""
hx-boost="false"
hx-trigger="keyup changed delay:500ms, search">
<input name="q"
id="search-input"
class="form-control form-control-sm search-input search"
@ -47,25 +47,20 @@
aria-label="Search"
placeholder="{{ _("Search") }}..."
value="{{ request.GET.q }}" />
<span class="fa fa-magnifying-glass search-box-icon"></span>
{% if request.GET.q %}
<button type="button"
class="btn-close position-absolute end-0 top-50 translate-middle cursor-pointer shadow-none"
id="clear-search"
aria-label="Clear Search"></button>
{% endif %}
</form>
</div>
<!-- Filter Dropdowns - Aligned in a row -->
<div class="d-flex flex-column flex-sm-row gap-3 w-100"
style="max-width: 400px">
<!-- Stage Filter -->
<!-- Stage Filter -->
<div class="flex-grow-1">
<select class="form-select"
name="stage"
@ -78,7 +73,9 @@
<option value="">{% trans "All Stages" %}</option>
{% for value, label in stage_choices %}
<option value="{{ value }}"
{% if request.GET.stage == value %}selected{% endif %}>{{ label }}</option>
{% if request.GET.stage == value %}selected{% endif %}>
{{ label }}
</option>
{% endfor %}
</select>
</div>
@ -93,19 +90,23 @@
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>
{% 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>
{% 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>
{% if request.GET.sort == 'closing' %}selected{% endif %}>
{% trans "Earliest Close Date" %}
</option>
</select>
</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' %}

View File

@ -14,12 +14,7 @@
{% endblock customCSS %}
{% for opportunity in opportunities %}
<div class="col-12 col-md-6 col-lg-4 col-xl-3">
<div class="card h-100
{% if opportunity.get_stage_display == 'Closed Won' %}
bg-success-soft
{% elif opportunity.get_stage_display == 'Closed Lost' %}
bg-danger-soft
{% endif %}">
<div class="card h-100 {% if opportunity.get_stage_display == 'Closed Won' %} bg-success-soft {% elif opportunity.get_stage_display == 'Closed Lost' %} bg-danger-soft {% endif %}">
<div class="card-body">
<div class="avatar avatar-xl me-3 mb-3">
{% if opportunity.car.id_car_make.logo %}
@ -53,12 +48,7 @@
<span class="badge badge-phoenix fs-10 badge-phoenix-secondary">
{% endif %}
{{ opportunity.stage }}</span>
<span class="badge badge-phoenix fs-10
{% if opportunity.get_stage_display == 'Won' %}
badge-phoenix-success
{% elif opportunity.get_stage_display == 'Lost' %}
badge-phoenix-danger
{% endif %}">
<span class="badge badge-phoenix fs-10 {% if opportunity.get_stage_display == 'Won' %} badge-phoenix-success {% elif opportunity.get_stage_display == 'Lost' %} badge-phoenix-danger {% endif %}">
{{ opportunity.get_status_display }}
</span>
</div>

View File

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

View File

@ -1,7 +1,6 @@
{% extends "base.html" %}
{% load i18n static %}
{% load crispy_forms_filters %}
{% block title %}
{% if object %}
{% trans 'Update Customer' %}
@ -9,7 +8,6 @@
{% trans 'Add New Customer' %}
{% endif %}
{% endblock %}
{% block content %}
<main class="d-flex align-items-center justify-content-center min-vh-100 py-5 ">
<div class="col-md-8">
@ -29,25 +27,27 @@
<form method="post" class="form" enctype="multipart/form-data" novalidate>
{% csrf_token %}
{{ form|crispy }}
{% if form.errors %}
<div class="alert alert-danger mt-4" role="alert">
<h4 class="alert-heading small">{% trans "Please correct the following errors:" %}</h4>
<ul class="mb-0">
{% for field, errors in form.errors.items %}
<li><strong>{{ field|capfirst }}:</strong> {% for error in errors %}{{ error }}{% endfor %}</li>
<li>
<strong>{{ field|capfirst }}:</strong>
{% for error in errors %}{{ error }}{% endfor %}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
<hr class="my-4">
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
<button class="btn btn-phoenix-primary btn-lg me-md-2" type="submit">
<i class="fa-solid fa-floppy-disk me-1"></i>
{% trans "Save" %}
</button>
<a href="{% url 'customer_list' request.dealer.slug %}" class="btn btn-phoenix-secondary btn-lg">
<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>

View File

@ -163,7 +163,6 @@
<tr>
<td colspan="6" class="text-center">{% trans "No Customers found." %}</td>
</tr>
{% endfor %}
</tbody>
{% endif %}

View File

@ -6,11 +6,12 @@
{% block content %}
{% include 'modal/delete_modal.html' %}
{% include 'components/note_modal.html' with content_type="customer" slug=customer.slug %}
<div class="mt-4">
<div class="row align-items-center justify-content-between g-3 mb-4">
<div class="col-auto">
<h3 class="mb-0">{% trans 'Customer details' %}<i class="fas fa-user ms-2 text-primary"></i></h3>
<h3 class="mb-0">
{% trans 'Customer details' %}<i class="fas fa-user ms-2 text-primary"></i>
</h3>
</div>
<div class="col-auto d-flex gap-2">
{% if perms.inventory.change_customer %}
@ -30,7 +31,6 @@
{% endif %}
</div>
</div>
<div class="row g-4 mb-4">
<div class="col-12 col-lg-4">
<div class="card h-100 shadow-sm">
@ -39,7 +39,9 @@
<div class="col-12 col-sm-auto mb-sm-2">
<div class="avatar avatar-5xl">
{% if customer.image %}
<img class="rounded-circle border border-2 border-primary" src="{{ customer.image.url }}" alt="{{ customer.full_name }}"/>
<img class="rounded-circle border border-2 border-primary"
src="{{ customer.image.url }}"
alt="{{ customer.full_name }}" />
{% else %}
<div class="avatar-text rounded-circle bg-secondary text-white border border-2 border-primary">
<span class="fs-4">{{ customer.full_name|first|default:"?" }}</span>
@ -65,7 +67,6 @@
</div>
</div>
</div>
<div class="col-12 col-lg-8">
<div class="card h-100 shadow-sm">
<div class="card-body p-4">
@ -79,18 +80,19 @@
</li>
<li class="mb-2">
<strong class="text-body-secondary d-block">{% trans 'Email' %}:</strong>
<a href="mailto:{{ customer.email|default:"" }}" class="text-decoration-none">{{ customer.email|default:_("N/A") }}</a>
<a href="mailto:{{ customer.email|default:"" }}"
class="text-decoration-none">{{ customer.email|default:_("N/A") }}</a>
</li>
<li class="mb-0">
<strong class="text-body-secondary d-block">{% trans 'Phone Number' %}:</strong>
<a href="tel:{{ customer.phone_number|default:"" }}" class="text-decoration-none">{{ customer.phone_number|default:_("N/A") }}</a>
<a href="tel:{{ customer.phone_number|default:"" }}"
class="text-decoration-none">{{ customer.phone_number|default:_("N/A") }}</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="row g-4 mb-4">
<div class="col-12">
<div class="card shadow-sm">
@ -134,7 +136,6 @@
</div>
</div>
</div>
<div class="row g-4 mb-3">
<div class="col-12">
<div class="card shadow-sm">
@ -142,48 +143,82 @@
<h5 class="card-title mb-3">{% trans 'Sales History' %}</h5>
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item me-6" role="presentation">
<button class="nav-link active" id="leads-tab" data-bs-toggle="tab" data-bs-target="#leads-tab-pane" type="button" role="tab" aria-controls="leads-tab-pane" aria-selected="true">{% trans 'Leads' %}</button>
<button class="nav-link active"
id="leads-tab"
data-bs-toggle="tab"
data-bs-target="#leads-tab-pane"
type="button"
role="tab"
aria-controls="leads-tab-pane"
aria-selected="true">{% trans 'Leads' %}</button>
</li>
<li class="nav-item me-6" role="presentation">
<button class="nav-link" id="opportunities-tab" data-bs-toggle="tab" data-bs-target="#opportunities-tab-pane" type="button" role="tab" aria-controls="opportunities-tab-pane" aria-selected="false">{% trans 'Opportunities' %}</button>
<button class="nav-link"
id="opportunities-tab"
data-bs-toggle="tab"
data-bs-target="#opportunities-tab-pane"
type="button"
role="tab"
aria-controls="opportunities-tab-pane"
aria-selected="false">{% trans 'Opportunities' %}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="estimates-tab" data-bs-toggle="tab" data-bs-target="#estimates-tab-pane" type="button" role="tab" aria-controls="estimates-tab-pane" aria-selected="false">{% trans 'Estimates' %}</button>
<button class="nav-link"
id="estimates-tab"
data-bs-toggle="tab"
data-bs-target="#estimates-tab-pane"
type="button"
role="tab"
aria-controls="estimates-tab-pane"
aria-selected="false">{% trans 'Estimates' %}</button>
</li>
</ul>
<div class="tab-content pt-3" id="myTabContent">
<div class="tab-pane fade show active" id="leads-tab-pane" role="tabpanel" aria-labelledby="leads-tab" tabindex="0">
<div class="tab-pane fade show active"
id="leads-tab-pane"
role="tabpanel"
aria-labelledby="leads-tab"
tabindex="0">
{% for lead in leads %}
<div class="d-flex align-items-center mb-2">
<i class="fas fa-handshake me-2 text-primary"></i>
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}" class="fw-bold">{{ lead }}</a>
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}"
class="fw-bold">{{ lead }}</a>
</div>
{% empty %}
<p class="text-body-secondary">{% trans 'No leads found for this customer.' %}</p>
{% endfor %}
</div>
<div class="tab-pane fade" id="opportunities-tab-pane" role="tabpanel" aria-labelledby="opportunities-tab" tabindex="0">
<div class="tab-pane fade"
id="opportunities-tab-pane"
role="tabpanel"
aria-labelledby="opportunities-tab"
tabindex="0">
{% for lead in leads %}
{% if lead.opportunity %}
<div class="d-flex align-items-center mb-2">
<i class="fas fa-chart-line me-2 text-success"></i>
<a href="{% url 'opportunity_detail' request.dealer.slug lead.opportunity.slug %}" class="fw-bold">{{ lead.opportunity }}</a>
<a href="{% url 'opportunity_detail' request.dealer.slug lead.opportunity.slug %}"
class="fw-bold">{{ lead.opportunity }}</a>
</div>
{% endif %}
{% empty %}
<p class="text-body-secondary">{% trans 'No opportunities found for this customer.' %}</p>
{% endfor %}
</div>
<div class="tab-pane fade" id="estimates-tab-pane" role="tabpanel" aria-labelledby="estimates-tab" tabindex="0">
<div class="tab-pane fade"
id="estimates-tab-pane"
role="tabpanel"
aria-labelledby="estimates-tab"
tabindex="0">
{% for estimate in estimates %}
<div class="card mb-3 shadow-sm">
<div class="card-body p-3">
<div class="d-flex align-items-center justify-content-between mb-3">
<h6 class="mb-0">
<i class="fas fa-file-invoice me-2 text-info"></i>
<a href="{% url 'estimate_detail' request.dealer.slug estimate.pk %}" class="text-decoration-none">{{ estimate }}</a>
<a href="{% url 'estimate_detail' request.dealer.slug estimate.pk %}"
class="text-decoration-none">{{ estimate }}</a>
</h6>
<span class="badge bg-success">{{ estimate.created|date:"d M Y" }}</span>
</div>
@ -197,8 +232,15 @@
{% for invoice in estimate.invoicemodel_set.all %}
<li class="mb-2">
<i class="fas fa-receipt me-2 {% if invoice.is_paid %}text-success{% else %}text-warning{% endif %}"></i>
<a href="{% url 'invoice_detail' request.dealer.slug request.entity.slug invoice.pk %}" class="text-decoration-none">{{ invoice }}</a>
<span class="badge rounded-pill {% if invoice.is_paid %}bg-success{% else %}bg-warning{% endif %} ms-2">{% if invoice.is_paid %}{% trans "Paid" %}{% else %}{% trans "Unpaid" %}{% endif %}</span>
<a href="{% url 'invoice_detail' request.dealer.slug request.entity.slug invoice.pk %}"
class="text-decoration-none">{{ invoice }}</a>
<span class="badge rounded-pill {% if invoice.is_paid %}bg-success{% else %}bg-warning{% endif %} ms-2">
{% if invoice.is_paid %}
{% trans "Paid" %}
{% else %}
{% trans "Unpaid" %}
{% endif %}
</span>
</li>
{% endfor %}
{% for item in estimate.itemtransactionmodel_set.all %}
@ -206,7 +248,6 @@
<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>
</li>
{% endfor %}
</ul>
</div>
@ -222,11 +263,7 @@
</div>
</div>
{% include "components/note_modal.html" with content_type="customer" slug=customer.slug %}
<!---->
<script>
document.addEventListener("DOMContentLoaded", function () {
const noteModal = document.getElementById("noteModal");
@ -253,5 +290,4 @@
});
</script>
{% endblock %}

View File

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

View File

@ -5,7 +5,6 @@
Monthly Performance Trends ({{ start_date }} - {{ end_date }})
{% endblocktrans %}
</h3>
<div class="row g-4 mb-5">
<div class="col-12">
<div class="card h-100 shadow-sm border-0">
@ -22,25 +21,25 @@
<div class="card-header bg-white border-bottom-0">
<h5 class="fw-bold mb-0 text-dark">{% trans "Monthly Cars Sold" %}</h5>
</div>
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;">
<div class="card-body d-flex align-items-center justify-content-center"
style="height: 400px">
<canvas id="CarsSoldByMonthChart"></canvas>
</div>
</div>
</div>
</div>
<div class="row g-4 mb-5">
<div class="col-lg-6 col-12">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0">
<h5 class="fw-bold mb-0 text-dark">{% trans "Sales by Make" %}</h5>
</div>
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;">
<div class="card-body d-flex align-items-center justify-content-center"
style="height: 400px">
<canvas id="salesByBrandChart"></canvas>
</div>
</div>
</div>
<div class="col-lg-6 col-12">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0 d-flex justify-content-between align-items-center">
@ -51,7 +50,10 @@
<select id="carMakeSelectSales" class="form-select" name="make_sold">
<option value="">{% trans "All Makes" %}</option>
{% for make_sold in all_makes_sold %}
<option value="{{ make_sold }}" {% if make_sold == selected_make_sales %}selected{% endif %}>{{ make_sold }}</option>
<option value="{{ make_sold }}"
{% if make_sold == selected_make_sales %}selected{% endif %}>
{{ make_sold }}
</option>
{% endfor %}
</select>
</div>
@ -67,22 +69,20 @@
</div>
</div>
{% endif %}
{% if request.is_dealer or request.is_manager or request.is_inventory %}
<h3 class="fw-bold mb-3">{% trans "Inventory Trends" %}</h3>
<div class="row g-4 mb-5">
<div class="col-lg-6 col-12">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0">
<h5 class="fw-bold mb-0 text-dark">{% trans "Inventory by Make" %}</h5>
</div>
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;">
<div class="card-body d-flex align-items-center justify-content-center"
style="height: 400px">
<canvas id="inventoryByMakeChart"></canvas>
</div>
</div>
</div>
<div class="col-lg-6 col-12">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0 d-flex justify-content-between align-items-center">
@ -93,7 +93,10 @@
<select id="carMakeSelectInventory" class="form-select" name="make_inventory">
<option value="">{% trans "All Makes" %}</option>
{% for make_inv in all_makes_inventory %}
<option value="{{ make_inv }}" {% if make_inv == selected_make_inventory %}selected{% endif %}>{{ make_inv }}</option>
<option value="{{ make_inv }}"
{% if make_inv == selected_make_inventory %}selected{% endif %}>
{{ make_inv }}
</option>
{% endfor %}
</select>
</div>

View File

@ -18,7 +18,9 @@
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Revenue from Cars" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_revenue_from_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
<h4 class="fw-bolder text-primary mb-3">
{{ total_revenue_from_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
@ -26,7 +28,9 @@
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Net Profit from Cars" %}</p>
<h4 class="fw-bolder text-success mb-3">{{ net_profit_from_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
<h4 class="fw-bolder text-success mb-3">
{{ net_profit_from_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
@ -34,7 +38,9 @@
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Discount on Cars" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_discount_on_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
<h4 class="fw-bolder text-primary mb-3">
{{ total_discount_on_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
@ -42,7 +48,9 @@
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Cost of Cars Sold" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_cost_of_cars_sold|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
<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>
@ -50,12 +58,13 @@
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total VAT from Cars" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_vat_collected_from_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
<h4 class="fw-bolder text-primary mb-3">
{{ total_vat_collected_from_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
</div>
<h4 class="fw-bold my-4">{% trans "Sales of New Cars" %}</h4>
<div class="row g-4 mb-5">
<div class="col-sm-6 col-md-4 col-lg-3">
@ -70,7 +79,9 @@
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Revenue" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_revenue_from_new_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
<h4 class="fw-bolder text-primary mb-3">
{{ total_revenue_from_new_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
@ -78,7 +89,9 @@
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Net Profit" %}</p>
<h4 class="fw-bolder text-success mb-3">{{ net_profit_from_new_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
<h4 class="fw-bolder text-success mb-3">
{{ net_profit_from_new_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
@ -86,7 +99,9 @@
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars VAT" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_vat_collected_from_new_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
<h4 class="fw-bolder text-primary mb-3">
{{ total_vat_collected_from_new_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
@ -94,12 +109,13 @@
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Cost" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_cost_of_new_cars_sold|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
<h4 class="fw-bolder text-primary mb-3">
{{ total_cost_of_new_cars_sold|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
</div>
<h4 class="fw-bold my-4">{% trans "Sales of Used Cars" %}</h4>
<div class="row g-4 mb-5">
<div class="col-sm-6 col-md-4 col-lg-3">
@ -114,7 +130,9 @@
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Revenue" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_revenue_from_used_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
<h4 class="fw-bolder text-primary mb-3">
{{ total_revenue_from_used_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
@ -122,7 +140,9 @@
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Net Profit" %}</p>
<h4 class="fw-bolder text-success mb-3">{{ net_profit_from_used_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
<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>
@ -130,7 +150,9 @@
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars VAT" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_vat_collected_from_used_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
<h4 class="fw-bolder text-primary mb-3">
{{ total_vat_collected_from_used_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
@ -138,13 +160,14 @@
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Cost" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_cost_of_used_cars_sold|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
<h4 class="fw-bolder text-primary mb-3">
{{ total_cost_of_used_cars_sold|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
</div>
{% endif %}
{% if request.is_dealer or request.is_manager or request.is_inventory %}
<h3 class="fw-bold mb-3">{% trans "Inventory KPIs" %}</h3>
<div class="row g-4 mb-5">
@ -160,7 +183,9 @@
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Inventory Value" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_inventory_value|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
<h4 class="fw-bolder text-primary mb-3">
{{ total_inventory_value|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
@ -184,7 +209,9 @@
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Inventory Value" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ new_car_value|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
<h4 class="fw-bolder text-primary mb-3">
{{ new_car_value|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
@ -192,21 +219,28 @@
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Inventory Value" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ used_car_value|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
<h4 class="fw-bolder text-primary mb-3">
{{ used_car_value|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-danger fw-bold small mb-1"><a class="text-danger" href="{% url 'aging_inventory_list' request.dealer.slug %}">{% trans "Aging Inventory (> 60 days)" %}</a></p>
<h4 class="fw-bolder text-danger mb-3"><a class="text-danger" href="{% url 'aging_inventory_list' request.dealer.slug %}">{{ aging_inventory_count }}</a></h4>
<p class="text-uppercase text-danger fw-bold small mb-1">
<a class="text-danger"
href="{% url 'aging_inventory_list' request.dealer.slug %}">{% trans "Aging Inventory (> 60 days)" %}</a>
</p>
<h4 class="fw-bolder text-danger mb-3">
<a class="text-danger"
href="{% url 'aging_inventory_list' request.dealer.slug %}">{{ aging_inventory_count }}</a>
</h4>
</div>
</div>
</div>
</div>
{% endif %}
{% if request.is_dealer or request.is_manager or request.is_accountant %}
<h3 class="fw-bold mb-3">
{% blocktrans with start_date=start_date|date:"F j, Y" end_date=end_date|date:"F j, Y" %}
@ -218,7 +252,9 @@
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Revenue from Services" %}</p>
<h4 class="fw-bolder text-info mb-3">{{ total_revenue_from_services|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
<h4 class="fw-bolder text-info mb-3">
{{ total_revenue_from_services|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
@ -226,7 +262,9 @@
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total VAT from Services" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_vat_collected_from_services|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
<h4 class="fw-bolder text-primary mb-3">
{{ total_vat_collected_from_services|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
@ -234,7 +272,9 @@
<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>
<h4 class="fw-bolder text-success mb-3">
{{ total_revenue_generated|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
@ -242,7 +282,9 @@
<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>
<h4 class="fw-bolder text-primary mb-3">
{{ total_vat_collected|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
@ -250,7 +292,9 @@
<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>
<h4 class="fw-bolder text-danger mb-3">
{{ total_expenses|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
@ -258,7 +302,9 @@
<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>
<h4 class="fw-bolder text-success mb-3">
{{ gross_profit|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>

View File

@ -1,11 +1,9 @@
{% extends 'base.html' %}
{% load i18n %}
{% load tenhal_tag %}
{% block title %}
{% trans "Dealership Dashboard"|capfirst %}
{% endblock title %}
{% block content %}
<div class="main-content flex-grow-1 container-fluid mt-4 mb-3">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 pb-3 border-bottom">
@ -25,13 +23,21 @@
<div class="row g-3">
<div class="col-12 col-md-4">
<label for="start-date" class="form-label">{% trans "Start Date" %}</label>
<input type="date" class="form-control" id="start-date" name="start_date"
value="{{ start_date|date:'Y-m-d' }}" required>
<input type="date"
class="form-control"
id="start-date"
name="start_date"
value="{{ start_date|date:'Y-m-d' }}"
required>
</div>
<div class="col-12 col-md-4">
<label for="end-date" class="form-label">{% trans "End Date" %}</label>
<input type="date" class="form-control" id="end-date" name="end_date"
value="{{ end_date|date:'Y-m-d' }}" required>
<input type="date"
class="form-control"
id="end-date"
name="end_date"
value="{{ end_date|date:'Y-m-d' }}"
required>
</div>
<div class="col-12 col-md-4 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">{% trans "Apply Filter" %}</button>
@ -40,19 +46,11 @@
<input type="hidden" name="make_sold" value="{{ selected_make_sales }}">
</form>
</div>
<div class="row g-4 mb-5">
{% include 'dashboards/financial_data_cards.html' %}
<div class="row g-4 mb-5">{% include 'dashboards/financial_data_cards.html' %}</div>
<div class="row g-4 mb-5">{% include 'dashboards/chart.html' %}</div>
</div>
<div class="row g-4 mb-5">
{% include 'dashboards/chart.html' %}
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
{% endblock content %}
{% block customJS %}
<script>
// Define a color palette that aligns with the Phoenix template

View File

@ -1,21 +1,28 @@
{% extends 'base.html' %}
{% block content %}
<div class="main-content flex-grow-1 container-fluid mt-4 mb-3">
<div class="d-flex justify-content-between align-items-center mb-5 pb-3 border-bottom">
<h2 class="h3 fw-bold mb-0">Manager Dashboard<i class="fas fa-chart-area text-primary ms-2"></i></h2>
<h2 class="h3 fw-bold mb-0">
Manager Dashboard<i class="fas fa-chart-area text-primary ms-2"></i>
</h2>
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
Last 30 Days
</button>
<button class="btn btn-outline-secondary dropdown-toggle"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false">Last 30 Days</button>
<ul class="dropdown-menu dropdown-menu-end shadow">
<li><a class="dropdown-item" href="#">Today</a></li>
<li><a class="dropdown-item" href="#">Last 7 Days</a></li>
<li><a class="dropdown-item" href="#">Last 90 Days</a></li>
<li>
<a class="dropdown-item" href="#">Today</a>
</li>
<li>
<a class="dropdown-item" href="#">Last 7 Days</a>
</li>
<li>
<a class="dropdown-item" href="#">Last 90 Days</a>
</li>
</ul>
</div>
</div>
<div class="row g-4 mb-5">
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
@ -122,7 +129,6 @@
</div>
</div>
</div>
<div class="row g-4 mb-5">
<div class="col-lg-8">
<div class="card h-100 shadow-sm border-0">
@ -139,20 +145,21 @@
<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;">
<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;">
<div class="card-body d-flex align-items-center justify-content-center"
style="height: 400px">
<canvas id="salesByBrandChart"></canvas>
</div>
</div>
@ -168,12 +175,9 @@
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
{% endblock content %}
{% block customJS %}
<script>
// Define a color palette that aligns with the Phoenix template

View File

@ -1,22 +1,30 @@
{% extends 'base.html' %}
{% load i18n %}
{% block content %}
<div class="main-content flex-grow-1 container-fluid mt-4 mb-3">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 pb-3 border-bottom">
<h2 class="h3 fw-bold mb-3 mb-md-0">{% trans "Sales Dashboard" %} <i class="fas fa-chart-area text-primary ms-2"></i></h2>
<h2 class="h3 fw-bold mb-3 mb-md-0">
{% trans "Sales Dashboard" %} <i class="fas fa-chart-area text-primary ms-2"></i>
</h2>
<form method="GET" class="date-filter-form">
<div class="row g-3">
<div class="col-12 col-md-4">
<label for="start-date" class="form-label">{% trans "Start Date" %}</label>
<input type="date" class="form-control" id="start-date" name="start_date"
value="{{ start_date|date:'Y-m-d' }}" required>
<input type="date"
class="form-control"
id="start-date"
name="start_date"
value="{{ start_date|date:'Y-m-d' }}"
required>
</div>
<div class="col-12 col-md-4">
<label for="end-date" class="form-label">{% trans "End Date" %}</label>
<input type="date" class="form-control" id="end-date" name="end_date"
value="{{ end_date|date:'Y-m-d' }}" required>
<input type="date"
class="form-control"
id="end-date"
name="end_date"
value="{{ end_date|date:'Y-m-d' }}"
required>
</div>
<div class="col-12 col-md-4 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">{% trans "Apply Filter" %}</button>
@ -24,7 +32,6 @@
</div>
</form>
</div>
<div class="row g-4 mb-5">
<h3 class="fw-bold mb-3">{% trans "Inventory KPIs" %}</h3>
<div class="col-sm-6 col-md-4 col-lg-3">
@ -35,7 +42,6 @@
</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">
@ -52,48 +58,48 @@
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-danger fw-bold small mb-1"><a class="text-danger" href="{% url 'aging_inventory_list' request.dealer.slug %}">{% trans "Aging Inventory (> 60 days)" %}</a></p>
<h4 class="fw-bolder text-danger mb-3"><a class="text-danger" href="{% url 'aging_inventory_list' request.dealer.slug %}">{{ aging_inventory_count }}</a></h4>
<p class="text-uppercase text-danger fw-bold small mb-1">
<a class="text-danger"
href="{% url 'aging_inventory_list' request.dealer.slug %}">{% trans "Aging Inventory (> 60 days)" %}</a>
</p>
<h4 class="fw-bolder text-danger mb-3">
<a class="text-danger"
href="{% url 'aging_inventory_list' request.dealer.slug %}">{{ aging_inventory_count }}</a>
</h4>
</div>
</div>
</div>
</div>
<div class="row g-4 mb-5">
<div class="col-md-6">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0">
<h5 class="fw-bold mb-0 text-dark">{% trans "Top Lead Sources" %}</h5>
</div>
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;">
<div class="card-body d-flex align-items-center justify-content-center"
style="height: 400px">
<canvas id="leadSourcesChart"></canvas>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0">
<h5 class="fw-bold mb-0 text-dark">{% trans "Lead Conversion Funnel" %}</h5>
</div>
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;">
<div class="card-body d-flex align-items-center justify-content-center"
style="height: 400px">
<canvas id="leadFunnelChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
{% endblock content %}
{% block customJS %}
<script>
// Define your color palette at the top

View File

@ -1,7 +1,8 @@
{% extends 'base.html' %}
{% load i18n static %}
{% block title %}
{% trans 'Activity' %}{% endblock %}
{% trans 'Activity' %}
{% endblock %}
{% block content %}
<div class="row">
<div class="ol-auto pt-5 pb-9">

View File

@ -1,7 +1,8 @@
{% extends "base.html" %}
{% load i18n static %}
{% block title %}
{% trans 'Car Makes' %}{% endblock %}
{% trans 'Car Makes' %}
{% endblock %}
{% block content %}
<style>
/* Your existing CSS styles here */
@ -57,7 +58,8 @@
}
</style>
<h2 class="text-center text-primary">{{ _("Select Car Makes You Sell") }}</h2>
<form method="post" class="mb-3"
<form method="post"
class="mb-3"
action="{% url 'assign_car_makes' request.dealer.slug %}">
{% csrf_token %}
<div class="car-makes-grid">
@ -66,9 +68,7 @@
<input type="checkbox"
name="car_makes"
value="{{ car_make.pk }}"
{% if car_make.pk in form.initial.car_makes or car_make.pk|stringformat:"s" in form.car_makes.value %}
checked
{% endif %}>
{% if car_make.pk in form.initial.car_makes or car_make.pk|stringformat:"s" in form.car_makes.value %} checked {% endif %}>
<div class="car-make-image-container">
{% if car_make.logo and car_make.logo.url %}
<img src="{{ car_make.logo.url }}"

View File

@ -3,7 +3,6 @@
{% block title %}
{% trans 'Profile' %}
{% endblock %}
{% block content %}
<div class="container-fluid mb-3">
<div class="row align-items-center justify-content-between g-3 mb-4">
@ -12,20 +11,33 @@
</div>
<div class="col-auto">
<div class="dropdown">
<button class="btn btn-phoenix-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<button class="btn btn-phoenix-primary dropdown-toggle"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false">
<span class="fas fa-cog me-2"></span>{{ _("Manage Profile") }}
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="{% url 'dealer_update' dealer.slug %}"><span class="fas fa-edit me-2"></span>{{ _("Edit Profile") }}</a></li>
<li><a class="dropdown-item" href="{% url 'billing_info' %}"><span class="fas fa-credit-card me-2"></span>{{ _("Billing Information") }}</a></li>
<li><a class="dropdown-item" href="{% url 'order_list' %}"><span class="fas fa-clipboard-list me-2"></span>{{ _("Plans History") }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="{% url 'account_change_password' %}"><span class="fas fa-key me-2"></span>{{ _("Change Password") }}</a></li>
<li>
<a class="dropdown-item" href="{% url 'dealer_update' dealer.slug %}"><span class="fas fa-edit me-2"></span>{{ _("Edit Profile") }}</a>
</li>
<li>
<a class="dropdown-item" href="{% url 'billing_info' %}"><span class="fas fa-credit-card me-2"></span>{{ _("Billing Information") }}</a>
</li>
<li>
<a class="dropdown-item" href="{% url 'order_list' %}"><span class="fas fa-clipboard-list me-2"></span>{{ _("Plans History") }}</a>
</li>
<li>
<hr class="dropdown-divider">
</li>
<li>
<a class="dropdown-item text-danger"
href="{% url 'account_change_password' %}"><span class="fas fa-key me-2"></span>{{ _("Change Password") }}</a>
</li>
</ul>
</div>
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-12">
<div class="card shadow-sm h-100">
@ -33,34 +45,49 @@
<div class="d-flex flex-column flex-sm-row align-items-sm-center g-3 g-sm-5 text-center text-sm-start">
<div class="col-12 col-sm-auto mb-3 mb-sm-0">
<input class="d-none" id="avatarFile" type="file" />
<label class="cursor-pointer avatar avatar-5xl border rounded-circle shadow-sm" for="avatarFile">
<label class="cursor-pointer avatar avatar-5xl border rounded-circle shadow-sm"
for="avatarFile">
{% if dealer.logo %}
<img src="{{ dealer.logo.url }}" alt="{{ dealer.get_local_name }}" class="rounded-circle" style="max-width: 150px" />
<img src="{{ dealer.logo.url }}"
alt="{{ dealer.get_local_name }}"
class="rounded-circle"
style="max-width: 150px" />
{% else %}
<img src="{% static 'images/logos/logo.png' %}" alt="{{ dealer.get_local_name }}" class="rounded-circle" style="max-width: 150px" />
<img src="{% static 'images/logos/logo.png' %}"
alt="{{ dealer.get_local_name }}"
class="rounded-circle"
style="max-width: 150px" />
{% endif %}
</label>
</div>
<div class="flex-1 col-12 col-sm ms-2">
<h3>{{ dealer.get_local_name }}</h3>
<p class="text-body-secondary mb-1">{% trans 'Joined' %} {{ dealer.joined_at|timesince }} {% trans 'ago' %}</p>
<span class="badge bg-primary-subtle text-primary">{% trans 'Last login' %}: {{ dealer.user.last_login|date:"D M d, Y H:i" }}</span>
</div>
<div class="col-12 col-sm-auto d-flex align-items-center justify-content-around flex-wrap mt-3 mt-sm-0">
<div class="text-center mx-3 mb-2 mb-sm-0">
<h6 class="mb-2 text-body-secondary">{% trans 'Total users'|capfirst %}</h6>
<h4 class="fs-7 text-body-highlight mb-2">{{ dealer.staff_count }} / {{ allowed_users }}</h4>
<div class="progress" style="height: 5px; width: 100px;">
<div class="progress-bar bg-success" role="progressbar" style="width: {{ dealer.staff_count|get_percentage:allowed_users }}%;" aria-valuenow="{{ dealer.staff_count|get_percentage:allowed_users }}" aria-valuemin="0" aria-valuemax="100"></div>
<div class="progress-bar bg-success"
role="progressbar"
style="width: {{ dealer.staff_count|get_percentage:allowed_users }}%"
aria-valuenow="{{ dealer.staff_count|get_percentage:allowed_users }}"
aria-valuemin="0"
aria-valuemax="100"></div>
</div>
</div>
<div class="text-center mx-3 mb-2 mb-sm-0">
<h6 class="mb-2 text-body-secondary">{% trans 'Total cars'|capfirst %}</h6>
<h4 class="fs-7 text-body-highlight mb-2">{{ cars_count }} / {{ allowed_cars }}</h4>
<div class="progress" style="height: 5px; width: 100px;">
<div class="progress-bar bg-info" role="progressbar" style="width: {{ cars_count|get_percentage:allowed_cars }}%;" aria-valuenow="{{ cars_count|get_percentage:allowed_cars }}" aria-valuemin="0" aria-valuemax="100"></div>
<div class="progress-bar bg-info"
role="progressbar"
style="width: {{ cars_count|get_percentage:allowed_cars }}%"
aria-valuenow="{{ cars_count|get_percentage:allowed_cars }}"
aria-valuemin="0"
aria-valuemax="100"></div>
</div>
</div>
</div>
@ -69,24 +96,53 @@
</div>
</div>
</div>
<div class="row g-3">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-body">
<ul class="nav nav-tabs nav-justified" id="profileTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="subscription-tab" data-bs-toggle="tab" data-bs-target="#subscription-pane" type="button" role="tab" aria-controls="subscription-pane" aria-selected="true"><span class="fas fa-star me-2"></span>{{ _("Plan & Subscription") }}</button>
<button class="nav-link active"
id="subscription-tab"
data-bs-toggle="tab"
data-bs-target="#subscription-pane"
type="button"
role="tab"
aria-controls="subscription-pane"
aria-selected="true">
<span class="fas fa-star me-2"></span>{{ _("Plan & Subscription") }}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="contact-tab" data-bs-toggle="tab" data-bs-target="#contact-pane" type="button" role="tab" aria-controls="contact-pane" aria-selected="false"><span class="fas fa-info-circle me-2"></span>{{ _("Company Details") }}</button>
<button class="nav-link"
id="contact-tab"
data-bs-toggle="tab"
data-bs-target="#contact-pane"
type="button"
role="tab"
aria-controls="contact-pane"
aria-selected="false">
<span class="fas fa-info-circle me-2"></span>{{ _("Company Details") }}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="makes-tab" data-bs-toggle="tab" data-bs-target="#makes-pane" type="button" role="tab" aria-controls="makes-pane" aria-selected="false"><span class="fas fa-car me-2"></span>{{ _("Car Brands") }}</button>
<button class="nav-link"
id="makes-tab"
data-bs-toggle="tab"
data-bs-target="#makes-pane"
type="button"
role="tab"
aria-controls="makes-pane"
aria-selected="false">
<span class="fas fa-car me-2"></span>{{ _("Car Brands") }}
</button>
</li>
</ul>
<div class="tab-content pt-4" id="profileTabsContent">
<div class="tab-pane fade show active" id="subscription-pane" role="tabpanel" aria-labelledby="subscription-tab">
<div class="tab-pane fade show active"
id="subscription-pane"
role="tabpanel"
aria-labelledby="subscription-tab">
<div class="row g-3">
<div class="col-12 col-lg-6">
<div class="card h-100 shadow-sm">
@ -108,14 +164,12 @@
{% 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">
@ -124,7 +178,6 @@
</li>
{% endfor %}
</ul>
{% comment %} <div class="d-flex justify-content-end gap-2">
{% if dealer.user.userplan.is_expired %}
<a href="{% url 'pricing_page' request.dealer.slug %}" class="btn btn-warning"><span class="fas fa-redo-alt me-2"></span>{{ _("Renew") }}</a>
@ -138,11 +191,14 @@
</div> {% endcomment %}
<div class="d-flex justify-content-end gap-2">
{% if not dealer.user.userplan %}
<a href="{% url 'pricing_page' request.dealer.slug %}" class="btn btn-outline-primary"><span class="fas fa-cart-plus me-2"></span>{{ _("Subscribe Now") }}</a>
<a href="{% url 'pricing_page' request.dealer.slug %}"
class="btn btn-outline-primary"><span class="fas fa-cart-plus me-2"></span>{{ _("Subscribe Now") }}</a>
{% elif dealer.user.userplan.is_expired %}
<a href="{% url 'pricing_page' request.dealer.slug %}" class="btn btn-outline-warning"><span class="fas fa-redo-alt me-2"></span>{{ _("Renew") }}</a>
<a href="{% url 'pricing_page' request.dealer.slug %}"
class="btn btn-outline-warning"><span class="fas fa-redo-alt me-2"></span>{{ _("Renew") }}</a>
{% elif dealer.user.userplan.plan.name != "Enterprise" %}
<a href="{% url 'pricing_page' request.dealer.slug %}" class="btn btn-outline-primary"><span class="fas fa-rocket me-2"></span>{{ _("Upgrade Plan") }}</a>
<a href="{% url 'pricing_page' request.dealer.slug %}"
class="btn btn-outline-primary"><span class="fas fa-rocket me-2"></span>{{ _("Upgrade Plan") }}</a>
{% endif %}
</div>
</div>
@ -157,7 +213,12 @@
<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 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>
@ -167,7 +228,12 @@
<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 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>
@ -180,8 +246,10 @@
</div>
</div>
</div>
<div class="tab-pane fade" id="contact-pane" role="tabpanel" aria-labelledby="contact-tab">
<div class="tab-pane fade"
id="contact-pane"
role="tabpanel"
aria-labelledby="contact-tab">
<div class="row g-3">
<div class="col-12 col-lg-6">
<div class="card h-100 shadow-sm">
@ -215,18 +283,23 @@
<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">
<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>
<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="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>
@ -234,7 +307,12 @@
{% 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;" />
<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>
@ -242,7 +320,8 @@
<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>
<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>

View File

@ -15,7 +15,11 @@
</h3>
</div>
<div class="card-body p-4 p-md-5">
<form hx-boost="false" method="post" enctype="multipart/form-data" class="needs-validation" novalidate>
<form hx-boost="false"
method="post"
enctype="multipart/form-data"
class="needs-validation"
novalidate>
{% csrf_token %}
{{ form|crispy }}
<hr class="my-4">

View File

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

View File

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

View File

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

View File

@ -16,11 +16,20 @@
<h2>Hello {{ user_name }},</h2>
<p>{% trans "This is a friendly reminder for your upcoming schedule" %}:</p>
<p>
<span class="highlight">{% trans "Purpose" %}:</span> {{ schedule_purpose }}<br>
<span class="highlight">{% trans "Scheduled At" %}:</span> {{ scheduled_at }}<br>
<span class="highlight">{% trans "Type" %}:</span> {{ schedule_type }}<br>
{% if customer_name != 'N/A' %}<span class="highlight">{% trans "Customer" %}:</span> {{ customer_name }}<br>{% endif %}
{% if notes %}<span class="highlight">{% trans "Notes" %}:</span> {{ notes }}<br>{% endif %}
<span class="highlight">{% trans "Purpose" %}:</span> {{ schedule_purpose }}
<br>
<span class="highlight">{% trans "Scheduled At" %}:</span> {{ scheduled_at }}
<br>
<span class="highlight">{% trans "Type" %}:</span> {{ schedule_type }}
<br>
{% if customer_name != 'N/A' %}
<span class="highlight">{% trans "Customer" %}:</span> {{ customer_name }}
<br>
{% endif %}
{% if notes %}
<span class="highlight">{% trans "Notes" %}:</span> {{ notes }}
<br>
{% endif %}
</p>
<p>{% trans "Please be prepared for your schedule" %}.</p>
<p>{% trans "Thank you" %}!</p>
@ -28,7 +37,6 @@
<div class="footer">
<p>{% trans "This is an automated reminder. Please do not reply to this email." %}</p>
</div>
</div>
</body>
</html>

View File

@ -1,6 +1,5 @@
{% load static %}
{% load i18n %}
<style>
.empty-state-container {
@ -36,35 +35,28 @@
}
/* No specific styles for .btn-add-new or .message-box are needed here as per previous updates */
</style>
<div class="empty-state-container">
<!-- Empty State Illustration -->
{% if image %}
{% static image as final_image_path %}
{% else %}
{% static 'images/no_content/no_item.jpg' as final_image_path %}
{% endif %}
<p class="sm">
<img src="{{ final_image_path }}" alt="No-empty-state-image" class="empty-state-image">
<img src="{{ final_image_path }}"
alt="No-empty-state-image"
class="empty-state-image">
<p>
<!-- Title -->
<h3 class="empty-state-title">
{% blocktrans %}No {{ value}} Yet{% endblocktrans %}
</h3>
<h3 class="empty-state-title">{% blocktrans %}No {{ value}} Yet{% endblocktrans %}</h3>
<!-- Description -->
<p class="empty-state-text">
{% blocktrans %}It looks like you haven't added any {{ value }} to your account.
Click the button below to get started and add your first {{ value }}!{% endblocktrans %}
</p>
<!-- Call to Action Button -->
<a class="btn btn-lg btn-primary" href="{{ url }}">
<i class="fa fa-plus me-2"></i>
{% blocktrans %}Create New {{value}}{% endblocktrans %}
</a>
</div>

View File

@ -15,7 +15,6 @@
</div>
</div>
</footer> {% endcomment %}
{% comment %} <footer class="footer position-absolute fs-9 bg-info-subtle">
<div class="row g-0 justify-content-between align-items-center h-100">
<div class="col-12 col-sm-auto text-center text-warning">
@ -32,7 +31,6 @@
</div>
</div>
</footer> {% endcomment %}
{% comment %} <footer class="footer position-absolute fs-9 bg-white text-secondary">
<div class="row g-0 justify-content-between align-items-center h-100">
<div class="col-12 col-sm-auto text-center">
@ -51,10 +49,6 @@
</div>
</div>
</footer> {% endcomment %}
<style>
@ -97,9 +91,6 @@
opacity: 0.6;
}
</style>
<footer class="improved-footer">
<div class="container">
<div class="row g-0 justify-content-between align-items-center h-100">
@ -119,5 +110,3 @@
</div>
</div>
</footer>

View File

@ -29,24 +29,26 @@
{% csrf_token %}
{{ redirect_field }}
{{ form|crispy }}
{% if form.errors %}
<div class="alert alert-danger mt-4" role="alert">
<h4 class="alert-heading small">{% trans "Please correct the following errors:" %}</h4>
<ul class="mb-0">
{% for field, errors in form.errors.items %}
<li><strong>{{ field|capfirst }}:</strong> {% for error in errors %}{{ error }}{% endfor %}</li>
<li>
<strong>{{ field|capfirst }}:</strong>
{% for error in errors %}{{ error }}{% endfor %}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
<hr class="my-4">
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
<button class="btn btn-phoenix-primary btn-lg md-me-2" type="submit">
<i class="fa-solid fa-floppy-disk me-1"></i>{% trans "Save" %}
</button>
<a href="{% url 'group_list' request.dealer.slug %}" class="btn btn-phoenix-secondary btn-lg">
<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>

View File

@ -11,7 +11,9 @@
{% if groups or request.GET.q %}
<div class="card border-0 rounded-4 animate__animated animate__fadeInUp">
<div class="card-header border-bottom d-flex flex-column flex-md-row justify-content-between align-items-md-center p-4">
<h5 class="card-title mb-2 mb-md-0 me-md-4 fw-bold"> <i class="fa-solid fa-user-group fs-3 me-1 text-primary "></i>{% trans "Groups" %}</h5>
<h5 class="card-title mb-2 mb-md-0 me-md-4 fw-bold">
<i class="fa-solid fa-user-group fs-3 me-1 text-primary "></i>{% trans "Groups" %}
</h5>
<div class="d-flex gap-2">
<a href="{% url 'group_create' request.dealer.slug %}"
class="btn btn-phoenix-primary">
@ -32,7 +34,10 @@
<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>
<th scope="col"
class="text-secondary text-uppercase fw-bold text-end pe-4">
{% trans 'actions'|capfirst %}
</th>
</tr>
</thead>
<tbody>
@ -60,9 +65,7 @@
</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 class="d-flex justify-content-end">{% include 'partials/pagination.html' %}</div>
</div>
{% endif %}
</div>

View File

@ -24,12 +24,13 @@
<div>
<form method="post" novalidate>
{% csrf_token %}
<div class="row mb-4">
<div class="col-md-6">
<div class="input-group">
<span class="input-group-text"><i class="fas fa-search"></i></span>
<input type="text" class="form-control" id="permissionSearch"
<input type="text"
class="form-control"
id="permissionSearch"
placeholder="{% trans 'Search permissions...' %}">
</div>
</div>
@ -40,10 +41,11 @@
</div>
</div>
</div>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4" id="permissionsGrid">
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4"
id="permissionsGrid">
{% for app_label, models in grouped_permissions.items %}
<div class="col"> {# This div opens for each app_label #}
<div class="col">
{# This div opens for each app_label #}
<div class="card h-100 border-{% if app_label in group_permission_apps %}primary{% else %}light{% endif %}">
<div class="card-header bg-{% if app_label in group_permission_apps %}primary text-white{% else %}light{% endif %}">
<div class="d-flex justify-content-between align-items-center">
@ -60,7 +62,8 @@
<div class="accordion" id="accordion-{{ app_label|slugify }}">
{% for model, perms in models.items %}
<div class="accordion-item border-0 mb-2">
<h6 class="accordion-header" id="heading-{{ app_label|slugify }}-{{ model|slugify }}">
<h6 class="accordion-header"
id="heading-{{ app_label|slugify }}-{{ model|slugify }}">
<button class="accordion-button collapsed bg-white shadow-none py-2"
type="button"
data-bs-toggle="collapse"
@ -115,21 +118,14 @@
</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>
<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>
@ -139,7 +135,6 @@
</div>
</form>
</div>
<style>
.bg-light-primary {
background-color: rgba(13, 110, 253, 0.1);
@ -156,7 +151,6 @@
border-color: rgba(0,0,0,.125);
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize all accordions

View File

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

View File

@ -3,35 +3,54 @@
<nav class="navbar navbar-vertical navbar-expand-lg ">
<div class="collapse navbar-collapse" id="navbarVerticalCollapse">
<div class="navbar-vertical-content d-flex flex-column">
<ul class="navbar-nav flex-column" id="navbarVerticalNav" hx-boost="false" hx-target="#main_content" hx-select="#main_content" hx-swap="outerHTML" hx-select-oob="#toast-container" hx-indicator="#spinner">
<ul class="navbar-nav flex-column"
id="navbarVerticalNav"
hx-boost="false"
hx-target="#main_content"
hx-select="#main_content"
hx-swap="outerHTML"
hx-select-oob="#toast-container"
hx-indicator="#spinner">
<li class="nav-item">
{% comment %} <p class="navbar-vertical-label text-primary fs-8 text-truncate">{{request.dealer|default:"Apps"}}</p>
<hr class="navbar-vertical-line"> {% endcomment %}
{% if perms.inventory.can_view_inventory %}
<div class="nav-item-wrapper">
<a id="inventory-nav" class="nav-link dropdown-indicator label-1 inventory-nav" href="#nv-inventory" role="button" data-bs-toggle="collapse" aria-expanded="false" aria-controls="nv-inventory">
<a id="inventory-nav"
class="nav-link dropdown-indicator label-1 inventory-nav"
href="#nv-inventory"
role="button"
data-bs-toggle="collapse"
aria-expanded="false"
aria-controls="nv-inventory">
<div class="d-flex align-items-center ">
<div class="dropdown-indicator-icon-wrapper"><span class="fas fa-caret-right dropdown-indicator-icon"></span></div>
<div class="dropdown-indicator-icon-wrapper">
<span class="fas fa-caret-right dropdown-indicator-icon"></span>
</div>
<span class="nav-link-icon "><span class="fas fa-warehouse"></span></span><span class="nav-link-text ">{% trans "Inventory"|capfirst %}</span>
</div>
</a>
<div class="parent-wrapper label-1">
<ul class="nav collapse parent" data-bs-parent="#navbarVerticalCollapse" id="nv-inventory">
<ul class="nav collapse parent"
data-bs-parent="#navbarVerticalCollapse"
id="nv-inventory">
<li class="collapsed-nav-item-title d-none">{% trans "Inventory"|capfirst %}</li>
{% if perms.inventory.add_car %}
<li class="nav-item">
<a hx-boost="false" id="btn-add-car" class="nav-link btn-add-car" href="{% url 'car_add' request.dealer.slug %}">
<a hx-boost="false"
id="btn-add-car"
class="nav-link btn-add-car"
href="{% url 'car_add' request.dealer.slug %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span class="fas fa-plus-circle"></span></span><span class="nav-link-text">{% trans "add car"|capfirst %}</span>
</div>
</a>
</li>
{% endif %}
{% if perms.inventory.view_car %}
<li class="nav-item">
<a class="nav-link" href="{% url 'inventory_stats' request.dealer.slug %}">
<a class="nav-link"
href="{% url 'inventory_stats' request.dealer.slug %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span class="fas fa-car-side"></span></span><span class="nav-link-text">{% trans 'Cars'|capfirst %}</span>
</div>
@ -45,7 +64,6 @@
</a>
</li>
{% endif %}
{% comment %} {% if perms.inventory.add_car %}
<li class="nav-item">
<a class="nav-link" href="{% url 'upload_cars' request.dealer.slug %}">
@ -57,36 +75,45 @@
{% endif %} {% endcomment %}
{% if perms.django_ledger.view_purchaseordermodel %}
<li class="nav-item">
<a class="nav-link" href="{% url 'purchase_order_list' request.dealer.slug request.dealer.entity.slug %}">
<a class="nav-link"
href="{% url 'purchase_order_list' request.dealer.slug request.dealer.entity.slug %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span class="fas fa-warehouse"></span></span><span class="nav-link-text">{% trans "purchase Orders"|capfirst %}</span>
</div>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'inventory_list' request.dealer.slug request.dealer.entity.slug %}">
<a class="nav-link"
href="{% url 'inventory_list' request.dealer.slug request.dealer.entity.slug %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span class="fas fa-boxes"></span></span><span class="nav-link-text">{% trans "Inventory List"|capfirst %}</span>
</div>
</a>
</li>
{% endif %}
</ul>
</div>
</div>
{% endif %}
{% if perms.inventory.can_view_crm %}
<div class="nav-item-wrapper">
<a class="nav-link dropdown-indicator label-1" href="#nv-crm" role="button" data-bs-toggle="collapse" aria-expanded="false" aria-controls="nv-crm">
<a class="nav-link dropdown-indicator label-1"
href="#nv-crm"
role="button"
data-bs-toggle="collapse"
aria-expanded="false"
aria-controls="nv-crm">
<div class="d-flex align-items-center">
<div class="dropdown-indicator-icon-wrapper"><span class="fas fa-caret-right dropdown-indicator-icon"></span></div>
<div class="dropdown-indicator-icon-wrapper">
<span class="fas fa-caret-right dropdown-indicator-icon"></span>
</div>
<span class="nav-link-icon"><span data-feather="phone"></span></span><span class="nav-link-text">{% trans 'crm'|upper %}</span>
</div>
</a>
<div class="parent-wrapper label-1">
<ul class="nav collapse parent" data-bs-parent="#navbarVerticalCollapse" id="nv-crm">
<ul class="nav collapse parent"
data-bs-parent="#navbarVerticalCollapse"
id="nv-crm">
<li class="collapsed-nav-item-title d-none">{% trans 'crm'|upper %}</li>
{% if perms.inventory.view_lead %}
<li class="nav-item">
@ -106,7 +133,8 @@
{% endif %}
{% if perms.inventory.view_opportunity %}
<li class="nav-item">
<a class="nav-link" href="{% url 'opportunity_list' request.dealer.slug %}">
<a class="nav-link"
href="{% url 'opportunity_list' request.dealer.slug %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span data-feather="users"></span></span><span class="nav-link-text">{% trans 'Opportunity'|capfirst %}</span>
</div>
@ -124,7 +152,8 @@
{% endif %}
{% if perms.inventory.view_organization %}
<li class="nav-item">
<a class="nav-link" href="{% url 'organization_list' request.dealer.slug %}">
<a class="nav-link"
href="{% url 'organization_list' request.dealer.slug %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span class="fas fa-city"></span></span><span class="nav-link-text">{% trans "Organizations"|capfirst %}</span>
</div>
@ -149,21 +178,31 @@
</div>
</div>
{% endif %}
{% if perms.django_ledger.can_view_sales %}
<div class="nav-item-wrapper">
<a class="nav-link dropdown-indicator label-1" href="#nv-sales" role="button" data-bs-toggle="collapse" aria-expanded="false" aria-controls="nv-sales">
<a class="nav-link dropdown-indicator label-1"
href="#nv-sales"
role="button"
data-bs-toggle="collapse"
aria-expanded="false"
aria-controls="nv-sales">
<div class="d-flex align-items-center">
<div class="dropdown-indicator-icon-wrapper"><span class="fas fa-caret-right dropdown-indicator-icon"></span></div>
<div class="dropdown-indicator-icon-wrapper">
<span class="fas fa-caret-right dropdown-indicator-icon"></span>
</div>
<span class="nav-link-icon"><span data-feather="shopping-cart"></span></span><span class="nav-link-text">{% trans 'sales'|capfirst %}</span>
</div>
</a>
<div class="parent-wrapper label-1">
<ul class="nav collapse parent" data-bs-parent="#navbarVerticalCollapse" id="nv-sales">
<ul class="nav collapse parent"
data-bs-parent="#navbarVerticalCollapse"
id="nv-sales">
<li class="collapsed-nav-item-title d-none">{% trans 'sales'|capfirst %}</li>
{% if perms.django_ledger.add_estimatemodel %}
<li class="nav-item">
<a hx-boost="false" class="nav-link" href="{% url 'estimate_create' request.dealer.slug %}">
<a hx-boost="false"
class="nav-link"
href="{% url 'estimate_create' request.dealer.slug %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span class="fas fa-handshake"></span></span><span class="nav-link-text">{% trans "create quotation"|capfirst %}</span>
</div>
@ -188,7 +227,6 @@
</a>
</li>
{% endif %}
{% if perms.django_ledger.view_invoicemodel %}
<li class="nav-item">
<a class="nav-link" href="{% url 'invoice_list' request.dealer.slug %}">
@ -202,21 +240,30 @@
</div>
</div>
{% endif %}
{% if perms.django_ledger.can_view_financials %}
<div class="nav-item-wrapper">
<a class="nav-link dropdown-indicator label-1" href="#nv-financial" role="button" data-bs-toggle="collapse" aria-expanded="false" aria-controls="nv-financial">
<a class="nav-link dropdown-indicator label-1"
href="#nv-financial"
role="button"
data-bs-toggle="collapse"
aria-expanded="false"
aria-controls="nv-financial">
<div class="d-flex align-items-center">
<div class="dropdown-indicator-icon-wrapper"><span class="fas fa-caret-right dropdown-indicator-icon"></span></div>
<div class="dropdown-indicator-icon-wrapper">
<span class="fas fa-caret-right dropdown-indicator-icon"></span>
</div>
<span class="nav-link-icon"><span class="fas fa-money-check-alt"></span></span><span class="nav-link-text">{% trans 'Financials' %}</span>
</div>
</a>
<div class="parent-wrapper label-1">
<ul class="nav collapse parent" data-bs-parent="#navbarVerticalCollapse" id="nv-financial">
<ul class="nav collapse parent"
data-bs-parent="#navbarVerticalCollapse"
id="nv-financial">
<li class="collapsed-nav-item-title d-none">{% trans 'Financials' %}</li>
{% if perms.django_ledger.view_accountmodel %}
<li class="nav-item">
<a class="nav-link" href="{% url 'coa-list' request.dealer.slug request.entity.slug %}">
<a class="nav-link"
href="{% url 'coa-list' request.dealer.slug request.entity.slug %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span class="fas fa-book-open"></span></span><span class="nav-link-text">{% trans 'Chart of Accounts'|capfirst %}</span>
</div>
@ -225,37 +272,38 @@
{% endif %}
{% if perms.django_ledger.view_bankaccountmodel %}
<li class="nav-item">
<a class="nav-link" href="{% url 'bank_account_list' request.dealer.slug %}">
<a class="nav-link"
href="{% url 'bank_account_list' request.dealer.slug %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span data-feather="credit-card"></span></span><span class="nav-link-text">{% trans 'Bank Accounts'|capfirst %}</span>
</div>
</a>
</li>
{% endif %}
{% if perms.django_ledger.view_journalentrymodel %}
<li class="nav-item">
<a class="nav-link" href="{% url 'ledger_list' request.dealer.slug request.dealer.entity.slug %}">
<a class="nav-link"
href="{% url 'ledger_list' request.dealer.slug request.dealer.entity.slug %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span class="fas fa-book"></span></span><span class="nav-link-text">{% trans "Ledgers"|capfirst %}</span>
</div>
</a>
</li>
{% endif %}
{% if perms.inventory.view_additionalservices %}
<li class="nav-item">
<a class="nav-link" href="{% url 'item_service_list' request.dealer.slug %}">
<a class="nav-link"
href="{% url 'item_service_list' request.dealer.slug %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span data-feather="activity"></span></span><span class="nav-link-text">{% trans "Services"|capfirst %}</span>
</div>
</a>
</li>
{% endif %}
{% if perms.django_ledger.view_itemmodel %}
<li class="nav-item">
<a class="nav-link" href="{% url 'item_expense_list' request.dealer.slug %}">
<a class="nav-link"
href="{% url 'item_expense_list' request.dealer.slug %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span class="fas fa-users-cog"></span></span><span class="nav-link-text">{% trans "Expenses"|capfirst %}</span>
</div>
@ -294,49 +342,51 @@
</div>
{% endif %}
<!---->
{% if perms.django_ledger.can_view_reports %}
<div class="nav-item-wrapper">
<a class="nav-link dropdown-indicator label-1" href="#nv-reports" role="button" data-bs-toggle="collapse" aria-expanded="false" aria-controls="nv-reports">
<a class="nav-link dropdown-indicator label-1"
href="#nv-reports"
role="button"
data-bs-toggle="collapse"
aria-expanded="false"
aria-controls="nv-reports">
<div class="d-flex align-items-center">
<div class="dropdown-indicator-icon-wrapper"><span class="fas fa-caret-right dropdown-indicator-icon"></span></div>
<div class="dropdown-indicator-icon-wrapper">
<span class="fas fa-caret-right dropdown-indicator-icon"></span>
</div>
<span class="nav-link-icon"><i class="fa-solid fa-book-open"></i></span><span class="nav-link-text">{% trans 'Reports' %}</span>
</div>
</a>
<div class="parent-wrapper label-1">
<ul class="nav collapse parent" data-bs-parent="#navbarVerticalCollapse" id="nv-reports">
<ul class="nav collapse parent"
data-bs-parent="#navbarVerticalCollapse"
id="nv-reports">
<li class="collapsed-nav-item-title d-none">{% trans 'Financials' %}</li>
{% if perms.django_ledger.view_accountmodel %}
<li class="nav-item">
<a class="nav-link" href="{% url 'entity-cf' request.dealer.slug request.dealer.entity.slug %}">
<a class="nav-link"
href="{% url 'entity-cf' request.dealer.slug request.dealer.entity.slug %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span class="fas fa-solid fa-sack-dollar"></span></span><span class="nav-link-text">{% trans 'Cash Flow'|capfirst %}</span>
</div>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'entity-ic' request.dealer.slug request.dealer.entity.slug %}">
<a class="nav-link"
href="{% url 'entity-ic' request.dealer.slug request.dealer.entity.slug %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span class="fa-solid fa-sheet-plastic"></span></span><span class="nav-link-text">{% trans 'Income Statement'|capfirst %}</span>
</div>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'entity-bs' request.dealer.slug request.dealer.entity.slug %}">
<a class="nav-link"
href="{% url 'entity-bs' request.dealer.slug request.dealer.entity.slug %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span class="fas fa-solid fa-scale-balanced"></span></span><span class="nav-link-text">{% trans 'Balance Sheet'|capfirst %}</span>
</div>
</a>
</li>
<li class="nav-item">
<!--car purchase report-->
<a class="nav-link" href="{% url 'po-report' request.dealer.slug %}">
@ -345,29 +395,25 @@
</div>
</a>
</li>
<li class="nav-item">
<!--car sale report-->
<a class="nav-link" href="{% url 'car-sale-report' request.dealer.slug %}">
<a class="nav-link"
href="{% url 'car-sale-report' request.dealer.slug %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span class="fas fa-chart-pie"></span></span><span class="nav-link-text">{% trans 'Car Sale Report'|capfirst %}</span>
</div>
</a>
</li>
{% endif %}
</ul>
</div>
</div>
{% endif %}
<!---->
</li>
</ul>
{# --- Support & Contact Section (New) --- #}
<div class="mt-auto">
<ul class="navbar-nav flex-column">
<li class="nav-item">
<a class="nav-link" href="#">
@ -385,7 +431,6 @@
</div>
</a>
</li>
<li class="nav-item mb-4">
<a class="nav-link" href="#">
<div class="d-flex align-items-center">
@ -394,7 +439,6 @@
</div>
</a>
</li>
<li class="nav-item ">
<a class="nav-link" href="#">
<div class="d-flex align-items-center">
@ -405,30 +449,21 @@
</div>
</a>
</li>
</ul>
</div>
{% endif %}
</div>
</div>
<div class="navbar-vertical-footer">
<button class="btn navbar-vertical-toggle border-0 fw-semibold w-100 white-space-nowrap d-flex align-items-center">
<span class="fas fa-angle-double-left fs-8"></span><span class="fas fa-angle-double-right fs-8"></span><span class="navbar-vertical-footer-text ms-2">Collapsed View</span>
</button>
</div>
</nav>
<nav class="navbar navbar-top fixed-top navbar-expand" id="navbarDefault">
<div class="collapse navbar-collapse justify-content-between">
<div class="navbar-logo">
<button
class="btn navbar-toggler navbar-toggler-humburger-icon hover-bg-transparent"
<button class="btn navbar-toggler navbar-toggler-humburger-icon hover-bg-transparent"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarVerticalCollapse"
@ -439,14 +474,19 @@
</button>
<a class="navbar-brand me-1 me-sm-3" href="{% url 'home' %}">
<div class="d-flex align-items-center">
<img class="logo-img d-dark-none" src="{% static 'images/logos/logo-d.png' %}" alt="haikal" width="27" />
<img class="logo-img d-light-none" src="{% static 'images/logos/logo.png' %}" alt="haikal" width="27" />
<img class="logo-img d-dark-none"
src="{% static 'images/logos/logo-d.png' %}"
alt="haikal"
width="27" />
<img class="logo-img d-light-none"
src="{% static 'images/logos/logo.png' %}"
alt="haikal"
width="27" />
<h5 class="logo-text ms-2 d-none d-sm-block">{% trans 'Haikal' %}</h5>
</div>
</a>
</div>
{% if request.user.is_authenticated %}
<div class="navbar-logo">
<div class="d-flex align-items-center">
{% with name_to_display=request.user.first_name|default:request.dealer.name %}
@ -460,31 +500,66 @@
</div>
</div>
{% endif %}
<ul class="navbar-nav navbar-nav-icons flex-row gap-2" hx-boost="false">
<li class="nav-item">
<div class="theme-control-toggle fa-icon-wait">
<input class="form-check-input ms-0 theme-control-toggle-input" type="checkbox" data-theme-control="phoenixTheme" value="dark" id="themeControlToggleSm" />
<label class="mb-0 theme-control-toggle-label theme-control-toggle-light" for="themeControlToggleSm" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="{{ _("Switch theme")}}" style="height:32px;width:32px;"><span class="icon" data-feather="moon"></span></label>
<label class="mb-0 theme-control-toggle-label theme-control-toggle-dark" for="themeControlToggleSm" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="{{ _("Switch theme")}}" style="height:32px;width:32px;"><span class="icon" data-feather="sun"></span></label>
<input class="form-check-input ms-0 theme-control-toggle-input"
type="checkbox"
data-theme-control="phoenixTheme"
value="dark"
id="themeControlToggleSm" />
<label class="mb-0 theme-control-toggle-label theme-control-toggle-light"
for="themeControlToggleSm"
data-bs-toggle="tooltip"
data-bs-placement="bottom"
data-bs-title="{{ _("Switch theme") }}"
style="height:32px;
width:32px">
<span class="icon" data-feather="moon"></span>
</label>
<label class="mb-0 theme-control-toggle-label theme-control-toggle-dark"
for="themeControlToggleSm"
data-bs-toggle="tooltip"
data-bs-placement="bottom"
data-bs-title="{{ _("Switch theme") }}"
style="height:32px;
width:32px">
<span class="icon" data-feather="sun"></span>
</label>
</div>
</li>
<li class="nav-item dropdown">
<a class="nav-link lh-1 pe-0" href="#" id="languageDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" data-bs-auto-close="outside" aria-haspopup="true" >
<a class="nav-link lh-1 pe-0"
href="#"
id="languageDropdown"
role="button"
data-bs-toggle="dropdown"
aria-expanded="false"
data-bs-auto-close="outside"
aria-haspopup="true">
<span class="me-1" data-feather="globe"></span>
</a>
<div class="dropdown-menu dropdown-menu-end navbar-dropdown-caret py-0 shadow border" aria-labelledby="languageDropdown">
<a class="dropdown-item fw-lighter" href="{% url 'switch_language' %}?language=en">English</a>
<a class="dropdown-item fw-lighter" href="{% url 'switch_language' %}?language=ar">عربي</a>
<div class="dropdown-menu dropdown-menu-end navbar-dropdown-caret py-0 shadow border"
aria-labelledby="languageDropdown">
<a class="dropdown-item fw-lighter"
href="{% url 'switch_language' %}?language=en">English</a>
<a class="dropdown-item fw-lighter"
href="{% url 'switch_language' %}?language=ar">عربي</a>
</div>
</li>
{% if user.is_authenticated %}
{% include "notifications.html" %}
{% endif %}
{% if user.is_authenticated and request.is_dealer or request.is_staff %}
<li class="nav-item dropdown">
<a hx-boost="false" class="nav-link lh-1 pe-0" id="navbarDropdownUser" role="button" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-haspopup="true" aria-expanded="false">
<a hx-boost="false"
class="nav-link lh-1 pe-0"
id="navbarDropdownUser"
role="button"
data-bs-toggle="dropdown"
data-bs-auto-close="outside"
aria-haspopup="true"
aria-expanded="false">
<div class="avatar avatar-l text-center align-middle">
{% if request.is_dealer and user.dealer.logo %}
<img class="rounded-circle" src="{{ user.dealer.logo.url }}" alt="" />
@ -495,7 +570,8 @@
{% endif %}
</div>
</a>
<div class="dropdown-menu dropdown-menu-end navbar-dropdown-caret py-0 dropdown-profile shadow border" aria-labelledby="navbarDropdownUser">
<div class="dropdown-menu dropdown-menu-end navbar-dropdown-caret py-0 dropdown-profile shadow border"
aria-labelledby="navbarDropdownUser">
<div class="card position-relative border-0">
<div class="card-body p-0">
<div class="text-center pt-4 pb-3">
@ -508,7 +584,6 @@
<span class="fa fa-user text-body-tertiary fa-2x" style="width: 32px;"></span>
{% endif %}
</div>
{% if request.is_dealer %}
<h6 class="mt-2 text-body-emphasis">{{ user.dealer.get_local_name }}</h6>
{% else %}
@ -520,38 +595,46 @@
<ul class="nav d-flex flex-column mb-2 pb-1">
{% if request.is_dealer %}
<li class="nav-item">
<a class="nav-link px-3 d-block" href="{% url 'dealer_detail' request.dealer.slug %}"> <span class="me-2 text-body align-bottom" data-feather="user"></span><span>{% translate 'profile'|capfirst %}</span></a>
<a class="nav-link px-3 d-block"
href="{% url 'dealer_detail' request.dealer.slug %}"> <span class="me-2 text-body align-bottom" data-feather="user"></span><span>{% translate 'profile'|capfirst %}</span></a>
</li>
{% else %}
<li class="nav-item">
<a hx-boost="false" class="nav-link px-3 d-block" href="{% url 'staff_detail' request.dealer.slug request.staff.slug %}"> <span class="me-2 text-body align-bottom" data-feather="user"></span><span>{% translate 'profile'|capfirst %}</span></a>
<a hx-boost="false"
class="nav-link px-3 d-block"
href="{% url 'staff_detail' request.dealer.slug request.staff.slug %}"> <span class="me-2 text-body align-bottom" data-feather="user"></span><span>{% translate 'profile'|capfirst %}</span></a>
</li>
{% endif %}
{% if request.is_dealer %}
<li class="nav-item">
<a class="nav-link px-3 d-block" href="{% url 'user_list' request.dealer.slug %}"><span class="me-2 text-body align-bottom" data-feather="users"></span>{{ _("Staff & Groups") }}</a>
<a class="nav-link px-3 d-block"
href="{% url 'user_list' request.dealer.slug %}"><span class="me-2 text-body align-bottom" data-feather="users"></span>{{ _("Staff & Groups") }}</a>
</li>
{% comment %} <li class="nav-item">
<a class="nav-link px-3 d-block" href="{% url 'dealer_activity' request.dealer.slug %}"> <span class="me-2 text-body align-bottom" data-feather="lock"></span>{{ _("Activities") }}</a>
</li> {% endcomment %}
{% endif %}
<li class="nav-item">
{% if request.is_dealer %}
<a class="nav-link px-3 d-block" href="{% url 'dealer_settings' request.dealer.slug %}"> <span class="me-2 text-body align-bottom" data-feather="settings"></span>{{ _("Settings") }}</a>
<a class="nav-link px-3 d-block"
href="{% url 'dealer_settings' request.dealer.slug %}"> <span class="me-2 text-body align-bottom" data-feather="settings"></span>{{ _("Settings") }}</a>
{% endif %}
</li>
<li class="nav-item">
{% if request.is_dealer %}
<a class="nav-link px-3 d-block" href="{% url 'management' request.dealer.slug %}"> <span class="me-2 text-body align-bottom" data-feather="shield"></span>{{ _("Admin Managemnet") }}</a>
<a class="nav-link px-3 d-block"
href="{% url 'management' request.dealer.slug %}"> <span class="me-2 text-body align-bottom" data-feather="shield"></span>{{ _("Admin Managemnet") }}</a>
{% endif %}
</li>
<li class="nav-item">
<a class="nav-link px-3 d-block" href="{% url 'ticket_list' request.dealer.slug %}"> <span class="me-2 text-body align-bottom" data-feather="help-circle"></span>{{ _("Help Center") }}</a>
<a class="nav-link px-3 d-block"
href="{% url 'ticket_list' request.dealer.slug %}"> <span class="me-2 text-body align-bottom" data-feather="help-circle"></span>{{ _("Help Center") }}</a>
</li>
{% if request.is_staff %}
<li class="nav-item">
<a hx-boost="false" class="nav-link px-3 d-block" href="{% url 'schedule_calendar' request.dealer.slug%}"> <span class="me-2 text-body align-bottom" data-feather="calendar"></span>{{ _("My Calendar") }}</a>
<a hx-boost="false"
class="nav-link px-3 d-block"
href="{% url 'schedule_calendar' request.dealer.slug %}"> <span class="me-2 text-body align-bottom" data-feather="calendar"></span>{{ _("My Calendar") }}</a>
</li>
{% endif %}
<!--<li class="nav-item"><a class="nav-link px-3 d-block" href=""> Language</a></li>-->
@ -565,17 +648,20 @@
</ul>
</hr>
<div class="px-3">
<a class="btn btn-sm btn-phoenix-danger d-flex flex-center w-100" href="{% url 'account_logout' %}"> <span class="fas fa-power-off me-2"> </span>{% trans 'Sign Out' %}</a>
<a class="btn btn-sm btn-phoenix-danger d-flex flex-center w-100"
href="{% url 'account_logout' %}"> <span class="fas fa-power-off me-2"></span>{% trans 'Sign Out' %}</a>
</div>
<div class="my-2 text-center fw-bold fs-10 text-body-quaternary">
<a class="text-body-quaternary me-1" href="">{% trans 'Privacy policy' %}</a>&bull;<a class="text-body-quaternary mx-1" href="">{% trans 'Terms' %}</a>&bull;<a class="text-body-quaternary ms-1" href="">Cookies</a>
</div>
{% else %}
<div class="px-3">
<a class="btn btn-phoenix-succes d-flex flex-center w-100" href="{% url 'account_login' %}"> <span class="me-2" data-feather="log-in"> </span>{% trans 'Sign In' %}</a>
<a class="btn btn-phoenix-succes d-flex flex-center w-100"
href="{% url 'account_login' %}"> <span class="me-2" data-feather="log-in"></span>{% trans 'Sign In' %}</a>
</div>
<div class="px-3">
<a class="btn btn-phoenix-primary d-flex flex-center w-100" href="{% url 'account_signup' %}"> <span class="me-2" data-feather="user-plus"> </span>{% trans 'Sign Up' %}</a>
<a class="btn btn-phoenix-primary d-flex flex-center w-100"
href="{% url 'account_signup' %}"> <span class="me-2" data-feather="user-plus"></span>{% trans 'Sign Up' %}</a>
</div>
{% endif %}
</div>

View File

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

View File

@ -1,6 +1,7 @@
{% extends "base.html" %}
{% load i18n %}
{%block title%} {%trans 'Add Colors'%} {% endblock%}
{% block title %}
{% trans 'Add Colors' %} {% endblock %}
{% block content %}
<div class="row mt-4 mb-3">
<h3 class="text-center">{% trans "Add Colors" %}</h3>
@ -19,8 +20,8 @@
<input class="color-radio"
type="radio"
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"
style="background-color: rgb({{ color.rgb }})">
<div class="">
@ -40,7 +41,8 @@
<input class="color-radio"
type="radio"
name="interior"
value="{{ color.id }}" {% 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"
style="background-color: rgb({{ color.rgb }})">
<div class="">
@ -52,12 +54,12 @@
</div>
{% endfor %}
</div>
<div class="d-flex justify-content-center mt-4">
<button class="btn btn-lg btn-phoenix-primary me-2" type="submit">
<i class="fa-solid fa-floppy-disk me-1"></i>{{ _("Save") }}
</button>
<a href="{% url 'car_detail' request.dealer.slug car.slug %}" class="btn btn-lg btn-phoenix-secondary"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
<a href="{% url 'car_detail' request.dealer.slug car.slug %}"
class="btn btn-lg btn-phoenix-secondary"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
</div>
</form>
</div>

View File

@ -1,36 +1,29 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}Delete Car{% endblock %}
{% block content %}
<main class="d-flex align-items-center justify-content-center min-vh-50 py-5">
<div class="col-md-6 ">
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
<div class="card-body p-4 p-md-5 text-center bg-gradient">
<div class="mb-4">
<i class="fa-solid fa-triangle-exclamation text-danger" style="font-size: 2rem;"></i>
<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-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">
<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">
<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>

View File

@ -76,8 +76,6 @@
{% if perms.inventory.view_car %}
<div class="row-fluid {% if car.status == 'sold' %}disabled{% endif %}">
<div class="row g-3 justify-content-between">
<div class="col-md-6">
<div class="row mb-2">
<div class="col-md-4">
@ -87,20 +85,16 @@
alt="{{ car.vin }}" />
</div>
</div>
<div class="col-md-8">
<div class="card rounded shadow d-flex align-content-center
{% if car.get_transfer %}transfer{% endif %}">
<div class="card rounded shadow d-flex align-content-center {% if car.get_transfer %}transfer{% endif %}">
<p class="card-header rounded-top fw-bold">{% trans 'Financial Details' %}</p>
<div class="card-body">
<div class="table-responsive scrollbar mb-3">
<table class="table table-sm fs-9 mb-0 overflow-hidden">
{% if car.marked_price %}
<tr>
<th>{% trans "Cost Price"|capfirst %}</th>
<td>{{ car.cost_price|floatformat:2 }}</td>
</tr>
<tr>
<th>{% trans "Marked Price"|capfirst %}</th>
@ -134,7 +128,6 @@
<th>{% trans "Total"|capfirst %}</th>
<td>{{ car.finances.total_vat|floatformat:2 }}</td>
</tr> {% endcomment %}
<tr>
<td colspan="2">
{% if not car.get_transfer %}
@ -145,27 +138,19 @@
{% endif %}
</td>
</tr>
{% else %}
<p>{% trans "No finance details available." %}</p>
<a href="{% url 'car_finance_update' request.dealer.slug car.slug %}"
class="btn btn-phoenix-success btn-sm mb-3">{% trans "Add" %}</a>
{% endif %}
</table>
</div>
</div>
</div>
</div>
</div>
{% if perms.inventory.view_carcolors %}
<div class="card rounded shadow d-flex align-content-center mt-3
{% if car.get_transfer %}transfer{% endif %}">
<div class="card rounded shadow d-flex align-content-center mt-3 {% if car.get_transfer %}transfer{% endif %}">
<p class="card-header rounded-top fw-bold">{% trans 'Colors Details' %}</p>
<div class="card-body">
<div class="table-responsive scrollbar mb-3">
@ -223,7 +208,6 @@
</div>
</div>
{% endif %}
{% if car.status != 'transfer' %}
{% if perms.inventory.view_carreservation %}
<div class="card rounded shadow d-flex align-content-center mt-3 h-full w-100">
@ -293,9 +277,6 @@
</div>
{% endif %}
{% endif %}
<!-- Transfer Table -->
{% if car.status == 'transfer' and car.get_transfer %}
<div class="card rounded shadow d-flex align-content-center mt-3">
@ -346,10 +327,8 @@
</div>
{% endif %}
</div>
<div class="col-md-6">
<div class="card mb-3 rounded shadow d-flex align-content-center
{% if car.get_transfer %}disabled{% endif %}">
<div class="card mb-3 rounded shadow d-flex align-content-center {% if car.get_transfer %}disabled{% endif %}">
<p class="card-header rounded-top fw-bold">{% trans 'Car Details' %}</p>
<div class="card-body">
<div class="table-responsive scrollbar mb-3">
@ -516,9 +495,6 @@
</div>
</div>
</div>
{% if car.status == 'sold' %}
<img class="car_status"
src="{% static 'images/sold.png' %}"
@ -527,7 +503,6 @@
alt="">
{% endif %}
</div>
<!-- Custom Card Modal -->
<div class="modal fade"
id="customCardModal"
@ -632,7 +607,6 @@
</div>
{% endif %}
{% endblock %}
{% block customJS %}
<script>
document.addEventListener("DOMContentLoaded", function () {

View File

@ -23,7 +23,10 @@
<h4 class="alert-heading small">{% trans "Please correct the following errors:" %}</h4>
<ul class="mb-0">
{% for field, errors in form.errors.items %}
<li><strong>{{ field|capfirst }}:</strong> {% for error in errors %}{{ error }}{% endfor %}</li>
<li>
<strong>{{ field|capfirst }}:</strong>
{% for error in errors %}{{ error }}{% endfor %}
</li>
{% endfor %}
</ul>
</div>
@ -41,7 +44,8 @@
<i class="fa-solid fa-floppy-disk me-1"></i>
{{ _("Save") }}
</button>
<a href="{% url 'car_detail' request.dealer.slug car.slug %}" class="btn btn-phoenix-secondary btn-lg">
<a href="{% url 'car_detail' request.dealer.slug car.slug %}"
class="btn btn-phoenix-secondary btn-lg">
<i class="fa-solid fa-ban me-1"></i>
{% trans "Cancel" %}
</a>

View File

@ -1,7 +1,8 @@
{% extends "base.html" %}
{% load i18n static custom_filters %}
{% block title %}
{% trans 'Add New Car' %} {% endblock %}
{% trans 'Add New Car' %}
{% endblock %}
{% block content %}
<style>
#video {
@ -23,7 +24,8 @@
{% include "empty-illustration-page.html" with value="Vendor" url=create_vendor_url %}
{% endif %}
<!---->
<div class="row justify-content-center mt-5 mb-3 {% if not vendor_exists %}d-none{% endif %}" hx-boost="false">
<div class="row justify-content-center mt-5 mb-3 {% if not vendor_exists %}d-none{% endif %}"
hx-boost="false">
<div class="col-lg-8 col-md-10">
<div class="card shadow-sm border-0 rounded-3">
<div class="card-header bg-gray-200 py-3 border-0 rounded-top-3">
@ -132,9 +134,7 @@
id="specification-btn"
data-bs-toggle="modal"
data-bs-target="#specificationsModal"
disabled>
{% trans 'specifications'|capfirst %}
</button>
disabled>{% trans 'specifications'|capfirst %}</button>
<button type="button"
class="btn btn-phoenix-warning"
id="options-btn"

View File

@ -1,7 +1,8 @@
{% extends "base.html" %}
{% load static i18n custom_filters humanize %}
{% block title %}
{% trans 'Inventory' %} {% endblock %}
{% trans 'Inventory' %}
{% endblock %}
{% block customCSS %}
<style>
.htmx-indicator {
@ -188,12 +189,10 @@
hx-on::before-request="on_before_request()"
hx-on::after-request="on_after_request()"></div>
<div class="w-100 list table-responsive">
{% for car in cars %}
<div class="card border mb-3 py-0 px-0" id="project-list-table-body">
<div class="card-body">
<div class="row align-items-center">
<!-- Vehicle Image/Icon -->
<div class="col-auto">
<div class="avatar avatar-3xl">
@ -302,12 +301,8 @@
</div>
</div>
</div>
{% else %}
{% url "car_add" request.dealer.slug as create_car_url %}
{% include "empty-illustration-page.html" with value="car" url=create_car_url %}
{% endif %}
{% endblock %}

View File

@ -4,7 +4,6 @@
{% trans "Inventory Stats"|capfirst %}
{% endblock %}
{% block content %}
{% block customCSS %}
<style>
.road {
@ -40,11 +39,9 @@
<div class="row justify-content-between">
<div class="col-sm-12 ">
<div class="card border h-100 w-100 p-lg-10" style="">
<div class="road">
<p class="moving-tenhal ">&nbsp;&nbsp;&nbsp;&nbsp;{% trans "Powered By Tenhal" %}&nbsp;&nbsp;&nbsp;&nbsp;</p>
</div>
<div class="bg-holder bg-card"
style="background-image:url({% static 'images/spot-illustrations/32.png' %});
background-position: top right"></div>
@ -130,17 +127,13 @@
{% endif %}
</div>
</div>
{% else %}
{% url "car_add" request.dealer.slug as create_car_url %}
{% include "empty-illustration-page.html" with value="car" url=create_car_url %}
{% endif %}
{% endblock %}
{% block customJS %}
<script>
document.addEventListener('DOMContentLoaded', () => {
const car = document.getElementById('car');
let positionX = 10;
@ -160,5 +153,4 @@ document.addEventListener('DOMContentLoaded', () => {
});
});
</script>
{% endblock %}

View File

@ -19,7 +19,6 @@
<form method="post" action="" class="needs-validation" novalidate>
{% csrf_token %}
{{ form|crispy }}
<hr class="my-4">
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
<button class="btn btn-phoenix-primary btn-lg me-md-2" type="submit">

View File

@ -4,12 +4,12 @@
{{ _("Expenses") }}
{% endblock title %}
{% block content %}
{% if expenses or request.GET.q %}
<div class="row mt-4">
<div class="d-flex justify-content-between mb-2">
<h3 class="">{% trans "Expenses" %} <span class="fas fa-money-bill-wave ms-2 text-primary"></span></h3>
<h3 class="">
{% trans "Expenses" %} <span class="fas fa-money-bill-wave ms-2 text-primary"></span>
</h3>
{% if perms.django_ledger.add_itemmodel %}
<a href="{% url 'item_expense_create' request.dealer.slug %}"
class="btn btn-md btn-phoenix-primary"><i class="fa fa-plus me-2"></i>{% trans "Add Expense" %}</a>
@ -35,7 +35,6 @@
<td class="align-middle product white-space-nowrap">{{ expense.uom }}</td>
<td class="align-middle product white-space-nowrap">
{% if perms.django_ledger.change_itemmodel %}
<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"
@ -47,8 +46,8 @@
<span class="fas fa-ellipsis-h fs-10"></span>
</button>
<div class="dropdown-menu dropdown-menu-end py-2">
<a class="dropdown-item" href="{% url 'item_expense_update' request.dealer.slug expense.pk %}" >
<a class="dropdown-item"
href="{% url 'item_expense_update' request.dealer.slug expense.pk %}">
<i class="fa fa-edit me-2"></i>{% trans "Update" %}
</a>
<a class="text-danger dropdown-item" href="#">
@ -58,7 +57,6 @@
</div>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
@ -75,7 +73,6 @@
{% endif %}
{% endif %}
</div>
{% else %}
{% url "item_expense_create" request.dealer.slug as create_expense_url %}
{% include "empty-illustration-page.html" with value="expense" url=create_expense_url %}

View File

@ -29,25 +29,27 @@
<form method="post" action="" class="needs-validation" novalidate>
{% csrf_token %}
{{ form|crispy }}
{% if form.errors %}
<div class="alert alert-danger mt-4" role="alert">
<h4 class="alert-heading small">{% trans "Please correct the following errors:" %}</h4>
<ul class="mb-0">
{% for field, errors in form.errors.items %}
<li><strong>{{ field|capfirst }}:</strong> {% for error in errors %}{{ error }}{% endfor %}</li>
<li>
<strong>{{ field|capfirst }}:</strong>
{% for error in errors %}{{ error }}{% endfor %}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
<hr class="my-4">
<div class="d-grid gap-2 d-md-flex justify-content-md-center 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 'item_service_list' request.dealer.slug %}" class="btn btn-phoenix-secondary btn-lg">
<a href="{% url 'item_service_list' request.dealer.slug %}"
class="btn btn-phoenix-secondary btn-lg">
<i class="fa-solid fa-ban me-1"></i>
{% trans "Cancel" %}
</a>

View File

@ -4,11 +4,12 @@
{{ _("Services") }}
{% endblock title %}
{% block content %}
{% if services or request.GET.q %}
<div class="row mt-4">
<div class="d-flex justify-content-between mb-2">
<h3 class="">{% trans "Services" %}<span class="fas fa-tools text-primary ms-2"></span></h3>
<h3 class="">
{% trans "Services" %}<span class="fas fa-tools text-primary ms-2"></span>
</h3>
{% if perms.inventory.add_additionalservices %}
<a href="{% url 'item_service_create' request.dealer.slug %}"
class="btn btn-md btn-phoenix-primary"><i class="fa fa-plus me-2"></i>{% trans "Add Service" %}</a>
@ -38,8 +39,6 @@
<td class="align-middle product white-space-nowrap">{{ service.item.co }}</td>
<td class="align-middle white-space-nowrap text-start">
{% if perms.inventory.add_additionalservices %}
<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"
@ -51,8 +50,8 @@
<span class="fas fa-ellipsis-h fs-10"></span>
</button>
<div class="dropdown-menu dropdown-menu-end py-2">
<a class="dropdown-item" href="{% url 'item_service_update' request.dealer.slug service.pk %}" >
<a class="dropdown-item"
href="{% url 'item_service_update' request.dealer.slug service.pk %}">
<i class="fa fa-edit me-2"></i>{% trans "Update" %}
</a>
<a class="text-danger dropdown-item" href="#">
@ -78,10 +77,8 @@
{% endif %}
{% endif %}
</div>
{% else %}
{% url 'item_service_create' request.dealer.slug as create_services_url %}
{% include "empty-illustration-page.html" with value="service" url=create_services_url %}
{% endif %}
{% endblock %}

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