Compare commits
4 Commits
70807da5fa
...
5ff9b4c573
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ff9b4c573 | |||
| 47a12e2965 | |||
| 543d8702f3 | |||
| f091254d98 |
@ -38,7 +38,7 @@ admin.site.register(models.DealerSettings)
|
||||
# admin.site.register(models.SaleQuotationCar)
|
||||
admin.site.register(models.SaleOrder)
|
||||
admin.site.register(models.CustomGroup)
|
||||
admin.site.register(models.CarFinance)
|
||||
# admin.site.register(models.CarFinance)
|
||||
admin.site.register(models.CarColors)
|
||||
admin.site.register(models.CarRegistration)
|
||||
admin.site.register(models.CustomCard)
|
||||
|
||||
@ -32,7 +32,7 @@ from .models import (
|
||||
Car,
|
||||
VatRate,
|
||||
CarTransfer,
|
||||
CarFinance,
|
||||
# CarFinance,
|
||||
CustomCard,
|
||||
CarRegistration,
|
||||
CarColors,
|
||||
@ -444,7 +444,7 @@ class CarFinanceForm(forms.ModelForm):
|
||||
return cleaned_data
|
||||
|
||||
class Meta:
|
||||
model = CarFinance
|
||||
model = Car
|
||||
fields = ["cost_price","marked_price"]
|
||||
|
||||
|
||||
|
||||
@ -603,7 +603,6 @@ class AdditionalServices(models.Model, LocalizedNameMixin):
|
||||
|
||||
|
||||
class Car(Base):
|
||||
|
||||
item_model = models.OneToOneField(
|
||||
ItemModel,
|
||||
models.DO_NOTHING,
|
||||
@ -668,6 +667,32 @@ class Car(Base):
|
||||
default=CarStockTypeChoices.NEW,
|
||||
verbose_name=_("Stock Type"),
|
||||
)
|
||||
#
|
||||
additional_services = models.ManyToManyField(
|
||||
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")
|
||||
)
|
||||
selling_price = models.DecimalField(
|
||||
max_digits=14,
|
||||
decimal_places=2,
|
||||
verbose_name=_("Selling Price"),
|
||||
default=Decimal("0.00"),
|
||||
)
|
||||
marked_price = models.DecimalField(
|
||||
max_digits=14,
|
||||
decimal_places=2,
|
||||
verbose_name=_("Marked Price"),
|
||||
default=Decimal("0.00"),
|
||||
)
|
||||
discount_amount = models.DecimalField(
|
||||
max_digits=14,
|
||||
decimal_places=2,
|
||||
verbose_name=_("Discount Amount"),
|
||||
default=Decimal("0.00"),
|
||||
)
|
||||
#
|
||||
remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks"))
|
||||
mileage = models.IntegerField(blank=True, null=True, verbose_name=_("Mileage"))
|
||||
receiving_date = models.DateTimeField(verbose_name=_("Receiving Date"))
|
||||
@ -734,20 +759,16 @@ class Car(Base):
|
||||
@property
|
||||
def logo(self):
|
||||
return getattr(self.id_car_make, "logo", "")
|
||||
@property
|
||||
def additional_services(self):
|
||||
return self.finances.additional_services.all()
|
||||
@property
|
||||
def total_additional_services(self):
|
||||
return sum([service.price_ for service in self.additional_services])
|
||||
# @property
|
||||
# def additional_services(self):
|
||||
# return self.additional_services.all()
|
||||
@property
|
||||
def ready(self):
|
||||
try:
|
||||
return all(
|
||||
[
|
||||
self.colors,
|
||||
self.finances,
|
||||
self.finances.marked_price > 0,
|
||||
self.marked_price > 0,
|
||||
]
|
||||
)
|
||||
except Exception:
|
||||
@ -838,10 +859,51 @@ 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()])
|
||||
|
||||
def get_additional_services(self):
|
||||
return {"services": [x for x in self.additional_services.all()],"total":self.get_additional_services_amount}
|
||||
|
||||
@property
|
||||
def vat_amount(self):
|
||||
vat = VatRate.objects.filter(dealer=self.dealer,is_active=True).first()
|
||||
return Decimal(self.marked_price) * (vat.rate / 100)
|
||||
|
||||
@property
|
||||
def total_vat(self):
|
||||
return Decimal(self.marked_price) + Decimal(self.vat_amount)
|
||||
# def get_discount_amount(self,estimate,user):
|
||||
# try:
|
||||
# instance = models.ExtraInfo.objects.get(
|
||||
# dealer=self.dealer,
|
||||
# content_object=estimate,
|
||||
# related_object=user
|
||||
# )
|
||||
# if instance:
|
||||
# return instance.data.get("discount",0)
|
||||
# return 0
|
||||
# except Exception:
|
||||
# print("Error getting discount amount")
|
||||
# return 0
|
||||
# @property
|
||||
# def total_discount(self):
|
||||
# if self.discount_amount > 0:
|
||||
# return self.marked_price - self.discount_amount
|
||||
# return self.marked_price
|
||||
|
||||
# @property
|
||||
# def total_vat(self):
|
||||
# return round(self.total_discount + self.vat_amount + self.total_additionals, 2)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class CarTransfer(models.Model):
|
||||
@ -880,7 +942,7 @@ class CarTransfer(models.Model):
|
||||
|
||||
@property
|
||||
def total_price(self):
|
||||
return self.quantity * self.car.finances.total_vat
|
||||
return self.quantity * self.car.total_vat # TODO : check later
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Car Transfer Log")
|
||||
@ -924,105 +986,105 @@ class CarReservation(models.Model):
|
||||
|
||||
|
||||
# Car Finance Model
|
||||
class CarFinance(models.Model):
|
||||
additional_services = models.ManyToManyField(
|
||||
AdditionalServices, related_name="additional_finances", blank=True
|
||||
)
|
||||
car = models.OneToOneField(Car, on_delete=models.CASCADE, related_name="finances")
|
||||
cost_price = models.DecimalField(
|
||||
max_digits=14, decimal_places=2, verbose_name=_("Cost Price")
|
||||
)
|
||||
selling_price = models.DecimalField(
|
||||
max_digits=14,
|
||||
decimal_places=2,
|
||||
verbose_name=_("Selling Price"),
|
||||
default=Decimal("0.00"),
|
||||
)
|
||||
marked_price = models.DecimalField(
|
||||
max_digits=14,
|
||||
decimal_places=2,
|
||||
verbose_name=_("Marked Price"),
|
||||
default=Decimal("0.00"),
|
||||
)
|
||||
discount_amount = models.DecimalField(
|
||||
max_digits=14,
|
||||
decimal_places=2,
|
||||
verbose_name=_("Discount Amount"),
|
||||
default=Decimal("0.00"),
|
||||
)
|
||||
# is_sold = models.BooleanField(default=False)
|
||||
# class CarFinance(models.Model):
|
||||
# additional_services = models.ManyToManyField(
|
||||
# AdditionalServices, related_name="additional_finances", blank=True
|
||||
# )
|
||||
# car = models.OneToOneField(Car, on_delete=models.CASCADE, related_name="finances")
|
||||
# cost_price = models.DecimalField(
|
||||
# max_digits=14, decimal_places=2, verbose_name=_("Cost Price")
|
||||
# )
|
||||
# selling_price = models.DecimalField(
|
||||
# max_digits=14,
|
||||
# decimal_places=2,
|
||||
# verbose_name=_("Selling Price"),
|
||||
# default=Decimal("0.00"),
|
||||
# )
|
||||
# marked_price = models.DecimalField(
|
||||
# max_digits=14,
|
||||
# decimal_places=2,
|
||||
# verbose_name=_("Marked Price"),
|
||||
# default=Decimal("0.00"),
|
||||
# )
|
||||
# discount_amount = models.DecimalField(
|
||||
# max_digits=14,
|
||||
# decimal_places=2,
|
||||
# verbose_name=_("Discount Amount"),
|
||||
# default=Decimal("0.00"),
|
||||
# )
|
||||
# # is_sold = models.BooleanField(default=False)
|
||||
|
||||
|
||||
@property
|
||||
def total(self):
|
||||
return self.marked_price
|
||||
# @property
|
||||
# def total(self):
|
||||
# return self.marked_price
|
||||
|
||||
@property
|
||||
def total_additionals_no_vat(self):
|
||||
return sum(x.price for x in self.additional_services.all())
|
||||
# @property
|
||||
# def total_additionals_no_vat(self):
|
||||
# return sum(x.price for x in self.additional_services.all())
|
||||
|
||||
@property
|
||||
def total_additionals(self):
|
||||
return sum(x.price_ for x in self.additional_services.all())
|
||||
# @property
|
||||
# def total_additionals(self):
|
||||
# return sum(x.price_ for x in self.additional_services.all())
|
||||
|
||||
@property
|
||||
def total_discount(self):
|
||||
if self.discount_amount > 0:
|
||||
return self.marked_price - self.discount_amount
|
||||
return self.marked_price
|
||||
# @property
|
||||
# def total_discount(self):
|
||||
# if self.discount_amount > 0:
|
||||
# return self.marked_price - self.discount_amount
|
||||
# return self.marked_price
|
||||
|
||||
@property
|
||||
def total_vat(self):
|
||||
return round(self.total_discount + self.vat_amount + self.total_additionals, 2)
|
||||
# @property
|
||||
# def total_vat(self):
|
||||
# return round(self.total_discount + self.vat_amount + self.total_additionals, 2)
|
||||
|
||||
@property
|
||||
def vat_amount(self):
|
||||
vat = VatRate.objects.filter(dealer=self.car.dealer, is_active=True).first()
|
||||
if vat:
|
||||
return (self.total_discount * Decimal(vat.rate)).quantize(Decimal("0.01"))
|
||||
return Decimal("0.00")
|
||||
# @property
|
||||
# def vat_amount(self):
|
||||
# vat = VatRate.objects.filter(dealer=self.car.dealer, is_active=True).first()
|
||||
# if vat:
|
||||
# return (self.total_discount * Decimal(vat.rate)).quantize(Decimal("0.01"))
|
||||
# return Decimal("0.00")
|
||||
|
||||
@property
|
||||
def revenue(self):
|
||||
return self.marked_price - self.cost_price
|
||||
# @property
|
||||
# def revenue(self):
|
||||
# return self.marked_price - self.cost_price
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"cost_price": str(self.cost_price),
|
||||
"selling_price": str(self.selling_price),
|
||||
"marked_price": str(self.marked_price),
|
||||
"discount_amount": str(self.discount_amount),
|
||||
"total": str(self.total),
|
||||
"total_discount": str(self.total_discount),
|
||||
"total_vat": str(self.total_vat),
|
||||
"vat_amount": str(self.vat_amount),
|
||||
}
|
||||
# def to_dict(self):
|
||||
# return {
|
||||
# "cost_price": str(self.cost_price),
|
||||
# "selling_price": str(self.selling_price),
|
||||
# "marked_price": str(self.marked_price),
|
||||
# "discount_amount": str(self.discount_amount),
|
||||
# "total": str(self.total),
|
||||
# "total_discount": str(self.total_discount),
|
||||
# "total_vat": str(self.total_vat),
|
||||
# "vat_amount": str(self.vat_amount),
|
||||
# }
|
||||
|
||||
def __str__(self):
|
||||
return f"Car: {self.car}, Marked Price: {self.marked_price}"
|
||||
# def __str__(self):
|
||||
# return f"Car: {self.car}, Marked Price: {self.marked_price}"
|
||||
|
||||
# def save(self, *args, **kwargs):
|
||||
# self.full_clean()
|
||||
# try:
|
||||
# price_after_discount = self.selling_price - self.discount_amount
|
||||
# self.profit_margin = price_after_discount - self.cost_price
|
||||
# self.vat_amount = settings.VAT_RATE
|
||||
# except InvalidOperation as e:
|
||||
# raise ValidationError(_("Invalid decimal operation: %s") % str(e))
|
||||
# super().save(*args, **kwargs)
|
||||
# # def save(self, *args, **kwargs):
|
||||
# # self.full_clean()
|
||||
# # try:
|
||||
# # price_after_discount = self.selling_price - self.discount_amount
|
||||
# # self.profit_margin = price_after_discount - self.cost_price
|
||||
# # self.vat_amount = settings.VAT_RATE
|
||||
# # except InvalidOperation as e:
|
||||
# # raise ValidationError(_("Invalid decimal operation: %s") % str(e))
|
||||
# # super().save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Car Financial Details")
|
||||
verbose_name_plural = _("Car Financial Details")
|
||||
indexes = [
|
||||
models.Index(fields=["car"], name="car_finance_car_idx"),
|
||||
models.Index(fields=["cost_price"], name="car_finance_cost_price_idx"),
|
||||
models.Index(
|
||||
fields=["selling_price"], name="car_finance_selling_price_idx"
|
||||
),
|
||||
models.Index(fields=["marked_price"], name="car_finance_marked_price_idx"),
|
||||
models.Index(fields=["discount_amount"], name="car_finance_discount_idx"),
|
||||
]
|
||||
# class Meta:
|
||||
# verbose_name = _("Car Financial Details")
|
||||
# verbose_name_plural = _("Car Financial Details")
|
||||
# indexes = [
|
||||
# models.Index(fields=["car"], name="car_finance_car_idx"),
|
||||
# models.Index(fields=["cost_price"], name="car_finance_cost_price_idx"),
|
||||
# models.Index(
|
||||
# fields=["selling_price"], name="car_finance_selling_price_idx"
|
||||
# ),
|
||||
# models.Index(fields=["marked_price"], name="car_finance_marked_price_idx"),
|
||||
# models.Index(fields=["discount_amount"], name="car_finance_discount_idx"),
|
||||
# ]
|
||||
|
||||
|
||||
class ExteriorColors(models.Model, LocalizedNameMixin):
|
||||
@ -2875,7 +2937,7 @@ class SaleOrder(models.Model):
|
||||
|
||||
@property
|
||||
def price(self):
|
||||
return self.car.finances.marked_price
|
||||
return self.car.marked_price
|
||||
|
||||
@property
|
||||
def items(self):
|
||||
|
||||
@ -270,6 +270,9 @@ def create_item_model(sender, instance, created, **kwargs):
|
||||
)
|
||||
instance.item_model = inventory
|
||||
inventory.save()
|
||||
else:
|
||||
instance.item_model.default_amount = instance.marked_price
|
||||
|
||||
# inventory = entity.create_item_inventory(
|
||||
# name=instance.vin,
|
||||
# uom_model=uom,
|
||||
@ -284,108 +287,108 @@ def create_item_model(sender, instance, created, **kwargs):
|
||||
|
||||
|
||||
# # update price - CarFinance
|
||||
@receiver(post_save, sender=models.CarFinance)
|
||||
def update_item_model_cost(sender, instance, created, **kwargs):
|
||||
"""
|
||||
Signal handler for updating an inventory item's cost and additional information
|
||||
when a CarFinance instance is saved. This function updates the corresponding
|
||||
inventory item of the car dealer's entity associated with the car's VIN by
|
||||
modifying its default amount and updating additional data fields.
|
||||
# @receiver(post_save, sender=models.CarFinance)
|
||||
# def update_item_model_cost(sender, instance, created, **kwargs):
|
||||
# """
|
||||
# Signal handler for updating an inventory item's cost and additional information
|
||||
# when a CarFinance instance is saved. This function updates the corresponding
|
||||
# inventory item of the car dealer's entity associated with the car's VIN by
|
||||
# modifying its default amount and updating additional data fields.
|
||||
|
||||
:param sender: The model class that triggered the signal.
|
||||
:param instance: The instance of the CarFinance that was saved.
|
||||
:param created: A boolean indicating whether the model instance was newly created.
|
||||
:param kwargs: Additional keyword arguments passed during the signal invocation.
|
||||
:return: None
|
||||
"""
|
||||
# if created and not instance.is_sold:
|
||||
# if created:
|
||||
# entity = instance.car.dealer.entity
|
||||
# coa = entity.get_default_coa()
|
||||
# inventory_account = (
|
||||
# entity.get_all_accounts()
|
||||
# .filter(name=f"Inventory:{instance.car.id_car_make.name}")
|
||||
# .first()
|
||||
# )
|
||||
# if not inventory_account:
|
||||
# inventory_account = create_make_accounts(
|
||||
# entity,
|
||||
# coa,
|
||||
# [instance.car.id_car_make],
|
||||
# "Inventory",
|
||||
# roles.ASSET_CA_INVENTORY,
|
||||
# "debit",
|
||||
# )
|
||||
# :param sender: The model class that triggered the signal.
|
||||
# :param instance: The instance of the CarFinance that was saved.
|
||||
# :param created: A boolean indicating whether the model instance was newly created.
|
||||
# :param kwargs: Additional keyword arguments passed during the signal invocation.
|
||||
# :return: None
|
||||
# """
|
||||
# # if created and not instance.is_sold:
|
||||
# # if created:
|
||||
# # entity = instance.car.dealer.entity
|
||||
# # coa = entity.get_default_coa()
|
||||
# # inventory_account = (
|
||||
# # entity.get_all_accounts()
|
||||
# # .filter(name=f"Inventory:{instance.car.id_car_make.name}")
|
||||
# # .first()
|
||||
# # )
|
||||
# # if not inventory_account:
|
||||
# # inventory_account = create_make_accounts(
|
||||
# # entity,
|
||||
# # coa,
|
||||
# # [instance.car.id_car_make],
|
||||
# # "Inventory",
|
||||
# # roles.ASSET_CA_INVENTORY,
|
||||
# # "debit",
|
||||
# # )
|
||||
|
||||
# cogs = (
|
||||
# entity.get_all_accounts()
|
||||
# .filter(name=f"Cogs:{instance.car.id_car_make.name}")
|
||||
# .first()
|
||||
# )
|
||||
# if not cogs:
|
||||
# cogs = create_make_accounts(
|
||||
# entity, coa, [instance.car.id_car_make], "Cogs", roles.COGS, "debit"
|
||||
# )
|
||||
# revenue = (
|
||||
# entity.get_all_accounts()
|
||||
# .filter(name=f"Revenue:{instance.car.id_car_make.name}")
|
||||
# .first()
|
||||
# )
|
||||
# if not revenue:
|
||||
# revenue = create_make_accounts(
|
||||
# entity,
|
||||
# coa,
|
||||
# [instance.car.id_car_make],
|
||||
# "Revenue",
|
||||
# roles.ASSET_CA_RECEIVABLES,
|
||||
# "credit",
|
||||
# )
|
||||
# # cogs = (
|
||||
# # entity.get_all_accounts()
|
||||
# # .filter(name=f"Cogs:{instance.car.id_car_make.name}")
|
||||
# # .first()
|
||||
# # )
|
||||
# # if not cogs:
|
||||
# # cogs = create_make_accounts(
|
||||
# # entity, coa, [instance.car.id_car_make], "Cogs", roles.COGS, "debit"
|
||||
# # )
|
||||
# # revenue = (
|
||||
# # entity.get_all_accounts()
|
||||
# # .filter(name=f"Revenue:{instance.car.id_car_make.name}")
|
||||
# # .first()
|
||||
# # )
|
||||
# # if not revenue:
|
||||
# # revenue = create_make_accounts(
|
||||
# # entity,
|
||||
# # coa,
|
||||
# # [instance.car.id_car_make],
|
||||
# # "Revenue",
|
||||
# # roles.ASSET_CA_RECEIVABLES,
|
||||
# # "credit",
|
||||
# # )
|
||||
|
||||
# cash_account = (
|
||||
# # entity.get_all_accounts()
|
||||
# # .filter(name="Cash", role=roles.ASSET_CA_CASH)
|
||||
# # .first()
|
||||
# entity.get_all_accounts()
|
||||
# .filter(role=roles.ASSET_CA_CASH, role_default=True)
|
||||
# .first()
|
||||
# )
|
||||
# # cash_account = (
|
||||
# # # entity.get_all_accounts()
|
||||
# # # .filter(name="Cash", role=roles.ASSET_CA_CASH)
|
||||
# # # .first()
|
||||
# # entity.get_all_accounts()
|
||||
# # .filter(role=roles.ASSET_CA_CASH, role_default=True)
|
||||
# # .first()
|
||||
# # )
|
||||
|
||||
# ledger = LedgerModel.objects.create(
|
||||
# entity=entity, name=f"Inventory Purchase - {instance.car}"
|
||||
# )
|
||||
# je = JournalEntryModel.objects.create(
|
||||
# ledger=ledger,
|
||||
# description=f"Acquired {instance.car} for inventory",
|
||||
# )
|
||||
# TransactionModel.objects.create(
|
||||
# journal_entry=je,
|
||||
# account=inventory_account,
|
||||
# amount=Decimal(instance.cost_price),
|
||||
# tx_type="debit",
|
||||
# description="",
|
||||
# )
|
||||
# # ledger = LedgerModel.objects.create(
|
||||
# # entity=entity, name=f"Inventory Purchase - {instance.car}"
|
||||
# # )
|
||||
# # je = JournalEntryModel.objects.create(
|
||||
# # ledger=ledger,
|
||||
# # description=f"Acquired {instance.car} for inventory",
|
||||
# # )
|
||||
# # TransactionModel.objects.create(
|
||||
# # journal_entry=je,
|
||||
# # account=inventory_account,
|
||||
# # amount=Decimal(instance.cost_price),
|
||||
# # tx_type="debit",
|
||||
# # description="",
|
||||
# # )
|
||||
|
||||
# TransactionModel.objects.create(
|
||||
# journal_entry=je,
|
||||
# account=cash_account,
|
||||
# amount=Decimal(instance.cost_price),
|
||||
# tx_type="credit",
|
||||
# description="",
|
||||
# )
|
||||
# # TransactionModel.objects.create(
|
||||
# # journal_entry=je,
|
||||
# # account=cash_account,
|
||||
# # amount=Decimal(instance.cost_price),
|
||||
# # tx_type="credit",
|
||||
# # description="",
|
||||
# # )
|
||||
|
||||
instance.car.item_model.default_amount = instance.marked_price
|
||||
# if not isinstance(instance.car.item_model.additional_info, dict):
|
||||
# instance.car.item_model.additional_info = {}
|
||||
# instance.car.item_model.additional_info.update({"car_finance": instance.to_dict()})
|
||||
# instance.car.item_model.additional_info.update(
|
||||
# {
|
||||
# "additional_services": [
|
||||
# service.to_dict() for service in instance.additional_services.all()
|
||||
# ]
|
||||
# }
|
||||
# )
|
||||
instance.car.item_model.save()
|
||||
print(f"Inventory item updated with CarFinance data for Car: {instance.car}")
|
||||
# instance.car.item_model.default_amount = instance.marked_price
|
||||
# # if not isinstance(instance.car.item_model.additional_info, dict):
|
||||
# # instance.car.item_model.additional_info = {}
|
||||
# # instance.car.item_model.additional_info.update({"car_finance": instance.to_dict()})
|
||||
# # instance.car.item_model.additional_info.update(
|
||||
# # {
|
||||
# # "additional_services": [
|
||||
# # service.to_dict() for service in instance.additional_services.all()
|
||||
# # ]
|
||||
# # }
|
||||
# # )
|
||||
# instance.car.item_model.save()
|
||||
# print(f"Inventory item updated with CarFinance data for Car: {instance.car}")
|
||||
|
||||
|
||||
# @receiver(pre_save, sender=models.SaleQuotation)
|
||||
@ -803,144 +806,144 @@ def create_dealer_settings(sender, instance, created, **kwargs):
|
||||
# )
|
||||
|
||||
|
||||
def save_journal(car_finance, ledger, vendor):
|
||||
"""
|
||||
Saves a journal entry pertaining to a car finance transaction for a specific ledger and vendor.
|
||||
# def save_journal(car_finance, ledger, vendor):
|
||||
# """
|
||||
# Saves a journal entry pertaining to a car finance transaction for a specific ledger and vendor.
|
||||
|
||||
This function ensures that relevant accounts are updated to record financial transactions. It handles
|
||||
debiting of the inventory account and crediting of the vendor account to maintain accurate bookkeeping.
|
||||
Additionally, it creates vendor accounts dynamically if required and ties the created journal entry to
|
||||
the ledger passed as a parameter. All transactions adhere to the ledger's entity-specific Chart of
|
||||
Accounts (COA) configuration.
|
||||
# This function ensures that relevant accounts are updated to record financial transactions. It handles
|
||||
# debiting of the inventory account and crediting of the vendor account to maintain accurate bookkeeping.
|
||||
# Additionally, it creates vendor accounts dynamically if required and ties the created journal entry to
|
||||
# the ledger passed as a parameter. All transactions adhere to the ledger's entity-specific Chart of
|
||||
# Accounts (COA) configuration.
|
||||
|
||||
:param car_finance: Instance of the car finance object containing details about the financed car
|
||||
and its associated costs.
|
||||
:type car_finance: `CarFinance` object
|
||||
:param ledger: Ledger instance to which the journal entry is tied. This ledger must provide
|
||||
entity-specific details, including its COA and related accounts.
|
||||
:type ledger: `Ledger` object
|
||||
:param vendor: Vendor instance representing the supplier or vendor related to the car finance
|
||||
transaction. This vendor is used to derive or create the vendor account in COA.
|
||||
:type vendor: `Vendor` object
|
||||
# :param car_finance: Instance of the car finance object containing details about the financed car
|
||||
# and its associated costs.
|
||||
# :type car_finance: `CarFinance` object
|
||||
# :param ledger: Ledger instance to which the journal entry is tied. This ledger must provide
|
||||
# entity-specific details, including its COA and related accounts.
|
||||
# :type ledger: `Ledger` object
|
||||
# :param vendor: Vendor instance representing the supplier or vendor related to the car finance
|
||||
# transaction. This vendor is used to derive or create the vendor account in COA.
|
||||
# :type vendor: `Vendor` object
|
||||
|
||||
:return: None
|
||||
"""
|
||||
entity = ledger.entity
|
||||
coa = entity.get_default_coa()
|
||||
journal = JournalEntryModel.objects.create(
|
||||
posted=False,
|
||||
description=f"Finances of Car:{car_finance.car.vin} for Vendor:{car_finance.car.vendor.name}",
|
||||
ledger=ledger,
|
||||
locked=False,
|
||||
origin="Payment",
|
||||
)
|
||||
ledger.additional_info["je_number"] = journal.je_number
|
||||
ledger.save()
|
||||
# :return: None
|
||||
# """
|
||||
# entity = ledger.entity
|
||||
# coa = entity.get_default_coa()
|
||||
# journal = JournalEntryModel.objects.create(
|
||||
# posted=False,
|
||||
# description=f"Finances of Car:{car_finance.car.vin} for Vendor:{car_finance.car.vendor.name}",
|
||||
# ledger=ledger,
|
||||
# locked=False,
|
||||
# origin="Payment",
|
||||
# )
|
||||
# ledger.additional_info["je_number"] = journal.je_number
|
||||
# ledger.save()
|
||||
|
||||
inventory_account = (
|
||||
entity.get_default_coa_accounts().filter(role=roles.ASSET_CA_INVENTORY).first()
|
||||
)
|
||||
vendor_account = entity.get_default_coa_accounts().filter(name=vendor.name).first()
|
||||
# inventory_account = (
|
||||
# entity.get_default_coa_accounts().filter(role=roles.ASSET_CA_INVENTORY).first()
|
||||
# )
|
||||
# vendor_account = entity.get_default_coa_accounts().filter(name=vendor.name).first()
|
||||
|
||||
if not vendor_account:
|
||||
last_account = (
|
||||
entity.get_all_accounts()
|
||||
.filter(role=roles.LIABILITY_CL_ACC_PAYABLE)
|
||||
.order_by("-created")
|
||||
.first()
|
||||
)
|
||||
if len(last_account.code) == 4:
|
||||
code = f"{int(last_account.code)}{1:03d}"
|
||||
elif len(last_account.code) > 4:
|
||||
code = f"{int(last_account.code) + 1}"
|
||||
# if not vendor_account:
|
||||
# last_account = (
|
||||
# entity.get_all_accounts()
|
||||
# .filter(role=roles.LIABILITY_CL_ACC_PAYABLE)
|
||||
# .order_by("-created")
|
||||
# .first()
|
||||
# )
|
||||
# if len(last_account.code) == 4:
|
||||
# code = f"{int(last_account.code)}{1:03d}"
|
||||
# elif len(last_account.code) > 4:
|
||||
# code = f"{int(last_account.code) + 1}"
|
||||
|
||||
vendor_account = entity.create_account(
|
||||
name=vendor.name,
|
||||
code=code,
|
||||
role=roles.LIABILITY_CL_ACC_PAYABLE,
|
||||
coa_model=coa,
|
||||
balance_type="credit",
|
||||
active=True,
|
||||
)
|
||||
additional_services_account = (
|
||||
entity.get_default_coa_accounts()
|
||||
.filter(name="Additional Services", role=roles.COGS)
|
||||
.first()
|
||||
)
|
||||
# vendor_account = entity.create_account(
|
||||
# name=vendor.name,
|
||||
# code=code,
|
||||
# role=roles.LIABILITY_CL_ACC_PAYABLE,
|
||||
# coa_model=coa,
|
||||
# balance_type="credit",
|
||||
# active=True,
|
||||
# )
|
||||
# additional_services_account = (
|
||||
# entity.get_default_coa_accounts()
|
||||
# .filter(name="Additional Services", role=roles.COGS)
|
||||
# .first()
|
||||
# )
|
||||
|
||||
# Debit Inventory Account
|
||||
TransactionModel.objects.create(
|
||||
journal_entry=journal,
|
||||
account=inventory_account,
|
||||
amount=car_finance.cost_price,
|
||||
tx_type="debit",
|
||||
)
|
||||
# # Debit Inventory Account
|
||||
# TransactionModel.objects.create(
|
||||
# journal_entry=journal,
|
||||
# account=inventory_account,
|
||||
# amount=car_finance.cost_price,
|
||||
# tx_type="debit",
|
||||
# )
|
||||
|
||||
# Credit Vendor Account
|
||||
TransactionModel.objects.create(
|
||||
journal_entry=journal,
|
||||
account=vendor_account,
|
||||
amount=car_finance.cost_price,
|
||||
tx_type="credit",
|
||||
)
|
||||
# # Credit Vendor Account
|
||||
# TransactionModel.objects.create(
|
||||
# journal_entry=journal,
|
||||
# account=vendor_account,
|
||||
# amount=car_finance.cost_price,
|
||||
# tx_type="credit",
|
||||
# )
|
||||
|
||||
|
||||
@receiver(post_save, sender=models.CarFinance)
|
||||
def update_finance_cost(sender, instance, created, **kwargs):
|
||||
"""
|
||||
Signal to handle `post_save` functionality for the `CarFinance` model. This function
|
||||
creates or updates financial records related to a car's finance details in the ledger
|
||||
associated with the car's vendor and dealer. For newly created instances, a ledger is
|
||||
created or retrieved, and the `save_journal` function is executed to log the financial
|
||||
transactions.
|
||||
# @receiver(post_save, sender=models.CarFinance)
|
||||
# def update_finance_cost(sender, instance, created, **kwargs):
|
||||
# """
|
||||
# Signal to handle `post_save` functionality for the `CarFinance` model. This function
|
||||
# creates or updates financial records related to a car's finance details in the ledger
|
||||
# associated with the car's vendor and dealer. For newly created instances, a ledger is
|
||||
# created or retrieved, and the `save_journal` function is executed to log the financial
|
||||
# transactions.
|
||||
|
||||
This function also has commented-out logic to handle updates for already created
|
||||
instances, including journal updates or adjustments to existing financial transactions.
|
||||
# This function also has commented-out logic to handle updates for already created
|
||||
# instances, including journal updates or adjustments to existing financial transactions.
|
||||
|
||||
:param sender: Model class that triggered the signal
|
||||
:type sender: Model
|
||||
:param instance: Instance of the `CarFinance` model passed to the signal
|
||||
:type instance: CarFinance
|
||||
:param created: Boolean value indicating if the instance was created (`True`) or updated (`False`)
|
||||
:type created: bool
|
||||
:param kwargs: Arbitrary keyword arguments passed to the signal
|
||||
:type kwargs: dict
|
||||
:return: None
|
||||
"""
|
||||
if created:
|
||||
entity = instance.car.dealer.entity
|
||||
vendor = instance.car.vendor
|
||||
vin = instance.car.vin if instance.car.vin else ""
|
||||
make = instance.car.id_car_make.name if instance.car.id_car_make else ""
|
||||
model = instance.car.id_car_model.name if instance.car.id_car_model else ""
|
||||
year = instance.car.year
|
||||
vendor_name = vendor.name if vendor else ""
|
||||
# :param sender: Model class that triggered the signal
|
||||
# :type sender: Model
|
||||
# :param instance: Instance of the `CarFinance` model passed to the signal
|
||||
# :type instance: CarFinance
|
||||
# :param created: Boolean value indicating if the instance was created (`True`) or updated (`False`)
|
||||
# :type created: bool
|
||||
# :param kwargs: Arbitrary keyword arguments passed to the signal
|
||||
# :type kwargs: dict
|
||||
# :return: None
|
||||
# """
|
||||
# if created:
|
||||
# entity = instance.car.dealer.entity
|
||||
# vendor = instance.car.vendor
|
||||
# vin = instance.car.vin if instance.car.vin else ""
|
||||
# make = instance.car.id_car_make.name if instance.car.id_car_make else ""
|
||||
# model = instance.car.id_car_model.name if instance.car.id_car_model else ""
|
||||
# year = instance.car.year
|
||||
# vendor_name = vendor.name if vendor else ""
|
||||
|
||||
name = f"{vin}-{make}-{model}-{year}-{vendor_name}"
|
||||
ledger, _ = LedgerModel.objects.get_or_create(name=name, entity=entity)
|
||||
save_journal(instance, ledger, vendor)
|
||||
# name = f"{vin}-{make}-{model}-{year}-{vendor_name}"
|
||||
# ledger, _ = LedgerModel.objects.get_or_create(name=name, entity=entity)
|
||||
# save_journal(instance, ledger, vendor)
|
||||
|
||||
# if not created:
|
||||
# if ledger.additional_info.get("je_number"):
|
||||
# journal = JournalEntryModel.objects.filter(je_number=ledger.additional_info.get("je_number")).first()
|
||||
# journal.description = f"Finances of Car:{instance.car.vin} for Vendor:{instance.car.vendor.vendor_name}"
|
||||
# journal.save()
|
||||
# debit = journal.get_transaction_queryset().filter(tx_type='debit').first()
|
||||
# credit = journal.get_transaction_queryset().filter(tx_type='credit').first()
|
||||
# if debit and credit:
|
||||
# if journal.is_locked():
|
||||
# journal.mark_as_unlocked()
|
||||
# journal.save()
|
||||
# debit.amount = instance.cost_price
|
||||
# credit.amount = instance.cost_price
|
||||
# debit.save()
|
||||
# credit.save()
|
||||
# else:
|
||||
# save_journal(instance,ledger,vendor,journal=journal)
|
||||
# else:
|
||||
# save_journal(instance,ledger,vendor)
|
||||
# else:
|
||||
# save_journal(instance,ledger,vendor)
|
||||
# # if not created:
|
||||
# # if ledger.additional_info.get("je_number"):
|
||||
# # journal = JournalEntryModel.objects.filter(je_number=ledger.additional_info.get("je_number")).first()
|
||||
# # journal.description = f"Finances of Car:{instance.car.vin} for Vendor:{instance.car.vendor.vendor_name}"
|
||||
# # journal.save()
|
||||
# # debit = journal.get_transaction_queryset().filter(tx_type='debit').first()
|
||||
# # credit = journal.get_transaction_queryset().filter(tx_type='credit').first()
|
||||
# # if debit and credit:
|
||||
# # if journal.is_locked():
|
||||
# # journal.mark_as_unlocked()
|
||||
# # journal.save()
|
||||
# # debit.amount = instance.cost_price
|
||||
# # credit.amount = instance.cost_price
|
||||
# # debit.save()
|
||||
# # credit.save()
|
||||
# # else:
|
||||
# # save_journal(instance,ledger,vendor,journal=journal)
|
||||
# # else:
|
||||
# # save_journal(instance,ledger,vendor)
|
||||
# # else:
|
||||
# # save_journal(instance,ledger,vendor)
|
||||
|
||||
|
||||
@receiver(post_save, sender=PurchaseOrderModel)
|
||||
|
||||
@ -40,12 +40,15 @@ urlpatterns = [
|
||||
views.assign_car_makes,
|
||||
name="assign_car_makes",
|
||||
),
|
||||
path(
|
||||
"dashboards/manager/",
|
||||
views.ManagerDashboard.as_view(),
|
||||
name="manager_dashboard",
|
||||
),
|
||||
|
||||
|
||||
#dashboards
|
||||
path("dashboards/dealer/", views.DealerDashboard.as_view(),name="dealer_dashboard"),
|
||||
path( "dashboards/manager/", views.ManagerDashboard.as_view(),name="manager_dashboard"),
|
||||
path("dashboards/sales/", views.SalesDashboard.as_view(), name="sales_dashboard"),
|
||||
path("dashboards/accountant/", views.AccountantDashboard.as_view(), name="accountant_dashboard"),
|
||||
path("dashboards/inventory/", views.InventoryDashboard.as_view(), name="inventory_dashboard"),
|
||||
|
||||
|
||||
path("cars/inventory/table/", views.CarListViewTable.as_view(), name="car_table"),
|
||||
path("export/format/", TableExport.export, name="export"),
|
||||
@ -345,13 +348,8 @@ urlpatterns = [
|
||||
name="car_delete",
|
||||
),
|
||||
path(
|
||||
"<slug:dealer_slug>/cars/<slug:slug>/finance/create/",
|
||||
views.CarFinanceCreateView.as_view(),
|
||||
name="car_finance_create",
|
||||
),
|
||||
path(
|
||||
"<slug:dealer_slug>/cars/finance/<int:pk>/update/",
|
||||
views.CarFinanceUpdateView.as_view(),
|
||||
"<slug:dealer_slug>/cars/<slug:slug>/finance/update/",
|
||||
views.CarFinanceUpdateView,
|
||||
name="car_finance_update",
|
||||
),
|
||||
path(
|
||||
@ -1091,6 +1089,10 @@ urlpatterns = [
|
||||
views.MonthlyIncomeStatementView.as_view(),
|
||||
name="entity-ic-date",
|
||||
),
|
||||
# Chart of Accounts...
|
||||
path('<slug:dealer_slug>/chart-of-accounts/<slug:entity_slug>/list/',
|
||||
views.ChartOfAccountModelListView.as_view(),
|
||||
name='coa-list'),
|
||||
# CASH FLOW STATEMENTS...
|
||||
# Entities...
|
||||
path(
|
||||
|
||||
@ -779,7 +779,6 @@ class CarTransfer:
|
||||
)
|
||||
self.bill.additional_info = {}
|
||||
self.bill.additional_info.update({"car_info": self.car.to_dict()})
|
||||
self.bill.additional_info.update({"car_finance": self.car.finances.to_dict()})
|
||||
|
||||
self.bill.mark_as_review()
|
||||
self.bill.mark_as_approved(
|
||||
@ -791,14 +790,14 @@ class CarTransfer:
|
||||
self.car.dealer = self.to_dealer
|
||||
self.car.vendor = self.vendor
|
||||
self.car.receiving_date = datetime.datetime.now()
|
||||
self.car.finances.additional_services.clear()
|
||||
self.car.additional_services.clear()
|
||||
if hasattr(self.car, "custom_cards"):
|
||||
self.car.custom_cards.delete()
|
||||
|
||||
self.car.finances.cost_price = self.transfer.total_price
|
||||
self.car.finances.marked_price = 0
|
||||
self.car.finances.discount_amount = 0
|
||||
self.car.finances.save()
|
||||
self.car.cost_price = self.transfer.total_price
|
||||
self.car.marked_price = 0
|
||||
self.car.discount_amount = 0
|
||||
self.car.save()
|
||||
self.car.location.owner = self.to_dealer
|
||||
self.car.location.showroom = self.to_dealer
|
||||
self.car.location.description = ""
|
||||
@ -920,7 +919,7 @@ class CarTransfer:
|
||||
# )
|
||||
|
||||
# bill.additional_info.update({"car_info": car.to_dict()})
|
||||
# bill.additional_info.update({"car_finance": car.finances.to_dict()})
|
||||
# bill.additional_info.update({"car_finance": car.to_dict()})
|
||||
|
||||
# bill.mark_as_review()
|
||||
# bill.mark_as_approved(to_dealer.entity.slug, to_dealer.entity.admin)
|
||||
@ -929,14 +928,14 @@ class CarTransfer:
|
||||
# car.dealer = to_dealer
|
||||
# car.vendor = vendor
|
||||
# car.receiving_date = datetime.datetime.now()
|
||||
# car.finances.additional_services.clear()
|
||||
# car.additional_services.clear()
|
||||
# if hasattr(car, "custom_cards"):
|
||||
# car.custom_cards.delete()
|
||||
|
||||
# car.finances.cost_price = transfer.total_price
|
||||
# car.finances.selling_price = 0
|
||||
# car.finances.discount_amount = 0
|
||||
# car.finances.save()
|
||||
# car.cost_price = transfer.total_price
|
||||
# car.selling_price = 0
|
||||
# car.discount_amount = 0
|
||||
# car.save()
|
||||
# car.location.owner = to_dealer
|
||||
# car.location.showroom = to_dealer
|
||||
# car.location.description = ""
|
||||
@ -980,6 +979,153 @@ def to_dict(obj):
|
||||
return obj_dict
|
||||
|
||||
|
||||
class CarFinanceCalculator1:
|
||||
"""
|
||||
Class responsible for calculating car financing details.
|
||||
|
||||
This class provides methods and attributes required for calculating various
|
||||
aspects related to car financing, such as VAT calculation, pricing, discounts,
|
||||
and additional services. It processes data about cars, computes totals (e.g.,
|
||||
price, VAT, discounts), and aggregates the financial data for reporting or
|
||||
further processing.
|
||||
|
||||
:ivar model: The data model passed to the calculator for retrieving transaction data.
|
||||
:type model: Any
|
||||
:ivar vat_rate: The current active VAT rate retrieved from the database.
|
||||
:type vat_rate: Decimal
|
||||
:ivar item_transactions: A collection of item transactions retrieved from the model.
|
||||
:type item_transactions: list
|
||||
:ivar additional_services: A list of additional services with details (e.g., name, price, taxable status).
|
||||
:type additional_services: list
|
||||
"""
|
||||
|
||||
VAT_OBJ_NAME = "vat_rate"
|
||||
CAR_FINANCE_KEY = "car_finance"
|
||||
CAR_INFO_KEY = "car_info"
|
||||
ADDITIONAL_SERVICES_KEY = "additional_services"
|
||||
|
||||
def __init__(self, model):
|
||||
if isinstance(model, InvoiceModel):
|
||||
self.dealer = models.Dealer.objects.get(entity=model.ce_model.entity)
|
||||
self.extra_info = models.ExtraInfo.objects.get(
|
||||
dealer=self.dealer,
|
||||
content_type=ContentType.objects.get_for_model(model.ce_model),
|
||||
object_id=model.ce_model.pk,
|
||||
)
|
||||
elif isinstance(model, EstimateModel):
|
||||
self.dealer = models.Dealer.objects.get(entity=model.entity)
|
||||
self.extra_info = models.ExtraInfo.objects.get(
|
||||
dealer=self.dealer,
|
||||
content_type=ContentType.objects.get_for_model(model),
|
||||
object_id=model.pk,
|
||||
)
|
||||
self.model = model
|
||||
self.vat_rate = self._get_vat_rate()
|
||||
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:
|
||||
raise ObjectDoesNotExist("No active VAT rate found")
|
||||
return vat.rate
|
||||
|
||||
def _get_additional_services(self):
|
||||
return [x for item in self.item_transactions
|
||||
for x in item.item_model.car.additional_services
|
||||
]
|
||||
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
|
||||
|
||||
# def _get_nested_value(self, item, *keys):
|
||||
# current = item.item_model.additional_info
|
||||
# for key in keys:
|
||||
# current = current.get(key, {})
|
||||
# return current
|
||||
|
||||
def _get_car_data(self, item):
|
||||
quantity = self._get_quantity(item)
|
||||
car = item.item_model.car
|
||||
unit_price = Decimal(car.marked_price)
|
||||
discount = self.extra_info.data.get("discount",0)
|
||||
sell_price = unit_price - Decimal(discount)
|
||||
return {
|
||||
"item_number": item.item_model.item_number,
|
||||
"vin": car.vin, #car_info.get("vin"),
|
||||
"make": car.id_car_make ,#car_info.get("make"),
|
||||
"model": car.id_car_model ,#car_info.get("model"),
|
||||
"year": car.year ,# car_info.get("year"),
|
||||
"logo": car.logo, # getattr(car.id_car_make, "logo", ""),
|
||||
"trim": car.id_car_trim ,# car_info.get("trim"),
|
||||
"mileage": car.mileage ,# car_info.get("mileage"),
|
||||
"cost_price": car.cost_price,
|
||||
"selling_price": car.selling_price,
|
||||
"marked_price": car.marked_price,
|
||||
"discount": car.discount_amount,
|
||||
"quantity": quantity,
|
||||
"unit_price": unit_price,
|
||||
"sell_price": sell_price,
|
||||
"total": unit_price,
|
||||
"total_vat": sell_price * self.vat_rate,
|
||||
"total_discount": discount,
|
||||
"final_price": sell_price + (sell_price * self.vat_rate),
|
||||
"total_additionals": car.total_additional_services,
|
||||
"grand_total": sell_price + (sell_price * self.vat_rate) + car.total_additional_services,
|
||||
"additional_services": car.additional_services,# self._get_nested_value(
|
||||
#item, self.ADDITIONAL_SERVICES_KEY
|
||||
#),
|
||||
}
|
||||
|
||||
def calculate_totals(self):
|
||||
total_price = sum(
|
||||
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())
|
||||
|
||||
total_discount = self.extra_info.data.get("discount",0)
|
||||
total_price_discounted = total_price
|
||||
if total_discount:
|
||||
total_price_discounted = total_price - Decimal(total_discount)
|
||||
print(total_price_discounted)
|
||||
total_vat_amount = total_price_discounted * self.vat_rate
|
||||
|
||||
return {
|
||||
"total_price_discounted":total_price_discounted,
|
||||
"total_price_before_discount":total_price,
|
||||
"total_price": total_price_discounted,
|
||||
"total_vat_amount": total_vat_amount,
|
||||
"total_discount": Decimal(total_discount),
|
||||
"total_additionals": total_additionals,
|
||||
"grand_total":total_price_discounted + total_vat_amount + total_additionals,
|
||||
}
|
||||
|
||||
def get_finance_data(self):
|
||||
totals = self.calculate_totals()
|
||||
return {
|
||||
"cars": [self._get_car_data(item) for item in self.item_transactions],
|
||||
"quantity": sum(
|
||||
self._get_quantity(item) for item in self.item_transactions
|
||||
),
|
||||
"total_price": round(totals["total_price"], 2),
|
||||
"total_price_discounted": round(totals["total_price_discounted"], 2),
|
||||
"total_price_before_discount": round(totals["total_price_before_discount"], 2),
|
||||
"total_vat": round(totals["total_vat_amount"] + totals["total_price"], 2),
|
||||
"total_vat_amount": round(totals["total_vat_amount"], 2),
|
||||
"total_discount": round(totals["total_discount"], 2),
|
||||
"total_additionals": round(totals["total_additionals"], 2),
|
||||
"grand_total": round(totals["grand_total"], 2),
|
||||
"additionals": self._get_additional_services(),
|
||||
"vat": round(self.vat_rate, 2),
|
||||
}
|
||||
class CarFinanceCalculator:
|
||||
"""
|
||||
Class responsible for calculating car financing details.
|
||||
@ -1054,7 +1200,7 @@ class CarFinanceCalculator:
|
||||
def _get_car_data(self, item):
|
||||
quantity = self._get_quantity(item)
|
||||
car = item.item_model.car
|
||||
unit_price = Decimal(car.finances.marked_price)
|
||||
unit_price = Decimal(car.marked_price)
|
||||
discount = self.extra_info.data.get("discount",0)
|
||||
sell_price = unit_price - Decimal(discount)
|
||||
return {
|
||||
@ -1066,10 +1212,10 @@ class CarFinanceCalculator:
|
||||
"logo": car.logo, # getattr(car.id_car_make, "logo", ""),
|
||||
"trim": car.id_car_trim ,# car_info.get("trim"),
|
||||
"mileage": car.mileage ,# car_info.get("mileage"),
|
||||
"cost_price": car.finances.cost_price,
|
||||
"selling_price": car.finances.selling_price,
|
||||
"marked_price": car.finances.marked_price,
|
||||
"discount": car.finances.discount_amount,
|
||||
"cost_price": car.cost_price,
|
||||
"selling_price": car.selling_price,
|
||||
"marked_price": car.marked_price,
|
||||
"discount": car.discount_amount,
|
||||
"quantity": quantity,
|
||||
"unit_price": unit_price,
|
||||
"sell_price": sell_price,
|
||||
@ -1086,7 +1232,7 @@ class CarFinanceCalculator:
|
||||
|
||||
def calculate_totals(self):
|
||||
total_price = sum(
|
||||
Decimal(item.item_model.car.finances.marked_price)
|
||||
Decimal(item.item_model.car.marked_price)
|
||||
for item in self.item_transactions
|
||||
)
|
||||
total_additionals = sum(
|
||||
@ -1112,7 +1258,7 @@ class CarFinanceCalculator:
|
||||
def get_finance_data(self):
|
||||
totals = self.calculate_totals()
|
||||
return {
|
||||
"cars": [self._get_car_data(item) for item in self.item_transactions],
|
||||
"car": [self._get_car_data(item) for item in self.item_transactions],
|
||||
"quantity": sum(
|
||||
self._get_quantity(item) for item in self.item_transactions
|
||||
),
|
||||
@ -1127,6 +1273,53 @@ class CarFinanceCalculator:
|
||||
"additionals": self._get_additional_services(),
|
||||
"vat": round(self.vat_rate, 2),
|
||||
}
|
||||
|
||||
def get_finance_data(estimate,dealer):
|
||||
vat = models.VatRate.objects.filter(dealer=dealer,is_active=True).first()
|
||||
item = estimate.get_itemtxs_data()[0].first()
|
||||
car = item.item_model.car
|
||||
if isinstance(estimate,InvoiceModel) and hasattr(estimate, "ce_model"):
|
||||
estimate = estimate.ce_model
|
||||
|
||||
extra_info = models.ExtraInfo.objects.get(
|
||||
dealer=dealer,
|
||||
content_type=ContentType.objects.get_for_model(EstimateModel),
|
||||
object_id=estimate.pk,
|
||||
)
|
||||
discount = extra_info.data.get("discount", 0)
|
||||
discount = Decimal(discount)
|
||||
vat_amount = car.marked_price * vat.rate
|
||||
additional_services = car.get_additional_services()
|
||||
|
||||
return {
|
||||
"car": car,
|
||||
"discounted_price": (Decimal(car.marked_price) - discount) or 0,
|
||||
"price_before_discount": car.marked_price,
|
||||
"vat_amount": vat_amount,
|
||||
"vat_rate": vat.rate,
|
||||
"discount_amount": discount,
|
||||
"additional_services": additional_services,
|
||||
"grand_total": (car.marked_price - discount) + vat_amount + additional_services.get("total")
|
||||
}
|
||||
|
||||
|
||||
# totals = self.calculate_totals()
|
||||
# return {
|
||||
# "car": [self._get_car_data(item) for item in self.item_transactions],
|
||||
# "quantity": sum(
|
||||
# self._get_quantity(item) for item in self.item_transactions
|
||||
# ),
|
||||
# "total_price": round(totals["total_price"], 2),
|
||||
# "total_price_discounted": round(totals["total_price_discounted"], 2),
|
||||
# "total_price_before_discount": round(totals["total_price_before_discount"], 2),
|
||||
# "total_vat": round(totals["total_vat_amount"] + totals["total_price"], 2),
|
||||
# "total_vat_amount": round(totals["total_vat_amount"], 2),
|
||||
# "total_discount": round(totals["total_discount"], 2),
|
||||
# "total_additionals": round(totals["total_additionals"], 2),
|
||||
# "grand_total": round(totals["grand_total"], 2),
|
||||
# "additionals": self._get_additional_services(),
|
||||
# "vat": round(self.vat_rate, 2),
|
||||
# }
|
||||
# class CarFinanceCalculator:
|
||||
# """
|
||||
# Class responsible for calculating car financing details.
|
||||
@ -1360,9 +1553,9 @@ def _post_sale_and_cogs(invoice, dealer):
|
||||
2) COGS / Inventory journal
|
||||
"""
|
||||
entity = invoice.ledger.entity
|
||||
calc = CarFinanceCalculator(invoice)
|
||||
data = calc.get_finance_data()
|
||||
|
||||
# calc = CarFinanceCalculator(invoice)
|
||||
data = get_finance_data(invoice,dealer)
|
||||
car = data.get("car")
|
||||
cash_acc = entity.get_all_accounts().filter(role_default=True, role=roles.ASSET_CA_CASH).first()
|
||||
ar_acc = entity.get_all_accounts().filter(role_default=True, role=roles.ASSET_CA_RECEIVABLES).first()
|
||||
vat_acc = entity.get_all_accounts().filter(role_default=True, role=roles.LIABILITY_CL_TAXES_PAYABLE).first()
|
||||
@ -1371,105 +1564,105 @@ def _post_sale_and_cogs(invoice, dealer):
|
||||
cogs_acc = entity.get_all_accounts().filter(role_default=True, role=roles.COGS).first()
|
||||
inv_acc = entity.get_all_accounts().filter(role_default=True, role=roles.ASSET_CA_INVENTORY).first()
|
||||
|
||||
for car_data in data['cars']:
|
||||
car = invoice.get_itemtxs_data()[0].filter(
|
||||
item_model__car__vin=car_data['vin']
|
||||
).first().item_model.car
|
||||
qty = Decimal(car_data['quantity'])
|
||||
# for car_data in data['cars']:
|
||||
# car = invoice.get_itemtxs_data()[0].filter(
|
||||
# item_model__car__vin=car_data['vin']
|
||||
# ).first().item_model.car
|
||||
# qty = Decimal(car_data['quantity'])
|
||||
|
||||
net_car_price = Decimal(car_data['total']) - Decimal(car_data['total_discount'])
|
||||
net_additionals_price = Decimal(data['total_additionals'])
|
||||
vat_amount = Decimal(data['total_vat_amount']) * qty
|
||||
grand_total = net_car_price + net_additionals_price + vat_amount
|
||||
cost_total = Decimal(car_data['cost_price']) * qty
|
||||
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 + net_additionals_price + vat_amount
|
||||
cost_total = Decimal(car.cost_price)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 2A. Journal: Cash / A-R / VAT / Sales
|
||||
# ------------------------------------------------------------------
|
||||
# ------------------------------------------------------------------
|
||||
# 2A. Journal: Cash / A-R / VAT / Sales
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
je_sale = JournalEntryModel.objects.create(
|
||||
ledger=invoice.ledger,
|
||||
description=f"Sale {car.vin}",
|
||||
origin=f"Invoice {invoice.invoice_number}",
|
||||
locked=False,
|
||||
posted=False
|
||||
)
|
||||
# Dr Cash (what the customer paid)
|
||||
je_sale = JournalEntryModel.objects.create(
|
||||
ledger=invoice.ledger,
|
||||
description=f"Sale {car.vin}",
|
||||
origin=f"Invoice {invoice.invoice_number}",
|
||||
locked=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'
|
||||
)
|
||||
|
||||
# # Cr A/R (clear the receivable)
|
||||
# TransactionModel.objects.create(
|
||||
# journal_entry=je_sale,
|
||||
# account=ar_acc,
|
||||
# amount=grand_total,
|
||||
# tx_type='credit'
|
||||
# )
|
||||
|
||||
# Cr VAT Payable
|
||||
TransactionModel.objects.create(
|
||||
journal_entry=je_sale,
|
||||
account=vat_acc,
|
||||
amount=vat_amount,
|
||||
tx_type='credit'
|
||||
)
|
||||
|
||||
# Cr Sales – Car
|
||||
TransactionModel.objects.create(
|
||||
journal_entry=je_sale,
|
||||
account=car_rev,
|
||||
amount=net_car_price,
|
||||
tx_type='credit'
|
||||
)
|
||||
|
||||
if net_additionals_price > 0:
|
||||
# Cr Sales – Additional Services
|
||||
TransactionModel.objects.create(
|
||||
journal_entry=je_sale,
|
||||
account=cash_acc,
|
||||
amount=grand_total,
|
||||
tx_type='debit'
|
||||
)
|
||||
|
||||
# # Cr A/R (clear the receivable)
|
||||
# TransactionModel.objects.create(
|
||||
# journal_entry=je_sale,
|
||||
# account=ar_acc,
|
||||
# amount=grand_total,
|
||||
# tx_type='credit'
|
||||
# )
|
||||
|
||||
# Cr VAT Payable
|
||||
TransactionModel.objects.create(
|
||||
journal_entry=je_sale,
|
||||
account=vat_acc,
|
||||
amount=vat_amount,
|
||||
account=add_rev,
|
||||
amount=net_additionals_price,
|
||||
tx_type='credit'
|
||||
)
|
||||
|
||||
# Cr Sales – Car
|
||||
TransactionModel.objects.create(
|
||||
journal_entry=je_sale,
|
||||
account=car_rev,
|
||||
amount=net_car_price,
|
||||
tx_type='credit'
|
||||
)
|
||||
# ------------------------------------------------------------------
|
||||
# 2B. Journal: COGS / Inventory reduction
|
||||
# ------------------------------------------------------------------
|
||||
je_cogs = JournalEntryModel.objects.create(
|
||||
ledger=invoice.ledger,
|
||||
description=f"COGS {car.vin}",
|
||||
origin=f"Invoice {invoice.invoice_number}",
|
||||
locked=False,
|
||||
posted=False
|
||||
)
|
||||
|
||||
if net_additionals_price > 0:
|
||||
# Cr Sales – Additional Services
|
||||
TransactionModel.objects.create(
|
||||
journal_entry=je_sale,
|
||||
account=add_rev,
|
||||
amount=net_additionals_price,
|
||||
tx_type='credit'
|
||||
)
|
||||
# Dr COGS
|
||||
TransactionModel.objects.create(
|
||||
journal_entry=je_cogs,
|
||||
account=cogs_acc,
|
||||
amount=cost_total,
|
||||
tx_type='debit'
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 2B. Journal: COGS / Inventory reduction
|
||||
# ------------------------------------------------------------------
|
||||
je_cogs = JournalEntryModel.objects.create(
|
||||
ledger=invoice.ledger,
|
||||
description=f"COGS {car.vin}",
|
||||
origin=f"Invoice {invoice.invoice_number}",
|
||||
locked=False,
|
||||
posted=False
|
||||
)
|
||||
|
||||
# Dr COGS
|
||||
TransactionModel.objects.create(
|
||||
journal_entry=je_cogs,
|
||||
account=cogs_acc,
|
||||
amount=cost_total,
|
||||
tx_type='debit'
|
||||
)
|
||||
|
||||
# Cr Inventory
|
||||
TransactionModel.objects.create(
|
||||
journal_entry=je_cogs,
|
||||
account=inv_acc,
|
||||
amount=cost_total,
|
||||
tx_type='credit'
|
||||
)
|
||||
# ------------------------------------------------------------------
|
||||
# 2C. Update car state flags inside the same transaction
|
||||
# ------------------------------------------------------------------
|
||||
entity.get_items_inventory().filter(name=car.vin).update(for_inventory=False)
|
||||
# car.item_model.for_inventory = False
|
||||
# car.item_model.save(update_fields=['for_inventory'])
|
||||
car.finances.selling_price = grand_total
|
||||
car.finances.is_sold = True
|
||||
car.finances.save()
|
||||
# Cr Inventory
|
||||
TransactionModel.objects.create(
|
||||
journal_entry=je_cogs,
|
||||
account=inv_acc,
|
||||
amount=cost_total,
|
||||
tx_type='credit'
|
||||
)
|
||||
# ------------------------------------------------------------------
|
||||
# 2C. Update car state flags inside the same transaction
|
||||
# ------------------------------------------------------------------
|
||||
entity.get_items_inventory().filter(name=car.vin).update(for_inventory=False)
|
||||
# car.item_model.for_inventory = False
|
||||
# car.item_model.save(update_fields=['for_inventory'])
|
||||
car.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,
|
||||
|
||||
@ -105,6 +105,9 @@ from django_ledger.forms.bank_account import (
|
||||
BankAccountCreateForm,
|
||||
BankAccountUpdateForm,
|
||||
)
|
||||
from django_ledger.views.chart_of_accounts import (
|
||||
ChartOfAccountModelListView as ChartOfAccountModelListViewBase
|
||||
)
|
||||
from django_ledger.views.bill import (
|
||||
BillModelCreateView,
|
||||
BillModelDetailView,
|
||||
@ -126,6 +129,7 @@ from django_ledger.forms.invoice import (
|
||||
from django_ledger.forms.item import (
|
||||
InventoryItemCreateForm,
|
||||
)
|
||||
|
||||
from django_ledger.forms.purchase_order import (
|
||||
PurchaseOrderModelCreateForm,
|
||||
BasePurchaseOrderModelUpdateForm,
|
||||
@ -195,6 +199,7 @@ from .services import (
|
||||
from .utils import (
|
||||
CarFinanceCalculator,
|
||||
get_car_finance_data,
|
||||
get_finance_data,
|
||||
get_item_transactions,
|
||||
handle_payment,
|
||||
reserve_car,
|
||||
@ -386,9 +391,7 @@ class TestView(TemplateView):
|
||||
|
||||
template_name = "inventory/cars_list_api.html"
|
||||
|
||||
|
||||
|
||||
class ManagerDashboard(LoginRequiredMixin, TemplateView):
|
||||
class DealerDashboard(LoginRequiredMixin, TemplateView):
|
||||
"""
|
||||
ManagerDashboard class is a view handling the dashboard for a manager.
|
||||
|
||||
@ -402,7 +405,7 @@ class ManagerDashboard(LoginRequiredMixin, TemplateView):
|
||||
:type template_name: str
|
||||
"""
|
||||
|
||||
template_name = "dashboards/manager.html"
|
||||
template_name = "dashboards/dealer_dashbaord.html"
|
||||
|
||||
# def dispatch(self, request, *args, **kwargs):
|
||||
# if not request.user.is_authenticated:
|
||||
@ -484,6 +487,105 @@ class ManagerDashboard(LoginRequiredMixin, TemplateView):
|
||||
|
||||
return context
|
||||
|
||||
class ManagerDashboard(LoginRequiredMixin, TemplateView):
|
||||
"""
|
||||
ManagerDashboard class is a view handling the dashboard for a manager.
|
||||
|
||||
Provides functionality to manage and view various statistics and data specific
|
||||
to the dealer associated with the authenticated manager. It uses a specific
|
||||
template and ensures authentication before granting access. The class
|
||||
aggregates data about cars, leads, financial statistics, and other related
|
||||
business information for display in the manager's dashboard.
|
||||
|
||||
:ivar template_name: Path to the template used for rendering the manager's dashboard.
|
||||
:type template_name: str
|
||||
"""
|
||||
|
||||
template_name = "dashboards/manager_dashboard.html"
|
||||
|
||||
# def dispatch(self, request, *args, **kwargs):
|
||||
# if not request.user.is_authenticated:
|
||||
# return redirect("welcome")
|
||||
# if not getattr(request.user, "dealer", False):
|
||||
# return HttpResponseForbidden(
|
||||
# "You are not authorized to view this dashboard."
|
||||
# )
|
||||
# return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
dealer = get_user_type(self.request)
|
||||
entity = dealer.entity
|
||||
|
||||
qs = models.Car.objects.filter(dealer=dealer)
|
||||
total_cars = qs.count()
|
||||
|
||||
stats = 0 # models.CarFinance.objects.filter(car__dealer=dealer).aggregate( #TODO:update_finance
|
||||
# total_cost_price=Sum("cost_price"),
|
||||
# total_selling_price=Sum("selling_price"),
|
||||
# )
|
||||
total_cost_price = stats["total_cost_price"] or 0
|
||||
total_selling_price = stats["total_selling_price"] or 0
|
||||
total_profit = total_selling_price - total_cost_price
|
||||
|
||||
new_leads = models.Lead.objects.filter(
|
||||
dealer=dealer, status=models.Status.NEW
|
||||
).count()
|
||||
pending_leads = models.Lead.objects.filter(
|
||||
dealer=dealer, status=models.Status.CONTACTED
|
||||
).count()
|
||||
canceled_leads = models.Lead.objects.filter(
|
||||
dealer=dealer, status=models.Status.UNQUALIFIED
|
||||
).count()
|
||||
|
||||
car_status_qs = qs.values("status").annotate(count=Count("status"))
|
||||
car_status = {status: count for status, count in car_status_qs}
|
||||
|
||||
car_by_make_qs = qs.values("id_car_make__name").annotate(count=Count("id"))
|
||||
car_by_make = list(car_by_make_qs)
|
||||
|
||||
context["dealer"] = dealer
|
||||
context["total_activity"] = models.UserActivityLog.objects.filter(
|
||||
user=dealer.user
|
||||
).count()
|
||||
context["total_cars"] = total_cars
|
||||
context["total_reservations"] = models.CarReservation.objects.filter(
|
||||
reserved_until__gte=timezone.now()
|
||||
).count()
|
||||
context["total_cost_price"] = total_cost_price
|
||||
context["total_selling_price"] = total_selling_price
|
||||
context["total_profit"] = total_profit
|
||||
context["new_leads"] = new_leads
|
||||
context["pending_leads"] = pending_leads
|
||||
context["canceled_leads"] = canceled_leads
|
||||
context["car"] = json.dumps(car_by_make)
|
||||
|
||||
|
||||
context["available_cars"] =qs.filter(status='available').count()
|
||||
context["sold_cars"] = qs.filter(status='sold').count()
|
||||
context["reserved_cars"] = qs.filter(status='reserved').count()
|
||||
context["hold_cars"] =qs.filter(status='hold').count()
|
||||
context["damaged_cars"] = qs.filter(status='damaged').count()
|
||||
context["transfer_cars"] = qs.filter(status='transfer').count()
|
||||
context["present_inventory_count"]=total_cars-context["sold_cars"]-context["damaged_cars"]
|
||||
cars_sold=qs.filter(status='sold')
|
||||
# cars_sold.aggregate(total_inventory_value=sum())
|
||||
|
||||
|
||||
context["customers"] = entity.get_customers().count()
|
||||
context["staff"] = models.Staff.objects.filter(dealer=dealer).count()
|
||||
context["total_leads"] = models.Lead.objects.filter(dealer=dealer).count()
|
||||
context["invoices"] = entity.get_invoices().count()
|
||||
context["estimates"] = entity.get_estimates().count()
|
||||
context["purchase_orders"] = entity.get_purchase_orders().count()
|
||||
|
||||
|
||||
|
||||
return context
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class SalesDashboard(LoginRequiredMixin, TemplateView):
|
||||
"""
|
||||
@ -503,7 +605,152 @@ class SalesDashboard(LoginRequiredMixin, TemplateView):
|
||||
:type template_name: str
|
||||
"""
|
||||
|
||||
template_name = "dashboards/sales.html"
|
||||
template_name = "dashboards/sales_dashboard.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
dealer = self.request.dealer
|
||||
staff = getattr(self.request, "staff", None)
|
||||
total_cars = models.Car.objects.filter(dealer=dealer).count()
|
||||
total_reservations = models.CarReservation.objects.filter(
|
||||
reserved_by=self.request.user, reserved_until__gte=timezone.now()
|
||||
).count()
|
||||
|
||||
available_cars = models.Car.objects.filter(
|
||||
dealer=dealer, status=models.CarStatusChoices.AVAILABLE
|
||||
).count()
|
||||
sold_cars = models.Car.objects.filter(
|
||||
dealer=dealer, status=models.CarStatusChoices.SOLD
|
||||
).count()
|
||||
reserved_cars = models.Car.objects.filter(
|
||||
dealer=dealer, status=models.CarStatusChoices.RESERVED
|
||||
).count()
|
||||
hold_cars = models.Car.objects.filter(
|
||||
dealer=dealer, status=models.CarStatusChoices.HOLD
|
||||
).count()
|
||||
damaged_cars = models.Car.objects.filter(
|
||||
dealer=dealer, status=models.CarStatusChoices.DAMAGED
|
||||
).count()
|
||||
transfer_cars = models.Car.objects.filter(
|
||||
dealer=dealer, status=models.CarStatusChoices.TRANSFER
|
||||
).count()
|
||||
reserved_percentage = reserved_cars / total_cars * 100
|
||||
sold_percentage = sold_cars / total_cars * 100
|
||||
qs = (
|
||||
models.Car.objects.values("id_car_make__name")
|
||||
.annotate(count=Count("id"))
|
||||
.order_by("id_car_make__name")
|
||||
)
|
||||
car_by_make = list(qs)
|
||||
|
||||
context["dealer"] = dealer
|
||||
context["staff"] = staff
|
||||
context["total_cars"] = total_cars
|
||||
context["total_reservations"] = total_reservations
|
||||
context["reserved_percentage"] = reserved_percentage
|
||||
context["sold_percentage"] = sold_percentage
|
||||
context["available_cars"] = available_cars
|
||||
context["sold_cars"] = sold_cars
|
||||
context["reserved_cars"] = reserved_cars
|
||||
context["hold_cars"] = hold_cars
|
||||
context["damaged_cars"] = damaged_cars
|
||||
context["transfer_cars"] = transfer_cars
|
||||
context["car"] = json.dumps(car_by_make)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class InventoryDashboard(LoginRequiredMixin, TemplateView):
|
||||
"""
|
||||
SalesDashboard class provides a view for the sales dashboard.
|
||||
|
||||
This class is responsible for generating the context data required to
|
||||
display various statistics and information on the sales dashboard. It
|
||||
inherits from `LoginRequiredMixin` and `TemplateView`, ensuring only
|
||||
authenticated users can access the view. The dashboard includes data
|
||||
such as the total number of cars, reservations, sold percentages,
|
||||
reserved percentages, and cars categorized by various statuses. The
|
||||
purpose of this dashboard is to provide dealership staff an overview
|
||||
of their inventory and related sales metrics.
|
||||
|
||||
:ivar template_name: The name of the HTML template used for rendering
|
||||
the sales dashboard.
|
||||
:type template_name: str
|
||||
"""
|
||||
|
||||
template_name = "dashboards/inventory_dashboard.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
dealer = self.request.dealer
|
||||
staff = getattr(self.request, "staff", None)
|
||||
total_cars = models.Car.objects.filter(dealer=dealer).count()
|
||||
total_reservations = models.CarReservation.objects.filter(
|
||||
reserved_by=self.request.user, reserved_until__gte=timezone.now()
|
||||
).count()
|
||||
|
||||
available_cars = models.Car.objects.filter(
|
||||
dealer=dealer, status=models.CarStatusChoices.AVAILABLE
|
||||
).count()
|
||||
sold_cars = models.Car.objects.filter(
|
||||
dealer=dealer, status=models.CarStatusChoices.SOLD
|
||||
).count()
|
||||
reserved_cars = models.Car.objects.filter(
|
||||
dealer=dealer, status=models.CarStatusChoices.RESERVED
|
||||
).count()
|
||||
hold_cars = models.Car.objects.filter(
|
||||
dealer=dealer, status=models.CarStatusChoices.HOLD
|
||||
).count()
|
||||
damaged_cars = models.Car.objects.filter(
|
||||
dealer=dealer, status=models.CarStatusChoices.DAMAGED
|
||||
).count()
|
||||
transfer_cars = models.Car.objects.filter(
|
||||
dealer=dealer, status=models.CarStatusChoices.TRANSFER
|
||||
).count()
|
||||
reserved_percentage = reserved_cars / total_cars * 100
|
||||
sold_percentage = sold_cars / total_cars * 100
|
||||
qs = (
|
||||
models.Car.objects.values("id_car_make__name")
|
||||
.annotate(count=Count("id"))
|
||||
.order_by("id_car_make__name")
|
||||
)
|
||||
car_by_make = list(qs)
|
||||
|
||||
context["dealer"] = dealer
|
||||
context["staff"] = staff
|
||||
context["total_cars"] = total_cars
|
||||
context["total_reservations"] = total_reservations
|
||||
context["reserved_percentage"] = reserved_percentage
|
||||
context["sold_percentage"] = sold_percentage
|
||||
context["available_cars"] = available_cars
|
||||
context["sold_cars"] = sold_cars
|
||||
context["reserved_cars"] = reserved_cars
|
||||
context["hold_cars"] = hold_cars
|
||||
context["damaged_cars"] = damaged_cars
|
||||
context["transfer_cars"] = transfer_cars
|
||||
context["car"] = json.dumps(car_by_make)
|
||||
|
||||
return context
|
||||
|
||||
class AccountantDashboard(LoginRequiredMixin, TemplateView):
|
||||
"""
|
||||
SalesDashboard class provides a view for the sales dashboard.
|
||||
|
||||
This class is responsible for generating the context data required to
|
||||
display various statistics and information on the sales dashboard. It
|
||||
inherits from `LoginRequiredMixin` and `TemplateView`, ensuring only
|
||||
authenticated users can access the view. The dashboard includes data
|
||||
such as the total number of cars, reservations, sold percentages,
|
||||
reserved percentages, and cars categorized by various statuses. The
|
||||
purpose of this dashboard is to provide dealership staff an overview
|
||||
of their inventory and related sales metrics.
|
||||
|
||||
:ivar template_name: The name of the HTML template used for rendering
|
||||
the sales dashboard.
|
||||
:type template_name: str
|
||||
"""
|
||||
|
||||
template_name = "dashboards/accountant_dashboard.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@ -1459,124 +1706,21 @@ class CarDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
||||
permission_required = ["inventory.view_car"]
|
||||
|
||||
|
||||
class CarFinanceCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView):
|
||||
"""
|
||||
Handles the creation of car finance records within the inventory system.
|
||||
def CarFinanceUpdateView(request,dealer_slug,slug):
|
||||
car = get_object_or_404(models.Car, slug=slug)
|
||||
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
||||
|
||||
This view provides functionality to create car finance records tied to a
|
||||
specific car in the inventory. It enforces that the user is logged in and
|
||||
has the required permissions to add a car finance record. It also customizes
|
||||
form behavior and context data to associate the finance record with the
|
||||
corresponding car and populate additional services based on the user's type.
|
||||
|
||||
:ivar model: The database model associated with the view.
|
||||
:type model: models.CarFinance
|
||||
:ivar form_class: The form class used to create car finance records.
|
||||
:type form_class: forms.CarFinanceForm
|
||||
:ivar template_name: The template used to render the car finance creation page.
|
||||
:type template_name: str
|
||||
:ivar permission_required: The list of permissions required to access this view.
|
||||
:type permission_required: list
|
||||
"""
|
||||
|
||||
model = models.CarFinance
|
||||
form_class = forms.CarFinanceForm
|
||||
template_name = "inventory/car_finance_form.html"
|
||||
permission_required = ["inventory.add_carfinance"]
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.car = get_object_or_404(models.Car, slug=self.kwargs["slug"])
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.car = self.car
|
||||
messages.success(self.request, _("Car finance details saved successfully"))
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse(
|
||||
"car_detail",
|
||||
kwargs={"dealer_slug": self.request.dealer.slug, "slug": self.car.slug},
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["car"] = self.car
|
||||
return context
|
||||
|
||||
# def get_form(self, form_class=None):
|
||||
# form = super().get_form(form_class)
|
||||
# dealer = get_user_type(self.request)
|
||||
# form.fields[
|
||||
# "additional_finances"
|
||||
# ].queryset = models.AdditionalServices.objects.filter(dealer=dealer)
|
||||
# return form
|
||||
|
||||
|
||||
class CarFinanceUpdateView(
|
||||
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
|
||||
):
|
||||
"""
|
||||
Manages the update of car finance details.
|
||||
|
||||
This class-based view provides functionality for updating car finance
|
||||
information in the system. It enforces login and specific permissions for
|
||||
access, and it leverages mixins to provide success messages upon completing
|
||||
updates. It is designed to interact with car finance data, update form
|
||||
handling, and enforce related business logic.
|
||||
|
||||
:ivar model: The model associated with the view.
|
||||
:type model: models.CarFinance
|
||||
:ivar form_class: The form class used by the view.
|
||||
:type form_class: forms.CarFinanceForm
|
||||
:ivar template_name: The template used for rendering the form.
|
||||
:type template_name: str
|
||||
:ivar success_message: The success message displayed after an update.
|
||||
:type success_message: str
|
||||
:ivar permission_required: List of permissions required to access the view.
|
||||
:type permission_required: list
|
||||
"""
|
||||
|
||||
model = models.CarFinance
|
||||
form_class = forms.CarFinanceForm
|
||||
template_name = "inventory/car_finance_form.html"
|
||||
success_message = _("Car finance details updated successfully")
|
||||
permission_required = ["inventory.change_carfinance"]
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse(
|
||||
"car_detail",
|
||||
kwargs={
|
||||
"dealer_slug": self.request.dealer.slug,
|
||||
"slug": self.object.car.slug,
|
||||
},
|
||||
)
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs["instance"] = self.get_object()
|
||||
return kwargs
|
||||
|
||||
def get_context_data(self , **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["car"] = self.object.car
|
||||
return context
|
||||
# def get_initial(self):
|
||||
# initial = super().get_initial()
|
||||
# instance = self.get_object()
|
||||
# dealer = get_user_type(self.request)
|
||||
# selected_items = instance.additional_services.filter(dealer=dealer)
|
||||
# initial["additional_finances"] = selected_items
|
||||
# return initial
|
||||
|
||||
# def get_form(self, form_class=None):
|
||||
# form = super().get_form(form_class)
|
||||
# dealer = get_user_type(self.request)
|
||||
# form.fields[
|
||||
# "additional_finances"
|
||||
# ].queryset = models.AdditionalServices.objects.filter(dealer=dealer)
|
||||
# return form
|
||||
if request.method == "POST":
|
||||
form = forms.CarFinanceForm(request.POST, instance=car)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect("car_detail", dealer_slug=dealer.slug, slug=car.slug)
|
||||
else:
|
||||
messages.error(request, "Please correct the errors below.")
|
||||
else:
|
||||
form = forms.CarFinanceForm(instance=car)
|
||||
|
||||
return render(request, "inventory/car_finance_form.html", {"car": car, "dealer": dealer, "form": form})
|
||||
|
||||
class CarUpdateView(
|
||||
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
|
||||
@ -3064,7 +3208,7 @@ def GroupPermissionView(request, dealer_slug, pk):
|
||||
# Define ALL permissions you want to manage
|
||||
MODEL_LIST = [
|
||||
("inventory", "car"),
|
||||
("inventory", "carfinance"),
|
||||
# ("inventory", "carfinance"),
|
||||
("inventory", "carlocation"),
|
||||
("inventory", "customcard"),
|
||||
("inventory", "cartransfer"),
|
||||
@ -3955,7 +4099,7 @@ class BankAccountListView(LoginRequiredMixin, PermissionRequiredMixin, ListView)
|
||||
|
||||
def get_queryset(self):
|
||||
query = self.request.GET.get("q")
|
||||
dealer = get_user_type(self.request)
|
||||
dealer = self.request.dealer
|
||||
bank_accounts = BankAccountModel.objects.filter(entity_model=dealer.entity)
|
||||
return apply_search_filters(bank_accounts, query)
|
||||
|
||||
@ -4659,8 +4803,7 @@ def create_estimate(request, dealer_slug, slug=None):
|
||||
hash=item.get("item_id"),
|
||||
# finances__is_sold=False,
|
||||
colors__isnull=False,
|
||||
finances__isnull=False,
|
||||
finances__marked_price__gt=1,
|
||||
marked_price__gt=1,
|
||||
status="available",
|
||||
).all()
|
||||
|
||||
@ -4670,9 +4813,9 @@ def create_estimate(request, dealer_slug, slug=None):
|
||||
{
|
||||
"item_number": i.item_model.item_number,
|
||||
"quantity": 1,
|
||||
"unit_cost": round(float(i.finances.marked_price)),
|
||||
"unit_revenue": round(float(i.finances.marked_price)),
|
||||
"total_amount": round(float(i.finances.total_vat)),
|
||||
"unit_cost": round(float(i.marked_price)),
|
||||
"unit_revenue": round(float(i.marked_price)),
|
||||
"total_amount": round(float(i.total_vat)),# TODO : check later
|
||||
}
|
||||
)
|
||||
|
||||
@ -4690,10 +4833,10 @@ def create_estimate(request, dealer_slug, slug=None):
|
||||
# instance = models.Car.objects.get(vin=item.name)
|
||||
# estimate_itemtxs = {
|
||||
# item.item_number: {
|
||||
# "unit_cost": instance.finances.cost_price,
|
||||
# "unit_revenue": instance.finances.marked_price,
|
||||
# "unit_cost": instance.cost_price,
|
||||
# "unit_revenue": instance.marked_price,
|
||||
# "quantity": Decimal(quantities),
|
||||
# "total_amount": instance.finances.total_vat * int(quantities),
|
||||
# "total_amount": instance.total_vat * int(quantities),# TODO : check later
|
||||
# }
|
||||
# }
|
||||
|
||||
@ -4773,8 +4916,7 @@ def create_estimate(request, dealer_slug, slug=None):
|
||||
models.Car.objects.filter(
|
||||
dealer=dealer,
|
||||
colors__isnull=False,
|
||||
finances__isnull=False,
|
||||
finances__marked_price__gt=1,
|
||||
marked_price__gt=1,
|
||||
status="available",
|
||||
)
|
||||
.annotate(
|
||||
@ -4849,17 +4991,18 @@ class EstimateDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView
|
||||
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
||||
estimate = kwargs.get("object")
|
||||
if estimate.get_itemtxs_data():
|
||||
calculator = CarFinanceCalculator(estimate)
|
||||
finance_data = calculator.get_finance_data()
|
||||
# calculator = CarFinanceCalculator(estimate)
|
||||
# finance_data = calculator.get_finance_data()
|
||||
finance_data = get_finance_data(estimate,dealer)
|
||||
invoice_obj = InvoiceModel.objects.all().filter(ce_model=estimate).first()
|
||||
kwargs["data"] = finance_data
|
||||
|
||||
kwargs["invoice"] = invoice_obj
|
||||
try:
|
||||
car_finances = estimate.get_itemtxs_data()[0].first().item_model.car.finances
|
||||
selected_items = car_finances.additional_services.filter(dealer=dealer)
|
||||
car = estimate.get_itemtxs_data()[0].first().item_model.car
|
||||
selected_items = car.additional_services.filter(dealer=dealer)
|
||||
form = forms.AdditionalFinancesForm()
|
||||
form.fields["additional_finances"].queryset = form.fields["additional_finances"].queryset.filter(dealer=dealer)
|
||||
form.fields["additional_finances"].queryset = form.fields["additional_finances"].queryset.filter(dealer=dealer) # TODO : check later
|
||||
form.initial["additional_finances"] = selected_items
|
||||
kwargs["additionals_form"] = form
|
||||
except Exception as e:
|
||||
@ -4938,8 +5081,8 @@ def create_sale_order(request, dealer_slug, pk):
|
||||
# else:
|
||||
# form.fields["opportunity"].widget = HiddenInput()
|
||||
|
||||
calculator = CarFinanceCalculator(estimate)
|
||||
finance_data = calculator.get_finance_data()
|
||||
# calculator = CarFinanceCalculator(estimate)
|
||||
finance_data = get_finance_data(estimate,dealer)
|
||||
return render(
|
||||
request,
|
||||
"sales/estimates/sale_order_form.html",
|
||||
@ -4957,15 +5100,16 @@ def update_estimate_discount(request, dealer_slug, pk):
|
||||
content_type=ContentType.objects.get_for_model(EstimateModel),
|
||||
object_id=estimate.pk,
|
||||
)
|
||||
# calculator = CarFinanceCalculator(estimate)
|
||||
# finance_data = calculator.get_finance_data()
|
||||
discount_amount = request.POST.get("discount_amount", 0)
|
||||
calculator = CarFinanceCalculator(estimate)
|
||||
finance_data = calculator.get_finance_data()
|
||||
if Decimal(discount_amount) >= finance_data.get('cars')[0]['marked_price']:
|
||||
finance_data = get_finance_data(estimate,dealer)
|
||||
car = finance_data.get('car')
|
||||
if Decimal(discount_amount) >= car.marked_price:
|
||||
messages.error(request, _("Discount amount cannot be greater than marked price"))
|
||||
return redirect("estimate_detail", dealer_slug=dealer_slug, pk=pk)
|
||||
|
||||
print(finance_data.get('cars')[0]['marked_price'] * Decimal('0.5'))
|
||||
if Decimal(discount_amount) > finance_data.get('cars')[0]['marked_price'] * Decimal('0.5'):
|
||||
if Decimal(discount_amount) > car.marked_price * Decimal('0.5'):
|
||||
messages.warning(request, _("Discount amount is greater than 50% of the marked price, proceed with caution."))
|
||||
else:
|
||||
messages.success(request, _("Discount updated successfully"))
|
||||
@ -4983,10 +5127,10 @@ def update_estimate_additionals(request, dealer_slug, pk):
|
||||
if form.is_valid():
|
||||
estimate = get_object_or_404(EstimateModel, pk=pk)
|
||||
car = estimate.get_itemtxs_data()[0].first().item_model.car
|
||||
car.finances.additional_services.set(
|
||||
car.additional_services.set(
|
||||
form.cleaned_data["additional_finances"]
|
||||
)
|
||||
car.finances.save()
|
||||
car.save()
|
||||
messages.success(request, "Additional Finances updated successfully")
|
||||
return redirect("estimate_detail", dealer_slug=dealer_slug, pk=pk)
|
||||
|
||||
@ -5005,10 +5149,12 @@ class SaleOrderDetail(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
saleorder = kwargs.get("object")
|
||||
dealer = self.request.dealer
|
||||
estimate = saleorder.estimate
|
||||
if estimate.get_itemtxs_data():
|
||||
calculator = CarFinanceCalculator(estimate)
|
||||
finance_data = calculator.get_finance_data()
|
||||
# calculator = CarFinanceCalculator(estimate)
|
||||
# finance_data = calculator.get_finance_data()
|
||||
finance_data = get_finance_data(estimate,dealer)
|
||||
kwargs["data"] = finance_data
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
@ -5106,9 +5252,10 @@ class EstimatePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailVie
|
||||
def get_context_data(self, **kwargs):
|
||||
estimate = kwargs.get("object")
|
||||
if estimate.get_itemtxs_data():
|
||||
|
||||
# data = get_financial_values(estimate)
|
||||
calculator = CarFinanceCalculator(estimate)
|
||||
kwargs["data"] = calculator.get_finance_data()
|
||||
# calculator = CarFinanceCalculator(estimate)
|
||||
kwargs["data"] = get_finance_data(estimate,self.request.dealer)
|
||||
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
@ -5288,8 +5435,9 @@ class InvoiceDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView)
|
||||
invoice = kwargs.get("object")
|
||||
|
||||
if invoice.get_itemtxs_data():
|
||||
calculator = CarFinanceCalculator(invoice)
|
||||
finance_data = calculator.get_finance_data()
|
||||
# calculator = CarFinanceCalculator(invoice)
|
||||
# finance_data = calculator.get_finance_data()
|
||||
finance_data = get_finance_data(invoice,self.request.dealer)
|
||||
kwargs["data"] = finance_data
|
||||
kwargs["payments"] = JournalEntryModel.objects.filter(
|
||||
ledger=invoice.ledger
|
||||
@ -5515,16 +5663,17 @@ def invoice_create(request, dealer_slug, pk):
|
||||
ledger.save()
|
||||
invoice.save()
|
||||
|
||||
calculator = CarFinanceCalculator(estimate)
|
||||
finance_data = calculator.get_finance_data()
|
||||
# calculator = CarFinanceCalculator(estimate)
|
||||
# finance_data = calculator.get_finance_data()
|
||||
|
||||
finance_data = get_finance_data(estimate,dealer)
|
||||
car = finance_data.get("car")
|
||||
invoice_itemtxs = {
|
||||
i.get("item_number"): {
|
||||
"unit_cost": i.get("grand_total"),
|
||||
car.item_model.item_number: {
|
||||
"unit_cost": finance_data.get("grand_total"),
|
||||
"quantity": 1,
|
||||
"total_amount": i.get("grand_total"),
|
||||
"total_amount": finance_data.get("grand_total"),
|
||||
}
|
||||
for i in finance_data.get("cars")
|
||||
}
|
||||
|
||||
invoice_itemtxs = invoice.migrate_itemtxs(
|
||||
@ -5592,8 +5741,8 @@ class InvoicePreviewView(LoginRequiredMixin, PermissionRequiredMixin, DetailView
|
||||
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
||||
invoice = kwargs.get("object")
|
||||
if invoice.get_itemtxs_data():
|
||||
calculator = CarFinanceCalculator(invoice)
|
||||
finance_data = calculator.get_finance_data()
|
||||
# calculator = CarFinanceCalculator(invoice)
|
||||
finance_data = get_finance_data(invoice,dealer)
|
||||
kwargs["data"] = finance_data
|
||||
kwargs["dealer"] = dealer
|
||||
return super().get_context_data(**kwargs)
|
||||
@ -6938,11 +7087,11 @@ class OpportunityCreateView(
|
||||
if self.request.is_dealer:
|
||||
form.fields["lead"].queryset = models.Lead.objects.filter(
|
||||
dealer=dealer
|
||||
)
|
||||
)
|
||||
elif self.request.is_staff:
|
||||
form.fields["lead"].queryset = models.Lead.objects.filter(
|
||||
dealer=dealer, staff=self.request.staff
|
||||
)
|
||||
)
|
||||
return form
|
||||
|
||||
def get_success_url(self):
|
||||
@ -9499,7 +9648,7 @@ def pricing_page(request, dealer_slug):
|
||||
else:
|
||||
messages.info(request,_("You already have an plan!!"))
|
||||
return redirect('home',dealer_slug=dealer_slug)
|
||||
|
||||
|
||||
|
||||
|
||||
@login_required
|
||||
@ -10559,7 +10708,7 @@ def upload_cars(request, dealer_slug, pk=None):
|
||||
|
||||
response = redirect("upload_cars", dealer_slug=dealer_slug, pk=pk)
|
||||
if po_item.status == "uploaded":
|
||||
messages.add_message(request, messages.ERROR, "Item already uploaded.")
|
||||
messages.add_message(request, messages.SUCCESS, "Item uploaded Sucessfully.")
|
||||
return redirect(
|
||||
"view_items_inventory",
|
||||
dealer_slug=dealer_slug,
|
||||
@ -10687,13 +10836,13 @@ def upload_cars(request, dealer_slug, pk=None):
|
||||
vendor=vendor,
|
||||
receiving_date=receiving_date,
|
||||
)
|
||||
if po_item:
|
||||
models.CarFinance.objects.create(
|
||||
car=car,
|
||||
cost_price=po_item.item.unit_cost,
|
||||
marked_price=0,
|
||||
selling_price=0,
|
||||
)
|
||||
# if po_item: #TODO:update
|
||||
# models.CarFinance.objects.create(
|
||||
# car=car,
|
||||
# cost_price=po_item.item.unit_cost,
|
||||
# marked_price=0,
|
||||
# selling_price=0,
|
||||
# )
|
||||
car.add_colors(exterior=exterior, interior=interior)
|
||||
cars_created += 1
|
||||
logger.debug(
|
||||
@ -10765,11 +10914,8 @@ def bulk_update_car_price(request):
|
||||
else:
|
||||
for car_pk in cars:
|
||||
car = models.Car.objects.get(pk=car_pk)
|
||||
if not hasattr(car, "finances"):
|
||||
models.CarFinance.objects.create(car=car, cost_price=Decimal(price))
|
||||
else:
|
||||
car.finances.cost_price = Decimal(price)
|
||||
car.finances.save()
|
||||
car.cost_price = Decimal(price)
|
||||
car.save()
|
||||
messages.success(request, "Price updated successfully")
|
||||
|
||||
response = HttpResponse()
|
||||
@ -10979,11 +11125,11 @@ def car_sale_report_csv_export(request,dealer_slug):
|
||||
car.stock_type,
|
||||
car.created_at.strftime("%Y-%m-%d %H:%M:%S") if car.created_at else '',
|
||||
car.sold_date.strftime("%Y-%m-%d %H:%M:%S") if car.sold_date else '',
|
||||
car.finances.cost_price,
|
||||
car.finances.marked_price,
|
||||
car.finances.discount_amount,
|
||||
car.finances.selling_price,
|
||||
car.finances.vat_amount,
|
||||
car.cost_price,
|
||||
car.marked_price,
|
||||
car.discount_amount,
|
||||
car.selling_price,
|
||||
car.vat_amount, # TODO : check later
|
||||
car.item_model.invoicemodel_set.first().invoice_number
|
||||
])
|
||||
|
||||
@ -11232,4 +11378,8 @@ def ticket_update(request, ticket_id):
|
||||
return render(request, 'support/ticket_update.html', {
|
||||
'ticket': ticket,
|
||||
'form': form
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
class ChartOfAccountModelListView(ChartOfAccountModelListViewBase):
|
||||
template_name = 'chart_of_accounts/coa_list.html'
|
||||
38
templates/chart_of_accounts/coa_list.html
Normal file
38
templates/chart_of_accounts/coa_list.html
Normal file
@ -0,0 +1,38 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load icon from django_ledger %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="display-4 mb-0">Chart of Accounts</h1>
|
||||
<a href="{{ entity_model.get_coa_create_url }}" class="btn btn-success btn-lg">
|
||||
{% icon 'carbon:add-alt' 24 %} Add New
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if not inactive %}
|
||||
<a class="btn btn-warning mb-4" href="{{ entity_model.get_coa_list_inactive_url }}">
|
||||
{% trans 'Show Inactive' %}
|
||||
</a>
|
||||
{% else %}
|
||||
<a class="btn btn-warning mb-4" href="{{ entity_model.get_coa_list_url }}">
|
||||
{% trans 'Show Active' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</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>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
116
templates/chart_of_accounts/includes/coa_card.html
Normal file
116
templates/chart_of_accounts/includes/coa_card.html
Normal file
@ -0,0 +1,116 @@
|
||||
{% 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-success text-white{% elif not coa_model.is_active %}bg-danger text-white{% endif %} py-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="me-3">
|
||||
<i class="fas fa-building fa-2x"></i>
|
||||
</div>
|
||||
<div class="fw-bold fs-5">
|
||||
{{ coa_model.name }}
|
||||
{% if coa_model.is_default %}
|
||||
<span class="badge bg-light text-dark ms-2">{% trans 'DEFAULT' %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<span class="fw-bold me-2">{% trans 'Entity Default' %}:</span>
|
||||
{% if coa_model.is_default %}
|
||||
<span class="badge bg-success"><i class="fas fa-check-circle"></i></span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger"><i class="fas fa-times-circle"></i></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<span class="fw-bold me-2">{% trans 'Is Active' %}:</span>
|
||||
{% if coa_model.is_active %}
|
||||
<span class="badge bg-success"><i class="fas fa-check-circle"></i></span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger"><i class="fas fa-times-circle"></i></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<span class="fw-bold">CoA ID:</span>
|
||||
<span class="text-muted ms-2">{{ coa_model.slug }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-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">
|
||||
<div class="col-md-6">
|
||||
<small class="text-muted">
|
||||
<i class="far fa-calendar-plus me-1"></i>
|
||||
<span class="fw-bold">{% trans 'Created' %}:</span>
|
||||
{{ coa_model.created|date }}
|
||||
</small>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<small class="text-muted">
|
||||
<i class="far fa-clock me-1"></i>
|
||||
<span class="fw-bold">{% trans 'Updated' %}:</span>
|
||||
{{ coa_model.created|timesince }} {% trans 'ago' %}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer bg-transparent border-top-0">
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a href="{{ coa_model.get_update_url }}" class="btn btn-sm btn-outline-warning fw-bold">
|
||||
<i class="fas fa-edit me-1"></i> {% trans 'Update' %}
|
||||
</a>
|
||||
|
||||
<a href="{{ coa_model.get_account_list_url }}" class="btn btn-sm btn-outline-success fw-bold">
|
||||
<i class="fas fa-book me-1"></i> {% trans 'Accounts' %}
|
||||
</a>
|
||||
|
||||
<a href="{{ coa_model.get_create_coa_account_url }}" class="btn btn-sm btn-outline-info fw-bold">
|
||||
<i class="fas fa-plus-circle me-1"></i> {% trans 'Add Account' %}
|
||||
</a>
|
||||
|
||||
{% if coa_model.can_mark_as_default %}
|
||||
<a href="{{ coa_model.mark_as_default_url }}" class="btn btn-sm btn-outline-danger fw-bold">
|
||||
<i class="fas fa-star me-1"></i> {% trans 'Mark as Default' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if coa_model.can_deactivate %}
|
||||
<a href="{{ coa_model.mark_as_inactive_url }}" class="btn btn-sm btn-outline-warning fw-bold">
|
||||
<i class="fas fa-toggle-off me-1"></i> {% trans 'Mark as Inactive' %}
|
||||
</a>
|
||||
{% elif coa_model.can_activate %}
|
||||
<a href="{{ coa_model.mark_as_active_url }}" class="btn btn-sm btn-outline-success fw-bold">
|
||||
<i class="fas fa-toggle-on me-1"></i> {% trans 'Mark as Active' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -70,9 +70,9 @@
|
||||
</h3>
|
||||
{% endif %}
|
||||
<div class="d-flex align-items-center mb-4">
|
||||
{% if opportunity.car.finances %}
|
||||
{% if opportunity.car.marked_price %}
|
||||
<h5 class="mb-0 me-4">
|
||||
{{ opportunity.car.finances.total }} <span class="fw-light"><span class="icon-saudi_riyal"></span></span>
|
||||
{{ opportunity.car.total }} <span class="fw-light"><span class="icon-saudi_riyal"></span></span># TODO : check later
|
||||
</h5>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -75,7 +75,7 @@
|
||||
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.finances.total }}</p>
|
||||
<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">
|
||||
@ -107,7 +107,7 @@
|
||||
<td class="d-none d-sm-block pe-sm-2">:</td>
|
||||
<td class="py-1">
|
||||
<p class="ps-6 ps-sm-0 fw-semibold fs-9 mb-0 mb-0 pb-3 pb-sm-0 text-body-emphasis">
|
||||
{{ opportunity.car.finances.total }}
|
||||
{{ opportunity.car.total }}# TODO : check later
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
218
templates/dashboards/accountant_dashboard.html
Normal file
218
templates/dashboards/accountant_dashboard.html
Normal file
@ -0,0 +1,218 @@
|
||||
{% 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">Accountant Dashboard<i class="fas fa-chart-area text-primary ms-2"></i></h2>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Last 30 Days
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow">
|
||||
<li><a class="dropdown-item" href="#">Today</a></li>
|
||||
<li><a class="dropdown-item" href="#">Last 7 Days</a></li>
|
||||
<li><a class="dropdown-item" href="#">Last 90 Days</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Total Revenue</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">$1.25M</h4>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+8% from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Net Profit</p>
|
||||
<h4 class="fw-bolder text-success mb-3">$1.25M</h4>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+8% from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Gross Profit</p>
|
||||
<h4 class="fw-bolder text-info mb-3">$1.25M</h4>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+8% from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Total Expense</p>
|
||||
<h4 class="fw-bolder text-danger mb-3">$1.25M</h4>
|
||||
</div>
|
||||
<span class="badge bg-danger-subtle text-danger fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+8% from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="h5 fw-bold mb-3 text-dark">Breakdown by Vehicle Type</h4>
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card h-100 p-4 shadow-sm border-0">
|
||||
<p class="text-primary fw-bold mb-1">New Cars</p>
|
||||
<h4 class="fw-bolder mb-0">$1.25M</h4>
|
||||
<p class="text-muted small">Total Revenue</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card h-100 p-4 shadow-sm border-0">
|
||||
<p class="text-success fw-bold mb-1">Used Cars</p>
|
||||
<h4 class="fw-bolder mb-0">$0.75M</h4>
|
||||
<p class="text-muted small">Total Revenue</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card h-100 p-4 shadow-sm border-0">
|
||||
<p class="text-info fw-bold mb-1">New Cars</p>
|
||||
<h4 class="fw-bolder mb-0">$0.25M</h4>
|
||||
<p class="text-muted small">Net Profit</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card h-100 p-4 shadow-sm border-0">
|
||||
<p class="text-warning fw-bold mb-1">Used Cars</p>
|
||||
<h4 class="fw-bolder mb-0">$0.15M</h4>
|
||||
<p class="text-muted small">Net Profit</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-12">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">Monthly Revenue & Profit</h5>
|
||||
</div>
|
||||
<div class="card-body" style="height: 400px;">
|
||||
<canvas id="revenueProfitChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block customJS %}
|
||||
<script>
|
||||
// Define a color palette that aligns with the Phoenix template
|
||||
const primaryColor = '#7249b6'; // A vibrant purple
|
||||
const secondaryColor = '#8193a6'; // A muted gray/blue
|
||||
const successColor = '#00d074'; // A bright green
|
||||
const dangerColor = '#e63757'; // A deep red
|
||||
|
||||
const ctx = document.getElementById('revenueProfitChart').getContext('2d');
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Monthly Revenue',
|
||||
data: [120000, 150000, 130000, 180000, 200000, 175000, 190000, 220000, 210000, 250000, 240000, 280000],
|
||||
borderColor: primaryColor,
|
||||
backgroundColor: 'rgba(114, 73, 182, 0.1)',
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointBackgroundColor: primaryColor,
|
||||
pointRadius: 5,
|
||||
pointHoverRadius: 8
|
||||
},
|
||||
{
|
||||
label: 'Monthly Net Profit',
|
||||
data: [25000, 35000, 28000, 40000, 45000, 38000, 42000, 50000, 48000, 55000, 52000, 60000],
|
||||
borderColor: successColor,
|
||||
backgroundColor: 'rgba(0, 208, 116, 0.1)',
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointBackgroundColor: successColor,
|
||||
pointRadius: 5,
|
||||
pointHoverRadius: 8
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
labels: {
|
||||
color: secondaryColor,
|
||||
boxWidth: 20
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(33, 37, 41, 0.9)',
|
||||
titleColor: 'white',
|
||||
bodyColor: 'white',
|
||||
padding: 10,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
if (context.parsed.y !== null) {
|
||||
label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(context.parsed.y);
|
||||
}
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: 'rgba(0, 0, 0, 0.05)'
|
||||
},
|
||||
ticks: {
|
||||
color: secondaryColor
|
||||
},
|
||||
border: {
|
||||
color: secondaryColor
|
||||
}
|
||||
},
|
||||
y: {
|
||||
grid: {
|
||||
color: 'rgba(0, 0, 0, 0.05)'
|
||||
},
|
||||
ticks: {
|
||||
color: secondaryColor
|
||||
},
|
||||
border: {
|
||||
color: secondaryColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
243
templates/dashboards/dealer_dashbaord.html
Normal file
243
templates/dashboards/dealer_dashbaord.html
Normal file
@ -0,0 +1,243 @@
|
||||
{% 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 text-dark mb-0">Business Health Dashboard <i class="fas fa-chart-area text-primary ms-2"></i></h2>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Last 30 Days
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow">
|
||||
<li><a class="dropdown-item" href="#">Today</a></li>
|
||||
<li><a class="dropdown-item" href="#">Last 7 Days</a></li>
|
||||
<li><a class="dropdown-item" href="#">Last 90 Days</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Total Revenue</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">$1.25M</h4>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+8% from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Net Profit</p>
|
||||
<h4 class="fw-bolder text-success mb-3">$1.25M</h4>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+8% from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Gross Profit</p>
|
||||
<h4 class="fw-bolder text-info mb-3">$1.25M</h4>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+8% from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Total Expense</p>
|
||||
<h4 class="fw-bolder text-danger mb-3">$1.25M</h4>
|
||||
</div>
|
||||
<span class="badge bg-danger-subtle text-danger fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
-2% from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Total VAT Collected</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">$1.25M</h4>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+8% from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Total Cars Sold</p>
|
||||
<h4 class="fw-bolder text-success mb-3">{{ sold_cars }}</h4>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+5 units from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-8">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">Monthly Revenue & Profit</h5>
|
||||
</div>
|
||||
<div class="card-body" style="height: 400px;">
|
||||
<canvas id="revenueProfitChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">Monthly Cars Sold</h5>
|
||||
</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;">
|
||||
<canvas id="CarsSoldByMonthChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
{% endblock content %}
|
||||
|
||||
|
||||
{% block customJS %}
|
||||
<script>
|
||||
// Define a color palette that aligns with the Phoenix template
|
||||
const primaryColor = '#7249b6'; // A vibrant purple
|
||||
const secondaryColor = '#8193a6'; // A muted gray/blue
|
||||
const successColor = '#00d074'; // A bright green
|
||||
const dangerColor = '#e63757'; // A deep red
|
||||
|
||||
// Monthly Cars Sold (Bar Chart)
|
||||
const ctx1 = document.getElementById('CarsSoldByMonthChart').getContext('2d');
|
||||
new Chart(ctx1, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
||||
datasets: [{
|
||||
label: 'Total Cars Sold',
|
||||
data: [2, 3, 10, 4, 30, 12, 8, 9, 20, 12, 15, 35],
|
||||
backgroundColor: primaryColor,
|
||||
borderColor: primaryColor,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false }
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
||||
ticks: { color: secondaryColor }
|
||||
},
|
||||
x: {
|
||||
grid: { display: false },
|
||||
ticks: { color: secondaryColor }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Monthly Revenue & Profit (Line Chart)
|
||||
const ctx2 = document.getElementById('revenueProfitChart').getContext('2d');
|
||||
new Chart(ctx2, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Monthly Revenue',
|
||||
data: [120000, 150000, 130000, 180000, 200000, 175000, 190000, 220000, 210000, 250000, 240000, 280000],
|
||||
borderColor: primaryColor,
|
||||
backgroundColor: 'rgba(114, 73, 182, 0.1)', // Using primaryColor with transparency
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointBackgroundColor: primaryColor,
|
||||
pointRadius: 5,
|
||||
pointHoverRadius: 8
|
||||
},
|
||||
{
|
||||
label: 'Monthly Net Profit',
|
||||
data: [25000, 35000, 28000, 40000, 45000, 38000, 42000, 50000, 48000, 55000, 52000, 60000],
|
||||
borderColor: successColor,
|
||||
backgroundColor: 'rgba(0, 208, 116, 0.1)', // Using successColor with transparency
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointBackgroundColor: successColor,
|
||||
pointRadius: 5,
|
||||
pointHoverRadius: 8
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
labels: { color: '#495057', boxWidth: 20 }
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(33, 37, 41, 0.9)',
|
||||
titleColor: 'white',
|
||||
bodyColor: 'white',
|
||||
padding: 10,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
if (context.parsed.y !== null) {
|
||||
label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(context.parsed.y);
|
||||
}
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
||||
ticks: { color: secondaryColor },
|
||||
border: { color: secondaryColor }
|
||||
},
|
||||
y: {
|
||||
grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
||||
ticks: { color: secondaryColor },
|
||||
border: { color: secondaryColor }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
300
templates/dashboards/inventory_dashboard.html
Normal file
300
templates/dashboards/inventory_dashboard.html
Normal file
@ -0,0 +1,300 @@
|
||||
{% 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">Inventory Dashboard <i class="fas fa-warehouse text-primary ms-2"></i></h2>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Last 30 Days
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow">
|
||||
<li><a class="dropdown-item" href="#">Today</a></li>
|
||||
<li><a class="dropdown-item" href="#">Last 7 Days</a></li>
|
||||
<li><a class="dropdown-item" href="#">Last 90 Days</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Total Cars in Inventory</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ total_cars_in_inventory }}</h4>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+5 units from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">New Cars in Inventory</p>
|
||||
<h4 class="fw-bolder text-success mb-3">{{ new_cars_in_inventory }}</h4>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+5 units from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Used Cars in Inventory</p>
|
||||
<h4 class="fw-bolder text-info mb-3">{{ old_cars_in_inventory }}</h4>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+5 units from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Inventory Value</p>
|
||||
<h4 class="fw-bolder text-warning mb-3">$5.8M</h4>
|
||||
</div>
|
||||
<span class="badge bg-danger-subtle text-danger fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
-2% from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card h-100 p-4 shadow-sm border-0">
|
||||
<p class="text-warning fw-bold mb-1">New Car Value</p>
|
||||
<h4 class="fw-bolder mb-0">$3.2M</h4>
|
||||
<p class="text-muted small">Total new cars value</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card h-100 p-4 shadow-sm border-0">
|
||||
<p class="text-info fw-bold mb-1">Used Car Value</p>
|
||||
<h4 class="fw-bolder mb-0">$2.6M</h4>
|
||||
<p class="text-muted small">Total used cars value</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card h-100 p-4 shadow-sm border-0">
|
||||
<p class="text-danger fw-bold mb-1">Average Time on Lot</p>
|
||||
<h4 class="fw-bolder mb-0">10 days</h4>
|
||||
<p class="text-muted small">Average for all vehicles</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card h-100 p-4 shadow-sm border-0">
|
||||
<p class="text-danger fw-bold mb-1">Aging Inventory</p>
|
||||
<h4 class="fw-bolder mb-0">12 cars</h4>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
-4 cars from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--charts-->
|
||||
<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">Inventory by Make</h5>
|
||||
</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;">
|
||||
<canvas id="inventoryByBrandChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">Car Models by Make in Inventory</h5>
|
||||
</div>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 mb-4">
|
||||
<label for="carMakeSelect" class="form-label fs-7">Select a Car Make:</label>
|
||||
<select id="carMakeSelect" class="form-select">
|
||||
<option value="" disabled selected>-- Choose a make --</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;">
|
||||
<canvas id="inventoryChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
{% endblock content %}
|
||||
|
||||
{% block customJS %}
|
||||
<script>
|
||||
// Define a custom color palette that matches the Phoenix template
|
||||
const primaryColor = '#7249b6'; // A vibrant purple
|
||||
const secondaryColor = '#8193a6'; // A muted gray/blue
|
||||
const successColor = '#00d074'; // A bright green
|
||||
const infoColor = '#0085ff'; // A medium blue
|
||||
const warningColor = '#ffc83b'; // A yellow
|
||||
const dangerColor = '#e63757'; // A deep red
|
||||
|
||||
const ctx4 = document.getElementById('inventoryByBrandChart').getContext('2d');
|
||||
|
||||
// Chart.js configuration for the Pie Chart
|
||||
new Chart(ctx4, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: ['Toyota', 'Ford', 'Honda', 'BMW', 'Other'],
|
||||
datasets: [{
|
||||
label: 'Car Count by Make',
|
||||
data: [45, 30, 25, 15, 10], // Sample data for car counts
|
||||
backgroundColor: [
|
||||
primaryColor,
|
||||
successColor,
|
||||
warningColor,
|
||||
infoColor,
|
||||
secondaryColor
|
||||
],
|
||||
hoverOffset: 15,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right', // Places the legend on the right side
|
||||
labels: {
|
||||
color: secondaryColor, // Use a muted color for a subtle look
|
||||
font: {
|
||||
size: 14
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(33, 37, 41, 0.9)', // Bootstrap dark
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#fff',
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const label = context.label || '';
|
||||
const value = context.parsed || 0;
|
||||
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
||||
const percentage = ((value / total) * 100).toFixed(2);
|
||||
return `${label}: ${value} cars (${percentage}%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//chart for number of cars of each model in the inventory
|
||||
|
||||
|
||||
|
||||
// Sample data representing your inventory
|
||||
const inventoryData = [
|
||||
{ make: 'Ford', model: 'Mustang', count: 5 },
|
||||
{ make: 'Ford', model: 'F-150', count: 12 },
|
||||
{ make: 'Ford', model: 'Explorer', count: 8 },
|
||||
{ make: 'Toyota', model: 'Camry', count: 10 },
|
||||
{ make: 'Toyota', model: 'Corolla', count: 15 },
|
||||
{ make: 'Toyota', model: 'RAV4', count: 7 },
|
||||
{ make: 'BMW', model: 'X5', count: 4 },
|
||||
{ make: 'BMW', model: '3 Series', count: 6 },
|
||||
{ make: 'BMW', model: 'i8', count: 2 },
|
||||
];
|
||||
|
||||
// Get the unique list of car makes for the dropdown
|
||||
const carMakes = [...new Set(inventoryData.map(item => item.make))];
|
||||
|
||||
const selectElement = document.getElementById('carMakeSelect');
|
||||
const canvasElement = document.getElementById('inventoryChart');
|
||||
let chartInstance = null; // Stores the current chart instance
|
||||
|
||||
// Populate the dropdown menu
|
||||
carMakes.forEach(make => {
|
||||
const option = document.createElement('option');
|
||||
option.value = make;
|
||||
option.textContent = make;
|
||||
selectElement.appendChild(option);
|
||||
});
|
||||
|
||||
// Function to create or update the chart
|
||||
function renderChart(data, type = 'bar') {
|
||||
// Destroy the old chart instance if it exists
|
||||
if (chartInstance) {
|
||||
chartInstance.destroy();
|
||||
}
|
||||
|
||||
const chartLabels = data.map(item => item.model);
|
||||
const chartValues = data.map(item => item.count);
|
||||
|
||||
// Generate random colors for the chart segments
|
||||
const backgroundColor = chartLabels.map(() => `rgba(${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, ${Math.floor(Math.random() * 255)}, 0.6)`);
|
||||
const borderColor = backgroundColor.map(color => color.replace('0.6', '1'));
|
||||
|
||||
// Create the new chart
|
||||
chartInstance = new Chart(canvasElement, {
|
||||
type: type, // Can be 'bar' or 'pie'
|
||||
data: {
|
||||
labels: chartLabels,
|
||||
datasets: [{
|
||||
label: 'Number of Cars',
|
||||
data: chartValues,
|
||||
backgroundColor: backgroundColor,
|
||||
borderColor: borderColor,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Event listener for the dropdown menu
|
||||
selectElement.addEventListener('change', (event) => {
|
||||
const selectedMake = event.target.value;
|
||||
if (selectedMake) {
|
||||
const filteredData = inventoryData.filter(item => item.make === selectedMake);
|
||||
// The second parameter can be 'pie' to switch chart types
|
||||
renderChart(filteredData, 'bar'); // Use 'bar' for a bar chart
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -1,641 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<div class="main-content flex-grow-1 mt-4 mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<p class="fs-2">Dashboard Overview<i class="fas fa-chart-area ms-3 text-primary fs-3"></i></p>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
Last 30 Days
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<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-md-6 col-lg-3">
|
||||
<div class="card h-100 p-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-primary">Total Revenue</h5>
|
||||
<p class="stat-value">$1.25M</p>
|
||||
<span class="text-success small">+8% from last month</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card h-100 p-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-primary">Net Profit</h5>
|
||||
<p class="stat-value">$1.25M</p>
|
||||
<span class="text-success small">+8% from last month</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card h-100 p-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-primary">Gross Profit</h5>
|
||||
<p class="stat-value">$1.25M</p>
|
||||
<span class="text-success small">+8% from last month</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card h-100 p-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-danger">Total Expense</h5>
|
||||
<p class="stat-value">$1.25M</p>
|
||||
<span class="text-success small">+8% from last month</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card h-100 p-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-primary">Total VAT collected</h5>
|
||||
<p class="stat-value">$1.25M</p>
|
||||
<span class="text-success small">+8% from last month</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card h-100 p-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-success">Cars Sold</h5>
|
||||
<p class="stat-value">{{sold_cars}}</p>
|
||||
<span class="text-success small">+5 units from last month</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card h-100 p-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-danger">Average Time on Lot</h5>
|
||||
<p class="stat-value">10 days</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card h-100 p-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-warning">Inventory Value</h5>
|
||||
<p class="stat-value">$5.8M</p>
|
||||
<span class="text-danger small">-2% from last month</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card h-100 p-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-danger">Aging Inventory</h5>
|
||||
<p class="stat-value">12</p>
|
||||
<span class="text-success small">-4 cars from last month</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-8">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">Monthly Revenue & Profit</div>
|
||||
<div class="card-body chart-container">
|
||||
<canvas id="revenueProfitChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">Monthly Cars Sold</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center">
|
||||
<canvas id="CarsSoldByMonthChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">Inventory By Make</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center">
|
||||
<canvas id="salesByBrandChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">Sales By Make</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center">
|
||||
<canvas id="inventoryByBrandChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">Salesperson Performance</div>
|
||||
<div class="card-body chart-container">
|
||||
<canvas id="salespersonChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">Top 5 Lead Sources</div>
|
||||
<div class="card-body chart-container">
|
||||
<canvas id="leadSourcesChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">Appointments by Weekday</div>
|
||||
<div class="card-body chart-container">
|
||||
<canvas id="appointmentsChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-12">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">Lead Conversion Funnel</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center">
|
||||
<canvas id="leadFunnelChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
{% endblock content %}
|
||||
|
||||
|
||||
{% block customJS%}
|
||||
|
||||
<script>
|
||||
ctx1=document.getElementById('CarsSoldByMonthChart')
|
||||
new Chart(ctx1,{
|
||||
type: 'bar',
|
||||
|
||||
data: {
|
||||
labels:['January','February','March','April','May','June','July','August','September','October', 'November','December' ],
|
||||
|
||||
datasets: [{
|
||||
label: 'Total Cars Sold',
|
||||
data: [2,3,10,4,30,12,8,9,20,12,15,35],
|
||||
borderWidth: 1
|
||||
}]
|
||||
|
||||
},
|
||||
|
||||
|
||||
options: {
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
// Get the canvas context for the chart
|
||||
const ctx2 = document.getElementById('revenueProfitChart').getContext('2d');
|
||||
|
||||
// Chart.js configuration
|
||||
new Chart(ctx2, {
|
||||
type: 'line', // Use a line chart
|
||||
data: {
|
||||
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Monthly Revenue',
|
||||
data: [120000, 150000, 130000, 180000, 200000, 175000, 190000, 220000, 210000, 250000, 240000, 280000],
|
||||
borderColor: '#007bff', // A vibrant blue
|
||||
backgroundColor: 'rgba(0, 123, 255, 0.2)',
|
||||
tension: 0.4, // Smooths the line
|
||||
fill: true, // Fills the area under the line
|
||||
pointBackgroundColor: '#007bff',
|
||||
pointRadius: 5,
|
||||
pointHoverRadius: 8
|
||||
},
|
||||
{
|
||||
label: 'Monthly Net Profit',
|
||||
data: [25000, 35000, 28000, 40000, 45000, 38000, 42000, 50000, 48000, 55000, 52000, 60000],
|
||||
borderColor: '#28a745', // A strong green
|
||||
backgroundColor: 'rgba(40, 167, 69, 0.2)',
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointBackgroundColor: '#28a745',
|
||||
pointRadius: 5,
|
||||
pointHoverRadius: 8
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
labels: {
|
||||
color: '#495057', // Darker legend text
|
||||
boxWidth: 20
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(52, 58, 64, 0.9)', // Darker tooltip background
|
||||
titleColor: 'white',
|
||||
bodyColor: 'white',
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
if (context.parsed.y !== null) {
|
||||
label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(context.parsed.y);
|
||||
}
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: 'rgba(0, 0, 0, 0.1)' // Lighter grid lines for better contrast
|
||||
},
|
||||
ticks: {
|
||||
color: '#495057' // Darker text for x-axis labels
|
||||
},
|
||||
border: {
|
||||
color: '#adb5bd' // A subtle border color
|
||||
}
|
||||
},
|
||||
y: {
|
||||
grid: {
|
||||
color: 'rgba(0, 0, 0, 0.1)'
|
||||
},
|
||||
ticks: {
|
||||
color: '#495057' // Darker text for y-axis labels
|
||||
},
|
||||
border: {
|
||||
color: '#adb5bd'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Get the canvas context for the chart
|
||||
const ctx3 = document.getElementById('salesByBrandChart').getContext('2d');
|
||||
|
||||
// Chart.js configuration for the Pie Chart
|
||||
new Chart(ctx3, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: ['Toyota', 'Ford', 'Honda', 'BMW', 'Other'],
|
||||
datasets: [{
|
||||
label: 'Car Count by Make',
|
||||
data: [45, 30, 25, 15, 10], // Sample data for car counts
|
||||
backgroundColor: [
|
||||
'#007bff', // Blue
|
||||
'#28a745', // Green
|
||||
'#ffc107', // Yellow
|
||||
'#dc3545', // Red
|
||||
'#6c757d' // Gray
|
||||
],
|
||||
hoverOffset: 15,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right', // Places the legend on the right side
|
||||
labels: {
|
||||
color: '#343a40', // Dark text color for labels
|
||||
font: {
|
||||
size: 14
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(52, 58, 64, 0.9)',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#fff',
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const label = context.label || '';
|
||||
const value = context.parsed || 0;
|
||||
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
||||
const percentage = ((value / total) * 100).toFixed(2);
|
||||
return `${label}: ${value} cars (${percentage}%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Get the canvas context for the chart
|
||||
const ctx4 = document.getElementById('inventoryByBrandChart').getContext('2d');
|
||||
|
||||
// Chart.js configuration for the Pie Chart
|
||||
new Chart(ctx4, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: ['Toyota', 'Ford', 'Honda', 'BMW', 'Other'],
|
||||
datasets: [{
|
||||
label: 'Car Count by Make',
|
||||
data: [45, 30, 25, 15, 10], // Sample data for car counts
|
||||
backgroundColor: [
|
||||
'#007bff', // Blue
|
||||
'#28a745', // Green
|
||||
'#ffc107', // Yellow
|
||||
'#dc3545', // Red
|
||||
'#6c757d' // Gray
|
||||
],
|
||||
hoverOffset: 15,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right', // Places the legend on the right side
|
||||
labels: {
|
||||
color: '#343a40', // Dark text color for labels
|
||||
font: {
|
||||
size: 14
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(52, 58, 64, 0.9)',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#fff',
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const label = context.label || '';
|
||||
const value = context.parsed || 0;
|
||||
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
||||
const percentage = ((value / total) * 100).toFixed(2);
|
||||
return `${label}: ${value} cars (${percentage}%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Get the canvas context for the chart
|
||||
const ctx_salesperson = document.getElementById('salespersonChart').getContext('2d');
|
||||
|
||||
// Chart.js configuration for the Salesperson Performance Bar Chart
|
||||
new Chart(ctx_salesperson, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['John Doe', 'Jane Smith', 'Peter Jones', 'Mary Brown'],
|
||||
datasets: [{
|
||||
label: 'Cars Sold',
|
||||
data: [15, 22, 18, 25], // Sample data for cars sold by each salesperson
|
||||
backgroundColor: [
|
||||
'#007bff', // Blue
|
||||
'#28a745', // Green
|
||||
'#ffc107', // Yellow
|
||||
'#dc3545' // Red
|
||||
],
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false // Hide the legend for a single dataset
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Salesperson Performance',
|
||||
font: {
|
||||
size: 16
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Salesperson Name'
|
||||
},
|
||||
ticks: {
|
||||
color: '#495057' // Dark text for x-axis labels
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Number of Cars Sold'
|
||||
},
|
||||
ticks: {
|
||||
color: '#495057' // Dark text for y-axis labels
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Get the canvas context for the chart
|
||||
const ctx_leadSources = document.getElementById('leadSourcesChart').getContext('2d');
|
||||
|
||||
// Chart.js configuration for the Top 5 Lead Sources Bar Chart
|
||||
new Chart(ctx_leadSources, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['Showroom', 'Referrals', 'WhatsApp', 'Facebook', 'TikTok'], // Labels from the provided list
|
||||
datasets: [{
|
||||
label: 'Number of Leads',
|
||||
data: [45, 35, 25, 20, 15], // Sample data for leads from each source
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.8)', // A consistent blue color for the bars
|
||||
borderColor: 'rgba(54, 162, 235, 1)',
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
indexAxis: 'y', // Makes it a horizontal bar chart
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Top 5 Lead Sources',
|
||||
font: {
|
||||
size: 16
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Number of Leads',
|
||||
color: '#495057'
|
||||
},
|
||||
ticks: {
|
||||
color: '#495057'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
ticks: {
|
||||
color: '#495057',
|
||||
font: {
|
||||
size: 8 // Decreases the font size to fit more text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// Get the canvas context for the chart
|
||||
const ctx_appointments = document.getElementById('appointmentsChart').getContext('2d');
|
||||
|
||||
// Chart.js configuration for the Appointments by Weekday Bar Chart
|
||||
new Chart(ctx_appointments, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'],
|
||||
datasets: [{
|
||||
label: 'Number of Appointments',
|
||||
data: [10, 15, 20, 18, 25, 30, 12], // Sample data for appointments per day
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.8)', // A consistent color for the bars
|
||||
borderColor: 'rgba(75, 192, 192, 1)',
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Appointments by Weekday',
|
||||
font: {
|
||||
size: 16
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Day of the Week',
|
||||
color: '#495057'
|
||||
},
|
||||
ticks: {
|
||||
color: '#495057'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Number of Appointments',
|
||||
color: '#495057'
|
||||
},
|
||||
ticks: {
|
||||
color: '#495057'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Get the canvas context for the funnel chart
|
||||
const ctx_funnel = document.getElementById('leadFunnelChart').getContext('2d');
|
||||
|
||||
// Sample data for the funnel stages
|
||||
const funnelData = {
|
||||
labels: ['Initial Contact', 'Qualified Leads', 'Test Drives', 'Negotiation', 'Closed Deals'],
|
||||
data: [250, 180, 120, 80, 45], // Example lead counts at each stage
|
||||
};
|
||||
|
||||
new Chart(ctx_funnel, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: funnelData.labels,
|
||||
datasets: [{
|
||||
label: 'Number of Leads',
|
||||
data: funnelData.data,
|
||||
backgroundColor: [
|
||||
'rgba(0, 123, 255, 0.8)',
|
||||
'rgba(0, 123, 255, 0.7)',
|
||||
'rgba(0, 123, 255, 0.6)',
|
||||
'rgba(0, 123, 255, 0.5)',
|
||||
'rgba(0, 123, 255, 0.4)'
|
||||
],
|
||||
borderColor: 'rgba(0, 123, 255, 1)',
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
indexAxis: 'y', // Makes it a horizontal bar chart
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Lead Conversion Funnel',
|
||||
font: {
|
||||
size: 16
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const totalLeads = funnelData.data[0];
|
||||
const currentLeads = context.parsed.x;
|
||||
const percentage = ((currentLeads / totalLeads) * 100).toFixed(1);
|
||||
return `Leads: ${currentLeads} (${percentage}%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
beginAtZero: true,
|
||||
display: false // Hide the x-axis to make it look like a funnel
|
||||
},
|
||||
y: {
|
||||
ticks: {
|
||||
color: '#495057'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
367
templates/dashboards/manager_dashboard.html
Normal file
367
templates/dashboards/manager_dashboard.html
Normal file
@ -0,0 +1,367 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
|
||||
<div class="main-content flex-grow-1 container-fluid mt-4 mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-5 pb-3 border-bottom">
|
||||
<h2 class="h3 fw-bold mb-0">Manager Dashboard<i class="fas fa-chart-area text-primary ms-2"></i></h2>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Last 30 Days
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow">
|
||||
<li><a class="dropdown-item" href="#">Today</a></li>
|
||||
<li><a class="dropdown-item" href="#">Last 7 Days</a></li>
|
||||
<li><a class="dropdown-item" href="#">Last 90 Days</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Total Revenue</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">$1.25M</h4>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+8% from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Net Profit</p>
|
||||
<h4 class="fw-bolder text-success mb-3">$1.25M</h4>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+8% from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Total Expense</p>
|
||||
<h4 class="fw-bolder text-danger mb-3">$1.25M</h4>
|
||||
</div>
|
||||
<span class="badge bg-danger-subtle text-danger fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+3% from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Total Cars Sold</p>
|
||||
<h4 class="fw-bolder text-success mb-3">{{ sold_cars }}</h4>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+5 units from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Avg. Time on Lot</p>
|
||||
<h4 class="fw-bolder text-warning mb-3">10 days</h4>
|
||||
</div>
|
||||
<span class="badge bg-danger-subtle text-danger fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+2 days from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Inventory Value</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">$5.8M</h4>
|
||||
</div>
|
||||
<span class="badge bg-danger-subtle text-danger fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
-2% from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Aging Inventory</p>
|
||||
<h4 class="fw-bolder text-danger mb-3">12 units</h4>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
-4 cars from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Gross Profit</p>
|
||||
<h4 class="fw-bolder text-info mb-3">$1.25M</h4>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+8% from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-lg-8">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">Monthly Revenue & Profit</h5>
|
||||
</div>
|
||||
<div class="card-body" style="height: 400px;">
|
||||
<canvas id="revenueProfitChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">Monthly Cars Sold</h5>
|
||||
</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;">
|
||||
<canvas id="CarsSoldByMonthChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">Sales by Make</h5>
|
||||
</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;">
|
||||
<canvas id="salesByBrandChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">Top Salesperson Performance</h5>
|
||||
</div>
|
||||
<div class="card-body" style="height: 400px;">
|
||||
<canvas id="salespersonChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
{% endblock content %}
|
||||
|
||||
|
||||
{% block customJS %}
|
||||
<script>
|
||||
// Define a color palette that aligns with the Phoenix template
|
||||
const primaryColor = '#7249b6'; // A vibrant purple
|
||||
const secondaryColor = '#8193a6'; // A muted gray/blue
|
||||
const successColor = '#00d074'; // A bright green
|
||||
const warningColor = '#ffc107'; // A strong yellow
|
||||
const dangerColor = '#e63757'; // A deep red
|
||||
const chartColors = ['#00d27a', '#7249b6', '#32b9ff', '#e63757', '#ffc107'];
|
||||
|
||||
// Monthly Cars Sold (Bar Chart)
|
||||
const ctx1 = document.getElementById('CarsSoldByMonthChart').getContext('2d');
|
||||
new Chart(ctx1, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
||||
datasets: [{
|
||||
label: 'Total Cars Sold',
|
||||
data: [2, 3, 10, 4, 30, 12, 8, 9, 20, 12, 15, 35],
|
||||
backgroundColor: primaryColor,
|
||||
borderColor: primaryColor,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false }
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
||||
ticks: { color: secondaryColor }
|
||||
},
|
||||
x: {
|
||||
grid: { display: false },
|
||||
ticks: { color: secondaryColor }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Monthly Revenue & Profit (Line Chart)
|
||||
const ctx2 = document.getElementById('revenueProfitChart').getContext('2d');
|
||||
new Chart(ctx2, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Monthly Revenue',
|
||||
data: [120000, 150000, 130000, 180000, 200000, 175000, 190000, 220000, 210000, 250000, 240000, 280000],
|
||||
borderColor: primaryColor,
|
||||
backgroundColor: 'rgba(114, 73, 182, 0.1)', // Using primaryColor with transparency
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointBackgroundColor: primaryColor,
|
||||
pointRadius: 5,
|
||||
pointHoverRadius: 8
|
||||
},
|
||||
{
|
||||
label: 'Monthly Net Profit',
|
||||
data: [25000, 35000, 28000, 40000, 45000, 38000, 42000, 50000, 48000, 55000, 52000, 60000],
|
||||
borderColor: successColor,
|
||||
backgroundColor: 'rgba(0, 208, 116, 0.1)', // Using successColor with transparency
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointBackgroundColor: successColor,
|
||||
pointRadius: 5,
|
||||
pointHoverRadius: 8
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
labels: { color: '#495057', boxWidth: 20 }
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(33, 37, 41, 0.9)',
|
||||
titleColor: 'white',
|
||||
bodyColor: 'white',
|
||||
padding: 10,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
if (context.parsed.y !== null) {
|
||||
label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(context.parsed.y);
|
||||
}
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
||||
ticks: { color: secondaryColor },
|
||||
border: { color: secondaryColor }
|
||||
},
|
||||
y: {
|
||||
grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
||||
ticks: { color: secondaryColor },
|
||||
border: { color: secondaryColor }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Sales by Make (Pie Chart)
|
||||
const ctx3 = document.getElementById('salesByBrandChart').getContext('2d');
|
||||
new Chart(ctx3, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: ['Toyota', 'Ford', 'Honda', 'BMW', 'Other'],
|
||||
datasets: [{
|
||||
label: 'Car Count by Make',
|
||||
data: [45, 30, 25, 15, 10],
|
||||
backgroundColor: chartColors,
|
||||
hoverOffset: 15,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
labels: { color: '#343a40', font: { size: 14 } }
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(33, 37, 41, 0.9)',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#fff',
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const label = context.label || '';
|
||||
const value = context.parsed || 0;
|
||||
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
||||
const percentage = ((value / total) * 100).toFixed(2);
|
||||
return `${label}: ${value} cars (${percentage}%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Salesperson Performance (Bar Chart)
|
||||
const ctx_salesperson = document.getElementById('salespersonChart').getContext('2d');
|
||||
new Chart(ctx_salesperson, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['John Doe', 'Jane Smith', 'Peter Jones', 'Mary Brown'],
|
||||
datasets: [{
|
||||
label: 'Cars Sold',
|
||||
data: [15, 22, 18, 25],
|
||||
backgroundColor: chartColors,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
title: { display: true, text: 'Top Salesperson Performance', font: { size: 16 } }
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: { display: true, text: 'Salesperson Name', color: secondaryColor },
|
||||
ticks: { color: secondaryColor }
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: { display: true, text: 'Number of Cars Sold', color: secondaryColor },
|
||||
ticks: { color: secondaryColor }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
356
templates/dashboards/sales_dashboard.html
Normal file
356
templates/dashboards/sales_dashboard.html
Normal file
@ -0,0 +1,356 @@
|
||||
{% 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">Sales & Leads Dashboard <i class="fas fa-chart-area text-primary ms-2"></i></h2>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Last 30 Days
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow">
|
||||
<li><a class="dropdown-item" href="#">Today</a></li>
|
||||
<li><a class="dropdown-item" href="#">Last 7 Days</a></li>
|
||||
<li><a class="dropdown-item" href="#">Last 90 Days</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Total Cars Sold</p>
|
||||
<h4 class="fw-bolder text-success mb-3">{{ sold_cars }}</h4>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+5 units from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">Used Cars Sold</p>
|
||||
<h4 class="fw-bolder text-info mb-3">{{ sold_used_cars }}</h4>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+5 units from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-body d-flex flex-column justify-content-between p-4">
|
||||
<div>
|
||||
<p class="text-uppercase text-muted fw-bold small mb-1">New Cars Sold</p>
|
||||
<h4 class="fw-bolder text-primary mb-3">{{ sold_new_cars }}</h4>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
|
||||
+5 units from last month
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">Monthly Cars Sold</h5>
|
||||
</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;">
|
||||
<canvas id="CarsSoldByMonthChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div 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">Sales by Make</h5>
|
||||
</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;">
|
||||
<canvas id="salesByBrandChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">Top 5 Lead Sources</h5>
|
||||
</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;">
|
||||
<canvas id="leadSourcesChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">Appointments by Weekday</h5>
|
||||
</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;">
|
||||
<canvas id="appointmentsChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-12">
|
||||
<div class="card h-100 shadow-sm border-0">
|
||||
<div class="card-header bg-white border-bottom-0">
|
||||
<h5 class="fw-bold mb-0 text-dark">Lead Conversion Funnel</h5>
|
||||
</div>
|
||||
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;">
|
||||
<canvas id="leadFunnelChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
{% endblock content %}
|
||||
|
||||
|
||||
{% block customJS%}
|
||||
<script>
|
||||
// Define a custom color palette that matches the Phoenix template
|
||||
const primaryColor = '#7249b6'; // A vibrant purple
|
||||
const secondaryColor = '#8193a6'; // A muted gray/blue
|
||||
const successColor = '#00d074'; // A bright green
|
||||
const infoColor = '#0085ff'; // A medium blue
|
||||
const warningColor = '#ffc83b'; // A yellow
|
||||
const dangerColor = '#e63757'; // A deep red
|
||||
|
||||
// Sales by Month Chart (Bar Chart)
|
||||
const ctx1 = document.getElementById('CarsSoldByMonthChart').getContext('2d');
|
||||
new Chart(ctx1, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
||||
datasets: [{
|
||||
label: 'Total Cars Sold',
|
||||
data: [2, 3, 10, 4, 30, 12, 8, 9, 20, 12, 15, 35],
|
||||
backgroundColor: primaryColor,
|
||||
borderColor: primaryColor,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false }
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
||||
ticks: { color: secondaryColor }
|
||||
},
|
||||
x: {
|
||||
grid: { display: false },
|
||||
ticks: { color: secondaryColor }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Sales by Make (Pie Chart)
|
||||
const ctx3 = document.getElementById('salesByBrandChart').getContext('2d');
|
||||
new Chart(ctx3, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: ['Toyota', 'Ford', 'Honda', 'BMW', 'Other'],
|
||||
datasets: [{
|
||||
label: 'Car Count by Make',
|
||||
data: [45, 30, 25, 15, 10],
|
||||
backgroundColor: [primaryColor, successColor, warningColor, infoColor, secondaryColor],
|
||||
hoverOffset: 15,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'right',
|
||||
labels: { color: secondaryColor, font: { size: 14 } }
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(33, 37, 41, 0.9)',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#fff',
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const label = context.label || '';
|
||||
const value = context.parsed || 0;
|
||||
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
||||
const percentage = ((value / total) * 100).toFixed(2);
|
||||
return `${label}: ${value} cars (${percentage}%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Top 5 Lead Sources (Horizontal Bar Chart)
|
||||
const ctx_leadSources = document.getElementById('leadSourcesChart').getContext('2d');
|
||||
new Chart(ctx_leadSources, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['Showroom', 'Referrals', 'WhatsApp', 'Facebook', 'TikTok'],
|
||||
datasets: [{
|
||||
label: 'Number of Leads',
|
||||
data: [45, 35, 25, 20, 15],
|
||||
backgroundColor: infoColor,
|
||||
borderColor: infoColor,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
indexAxis: 'y',
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
title: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(33, 37, 41, 0.9)',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#fff',
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
beginAtZero: true,
|
||||
title: { display: true, text: 'Number of Leads', color: secondaryColor },
|
||||
ticks: { color: secondaryColor },
|
||||
grid: { color: 'rgba(0, 0, 0, 0.05)' }
|
||||
},
|
||||
y: {
|
||||
grid: { display: false },
|
||||
ticks: { color: secondaryColor }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Appointments by Weekday (Bar Chart)
|
||||
const ctx_appointments = document.getElementById('appointmentsChart').getContext('2d');
|
||||
new Chart(ctx_appointments, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
|
||||
datasets: [{
|
||||
label: 'Number of Appointments',
|
||||
data: [10, 15, 20, 18, 25, 30, 12],
|
||||
backgroundColor: successColor,
|
||||
borderColor: successColor,
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
title: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(33, 37, 41, 0.9)',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#fff',
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: { display: true, text: 'Day of the Week', color: secondaryColor },
|
||||
ticks: { color: secondaryColor },
|
||||
grid: { display: false }
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: { display: true, text: 'Number of Appointments', color: secondaryColor },
|
||||
ticks: { color: secondaryColor },
|
||||
grid: { color: 'rgba(0, 0, 0, 0.05)' }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Lead Conversion Funnel (Horizontal Bar Chart)
|
||||
const ctx_funnel = document.getElementById('leadFunnelChart').getContext('2d');
|
||||
const funnelData = {
|
||||
labels: ['Initial Contact', 'Qualified Leads', 'Test Drives', 'Negotiation', 'Closed Deals'],
|
||||
data: [250, 180, 120, 80, 45],
|
||||
};
|
||||
new Chart(ctx_funnel, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: funnelData.labels,
|
||||
datasets: [{
|
||||
label: 'Number of Leads',
|
||||
data: funnelData.data,
|
||||
backgroundColor: [
|
||||
primaryColor,
|
||||
infoColor,
|
||||
successColor,
|
||||
warningColor,
|
||||
dangerColor
|
||||
],
|
||||
borderColor: [
|
||||
primaryColor,
|
||||
infoColor,
|
||||
successColor,
|
||||
warningColor,
|
||||
dangerColor
|
||||
],
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
indexAxis: 'y',
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
title: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(33, 37, 41, 0.9)',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#fff',
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const totalLeads = funnelData.data[0];
|
||||
const currentLeads = context.parsed.x;
|
||||
const percentage = ((currentLeads / totalLeads) * 100).toFixed(1);
|
||||
return `Leads: ${currentLeads} (${percentage}%)`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
beginAtZero: true,
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
grid: { display: false },
|
||||
ticks: { color: secondaryColor }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -217,7 +217,7 @@
|
||||
<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 'account_list' request.dealer.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>
|
||||
@ -295,7 +295,7 @@
|
||||
</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">
|
||||
@ -304,40 +304,40 @@
|
||||
<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">
|
||||
<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 %}">
|
||||
<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 %}">
|
||||
<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>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="nav-item">
|
||||
<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>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
<li class="nav-item">
|
||||
<!--car purchase report-->
|
||||
<a class="nav-link" href="{% url 'po-report' request.dealer.slug %}">
|
||||
@ -346,7 +346,7 @@
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
<li class="nav-item">
|
||||
<!--car sale report-->
|
||||
<a class="nav-link" href="{% url 'car-sale-report' request.dealer.slug %}">
|
||||
@ -356,14 +356,14 @@
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<!---->
|
||||
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
{# --- Support & Contact Section (New) --- #}
|
||||
|
||||
@ -3,10 +3,16 @@
|
||||
{% block content %}
|
||||
{% if request.user.is_authenticated %}
|
||||
<div id="dashboard-content"
|
||||
hx-get="{% if request.dealer %}
|
||||
hx-get="{% if request.is_dealer %}
|
||||
{% url 'dealer_dashboard' %}
|
||||
{% elif request.is_manger %}
|
||||
{% url 'manager_dashboard' %}
|
||||
{% else %}
|
||||
{% elif request.is_sales %}
|
||||
{% url 'sales_dashboard' %}
|
||||
{% elif request.is_inventory %}
|
||||
{% url 'inventory_dashboard' %}
|
||||
{% else %}
|
||||
{% url 'accountant_dashboard' %}
|
||||
{% endif %}"
|
||||
hx-trigger="load"
|
||||
hx-target="#dashboard-content"
|
||||
|
||||
@ -88,13 +88,13 @@
|
||||
}
|
||||
|
||||
.color-radio:checked + .color-display {
|
||||
border: 2px solid #0d6efd;
|
||||
box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.25);
|
||||
border: 3px solid rgb(44, 229, 44);
|
||||
box-shadow: 0 0 10px rgba(44, 123, 229, 0.5);
|
||||
}
|
||||
|
||||
.color-radio:focus + .color-display {
|
||||
border-color: #86b7fe;
|
||||
box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.25);
|
||||
border: 3px solid rgb(44, 229, 44);
|
||||
box-shadow: 0 0 10px rgba(44, 123, 229, 0.5);
|
||||
}
|
||||
|
||||
.color-display {
|
||||
|
||||
@ -20,11 +20,11 @@
|
||||
<div class="alert alert-outline-warning d-flex align-items-center"
|
||||
role="alert">
|
||||
<i class="fa-solid fa-circle-info fs-6"></i>
|
||||
{% if not car.finances and not car.colors %}
|
||||
{% if not car.marked_price and not car.colors %}
|
||||
<p class="mb-0 flex-1">
|
||||
{{ _("This car information is not complete , please add colors and finances both before making it ready for sale .") }}
|
||||
</p>
|
||||
{% elif car.finances and not car.colors %}
|
||||
{% elif car.marked_price and not car.colors %}
|
||||
<p class="mb-0 flex-1">
|
||||
{{ _("This car information is not complete , please add colors before making it ready for sale .") }}
|
||||
</p>
|
||||
@ -205,7 +205,7 @@
|
||||
<tr>
|
||||
<th>{% trans 'Location'|capfirst %}</th>
|
||||
<td>
|
||||
{% if car.finances and not car.get_transfer %}
|
||||
{% if car.marked_price and not car.get_transfer %}
|
||||
{% if car.location %}
|
||||
{% if car.location.is_owner_showroom %}
|
||||
{% trans 'Our Showroom' %}
|
||||
@ -257,16 +257,16 @@
|
||||
<div class="card-body">
|
||||
<div class="table-responsive scrollbar mb-3">
|
||||
<table class="table table-sm fs-9 mb-0 overflow-hidden">
|
||||
{% if car.finances %}
|
||||
{% if car.marked_price %}
|
||||
<tr>
|
||||
{% if perms.inventory.view_carfinance %}
|
||||
<th>{% trans "Cost Price"|capfirst %}</th>
|
||||
<td>{{ car.finances.cost_price|floatformat:2 }}</td>
|
||||
<td>{{ car.cost_price|floatformat:2 }}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "Marked Price"|capfirst %}</th>
|
||||
<td>{{ car.finances.marked_price|floatformat:2 }}</td>
|
||||
<td>{{ car.marked_price|floatformat:2 }}</td>
|
||||
</tr>
|
||||
{% comment %} <tr>
|
||||
<th>{% trans "Selling Price"|capfirst %}</th>
|
||||
@ -300,7 +300,7 @@
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
{% if not car.get_transfer %}
|
||||
<a href="{% url 'car_finance_update' request.dealer.slug car.finances.pk %}"
|
||||
<a href="{% url 'car_finance_update' request.dealer.slug car.slug %}"
|
||||
class="btn btn-phoenix-warning btn-sm mb-3">{% trans "Edit" %}</a>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">{% trans "Cannot Edit, Car in Transfer." %}</span>
|
||||
@ -311,7 +311,7 @@
|
||||
{% else %}
|
||||
<p>{% trans "No finance details available." %}</p>
|
||||
{% if perms.inventory.add_carfinance %}
|
||||
<a href="{% url 'car_finance_create' request.dealer.slug car.slug %}"
|
||||
<a href="{% url 'car_finance_update' request.dealer.slug car.slug %}"
|
||||
class="btn btn-phoenix-success btn-sm mb-3">{% trans "Add" %}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@ -189,23 +189,12 @@
|
||||
hx-on::before-request="on_before_request()"
|
||||
hx-on::after-request="on_after_request()"></div>
|
||||
<div class="w-100 list table-responsive">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input ms-4" type="checkbox" id="select-all" />
|
||||
<span class="ms-1 text-body-tertiary">{{ _("Select All") }}</span>
|
||||
</div>
|
||||
|
||||
{% 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">
|
||||
<div class="col-auto">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input car-checkbox"
|
||||
type="checkbox"
|
||||
name="car"
|
||||
value="{{ car.pk }}"
|
||||
id="car-{{ car.pk }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vehicle Image/Icon -->
|
||||
<div class="col-auto">
|
||||
<div class="avatar avatar-3xl">
|
||||
@ -322,68 +311,5 @@
|
||||
|
||||
|
||||
{% endblock %}
|
||||
{% block customJS %}
|
||||
<script>
|
||||
links = document.querySelectorAll(".nav-link");
|
||||
links.forEach((link) => {
|
||||
link.addEventListener("click", () => {
|
||||
links.forEach((link) => {
|
||||
link.classList.remove("active");
|
||||
});
|
||||
link.classList.add("active");
|
||||
});
|
||||
});
|
||||
function on_before_request() {
|
||||
document.querySelector(".table").classList.toggle("on-before-request");
|
||||
document.querySelector(".model-select").classList.add("on-after-request");
|
||||
}
|
||||
function on_after_request() {
|
||||
document.querySelector(".table").classList.remove("on-before-request");
|
||||
document.querySelector(".model-select").classList.remove("on-after-request");
|
||||
}
|
||||
function toggle_filter() {
|
||||
document.querySelector(".filter").classList.toggle("d-none");
|
||||
document.querySelector(".filter-icon").classList.toggle("fa-caret-down");
|
||||
document.querySelector(".filter-icon").classList.toggle("fa-caret-up");
|
||||
}
|
||||
function filter_before_request() {
|
||||
document.querySelector(".model-select").setAttribute("disabled", true);
|
||||
document.querySelector(".year").setAttribute("disabled", true);
|
||||
document.querySelector(".car_status").setAttribute("disabled", true);
|
||||
}
|
||||
function filter_after_request() {
|
||||
document.querySelector(".model-select").removeAttribute("disabled");
|
||||
document.querySelector(".year").removeAttribute("disabled");
|
||||
document.querySelector(".car_status").removeAttribute("disabled");
|
||||
}
|
||||
|
||||
document.getElementById("select-all").addEventListener("change", function () {
|
||||
const checkboxes = document.querySelectorAll('#project-list-table-body input[type="checkbox"]');
|
||||
if (this.checked) {
|
||||
checkboxes.forEach((checkbox) => (checkbox.checked = true));
|
||||
} else {
|
||||
checkboxes.forEach((checkbox) => (checkbox.checked = false));
|
||||
}
|
||||
updateFormVisibility();
|
||||
});
|
||||
|
||||
const cbox = document.querySelectorAll(".car-checkbox");
|
||||
cbox.forEach((checkbox) => {
|
||||
checkbox.addEventListener("change", function () {
|
||||
updateFormVisibility();
|
||||
});
|
||||
});
|
||||
|
||||
function updateFormVisibility() {
|
||||
const form = document.querySelector(".update-price-form");
|
||||
const checkedCount = document.querySelectorAll(".car-checkbox:checked").length;
|
||||
const submitButton = form.querySelector('button[type="submit"]');
|
||||
if (checkedCount > 0) {
|
||||
form.classList.remove("d-none");
|
||||
submitButton.textContent = `Update Cost Price (${checkedCount})`;
|
||||
} else {
|
||||
form.classList.add("d-none");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock customJS %}
|
||||
|
||||
|
||||
@ -98,10 +98,10 @@
|
||||
<td>{{ transfer.car.vin }}</td>
|
||||
<td>{{ transfer.car }}</td>
|
||||
<td class="text-center">
|
||||
{{ transfer.car.finances.selling_price }} <span class="icon-saudi_riyal"></span>
|
||||
{{ transfer.car.selling_price }} <span class="icon-saudi_riyal"></span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{{ transfer.car.finances.vat_amount }} <span class="icon-saudi_riyal"></span>
|
||||
{{ transfer.car.vat_amount }} <span class="icon-saudi_riyal"></span># TODO : check later
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{{ transfer.total_price }} <span class="icon-saudi_riyal"></span>
|
||||
|
||||
@ -296,7 +296,7 @@
|
||||
<tr>
|
||||
<td>{{ transfer.car }}</td>
|
||||
<td class="text-center">{{ transfer.quantity }}</td>
|
||||
<td class="text-center">{{ transfer.car.finances.selling_price }}</td>
|
||||
<td class="text-center">{{ transfer.car.selling_price }}</td>
|
||||
<td class="text-center">{{ transfer.total_price }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@ -345,12 +345,12 @@
|
||||
});
|
||||
|
||||
document.getElementById('confirmAccept').addEventListener('click', function () {
|
||||
// Handle the accept action here
|
||||
// Handle the accept action here
|
||||
$('#acceptModal').modal('hide');
|
||||
});
|
||||
|
||||
document.getElementById('confirmReject').addEventListener('click', function () {
|
||||
// Handle the reject action here
|
||||
// Handle the reject action here
|
||||
$('#rejectModal').modal('hide');
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -178,11 +178,11 @@
|
||||
<td class="fs-9">{{ car.stock_type|capfirst }}</td>
|
||||
<td class="fs-9">{{ car.created_at|date }}</td>
|
||||
<td class="fs-9">{{ car.invoice.date_paid|date|default_if_none:"-" }}</td>
|
||||
<td class="fs-9">{{ car.finances.cost_price }} <span class="icon-saudi_riyal"></span></td>
|
||||
<td class="fs-9">{{ car.finances.marked_price }} <span class="icon-saudi_riyal"></span></td>
|
||||
<td class="fs-9">{{ car.finances.total_discount }} <span class="icon-saudi_riyal"></span></td>
|
||||
<td class="fs-9">{{ car.finances.selling_price }} <span class="icon-saudi_riyal"></span></td>
|
||||
<td class="fs-9">{{ car.finances.vat_amount }} <span class="icon-saudi_riyal"></span></td>
|
||||
<td class="fs-9">{{ car.cost_price }} <span class="icon-saudi_riyal"></span></td>
|
||||
<td class="fs-9">{{ car.marked_price }} <span class="icon-saudi_riyal"></span></td>
|
||||
<td class="fs-9">{{ car.total_discount }} <span class="icon-saudi_riyal"></span></td># TODO : check later
|
||||
<td class="fs-9">{{ car.selling_price }} <span class="icon-saudi_riyal"></span></td>
|
||||
<td class="fs-9">{{ car.vat_amount }} <span class="icon-saudi_riyal"></span></td># TODO : check later
|
||||
<td class="fs-9">{{ car.invoice.invoice_number }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@ -36,13 +36,13 @@
|
||||
}
|
||||
|
||||
.color-radio:checked + .color-display {
|
||||
border: 2px solid #0d6efd;
|
||||
box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.25);
|
||||
border: 3px solid rgb(44, 229, 44);
|
||||
box-shadow: 0 0 10px rgba(44, 123, 229, 0.5);
|
||||
}
|
||||
|
||||
.color-radio:focus + .color-display {
|
||||
border-color: #86b7fe;
|
||||
box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.25);
|
||||
border: 3px solid rgb(44, 229, 44);
|
||||
box-shadow: 0 0 10px rgba(44, 123, 229, 0.5);
|
||||
}
|
||||
|
||||
.color-display {
|
||||
|
||||
@ -104,7 +104,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="d-flex align-items-center gap-2" hx-boost="false">
|
||||
{% if estimate.status == 'draft' %}
|
||||
{% if perms.django_ledger.change_estimatemodel %}
|
||||
<button id="mark_as_sent_estimate"
|
||||
@ -112,7 +112,7 @@
|
||||
onclick="setFormAction('review')"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#confirmModal">
|
||||
<span class="d-none d-sm-inline-block"><i class="fa-solid fa-check-double"></i> {% trans 'Mark As Review' %}</span>
|
||||
<i class="fa-solid fa-check-double me-1"></i><span class="d-none d-sm-inline-block"> {% trans 'Mark As Review' %}</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% elif estimate.status == 'in_review' %}
|
||||
@ -122,45 +122,46 @@
|
||||
class="btn btn-phoenix-secondary"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#confirmModal">
|
||||
<span class="d-none d-sm-inline-block"><i class="fa-solid fa-check-double"></i> {% trans 'Mark As Approved' %}</span>
|
||||
<i class="fa-solid fa-check-double me-1"></i> <span class="d-none d-sm-inline-block">{% trans 'Mark As Approved' %}</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if estimate.can_approve and not request.is_manager %}
|
||||
<button class="btn btn-phoenix-warning" disabled>
|
||||
<i class="fas fa-hourglass-start me-2"></i><span class="text-warning">{% trans 'Waiting for Manager Approval' %}</span>
|
||||
<i class="fas fa-hourglass-start me-1"></i><span class="text-warning d-none d-sm-inline-block ">{% trans 'Waiting for Manager Approval' %}</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% elif estimate.status == 'approved' %}
|
||||
{% if perms.django_ledger.change_estimatemodel %}
|
||||
<a href="{% url 'send_email' request.dealer.slug estimate.pk %}"
|
||||
class="btn btn-phoenix-primary me-2"><span class="fa-regular fa-paper-plane me-sm-2"></span><span class="d-none d-sm-inline-block">{% trans 'Send Quotation' %}</span></a>
|
||||
class="btn btn-phoenix-primary me-2"><span class="fa-regular fa-paper-plane me-1"></span><span class="d-none d-sm-inline-block">{% trans 'Send Quotation' %}</span></a>
|
||||
{% endif %}
|
||||
{% if estimate.sale_orders.first %}
|
||||
<!--if sale order exist-->
|
||||
{% if perms.django_ledger.add_invoicemodel %}
|
||||
<a href="{% url 'invoice_create' request.dealer.slug estimate.pk %}"
|
||||
class="btn btn-phoenix-primary"><span class="d-none d-sm-inline-block"><i class="fa-solid fa-receipt"></i> {% trans 'Create Invoice' %}</span></a>
|
||||
class="btn btn-phoenix-primary"><i class="fa-solid fa-receipt me-1"></i><span class="d-none d-sm-inline-block me-1"> {% trans 'Create Invoice' %}</span></a>
|
||||
{% endif %}
|
||||
{% if perms.inventory.view_saleorder %}
|
||||
<a href="{% url 'order_detail' request.dealer.slug estimate.sale_orders.first.pk %}"
|
||||
class="btn btn-phoenix-primary"><span class="d-none d-sm-inline-block">{{ _("Preview Sale Order") }}</span></a>
|
||||
class="btn btn-phoenix-primary"><i class="fas fa-shopping-cart me-1"></i><span class="d-none d-sm-inline-block">{{ _("Preview Sale Order") }}</span></a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if perms.inventory.add_saleorder %}
|
||||
<a href="{% url 'create_sale_order' request.dealer.slug estimate.pk %}"
|
||||
class="btn btn-phoenix-primary"><span class="d-none d-sm-inline-block"><i class="fa-solid fa-file-import"></i> {% trans 'Create Sale Order' %}</span></a>
|
||||
class="btn btn-phoenix-primary"><i class="fa-solid fa-file-import me-1"></i><span class="d-none d-sm-inline-block"> {% trans 'Create Sale Order' %}</span></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% elif estimate.status == 'completed' %}
|
||||
{% if perms.inventory.view_saleorder %}
|
||||
<a href="{% url 'order_detail' request.dealer.slug estimate.sale_orders.first.pk %}"
|
||||
class="btn btn-phoenix-primary"><span class="d-none d-sm-inline-block">{{ _("Preview Sale Order") }}</span></a>
|
||||
class="btn btn-phoenix-primary"><i class="fas fa-shopping-cart me-1"></i><span class="d-none d-sm-inline-block">{{ _("Preview Sale Order") }}</span></a>
|
||||
{% endif %}
|
||||
{% if perms.django_ledger.view_invoicemodel %}
|
||||
<a href="{% url 'invoice_detail' request.dealer.slug request.entity.slug estimate.invoicemodel_set.first.pk %}"
|
||||
class="btn btn-phoenix-primary btn-sm"
|
||||
type="button"><i class="fa-solid fa-receipt"></i>
|
||||
{{ _("View Invoice") }}</a>
|
||||
type="button"><i class="fa-solid fa-receipt me-1"></i>
|
||||
<span class="d-none d-sm-inline-block">{{ _("View Invoice") }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if estimate.can_cancel %}
|
||||
@ -168,7 +169,7 @@
|
||||
<button class="btn btn-phoenix-danger"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#CancelModal">
|
||||
<i class="fa-solid fa-ban"></i> {% trans "Cancel" %}
|
||||
<i class="fa-solid fa-ban me-1"></i> <span class="d-none d-sm-inline-block">{% trans "Cancel" %}</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
@ -178,31 +179,31 @@
|
||||
<div class="row">
|
||||
<div class="col mb-2">
|
||||
<h6>
|
||||
<i class="fa-solid fa-hashtag"></i> {% trans 'Quotation Number' %}:
|
||||
<i class="fa-solid fa-hashtag me-1"></i> {% trans 'Quotation Number' %}:
|
||||
</h6>
|
||||
<p class="fs-9 text-body-secondary fw-semibold mb-0">{{ estimate.estimate_number }}</p>
|
||||
</div>
|
||||
<div class="col mb-2">
|
||||
<h6>
|
||||
<i class="fa-solid fa-calendar-days"></i> {% trans 'Quotation Date' %}:
|
||||
<i class="fa-solid fa-calendar-days me-1"></i> {% trans 'Quotation Date' %}:
|
||||
</h6>
|
||||
<p class="fs-9 text-body-secondary fw-semibold mb-0">{{ estimate.created }}</p>
|
||||
</div>
|
||||
<div class="col mb-2">
|
||||
<h6>
|
||||
<i class="fa-solid fa-user"></i> {% trans 'Customer' %}:
|
||||
<i class="fa-solid fa-user me-1"></i> {% trans 'Customer' %}:
|
||||
</h6>
|
||||
<p class="fs-9 text-body-secondary fw-semibold mb-0">{{ estimate.customer.customer_name }}</p>
|
||||
</div>
|
||||
<div class="col mb-2">
|
||||
<h6>
|
||||
<i class="fa-solid fa-envelope"></i> {% trans 'Email' %}:
|
||||
<i class="fa-solid fa-envelope me-1"></i> {% trans 'Email' %}:
|
||||
</h6>
|
||||
<p class="fs-9 text-body-secondary fw-semibold mb-0">{{ estimate.customer.email }}</p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h6>
|
||||
<i class="fa-solid fa-list"></i> {% trans "Quotation Status" %}:
|
||||
<i class="fa-solid fa-list me-1"></i> {% trans "Quotation Status" %}:
|
||||
</h6>
|
||||
<div class="fs-9 text-body-secondary fw-semibold mb-0">
|
||||
{% if estimate.status == 'draft' %}
|
||||
@ -237,21 +238,18 @@
|
||||
<th scope="col" style="min-width: 100px;">{% trans "Total" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in data.cars %}
|
||||
<tr>
|
||||
<td class="align-middle">
|
||||
<img src="{{item.logo}}" width="40" height="40" class="ps-2"></img>
|
||||
<img src="{{data.car.logo}}" width="40" height="40" class="ps-2"></img>
|
||||
</td>
|
||||
<td class="align-middle">{{ item.make }}</td>
|
||||
<td class="align-middle">{{ item.model }}</td>
|
||||
<td class="align-middle">{{ item.year }}</td>
|
||||
<td class="align-middle">{{ item.vin }}</td>
|
||||
<td class="align-middle">{{ item.quantity }}</td>
|
||||
<td class="align-middle ps-5">{{ item.unit_price }}</td>
|
||||
<td class="align-middle text-body-tertiary fw-semibold">{{ item.total }}</td>
|
||||
<td class="align-middle">{{ data.car.id_car_make }}</td>
|
||||
<td class="align-middle">{{ data.car.id_car_model }}</td>
|
||||
<td class="align-middle">{{ data.car.year }}</td>
|
||||
<td class="align-middle">{{ data.car.vin }}</td>
|
||||
<td class="align-middle">1</td>
|
||||
<td class="align-middle ps-5">{{ data.car.marked_price }}</td>
|
||||
<td class="align-middle text-body-tertiary fw-semibold">{{ data.car.marked_price }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
<tr class="bg-body-secondary total-sum">
|
||||
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="7">{% trans "Discount Amount" %}</td>
|
||||
@ -264,7 +262,7 @@
|
||||
<input type="number"
|
||||
class="form-control"
|
||||
name="discount_amount"
|
||||
value="{{ data.total_discount }}"
|
||||
value="{{ data.discount_amount }}"
|
||||
step="0.01"
|
||||
style="width: 1px">
|
||||
<span class="input-group-text"><span class="icon-saudi_riyal"></span></span>
|
||||
@ -272,20 +270,20 @@
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
{{ data.total_discount }} <span class="icon-saudi_riyal"></span>
|
||||
{{ data.discount_amount }} <span class="icon-saudi_riyal"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="bg-body-secondary total-sum">
|
||||
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="7">{% trans "Vat" %} ({{ data.vat }})</td>
|
||||
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="7">{% trans "Vat" %} ({{ data.vat_rate }})</td>
|
||||
<td class="align-middle text-start fw-semibold">
|
||||
<span id="">+ {{ data.total_vat_amount }}<span class="icon-saudi_riyal"></span></span>
|
||||
<span id="">+ {{ data.vat_amount }}<span class="icon-saudi_riyal"></span></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="bg-body-secondary total-sum">
|
||||
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="7">{% trans "Additional Services" %}</td>
|
||||
<td class="align-middle text-start fw-semibold">
|
||||
{% for service in data.additionals %}
|
||||
{% for service in data.additional_services.services %}
|
||||
<small><span class="fw-semibold">+ {{ service.name }} - {{ service.price_|floatformat }}<span class="icon-saudi_riyal"></span></span></small>
|
||||
<br>
|
||||
{% endfor %}
|
||||
|
||||
@ -111,7 +111,7 @@
|
||||
<td>{{ car.year }} - {{ car.id_car_make.name }} - {{ car.id_car_model.name }} - {{ car.id_car_trim.name }}</td>
|
||||
<td>{{ car.colors.first.exterior.name }}</td>
|
||||
<td>{{ car.colors.first.interior.name }}</td>
|
||||
<td>{{ car.finances.selling_price }}</td>
|
||||
<td>{{ car.selling_price }}</td>
|
||||
<td>{{ car.get_specifications }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@ -330,34 +330,34 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in data.cars %}
|
||||
<tr>
|
||||
<td class="align-middle"></td>
|
||||
<td class="align-middle">{{ item.make }}</td>
|
||||
<td class="align-middle">{{ item.model }}</td>
|
||||
<td class="align-middle">{{ item.year }}</td>
|
||||
<td class="align-middle">{{ item.vin }}</td>
|
||||
<td class="align-middle">{{ item.quantity|floatformat:-1 }}</td>
|
||||
<td class="align-middle ps-5">{{ item.total }}</td>
|
||||
<td class="align-middle text-body-tertiary fw-semibold">{{ item.total }}</td>
|
||||
<td class="align-middle">
|
||||
<img src="{{data.car.logo}}" width="40" height="40" class="ps-2"></img>
|
||||
</td>
|
||||
<td class="align-middle">{{ data.car.id_car_make }}</td>
|
||||
<td class="align-middle">{{ data.car.id_car_model }}</td>
|
||||
<td class="align-middle">{{ data.car.year }}</td>
|
||||
<td class="align-middle">{{ data.car.vin }}</td>
|
||||
<td class="align-middle">1</td>
|
||||
<td class="align-middle ps-5">{{ data.car.marked_price }}</td>
|
||||
<td class="align-middle text-body-tertiary fw-semibold">{{ data.car.marked_price }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr class="bg-body-secondary total-sum">
|
||||
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="7">{% trans "Discount Amount" %}</td>
|
||||
<td class="align-middle text-start fw-semibold">
|
||||
<span id="grand-total">- {{ data.total_discount|floatformat }}<span class="icon-saudi_riyal"></span></span>
|
||||
<span id="grand-total">- {{ data.discount_amount|floatformat }}<span class="icon-saudi_riyal"></span></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="bg-body-secondary total-sum">
|
||||
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="7">{% trans "VAT" %} ({{ data.vat }}%)</td>
|
||||
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="7">{% trans "VAT" %} ({{ data.vat_rate }}%)</td>
|
||||
<td class="align-middle text-start fw-semibold">
|
||||
<span id="grand-total">+ {{ data.total_vat_amount|floatformat }}<span class="icon-saudi_riyal"></span></span>
|
||||
<span id="grand-total">+ {{ data.vat_amount|floatformat }}<span class="icon-saudi_riyal"></span></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="bg-body-secondary total-sum">
|
||||
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="7">{% trans "Additional Services" %}</td>
|
||||
<td class="align-middle text-start fw-bold">
|
||||
{% for service in data.additionals %}
|
||||
{% for service in data.additional_services.services %}
|
||||
<small><span class="fw-bold">+ {{ service.name }} - {{ service.price_|floatformat }}<span class="icon-saudi_riyal"></span></span></small>
|
||||
<br>
|
||||
{% endfor %}
|
||||
|
||||
@ -2,108 +2,107 @@
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% block title %}
|
||||
{{ _("View Staff") }}
|
||||
{{ user_.name }}
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
<div class="row my-5">
|
||||
<div class="card rounded ">
|
||||
<div class="card-header ">
|
||||
<p class="mb-0">
|
||||
<i class="fa-solid fa-user"></i> {{ _("User Details") }}
|
||||
</p>
|
||||
<div class="container py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div class="d-flex align-items-center">
|
||||
<a href="{% url 'user_list' request.dealer.slug %}" class="btn btn-phoenix-secondary btn-sm me-3">
|
||||
<i class="fa-regular fa-circle-left me-2"></i>{% trans "Back to Staffs" %}
|
||||
</a>
|
||||
<h1 class="h5 fw-bold mb-0">{% trans "Staff Profile" %}</h1>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
{% if user_.logo %}
|
||||
<img class="rounded-circle"
|
||||
src="{{ user_.logo.url }}"
|
||||
style="width: 100px;
|
||||
height: 100px"
|
||||
alt="User Image">
|
||||
{% else %}
|
||||
<img class="rounded-circle"
|
||||
src="{% static 'img/default-user.png' %}"
|
||||
style="width: 100px;
|
||||
height: 100px"
|
||||
alt="Default User Image">
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<p>
|
||||
<strong>{{ _("Name") }}:</strong> {{ user_.name }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{{ _("Arabic Name") }}:</strong> {{ user_.arabic_name }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{{ _("Email") }}:</strong> {{ user_.email }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<p>
|
||||
<strong>{{ _("Phone Number") }}:</strong> {{ user_.phone_number }}
|
||||
</p>
|
||||
<div>
|
||||
<span><strong>{{ _("Roles") }}:</strong></span>
|
||||
{% for group in user_.groups %}
|
||||
<span><strong>{{ group.name }}</strong></span>
|
||||
{% if not forloop.last %}<span>&</span>{% endif %}
|
||||
{% endfor %}
|
||||
<div>
|
||||
<a href="{% url 'user_update' request.dealer.slug user_.slug %}" class="btn btn-phoenix-primary btn-sm me-2">
|
||||
<i class="fa-solid fa-pen-to-square me-2"></i>{% trans "Edit" %}
|
||||
</a>
|
||||
<button class="btn btn-phoenix-danger btn-sm delete-btn" data-url="{% url 'user_delete' request.dealer.slug user_.slug %}"
|
||||
data-message='{% trans "Are you sure you want to delete this user?" %}'
|
||||
data-bs-toggle="modal" data-bs-target="#deleteModal">
|
||||
<i class="fas fa-trash-alt me-2"></i>{% trans "Delete" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-4">
|
||||
<div class="card h-100 border-0 shadow-sm rounded-3">
|
||||
<div class="card-body text-center p-3">
|
||||
<div class="position-relative d-inline-block mb-3">
|
||||
{% if user_.logo %}
|
||||
<img class="rounded-circle border border-4 border-body-tertiary"
|
||||
src="{{ user_.logo.url }}" alt="{{ user_.name }}"
|
||||
style="object-fit: cover; width: 100px; height: 100px;">
|
||||
{% else %}
|
||||
<div class="rounded-circle d-inline-flex align-items-center justify-content-center bg-primary text-white"
|
||||
style="width: 100px; height: 100px; font-size: 2.5rem;">
|
||||
{{ user_.name|first|upper }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h2 class="h6 fw-bold mb-1">{{ user_.name }}</h2>
|
||||
<p class="small mb-3"> <a href="mailto:{{ user_.email }}" class="text-secondary">{{ user_.email }}</a></p>
|
||||
<div class="d-grid gap-1">
|
||||
<a href="tel:{{ user_.phone_number }}" class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-phone-alt me-2"></i>{{ user_.phone_number|default:"N/A" }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive scrollbar mx-n1 px-1">
|
||||
<div class="card-header "></div>
|
||||
<h4 class="my-4">Groups</h4>
|
||||
<a class="btn btn-sm btn-phoenix-primary mt-2 mb-4"
|
||||
href="{% url 'user_groups' request.dealer.slug user_.slug %}"><i class="fa-solid fa-users"></i> Manage Groups</a>
|
||||
<table class="table table-hover table-responsive-sm fs-9 mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans 'name'|capfirst %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for group in user_.groups %}
|
||||
<tr>
|
||||
<td>{{ group }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td>{% trans "No Group" %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-footer d-flex ">
|
||||
<a class="btn btn-sm btn-phoenix-primary me-3"
|
||||
href="{% url 'user_update' request.dealer.slug user_.slug %}">
|
||||
{{ _("Edit") }}
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
</a>
|
||||
<button class="btn btn-phoenix-danger btn-sm delete-btn me-3"
|
||||
data-url="{% url 'user_delete' request.dealer.slug user_.slug %}"
|
||||
data-message='{{ _("Are you sure you want to delete this user?") }}'
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#deleteModal">
|
||||
{{ _("Delete") }}
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<a class="btn btn-sm btn-phoenix-secondary me-3"
|
||||
href="{% url 'user_list' request.dealer.slug %}">
|
||||
{{ _("Back to Staffs List") }}
|
||||
<i class="fa-regular fa-circle-left"></i>
|
||||
</a>
|
||||
<a class="btn btn-sm btn-phoenix-secondary"
|
||||
href="{% url 'staff_password_reset' request.dealer.slug user_.pk %}">
|
||||
{{ _("Reset Password") }}
|
||||
<i class="fa-solid fa-key"></i>
|
||||
</a>
|
||||
|
||||
<div class="col-lg-8">
|
||||
<div class="card h-100 border-0 shadow-sm rounded-3">
|
||||
<div class="card-body p-3">
|
||||
<h4 class="mb-3 text-primary h6">{% trans "Personal Information" %}</h4>
|
||||
<div class="row g-2 mb-4">
|
||||
<div class="col-md-6">
|
||||
<p class="small mb-0">{% trans "Arabic Name" %}</p>
|
||||
<p class="fw-bold small mb-0">{{ user_.arabic_name|default:"N/A" }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p class="small mb-0">{% trans "Roles" %}</p>
|
||||
<p class="fw-bold small mb-0">
|
||||
{% for group in user_.groups %}
|
||||
{{ group.name }} {% if not forloop.last %}·{% endif %}
|
||||
{% empty %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endfor %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 class="mb-0 text-primary h6">{% trans "Assigned Groups" %}</h4>
|
||||
<a class="btn btn-sm btn-phoenix-primary" href="{% url 'user_groups' request.dealer.slug user_.slug %}">
|
||||
<i class="fa-solid fa-users me-2"></i>{% trans "Manage Groups" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="table-responsive scrollbar">
|
||||
<table class="table table-hover table-striped fs--1 mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="text-nowrap">{% trans 'Group Name'|capfirst %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for group in user_.groups %}
|
||||
<tr>
|
||||
<td class="small">{{ group }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td class="text-secondary small">{% trans "This user is not assigned to any groups." %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'modal/delete_modal.html' %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@ -5,32 +5,37 @@
|
||||
{% trans "Group" %}
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="row">
|
||||
<div class="col-sm-9">
|
||||
<div class="d-sm-flex justify-content-between">
|
||||
<h3 class="mb-3"><i class="fa-solid fa-users"></i> {{ _("Manage Groups") }}</h3>
|
||||
|
||||
<div class="row justify-content-center mt-5 mb-3">
|
||||
|
||||
<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">
|
||||
<h3 class="mb-0 fs-5 text-center">
|
||||
{{ _("Manage Groups") }}<i class="fa-solid fa-users ms-2 text-primary"></i>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body bg-light-subtle">
|
||||
<form class="row g-3 mb-3" method="post" class="form" enctype="multipart/form-data" novalidate >
|
||||
{% csrf_token %}
|
||||
{{ redirect_field }}
|
||||
{{ form|crispy }}
|
||||
{% for error in form.errors %}<div class="text-danger">{{ error }}</div>{% endfor %}
|
||||
<hr class="my-2">
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
|
||||
<button class="btn btn-lg btn-phoenix-primary md-me-2" type="submit"><i class="fa-solid fa-floppy-disk me-1"></i>{{ _("Save") }}</button>
|
||||
<a href="{% url 'user_detail' request.dealer.slug staff.slug %}" class="btn btn-lg btn-phoenix-secondary"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-9">
|
||||
<form class="row g-3 mb-9" method="post" class="form" novalidate>
|
||||
{% csrf_token %}
|
||||
{{ redirect_field }}
|
||||
{{ form|crispy }}
|
||||
{% for error in form.errors %}
|
||||
<div class="text-danger">{{ error }}</div>
|
||||
{% endfor %}
|
||||
<div class="d-flex mb-3">
|
||||
<a href="{% url 'user_detail' request.dealer.slug staff.slug %}" class="btn btn-phoenix-primary me-2 px-6"><i class="fa-solid fa-ban"></i> {% trans "Cancel" %}</a>
|
||||
<button class="btn btn-phoenix-primary" type="submit">
|
||||
<i class="fa-solid fa-floppy-disk"></i>
|
||||
{{ _("Save") }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,94 +1,100 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
{% block title %}
|
||||
{% trans "Staffs" %}
|
||||
{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{%if users or request.GET.q%}
|
||||
<section class="">
|
||||
<div class="row mt-4">
|
||||
<div class="col-auto">
|
||||
<div class="d-md-flex justify-content-between">
|
||||
<div>
|
||||
{% if request.user.userplan %}
|
||||
<a href="{% url 'user_create' request.dealer.slug %}"
|
||||
class="btn btn-sm btn-phoenix-primary me-4"><i class="fa-solid fa-user-tie me-1"></i> {% trans "Add New Staff" %}</a>
|
||||
<a href="{% url 'group_list' request.dealer.slug %}"
|
||||
class="btn btn-sm btn-phoenix-success"><i class="fa-solid fa-user-group me-1"></i> {% trans "Manage Groups & Permissions" %}</a>
|
||||
{% else %}
|
||||
{%if users or request.GET.q%}
|
||||
<div class="container py-4">
|
||||
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||
<h1 class="h3 mb-0">{% trans "Staffs" %}<i class="fa-solid fa-user-tie text-primary ms-2"></i></h1>
|
||||
{% if request.user.userplan %}
|
||||
<div>
|
||||
<a href="{% url 'user_create' request.dealer.slug %}" class="btn btn-phoenix-primary me-2">
|
||||
<i class="fa-solid fa-plus me-2"></i>{% trans "Add New Staff" %}
|
||||
</a>
|
||||
<a href="{% url 'group_list' request.dealer.slug %}" class="btn btn-phoenix-secondary">
|
||||
<i class="fa-solid fa-user-group me-2"></i>{% trans "Manage Groups" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% url "pricing_page" request.dealer.slug as pricing_page_url %}
|
||||
{% include "message-illustration.html" with value1="No Active Plan, please create your subscription plan." value2="Manage Subscription" url=pricing_page_url %}
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive scrollbar">
|
||||
<table class="table table-hover table-striped mb-0">
|
||||
<thead>
|
||||
<tr class="bg-body-highlight">
|
||||
<th>{% trans 'name'|capfirst %}</th>
|
||||
<th>{% trans 'email'|capfirst %}</th>
|
||||
<th>{% trans 'phone number'|capfirst %}</th>
|
||||
<th>{% trans 'role'|capfirst %}</th>
|
||||
<th>{% trans 'actions'|capfirst %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td class="align-middle white-space-nowrap ps-2">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="avatar avatar-tiny me-2">
|
||||
{% if user.logo %}
|
||||
<img class="avatar-img rounded-circle"
|
||||
src="{{ user.thumbnail.url }}"
|
||||
onerror="this.src='/static/user-logo.jpg'"
|
||||
alt="{{ user.name }}'s logo">
|
||||
{% else %}
|
||||
<div class="rounded-circle bg-light d-flex justify-content-center align-items-center" style="width: 2rem; height: 2rem; font-size: 0.8rem;">
|
||||
{{ user.name|first|upper }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<a class="fw-bold text-decoration-none text-dark"
|
||||
href="{% url 'user_detail' request.dealer.slug user.slug %}">
|
||||
{{ user.name }}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap">{{ user.email }}</td>
|
||||
<td class="align-middle white-space-nowrap">{{ user.phone_number }}</td>
|
||||
<td class="align-middle white-space-nowrap">
|
||||
{% for group in user.groups %}
|
||||
<span class="badge bg-primary me-1">{{ group.name|title }}</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap">
|
||||
<a class="btn btn-phoenix-success"
|
||||
href="{% url 'user_detail' request.dealer.slug user.slug %}">
|
||||
<i class="fa-solid fa-eye me-1"></i>{% trans 'View' %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive scrollbar mx-n1 px-1 mt-3">
|
||||
<table class="table align-items-center table-flush table-hover">
|
||||
<thead>
|
||||
<tr class="bg-body-highlight">
|
||||
<th>{% trans 'name'|capfirst %}</th>
|
||||
<th>{% trans 'email'|capfirst %}</th>
|
||||
<th>{% trans 'phone number'|capfirst %}</th>
|
||||
<th>{% trans 'role'|capfirst %}</th>
|
||||
<th>{% trans 'actions'|capfirst %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td class="align-middle white-space-nowrap ps-1">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="avatar avatar-tiny me-2">
|
||||
{% if user.logo %}
|
||||
<img class="avatar-img rounded-circle"
|
||||
src="{{user.thumbnail.url}}"
|
||||
onerror="this.src='/static/user-logo.jpg'"
|
||||
alt="Logo">
|
||||
{% endif %}
|
||||
</div>
|
||||
<a class="fs-8 fw-bold"
|
||||
href="{% url 'user_detail' request.dealer.slug user.slug %}">{{ user.arabic_name }}</a>
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap align-items-center">{{ user.email }}</td>
|
||||
<td class="align-middle white-space-nowrap align-items-center justify-content-center">{{ user.phone_number }}</td>
|
||||
<td>
|
||||
{% for group in user.groups %}
|
||||
<span class="badge badge-sm bg-primary text-center"><i class="fa-solid fa-scroll"></i> {% trans group.name|title %}</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap">
|
||||
<a class="btn btn-phoenix-success"
|
||||
href="{% url 'user_detail' request.dealer.slug user.slug %}">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
{% trans 'view'|capfirst %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="d-flex justify-content-center">
|
||||
{% if is_paginated %}
|
||||
|
||||
{% if is_paginated %}
|
||||
<div class="d-flex justify-content-center mt-3">
|
||||
{% include 'partials/pagination.html' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% else %}
|
||||
{% if request.user.userplan %}
|
||||
{% url "user_create" request.dealer.slug as create_staff_url %}
|
||||
{% url "user_create" request.dealer.slug as create_staff_url %}
|
||||
{% include "empty-illustration-page.html" with value="staff" url=create_staff_url %}
|
||||
|
||||
{% else %}
|
||||
{% url "pricing_page" request.dealer.slug as pricing_page_url %}
|
||||
{% include "message-illustration.html" with value1=_("No active plan, Please create a subscription plan.") value2=_("Buy Plan") url=pricing_page_url %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% endblock %}
|
||||
Loading…
x
Reference in New Issue
Block a user