Compare commits
11 Commits
b9f6018e9a
...
e6e2ed0150
| Author | SHA1 | Date | |
|---|---|---|---|
| e6e2ed0150 | |||
| bd1e4d73ae | |||
| d9d1a8c376 | |||
| f16c6b6a51 | |||
| 9a42c82da5 | |||
| e1b680603e | |||
| a6461f34e0 | |||
| 699b2dfdbd | |||
| 150eea38df | |||
| 1ed2bbcb75 | |||
| 18ba539d08 |
@ -1179,10 +1179,16 @@ class ScheduleForm(forms.ModelForm):
|
|||||||
"purpose",
|
"purpose",
|
||||||
"scheduled_type",
|
"scheduled_type",
|
||||||
"scheduled_at",
|
"scheduled_at",
|
||||||
"duration",
|
"start_time",
|
||||||
|
"end_time",
|
||||||
"notes",
|
"notes",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
widgets = {
|
||||||
|
"start_time": forms.TimeInput(attrs={"type": "time"}),
|
||||||
|
"end_time": forms.TimeInput(attrs={"type": "time"}),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class NoteForm(forms.ModelForm):
|
class NoteForm(forms.ModelForm):
|
||||||
"""
|
"""
|
||||||
@ -1286,6 +1292,32 @@ class OpportunityForm(forms.ModelForm):
|
|||||||
if self.instance and self.instance.pk:
|
if self.instance and self.instance.pk:
|
||||||
self.fields["probability"].initial = self.instance.probability
|
self.fields["probability"].initial = self.instance.probability
|
||||||
|
|
||||||
|
class OpportunityStageForm(forms.ModelForm):
|
||||||
|
"""
|
||||||
|
Represents a form for creating or editing Opportunity instances.
|
||||||
|
|
||||||
|
This class is a Django ModelForm designed to simplify the process of
|
||||||
|
validating and persisting data for Opportunity model instances. It
|
||||||
|
maps fields from the Opportunity model to form fields, making it
|
||||||
|
convenient to handle user input for operations such as creating and
|
||||||
|
updating opportunities.
|
||||||
|
|
||||||
|
:ivar Meta.model: The model associated with the form.
|
||||||
|
:type Meta.model: type
|
||||||
|
:ivar Meta.fields: List of fields from the model included in the form.
|
||||||
|
:type Meta.fields: list
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Opportunity
|
||||||
|
fields = [
|
||||||
|
"stage",
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class InvoiceModelCreateForm(InvoiceModelCreateFormBase):
|
class InvoiceModelCreateForm(InvoiceModelCreateFormBase):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -69,12 +69,12 @@ class Command(BaseCommand):
|
|||||||
user.is_staff = True
|
user.is_staff = True
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
staff_member = StaffMember.objects.create(user=user)
|
# staff_member = StaffMember.objects.create(user=user)
|
||||||
services = Service.objects.all()
|
# services = Service.objects.all()
|
||||||
for service in services:
|
# for service in services:
|
||||||
staff_member.services_offered.add(service)
|
# staff_member.services_offered.add(service)
|
||||||
|
|
||||||
staff = Staff.objects.create(dealer=dealer,staff_member=staff_member,name=name,arabic_name=name,phone_number=fake.phone_number(),active=True)
|
staff = Staff.objects.create(dealer=dealer,user=user,name=name,arabic_name=name,phone_number=fake.phone_number(),active=True)
|
||||||
|
|
||||||
groups = CustomGroup.objects.filter(dealer=dealer)
|
groups = CustomGroup.objects.filter(dealer=dealer)
|
||||||
random_group = random.choice(list(groups))
|
random_group = random.choice(list(groups))
|
||||||
|
|||||||
@ -108,13 +108,13 @@ class InjectDealerMiddleware:
|
|||||||
request.is_dealer = True
|
request.is_dealer = True
|
||||||
request.dealer = request.user.dealer
|
request.dealer = request.user.dealer
|
||||||
|
|
||||||
elif hasattr(request.user, "staffmember"):
|
elif hasattr(request.user, "staff"):
|
||||||
request.is_staff = True
|
request.staff = getattr(request.user, "staff")
|
||||||
request.staff = request.user.staffmember.staff
|
|
||||||
request.dealer = request.staff.dealer
|
request.dealer = request.staff.dealer
|
||||||
|
request.is_staff = True
|
||||||
|
|
||||||
|
staff_groups = request.staff.groups.values_list("name", flat=True)
|
||||||
|
|
||||||
staff = getattr(request.user.staffmember, "staff")
|
|
||||||
staff_groups = staff.groups.values_list("name", flat=True)
|
|
||||||
if "Accountant" in staff_groups:
|
if "Accountant" in staff_groups:
|
||||||
request.is_accountant = True
|
request.is_accountant = True
|
||||||
elif "Manager" in staff_groups:
|
elif "Manager" in staff_groups:
|
||||||
|
|||||||
@ -40,7 +40,7 @@ from django_ledger.models import (
|
|||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
from appointment.models import StaffMember
|
# from appointment.models import StaffMember
|
||||||
from plans.quota import get_user_quota
|
from plans.quota import get_user_quota
|
||||||
from plans.models import UserPlan
|
from plans.models import UserPlan
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
@ -728,6 +728,9 @@ class Car(Base):
|
|||||||
def additional_services(self):
|
def additional_services(self):
|
||||||
return self.finances.additional_services.all()
|
return self.finances.additional_services.all()
|
||||||
@property
|
@property
|
||||||
|
def total_additional_services(self):
|
||||||
|
return sum([service.price_ for service in self.additional_services])
|
||||||
|
@property
|
||||||
def ready(self):
|
def ready(self):
|
||||||
try:
|
try:
|
||||||
return all(
|
return all(
|
||||||
@ -1277,8 +1280,11 @@ class StaffTypes(models.TextChoices):
|
|||||||
|
|
||||||
|
|
||||||
class Staff(models.Model, LocalizedNameMixin):
|
class Staff(models.Model, LocalizedNameMixin):
|
||||||
staff_member = models.OneToOneField(
|
# staff_member = models.OneToOneField(
|
||||||
StaffMember, on_delete=models.CASCADE, related_name="staff"
|
# StaffMember, on_delete=models.CASCADE, related_name="staff"
|
||||||
|
# )
|
||||||
|
user = models.OneToOneField(
|
||||||
|
User, on_delete=models.CASCADE, related_name="staff"
|
||||||
)
|
)
|
||||||
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="staff")
|
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="staff")
|
||||||
first_name = models.CharField(max_length=255, verbose_name=_("First Name"))
|
first_name = models.CharField(max_length=255, verbose_name=_("First Name"))
|
||||||
@ -1338,17 +1344,17 @@ class Staff(models.Model, LocalizedNameMixin):
|
|||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def permenant_delete(self):
|
def permenant_delete(self):
|
||||||
# self.user.delete()
|
self.user.delete()
|
||||||
self.staff_member.delete()
|
# self.staff_member.delete()
|
||||||
self.delete()
|
self.delete()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def email(self):
|
def email(self):
|
||||||
return self.staff_member.user.email
|
return self.user.email
|
||||||
|
|
||||||
@property
|
# @property
|
||||||
def user(self):
|
# def user(self):
|
||||||
return self.staff_member.user
|
# return self.staff_member.user
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def groups(self):
|
def groups(self):
|
||||||
@ -1388,6 +1394,12 @@ class Staff(models.Model, LocalizedNameMixin):
|
|||||||
models.Index(fields=["staff_type"]),
|
models.Index(fields=["staff_type"]),
|
||||||
]
|
]
|
||||||
permissions = []
|
permissions = []
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=['dealer', 'user'],
|
||||||
|
name='unique_staff_email_per_dealer'
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name}"
|
return f"{self.name}"
|
||||||
@ -1504,7 +1516,7 @@ class Customer(models.Model):
|
|||||||
verbose_name=_("Gender"),
|
verbose_name=_("Gender"),
|
||||||
)
|
)
|
||||||
dob = models.DateField(verbose_name=_("Date of Birth"), null=True, blank=True)
|
dob = models.DateField(verbose_name=_("Date of Birth"), null=True, blank=True)
|
||||||
email = models.EmailField(unique=True, verbose_name=_("Email"))
|
email = models.EmailField(verbose_name=_("Email"))
|
||||||
national_id = models.CharField(
|
national_id = models.CharField(
|
||||||
max_length=10, unique=True, verbose_name=_("National ID"), null=True, blank=True
|
max_length=10, unique=True, verbose_name=_("National ID"), null=True, blank=True
|
||||||
)
|
)
|
||||||
@ -1546,6 +1558,12 @@ class Customer(models.Model):
|
|||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=['dealer', 'email'],
|
||||||
|
name='unique_customer_email_per_dealer'
|
||||||
|
)
|
||||||
|
]
|
||||||
verbose_name = _("Customer")
|
verbose_name = _("Customer")
|
||||||
verbose_name_plural = _("Customers")
|
verbose_name_plural = _("Customers")
|
||||||
indexes = [
|
indexes = [
|
||||||
@ -1566,19 +1584,21 @@ class Customer(models.Model):
|
|||||||
|
|
||||||
def create_customer_model(self, for_lead=False):
|
def create_customer_model(self, for_lead=False):
|
||||||
customer_dict = to_dict(self)
|
customer_dict = to_dict(self)
|
||||||
customer = self.dealer.entity.create_customer(
|
customer = self.dealer.entity.get_customers().filter(email=self.email).first()
|
||||||
commit=False,
|
if not customer:
|
||||||
customer_model_kwargs={
|
customer = self.dealer.entity.create_customer(
|
||||||
"customer_name": self.full_name,
|
commit=False,
|
||||||
"address_1": self.address,
|
customer_model_kwargs={
|
||||||
"phone": self.phone_number,
|
"customer_name": self.full_name,
|
||||||
"email": self.email,
|
"address_1": self.address,
|
||||||
},
|
"phone": self.phone_number,
|
||||||
)
|
"email": self.email,
|
||||||
try:
|
},
|
||||||
customer.additional_info.update({"customer_info": customer_dict})
|
)
|
||||||
except Exception:
|
try:
|
||||||
pass
|
customer.additional_info.update({"customer_info": customer_dict})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
customer.active = False if for_lead else True
|
customer.active = False if for_lead else True
|
||||||
customer.save()
|
customer.save()
|
||||||
self.customer_model = customer
|
self.customer_model = customer
|
||||||
@ -1608,15 +1628,17 @@ class Customer(models.Model):
|
|||||||
return customer
|
return customer
|
||||||
|
|
||||||
def create_user_model(self, for_lead=False):
|
def create_user_model(self, for_lead=False):
|
||||||
user = User.objects.create_user(
|
user, created = User.objects.get_or_create(
|
||||||
username=self.email,
|
username=self.email,
|
||||||
email=self.email,
|
defaults={
|
||||||
first_name=self.first_name,
|
'email': self.email,
|
||||||
last_name=self.last_name,
|
'first_name': self.first_name,
|
||||||
password=make_random_password(),
|
'last_name': self.last_name,
|
||||||
is_staff=False,
|
'password': make_random_password(),
|
||||||
is_superuser=False,
|
'is_staff': False,
|
||||||
is_active=False if for_lead else True,
|
'is_superuser': False,
|
||||||
|
'is_active': False if for_lead else True,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
self.user = user
|
self.user = user
|
||||||
self.save()
|
self.save()
|
||||||
@ -2057,11 +2079,12 @@ class Schedule(models.Model):
|
|||||||
scheduled_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
scheduled_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
purpose = models.CharField(max_length=200, choices=PURPOSE_CHOICES)
|
purpose = models.CharField(max_length=200, choices=PURPOSE_CHOICES)
|
||||||
scheduled_at = models.DateTimeField()
|
scheduled_at = models.DateTimeField()
|
||||||
|
start_time = models.TimeField(verbose_name=_("Start Time"), null=True, blank=True)
|
||||||
|
end_time = models.TimeField(verbose_name=_("End Time"), null=True, blank=True)
|
||||||
scheduled_type = models.CharField(
|
scheduled_type = models.CharField(
|
||||||
max_length=200, choices=ScheduledType, default="Call"
|
max_length=200, choices=ScheduledType, default="Call"
|
||||||
)
|
)
|
||||||
completed = models.BooleanField(default=False, verbose_name=_("Completed"))
|
completed = models.BooleanField(default=False, verbose_name=_("Completed"))
|
||||||
duration = models.DurationField(default=timedelta(minutes=5))
|
|
||||||
notes = models.TextField(blank=True, null=True)
|
notes = models.TextField(blank=True, null=True)
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
max_length=200, choices=ScheduleStatusChoices, default="Scheduled"
|
max_length=200, choices=ScheduleStatusChoices, default="Scheduled"
|
||||||
@ -2073,11 +2096,17 @@ class Schedule(models.Model):
|
|||||||
return f"Scheduled {self.purpose} on {self.scheduled_at}"
|
return f"Scheduled {self.purpose} on {self.scheduled_at}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
def duration(self):
|
||||||
|
return (self.end_time - self.start_time).seconds
|
||||||
|
@property
|
||||||
def schedule_past_date(self):
|
def schedule_past_date(self):
|
||||||
if self.scheduled_at < now():
|
if self.scheduled_at < now():
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def get_purpose(self):
|
||||||
|
return self.purpose.replace("_", " ").title()
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["-scheduled_at"]
|
ordering = ["-scheduled_at"]
|
||||||
verbose_name = _("Schedule")
|
verbose_name = _("Schedule")
|
||||||
@ -2319,6 +2348,8 @@ class Tasks(models.Model):
|
|||||||
title = models.CharField(max_length=255, verbose_name=_("Title"))
|
title = models.CharField(max_length=255, verbose_name=_("Title"))
|
||||||
description = models.TextField(verbose_name=_("Description"), null=True, blank=True)
|
description = models.TextField(verbose_name=_("Description"), null=True, blank=True)
|
||||||
due_date = models.DateField(verbose_name=_("Due Date"))
|
due_date = models.DateField(verbose_name=_("Due Date"))
|
||||||
|
start_time = models.TimeField(verbose_name=_("Start Time"), null=True, blank=True)
|
||||||
|
end_time = models.TimeField(verbose_name=_("End Time"), null=True, blank=True)
|
||||||
completed = models.BooleanField(default=False, verbose_name=_("Completed"))
|
completed = models.BooleanField(default=False, verbose_name=_("Completed"))
|
||||||
assigned_to = models.ForeignKey(
|
assigned_to = models.ForeignKey(
|
||||||
User,
|
User,
|
||||||
@ -2439,7 +2470,7 @@ class Notification(models.Model):
|
|||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
User, on_delete=models.CASCADE, related_name="notifications"
|
User, on_delete=models.CASCADE, related_name="notifications"
|
||||||
)
|
)
|
||||||
message = models.CharField(max_length=255, verbose_name=_("Message"))
|
message = models.TextField(verbose_name=_("Message"))
|
||||||
is_read = models.BooleanField(default=False, verbose_name=_("Is Read"))
|
is_read = models.BooleanField(default=False, verbose_name=_("Is Read"))
|
||||||
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
|
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
|
||||||
|
|
||||||
@ -2723,7 +2754,7 @@ class SaleOrder(models.Model):
|
|||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
comments = models.TextField(blank=True, null=True)
|
comments = models.TextField(blank=True, null=True)
|
||||||
formatted_order_id = models.CharField(max_length=10, unique=True, editable=False)
|
formatted_order_id = models.CharField(max_length=255, unique=True, editable=False)
|
||||||
|
|
||||||
# Status and Dates
|
# Status and Dates
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
|
|||||||
@ -298,79 +298,79 @@ def update_item_model_cost(sender, instance, created, **kwargs):
|
|||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
# if created and not instance.is_sold:
|
# if created and not instance.is_sold:
|
||||||
if created:
|
# if created:
|
||||||
entity = instance.car.dealer.entity
|
# entity = instance.car.dealer.entity
|
||||||
coa = entity.get_default_coa()
|
# coa = entity.get_default_coa()
|
||||||
inventory_account = (
|
# inventory_account = (
|
||||||
entity.get_all_accounts()
|
# entity.get_all_accounts()
|
||||||
.filter(name=f"Inventory:{instance.car.id_car_make.name}")
|
# .filter(name=f"Inventory:{instance.car.id_car_make.name}")
|
||||||
.first()
|
# .first()
|
||||||
)
|
# )
|
||||||
if not inventory_account:
|
# if not inventory_account:
|
||||||
inventory_account = create_make_accounts(
|
# inventory_account = create_make_accounts(
|
||||||
entity,
|
# entity,
|
||||||
coa,
|
# coa,
|
||||||
[instance.car.id_car_make],
|
# [instance.car.id_car_make],
|
||||||
"Inventory",
|
# "Inventory",
|
||||||
roles.ASSET_CA_INVENTORY,
|
# roles.ASSET_CA_INVENTORY,
|
||||||
"debit",
|
# "debit",
|
||||||
)
|
# )
|
||||||
|
|
||||||
cogs = (
|
# cogs = (
|
||||||
entity.get_all_accounts()
|
# entity.get_all_accounts()
|
||||||
.filter(name=f"Cogs:{instance.car.id_car_make.name}")
|
# .filter(name=f"Cogs:{instance.car.id_car_make.name}")
|
||||||
.first()
|
# .first()
|
||||||
)
|
# )
|
||||||
if not cogs:
|
# if not cogs:
|
||||||
cogs = create_make_accounts(
|
# cogs = create_make_accounts(
|
||||||
entity, coa, [instance.car.id_car_make], "Cogs", roles.COGS, "debit"
|
# entity, coa, [instance.car.id_car_make], "Cogs", roles.COGS, "debit"
|
||||||
)
|
# )
|
||||||
revenue = (
|
# revenue = (
|
||||||
entity.get_all_accounts()
|
# entity.get_all_accounts()
|
||||||
.filter(name=f"Revenue:{instance.car.id_car_make.name}")
|
# .filter(name=f"Revenue:{instance.car.id_car_make.name}")
|
||||||
.first()
|
# .first()
|
||||||
)
|
# )
|
||||||
if not revenue:
|
# if not revenue:
|
||||||
revenue = create_make_accounts(
|
# revenue = create_make_accounts(
|
||||||
entity,
|
# entity,
|
||||||
coa,
|
# coa,
|
||||||
[instance.car.id_car_make],
|
# [instance.car.id_car_make],
|
||||||
"Revenue",
|
# "Revenue",
|
||||||
roles.ASSET_CA_RECEIVABLES,
|
# roles.ASSET_CA_RECEIVABLES,
|
||||||
"credit",
|
# "credit",
|
||||||
)
|
# )
|
||||||
|
|
||||||
cash_account = (
|
# cash_account = (
|
||||||
# entity.get_all_accounts()
|
# # entity.get_all_accounts()
|
||||||
# .filter(name="Cash", role=roles.ASSET_CA_CASH)
|
# # .filter(name="Cash", role=roles.ASSET_CA_CASH)
|
||||||
# .first()
|
# # .first()
|
||||||
entity.get_all_accounts()
|
# entity.get_all_accounts()
|
||||||
.filter(role=roles.ASSET_CA_CASH, role_default=True)
|
# .filter(role=roles.ASSET_CA_CASH, role_default=True)
|
||||||
.first()
|
# .first()
|
||||||
)
|
# )
|
||||||
|
|
||||||
ledger = LedgerModel.objects.create(
|
# ledger = LedgerModel.objects.create(
|
||||||
entity=entity, name=f"Inventory Purchase - {instance.car}"
|
# entity=entity, name=f"Inventory Purchase - {instance.car}"
|
||||||
)
|
# )
|
||||||
je = JournalEntryModel.objects.create(
|
# je = JournalEntryModel.objects.create(
|
||||||
ledger=ledger,
|
# ledger=ledger,
|
||||||
description=f"Acquired {instance.car} for inventory",
|
# description=f"Acquired {instance.car} for inventory",
|
||||||
)
|
# )
|
||||||
TransactionModel.objects.create(
|
# TransactionModel.objects.create(
|
||||||
journal_entry=je,
|
# journal_entry=je,
|
||||||
account=inventory_account,
|
# account=inventory_account,
|
||||||
amount=Decimal(instance.cost_price),
|
# amount=Decimal(instance.cost_price),
|
||||||
tx_type="debit",
|
# tx_type="debit",
|
||||||
description="",
|
# description="",
|
||||||
)
|
# )
|
||||||
|
|
||||||
TransactionModel.objects.create(
|
# TransactionModel.objects.create(
|
||||||
journal_entry=je,
|
# journal_entry=je,
|
||||||
account=cash_account,
|
# account=cash_account,
|
||||||
amount=Decimal(instance.cost_price),
|
# amount=Decimal(instance.cost_price),
|
||||||
tx_type="credit",
|
# tx_type="credit",
|
||||||
description="",
|
# description="",
|
||||||
)
|
# )
|
||||||
|
|
||||||
instance.car.item_model.default_amount = instance.marked_price
|
instance.car.item_model.default_amount = instance.marked_price
|
||||||
# if not isinstance(instance.car.item_model.additional_info, dict):
|
# if not isinstance(instance.car.item_model.additional_info, dict):
|
||||||
@ -952,11 +952,11 @@ def create_po_item_upload(sender, instance, created, **kwargs):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=models.Staff)
|
# @receiver(post_save, sender=models.Staff)
|
||||||
def add_service_to_staff(sender, instance, created, **kwargs):
|
# def add_service_to_staff(sender, instance, created, **kwargs):
|
||||||
if created:
|
# if created:
|
||||||
for service in Service.objects.all():
|
# for service in Service.objects.all():
|
||||||
instance.staff_member.services_offered.add(service)
|
# instance.services_offered.add(service)
|
||||||
|
|
||||||
|
|
||||||
##########################################################
|
##########################################################
|
||||||
@ -1026,10 +1026,11 @@ def po_fullfilled_notification(sender, instance, created, **kwargs):
|
|||||||
user=recipient,
|
user=recipient,
|
||||||
message=_(
|
message=_(
|
||||||
"""
|
"""
|
||||||
New Purchase Order has been added.
|
PO {po_number} has been fulfilled.
|
||||||
<a href="{url}" target="_blank">View</a>
|
<a href="{url}" target="_blank">View</a>
|
||||||
"""
|
"""
|
||||||
).format(
|
).format(
|
||||||
|
po_number=instance.po_number,
|
||||||
url=reverse(
|
url=reverse(
|
||||||
"purchase_order_detail",
|
"purchase_order_detail",
|
||||||
kwargs={"dealer_slug": dealer.slug,"entity_slug":instance.entity.slug, "pk": instance.pk},
|
kwargs={"dealer_slug": dealer.slug,"entity_slug":instance.entity.slug, "pk": instance.pk},
|
||||||
@ -1124,7 +1125,7 @@ def estimate_in_review_notification(sender, instance, created, **kwargs):
|
|||||||
url=reverse(
|
url=reverse(
|
||||||
"estimate_detail",
|
"estimate_detail",
|
||||||
kwargs={"dealer_slug": dealer.slug, "pk": instance.pk},
|
kwargs={"dealer_slug": dealer.slug, "pk": instance.pk},
|
||||||
),
|
)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1180,38 +1181,38 @@ def bill_model_in_approve_notification(sender, instance, created, **kwargs):
|
|||||||
).format(
|
).format(
|
||||||
bill_number=instance.bill_number,
|
bill_number=instance.bill_number,
|
||||||
url=reverse(
|
url=reverse(
|
||||||
"bill-detail",
|
"bill-update",
|
||||||
kwargs={"dealer_slug": dealer.slug, "entity_slug": dealer.entity.slug, "bill_pk": instance.pk},
|
kwargs={"dealer_slug": dealer.slug, "entity_slug": dealer.entity.slug, "bill_pk": instance.pk},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=BillModel)
|
# @receiver(post_save, sender=BillModel)
|
||||||
def bill_model_after_approve_notification(sender, instance, created, **kwargs):
|
# def bill_model_after_approve_notification(sender, instance, created, **kwargs):
|
||||||
if instance.is_approved():
|
# if instance.is_approved():
|
||||||
dealer = models.Dealer.objects.get(entity=instance.ledger.entity)
|
# dealer = models.Dealer.objects.get(entity=instance.ledger.entity)
|
||||||
recipients = (
|
# recipients = (
|
||||||
models.CustomGroup.objects.filter(dealer=dealer, name="Accountant")
|
# models.CustomGroup.objects.filter(dealer=dealer, name="Accountant")
|
||||||
.first()
|
# .first()
|
||||||
.group.user_set.exclude(email=dealer.user.email)
|
# .group.user_set.exclude(email=dealer.user.email)
|
||||||
.distinct()
|
# .distinct()
|
||||||
)
|
# )
|
||||||
|
|
||||||
for recipient in recipients:
|
# for recipient in recipients:
|
||||||
models.Notification.objects.create(
|
# models.Notification.objects.create(
|
||||||
user=recipient,
|
# user=recipient,
|
||||||
message=_(
|
# message=_(
|
||||||
"""
|
# """
|
||||||
Bill {bill_number} has been approved.
|
# Bill {bill_number} has been approved.
|
||||||
<a href="{url}" target="_blank">View</a>.
|
# <a href="{url}" target="_blank">View</a>.
|
||||||
please complete the bill payment.
|
# please complete the bill payment.
|
||||||
"""
|
# """
|
||||||
).format(
|
# ).format(
|
||||||
bill_number=instance.bill_number,
|
# bill_number=instance.bill_number,
|
||||||
url=reverse(
|
# url=reverse(
|
||||||
"bill-detail",
|
# "bill-detail",
|
||||||
kwargs={"dealer_slug": dealer.slug, "entity_slug": dealer.entity.slug, "bill_pk": instance.pk},
|
# kwargs={"dealer_slug": dealer.slug, "entity_slug": dealer.entity.slug, "bill_pk": instance.pk},
|
||||||
),
|
# ),
|
||||||
),
|
# ),
|
||||||
)
|
# )
|
||||||
@ -1,7 +1,9 @@
|
|||||||
import base64
|
import base64
|
||||||
import logging
|
import logging
|
||||||
from plans.models import Plan
|
from plans.models import Plan
|
||||||
|
from django.urls import reverse
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.utils import timezone
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django_ledger.io import roles
|
from django_ledger.io import roles
|
||||||
from django_q.tasks import async_task
|
from django_q.tasks import async_task
|
||||||
@ -11,11 +13,11 @@ from django.utils.translation import activate
|
|||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from allauth.account.models import EmailAddress
|
from allauth.account.models import EmailAddress
|
||||||
from inventory.models import DealerSettings, Dealer
|
|
||||||
from django.core.mail import EmailMultiAlternatives
|
from django.core.mail import EmailMultiAlternatives
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.contrib.auth.models import User, Group, Permission
|
from django.contrib.auth.models import User, Group, Permission
|
||||||
|
from inventory.models import DealerSettings, Dealer,Schedule,Notification
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
@ -67,7 +69,7 @@ def create_coa_accounts(instance):
|
|||||||
"role": roles.ASSET_CA_CASH,
|
"role": roles.ASSET_CA_CASH,
|
||||||
"balance_type": roles.DEBIT,
|
"balance_type": roles.DEBIT,
|
||||||
"locked": False,
|
"locked": False,
|
||||||
"default": False,
|
"default": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": "1030",
|
"code": "1030",
|
||||||
@ -1166,7 +1168,7 @@ def create_user_dealer(email, password, name, arabic_name, phone, crn, vrn, addr
|
|||||||
):
|
):
|
||||||
group.permissions.add(perm)
|
group.permissions.add(perm)
|
||||||
|
|
||||||
StaffMember.objects.create(user=user)
|
# StaffMember.objects.create(user=user)
|
||||||
dealer = Dealer.objects.create(
|
dealer = Dealer.objects.create(
|
||||||
user=user,
|
user=user,
|
||||||
name=name,
|
name=name,
|
||||||
@ -1255,4 +1257,66 @@ def handle_email_result(task):
|
|||||||
if task.success:
|
if task.success:
|
||||||
logger.info(f"Email task succeeded: {task.result}")
|
logger.info(f"Email task succeeded: {task.result}")
|
||||||
else:
|
else:
|
||||||
logger.error(f"Email task failed: {task.result}")
|
logger.error(f"Email task failed: {task.result}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def send_schedule_reminder_email(schedule_id):
|
||||||
|
"""
|
||||||
|
Sends an email reminder for a specific schedule.
|
||||||
|
This function is designed to be called by django-q.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
schedule = Schedule.objects.get(pk=schedule_id)
|
||||||
|
|
||||||
|
# Ensure the user has an email and the schedule is not completed/canceled
|
||||||
|
if not schedule.scheduled_by.email or schedule.status in ["completed", "canceled"]:
|
||||||
|
logger.error(f"Skipping email for Schedule ID {schedule_id}: No email or schedule status is {schedule.status}.")
|
||||||
|
return
|
||||||
|
|
||||||
|
user_email = schedule.scheduled_by.email
|
||||||
|
Notification.objects.create(
|
||||||
|
user=schedule.scheduled_by,
|
||||||
|
message=_(
|
||||||
|
"""
|
||||||
|
Reminder: You have an appointment scheduled for {scheduled_type} After 15 minutes <a href="{url}" target="_blank">View</a>.
|
||||||
|
"""
|
||||||
|
).format(scheduled_type=schedule.scheduled_type, url=reverse("schedule_calendar", kwargs={"dealer_slug": schedule.dealer.slug})),)
|
||||||
|
# Prepare context for email templates
|
||||||
|
context = {
|
||||||
|
'schedule_purpose': schedule.purpose,
|
||||||
|
'scheduled_at': schedule.scheduled_at.astimezone(timezone.get_current_timezone()).strftime('%Y-%m-%d %H:%M %Z'), # Format with timezone
|
||||||
|
'schedule_type': schedule.scheduled_type,
|
||||||
|
'customer_name': schedule.customer.customer_name if schedule.customer else 'N/A',
|
||||||
|
'notes': schedule.notes,
|
||||||
|
'user_name': schedule.scheduled_by.get_full_name() or schedule.scheduled_by.email,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Render email content from templates
|
||||||
|
html_message = render_to_string('emails/schedule_reminder.html', context)
|
||||||
|
plain_message = render_to_string('emails/schedule_reminder.txt', context)
|
||||||
|
|
||||||
|
send_mail(
|
||||||
|
f'Reminder: Your Upcoming Schedule - {schedule.purpose}',
|
||||||
|
plain_message,
|
||||||
|
settings.DEFAULT_FROM_EMAIL,
|
||||||
|
[user_email],
|
||||||
|
html_message=html_message,
|
||||||
|
)
|
||||||
|
logger.info(f"Successfully sent reminder email for Schedule ID: {schedule_id} to {user_email}")
|
||||||
|
|
||||||
|
except Schedule.DoesNotExist:
|
||||||
|
logger.info(f"Schedule with ID {schedule_id} does not exist. Cannot send reminder.")
|
||||||
|
except Exception as e:
|
||||||
|
logger.info(f"Error sending reminder email for Schedule ID {schedule_id}: {e}")
|
||||||
|
|
||||||
|
# Optional: A hook function to log the status of the email task (add to your_app/tasks.py)
|
||||||
|
def log_email_status(task):
|
||||||
|
"""
|
||||||
|
This function will be called by django-q after the send_schedule_reminder_email task completes.
|
||||||
|
It logs whether the task was successful or not.
|
||||||
|
"""
|
||||||
|
if task.success:
|
||||||
|
logger.info(f"Email task for Schedule ID {task.args[0]} completed successfully. Result: {task.result}")
|
||||||
|
else:
|
||||||
|
logger.error(f"Email task for Schedule ID {task.args[0]} failed. Error: {task.result}")
|
||||||
@ -228,6 +228,11 @@ urlpatterns = [
|
|||||||
views.OpportunityUpdateView.as_view(),
|
views.OpportunityUpdateView.as_view(),
|
||||||
name="update_opportunity",
|
name="update_opportunity",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"<slug:dealer_slug>/crm/opportunities/<slug:slug>/stage/edit",
|
||||||
|
views.OpportunityStageUpdateView.as_view(),
|
||||||
|
name="update_opportunity_stage",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"<slug:dealer_slug>/crm/opportunities/",
|
"<slug:dealer_slug>/crm/opportunities/",
|
||||||
views.OpportunityListView.as_view(),
|
views.OpportunityListView.as_view(),
|
||||||
@ -928,6 +933,7 @@ urlpatterns = [
|
|||||||
views.ItemServiceUpdateView.as_view(),
|
views.ItemServiceUpdateView.as_view(),
|
||||||
name="item_service_update",
|
name="item_service_update",
|
||||||
),
|
),
|
||||||
|
|
||||||
# Expanese
|
# Expanese
|
||||||
path(
|
path(
|
||||||
"<slug:dealer_slug>/items/expeneses/",
|
"<slug:dealer_slug>/items/expeneses/",
|
||||||
@ -1281,6 +1287,9 @@ urlpatterns = [
|
|||||||
path('feature/recall/<int:pk>/view/', views.RecallDetailView.as_view(), name='recall_detail'),
|
path('feature/recall/<int:pk>/view/', views.RecallDetailView.as_view(), name='recall_detail'),
|
||||||
path('feature/recall/create/', views.RecallCreateView.as_view(), name='recall_create'),
|
path('feature/recall/create/', views.RecallCreateView.as_view(), name='recall_create'),
|
||||||
path('feature/recall/success/', views.RecallSuccessView.as_view(), name='recall_success'),
|
path('feature/recall/success/', views.RecallSuccessView.as_view(), name='recall_success'),
|
||||||
|
|
||||||
|
path('<slug:dealer_slug>/schedules/calendar/', views.schedule_calendar, name='schedule_calendar'),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
handler404 = "inventory.views.custom_page_not_found_view"
|
handler404 = "inventory.views.custom_page_not_found_view"
|
||||||
|
|||||||
@ -186,7 +186,7 @@ def get_user_type(request):
|
|||||||
if request.is_dealer:
|
if request.is_dealer:
|
||||||
return request.user.dealer
|
return request.user.dealer
|
||||||
elif request.is_staff:
|
elif request.is_staff:
|
||||||
return request.user.staffmember.staff.dealer
|
return request.user.staff.dealer
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@ -1055,7 +1055,8 @@ class CarFinanceCalculator:
|
|||||||
quantity = self._get_quantity(item)
|
quantity = self._get_quantity(item)
|
||||||
car = item.item_model.car
|
car = item.item_model.car
|
||||||
unit_price = Decimal(car.finances.marked_price)
|
unit_price = Decimal(car.finances.marked_price)
|
||||||
|
discount = self.extra_info.data.get("discount",0)
|
||||||
|
sell_price = unit_price - Decimal(discount)
|
||||||
return {
|
return {
|
||||||
"item_number": item.item_model.item_number,
|
"item_number": item.item_model.item_number,
|
||||||
"vin": car.vin, #car_info.get("vin"),
|
"vin": car.vin, #car_info.get("vin"),
|
||||||
@ -1071,8 +1072,13 @@ class CarFinanceCalculator:
|
|||||||
"discount": car.finances.discount_amount,
|
"discount": car.finances.discount_amount,
|
||||||
"quantity": quantity,
|
"quantity": quantity,
|
||||||
"unit_price": unit_price,
|
"unit_price": unit_price,
|
||||||
"total": unit_price * Decimal(quantity),
|
"sell_price": sell_price,
|
||||||
"total_vat": car.finances.total_vat,
|
"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(
|
"additional_services": car.additional_services,# self._get_nested_value(
|
||||||
#item, self.ADDITIONAL_SERVICES_KEY
|
#item, self.ADDITIONAL_SERVICES_KEY
|
||||||
#),
|
#),
|
||||||
@ -1087,10 +1093,10 @@ class CarFinanceCalculator:
|
|||||||
Decimal(item.price_) for item in self._get_additional_services())
|
Decimal(item.price_) for item in self._get_additional_services())
|
||||||
|
|
||||||
total_discount = self.extra_info.data.get("discount",0)
|
total_discount = self.extra_info.data.get("discount",0)
|
||||||
|
|
||||||
total_price_discounted = total_price
|
total_price_discounted = total_price
|
||||||
if total_discount:
|
if total_discount:
|
||||||
total_price_discounted = total_price - Decimal(total_discount)
|
total_price_discounted = total_price - Decimal(total_discount)
|
||||||
|
print(total_price_discounted)
|
||||||
total_vat_amount = total_price_discounted * self.vat_rate
|
total_vat_amount = total_price_discounted * self.vat_rate
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -1110,16 +1116,16 @@ class CarFinanceCalculator:
|
|||||||
"quantity": sum(
|
"quantity": sum(
|
||||||
self._get_quantity(item) for item in self.item_transactions
|
self._get_quantity(item) for item in self.item_transactions
|
||||||
),
|
),
|
||||||
"total_price": totals["total_price"],
|
"total_price": round(totals["total_price"], 2),
|
||||||
"total_price_discounted": totals["total_price_discounted"],
|
"total_price_discounted": round(totals["total_price_discounted"], 2),
|
||||||
"total_price_before_discount": totals["total_price_before_discount"],
|
"total_price_before_discount": round(totals["total_price_before_discount"], 2),
|
||||||
"total_vat": totals["total_vat_amount"] + totals["total_price"],
|
"total_vat": round(totals["total_vat_amount"] + totals["total_price"], 2),
|
||||||
"total_vat_amount": totals["total_vat_amount"],
|
"total_vat_amount": round(totals["total_vat_amount"], 2),
|
||||||
"total_discount": totals["total_discount"],
|
"total_discount": round(totals["total_discount"], 2),
|
||||||
"total_additionals": totals["total_additionals"],
|
"total_additionals": round(totals["total_additionals"], 2),
|
||||||
"grand_total": totals["grand_total"],
|
"grand_total": round(totals["grand_total"], 2),
|
||||||
"additionals": self._get_additional_services(),
|
"additionals": self._get_additional_services(),
|
||||||
"vat": self.vat_rate,
|
"vat": round(self.vat_rate, 2),
|
||||||
}
|
}
|
||||||
# class CarFinanceCalculator:
|
# class CarFinanceCalculator:
|
||||||
# """
|
# """
|
||||||
@ -1371,10 +1377,10 @@ def _post_sale_and_cogs(invoice, dealer):
|
|||||||
).first().item_model.car
|
).first().item_model.car
|
||||||
qty = Decimal(car_data['quantity'])
|
qty = Decimal(car_data['quantity'])
|
||||||
|
|
||||||
net_car_price = Decimal(car_data['total'])
|
net_car_price = Decimal(car_data['total']) - Decimal(car_data['total_discount'])
|
||||||
net_add_price = Decimal(data['total_additionals'])
|
net_additionals_price = Decimal(data['total_additionals'])
|
||||||
vat_amount = Decimal(data['total_vat_amount']) * qty
|
vat_amount = Decimal(data['total_vat_amount']) * qty
|
||||||
grand_total = net_car_price + net_add_price + vat_amount
|
grand_total = net_car_price + net_additionals_price + vat_amount
|
||||||
cost_total = Decimal(car_data['cost_price']) * qty
|
cost_total = Decimal(car_data['cost_price']) * qty
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@ -1420,12 +1426,12 @@ def _post_sale_and_cogs(invoice, dealer):
|
|||||||
tx_type='credit'
|
tx_type='credit'
|
||||||
)
|
)
|
||||||
|
|
||||||
if net_add_price > 0:
|
if net_additionals_price > 0:
|
||||||
# Cr Sales – Additional Services
|
# Cr Sales – Additional Services
|
||||||
TransactionModel.objects.create(
|
TransactionModel.objects.create(
|
||||||
journal_entry=je_sale,
|
journal_entry=je_sale,
|
||||||
account=add_rev,
|
account=add_rev,
|
||||||
amount=net_add_price,
|
amount=net_additionals_price,
|
||||||
tx_type='credit'
|
tx_type='credit'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -511,8 +511,8 @@ class SalesDashboard(LoginRequiredMixin, TemplateView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
dealer = get_user_type(self.request)
|
dealer = self.request.dealer
|
||||||
staff = getattr(self.request.user, "staff", None)
|
staff = self.request.staff
|
||||||
total_cars = models.Car.objects.filter(dealer=dealer).count()
|
total_cars = models.Car.objects.filter(dealer=dealer).count()
|
||||||
total_reservations = models.CarReservation.objects.filter(
|
total_reservations = models.CarReservation.objects.filter(
|
||||||
reserved_by=self.request.user, reserved_until__gte=timezone.now()
|
reserved_by=self.request.user, reserved_until__gte=timezone.now()
|
||||||
@ -2478,7 +2478,7 @@ class CustomerCreateView(
|
|||||||
customer = form.instance.create_customer_model()
|
customer = form.instance.create_customer_model()
|
||||||
|
|
||||||
form.instance.user = user
|
form.instance.user = user
|
||||||
form.instance.customer_model = customer
|
# form.instance.customer_model = customer
|
||||||
|
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
@ -3417,7 +3417,7 @@ class UserCreateView(
|
|||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
|
|
||||||
email = form.cleaned_data["email"]
|
email = form.cleaned_data["email"]
|
||||||
if User.objects.filter(email=email).exists():
|
if models.Staff.objects.filter(dealer=dealer, user__email=email).exists():
|
||||||
messages.error(
|
messages.error(
|
||||||
self.request,
|
self.request,
|
||||||
_(
|
_(
|
||||||
@ -3425,16 +3425,25 @@ class UserCreateView(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
return redirect("user_create", dealer_slug=dealer.slug)
|
return redirect("user_create", dealer_slug=dealer.slug)
|
||||||
|
|
||||||
password = "Tenhal@123"
|
password = "Tenhal@123"
|
||||||
|
|
||||||
user = User.objects.create_user(first_name=staff.first_name, last_name=staff.last_name, username=email, email=email, password=password)
|
user, created = User.objects.get_or_create(
|
||||||
|
email=email,
|
||||||
|
defaults={
|
||||||
|
"first_name": staff.first_name,
|
||||||
|
"last_name": staff.last_name,
|
||||||
|
"username": email,
|
||||||
|
"password": password,
|
||||||
|
},
|
||||||
|
)
|
||||||
user.is_staff = True
|
user.is_staff = True
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
staff_member = StaffMember.objects.create(user=user)
|
# staff_member, _ = StaffMember.objects.get_or_create(user=user)
|
||||||
for service in form.cleaned_data["service_offered"]:
|
# for service in form.cleaned_data["service_offered"]:
|
||||||
staff_member.services_offered.add(service)
|
# staff_member.services_offered.add(service)
|
||||||
staff.staff_member = staff_member
|
staff.user = user
|
||||||
staff.dealer = dealer
|
staff.dealer = dealer
|
||||||
staff.save()
|
staff.save()
|
||||||
self.staff_pk = staff.pk
|
self.staff_pk = staff.pk
|
||||||
@ -3495,7 +3504,7 @@ class UserUpdateView(
|
|||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
initial = super().get_initial()
|
initial = super().get_initial()
|
||||||
initial["email"] = self.object.staff_member.user.email
|
initial["email"] = self.object.user.email
|
||||||
initial["group"] = self.object.groups
|
initial["group"] = self.object.groups
|
||||||
|
|
||||||
return initial
|
return initial
|
||||||
@ -4350,7 +4359,7 @@ def sales_list_view(request, dealer_slug):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
||||||
staff = getattr(request.user.staffmember, "staff", None)
|
staff = getattr(request.user, "staff", None)
|
||||||
qs = []
|
qs = []
|
||||||
try:
|
try:
|
||||||
if any([request.is_dealer, request.is_manager, request.is_accountant]):
|
if any([request.is_dealer, request.is_manager, request.is_accountant]):
|
||||||
@ -4525,7 +4534,7 @@ def create_estimate(request, dealer_slug, slug=None):
|
|||||||
data = json.loads(request.body)
|
data = json.loads(request.body)
|
||||||
title = data.get("title")
|
title = data.get("title")
|
||||||
customer_id = data.get("customer")
|
customer_id = data.get("customer")
|
||||||
customer = models.Customer.objects.filter(pk=int(customer_id)).first()
|
customer = models.Customer.objects.filter(pk=int(customer_id),dealer=dealer).first()
|
||||||
|
|
||||||
items = data.get("item", [])
|
items = data.get("item", [])
|
||||||
quantities = data.get("quantity", [])
|
quantities = data.get("quantity", [])
|
||||||
@ -4798,7 +4807,7 @@ class EstimateDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView
|
|||||||
finance_data = calculator.get_finance_data()
|
finance_data = calculator.get_finance_data()
|
||||||
invoice_obj = InvoiceModel.objects.all().filter(ce_model=estimate).first()
|
invoice_obj = InvoiceModel.objects.all().filter(ce_model=estimate).first()
|
||||||
kwargs["data"] = finance_data
|
kwargs["data"] = finance_data
|
||||||
print(kwargs["data"])
|
|
||||||
kwargs["invoice"] = invoice_obj
|
kwargs["invoice"] = invoice_obj
|
||||||
try:
|
try:
|
||||||
car_finances = estimate.get_itemtxs_data()[0].first().item_model.car.finances
|
car_finances = estimate.get_itemtxs_data()[0].first().item_model.car.finances
|
||||||
@ -5173,7 +5182,7 @@ class InvoiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
||||||
entity = dealer.entity
|
entity = dealer.entity
|
||||||
staff = getattr(self.request.user.staffmember, "staff", None)
|
staff = getattr(self.request.user, "staff", None)
|
||||||
qs = []
|
qs = []
|
||||||
try:
|
try:
|
||||||
if any(
|
if any(
|
||||||
@ -5456,9 +5465,9 @@ def invoice_create(request, dealer_slug, pk):
|
|||||||
|
|
||||||
invoice_itemtxs = {
|
invoice_itemtxs = {
|
||||||
i.get("item_number"): {
|
i.get("item_number"): {
|
||||||
"unit_cost": i.get("total_vat"),
|
"unit_cost": i.get("grand_total"),
|
||||||
"quantity": i.get("quantity"),
|
"quantity": 1,
|
||||||
"total_amount": i.get("total_vat"),
|
"total_amount": i.get("grand_total"),
|
||||||
}
|
}
|
||||||
for i in finance_data.get("cars")
|
for i in finance_data.get("cars")
|
||||||
}
|
}
|
||||||
@ -5933,12 +5942,12 @@ class LeadDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|||||||
)
|
)
|
||||||
context["transfer_form"] = forms.LeadTransferForm()
|
context["transfer_form"] = forms.LeadTransferForm()
|
||||||
context["transfer_form"].fields["transfer_to"].queryset = (
|
context["transfer_form"].fields["transfer_to"].queryset = (
|
||||||
models.Staff.objects.select_related("staff_member", "staff_member__user")
|
models.Staff.objects.select_related("user")
|
||||||
.filter(
|
.filter(
|
||||||
dealer=dealer,
|
dealer=dealer,
|
||||||
staff_member__user__groups__permissions__codename__contains="can_reassign_lead",
|
user__groups__permissions__codename__contains="can_reassign_lead",
|
||||||
)
|
)
|
||||||
.exclude(staff_member__user=self.request.user)
|
.exclude(user=self.request.user)
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -6062,10 +6071,10 @@ def lead_create(request, dealer_slug):
|
|||||||
)
|
)
|
||||||
form.fields["staff"].queryset = (
|
form.fields["staff"].queryset = (
|
||||||
form.fields["staff"]
|
form.fields["staff"]
|
||||||
.queryset.select_related("staff_member", "staff_member__user")
|
.queryset.select_related("user")
|
||||||
.filter(
|
.filter(
|
||||||
dealer=dealer,
|
dealer=dealer,
|
||||||
staff_member__user__groups__permissions__codename__contains="add_lead",
|
user__groups__permissions__codename__contains="add_lead",
|
||||||
)
|
)
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
@ -6093,8 +6102,8 @@ def lead_create(request, dealer_slug):
|
|||||||
def lead_tracking(request, dealer_slug):
|
def lead_tracking(request, dealer_slug):
|
||||||
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
||||||
staff = (
|
staff = (
|
||||||
models.Staff.objects.select_related("staff_member", "staff_member__user")
|
models.Staff.objects.select_related("user")
|
||||||
.filter(dealer=dealer, staff_member__user=request.user)
|
.filter(dealer=dealer, user=request.user)
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -6275,10 +6284,10 @@ class LeadUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
|||||||
].queryset = form.instance.id_car_make.carmodel_set.all()
|
].queryset = form.instance.id_car_make.carmodel_set.all()
|
||||||
form.fields["staff"].queryset = (
|
form.fields["staff"].queryset = (
|
||||||
form.fields["staff"]
|
form.fields["staff"]
|
||||||
.queryset.select_related("staff_member", "staff_member__user")
|
.queryset.select_related("user")
|
||||||
.filter(
|
.filter(
|
||||||
dealer=dealer,
|
dealer=dealer,
|
||||||
staff_member__user__groups__permissions__codename__contains="add_lead",
|
user__groups__permissions__codename__contains="add_lead",
|
||||||
)
|
)
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
@ -6522,6 +6531,8 @@ def schedule_event(request, dealer_slug, content_type, slug):
|
|||||||
return redirect(f"{content_type}_detail", dealer_slug=dealer_slug, slug=slug)
|
return redirect(f"{content_type}_detail", dealer_slug=dealer_slug, slug=slug)
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
from django_q.models import Schedule as DjangoQSchedule
|
||||||
|
|
||||||
form = forms.ScheduleForm(request.POST)
|
form = forms.ScheduleForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
instance = form.save(commit=False)
|
instance = form.save(commit=False)
|
||||||
@ -6534,34 +6545,33 @@ def schedule_event(request, dealer_slug, content_type, slug):
|
|||||||
elif obj.organization:
|
elif obj.organization:
|
||||||
instance.cutsomer = obj.organization.customer_model
|
instance.cutsomer = obj.organization.customer_model
|
||||||
|
|
||||||
service = Service.objects.get(name=instance.scheduled_type)
|
# service = Service.objects.get(name=instance.scheduled_type)
|
||||||
# Log attempt to create AppointmentRequest
|
# # Log attempt to create AppointmentRequest
|
||||||
logger.debug(
|
# logger.debug(
|
||||||
f"User {user_username} attempting to create AppointmentRequest "
|
# f"User {user_username} attempting to create AppointmentRequest "
|
||||||
f"for {content_type} ID: {obj.pk} ('{obj.slug}'). Service: '{service.name}', "
|
# f"for {content_type} ID: {obj.pk} ('{obj.slug}'). Service: '{service.name}', "
|
||||||
f"Scheduled At: '{instance.scheduled_at}', Duration: '{instance.duration}'."
|
# f"Scheduled At: '{instance.scheduled_at}', Duration: '{instance.duration}'."
|
||||||
)
|
# )
|
||||||
|
|
||||||
try:
|
# try:
|
||||||
appointment_request = AppointmentRequest.objects.create(
|
# appointment_request = AppointmentRequest.objects.create(
|
||||||
date=instance.scheduled_at.date(),
|
# date=instance.scheduled_at.date(),
|
||||||
start_time=instance.scheduled_at.time(),
|
|
||||||
end_time=(instance.scheduled_at + instance.duration).time(),
|
|
||||||
service=service,
|
|
||||||
staff_member=request.user.staffmember,
|
|
||||||
)
|
|
||||||
except ValidationError as e:
|
|
||||||
messages.error(request, str(e))
|
|
||||||
return redirect(request.META.get("HTTP_REFERER"))
|
|
||||||
|
|
||||||
client = get_object_or_404(User, email=instance.customer.email)
|
# service=service,
|
||||||
|
# staff_member=request.user.staffmember,
|
||||||
|
# )
|
||||||
|
# except ValidationError as e:
|
||||||
|
# messages.error(request, str(e))
|
||||||
|
# return redirect(request.META.get("HTTP_REFERER"))
|
||||||
|
|
||||||
|
# client = get_object_or_404(User, email=instance.customer.email)
|
||||||
# Create Appointment
|
# Create Appointment
|
||||||
Appointment.objects.create(
|
# Appointment.objects.create(
|
||||||
client=client,
|
# client=client,
|
||||||
appointment_request=appointment_request,
|
# appointment_request=appointment_request,
|
||||||
phone=instance.customer.phone,
|
# phone=instance.customer.phone,
|
||||||
address=instance.customer.address_1,
|
# address=instance.customer.address_1,
|
||||||
)
|
# )
|
||||||
|
|
||||||
instance.save()
|
instance.save()
|
||||||
models.Activity.objects.create(
|
models.Activity.objects.create(
|
||||||
@ -6571,12 +6581,20 @@ def schedule_event(request, dealer_slug, content_type, slug):
|
|||||||
created_by=request.user,
|
created_by=request.user,
|
||||||
activity_type=instance.scheduled_type,
|
activity_type=instance.scheduled_type,
|
||||||
)
|
)
|
||||||
|
scheduled_at_aware = timezone.make_aware(instance.scheduled_at, timezone.get_current_timezone()) if timezone.is_naive(instance.scheduled_at) else instance.scheduled_at
|
||||||
|
|
||||||
# --- Logging for successful AppointmentRequest and Appointment creation ---
|
reminder_time = scheduled_at_aware - timezone.timedelta(minutes=15)
|
||||||
logger.info(
|
# Only schedule if the reminder time is in the future
|
||||||
f"User {user_username} successfully scheduled {content_type} ID: {obj.pk} ('{obj.slug}'). "
|
# Reminder emails are scheduled to be sent 15 minutes before the scheduled time
|
||||||
f"AppointmentRequest ID: {appointment_request.pk}, Appointment ID: {appointment_request.appointment.pk}."
|
if reminder_time > timezone.now():
|
||||||
)
|
DjangoQSchedule.objects.create(
|
||||||
|
name=f"send_schedule_reminder_email_to_{instance.scheduled_by.email}_for_{content_type}_with_PK_{instance.pk}",
|
||||||
|
func='inventory.tasks.send_schedule_reminder_email',
|
||||||
|
args=f'"{instance.pk}"',
|
||||||
|
schedule_type=DjangoQSchedule.ONCE,
|
||||||
|
next_run=reminder_time,
|
||||||
|
hook='inventory.tasks.log_email_status',
|
||||||
|
)
|
||||||
messages.success(request, _("Appointment Created Successfully"))
|
messages.success(request, _("Appointment Created Successfully"))
|
||||||
|
|
||||||
return redirect(f'{content_type}_detail',dealer_slug=dealer_slug, slug=slug)
|
return redirect(f'{content_type}_detail',dealer_slug=dealer_slug, slug=slug)
|
||||||
@ -6762,27 +6780,37 @@ def send_lead_email(request, dealer_slug, slug, email_pk=None):
|
|||||||
# return response
|
# return response
|
||||||
# return redirect("lead_list", dealer_slug=dealer_slug)
|
# return redirect("lead_list", dealer_slug=dealer_slug)
|
||||||
msg = f"""
|
msg = f"""
|
||||||
السلام عليكم
|
السلام عليكم {lead.full_name},
|
||||||
Dear {lead.full_name},
|
|
||||||
|
|
||||||
أود أن أشارككم تقدير المشروع الذي ناقشناه. يرجى العثور على الوثيقة التفصيلية للمقترح المرفقة.
|
شكراً لزيارتك لـ {lead.dealer.name}! لقد كان من دواعي سرورنا مساعدتك اليوم.
|
||||||
|
|
||||||
I hope this email finds you well. I wanted to share with you the estimate for the project we discussed. Please find the detailed estimate document attached.
|
لقد أنشأنا ملفاً شخصياً لك في نظامنا لتتبع تفضيلاتك والسيارات التي تهتم بها. سنتواصل معك قريباً للمتابعة والإجابة على أي أسئلة أخرى قد تكون لديك.
|
||||||
|
|
||||||
يرجى مراجعة المقترح وإعلامي إذا كانت لديك أي أسئلة أو مخاوف. إذا كانت كل شيء يبدو جيدًا، يمكننا المضي قدمًا في المشروع.
|
في هذه الأثناء، لا تتردد في الاتصال بنا مباشرة على {lead.dealer.phone_number} أو زيارتنا مرة أخرى في أي وقت يناسبك.
|
||||||
|
|
||||||
Please review the estimate and let me know if you have any questions or concerns. If everything looks good, we can proceed with the project.
|
نتطلع إلى مساعدتك في العثور على سيارتك القادمة!
|
||||||
|
|
||||||
شكراً لاهتمامكم بهذا الأمر.
|
تحياتي،
|
||||||
Thank you for your attention to this matter.
|
{lead.dealer.arabic_name}
|
||||||
|
{lead.dealer.address}
|
||||||
|
{lead.dealer.phone_number}
|
||||||
|
-----
|
||||||
|
Dear {lead.full_name},
|
||||||
|
|
||||||
تحياتي,
|
Thank you for visiting {lead.dealer.name}! It was a pleasure to assist you today.
|
||||||
Best regards,
|
|
||||||
[Your Name]
|
We've created a profile for you in our system to keep track of your preferences and the vehicles you're interested in. We'll be in touch shortly to follow up and answer any further questions you may have.
|
||||||
[Your Position]
|
|
||||||
[Your Company]
|
In the meantime, feel free to contact us directly at {lead.dealer.phone_number} or visit us again at your convenience.
|
||||||
[Your Contact Information]
|
|
||||||
"""
|
We look forward to helping you find your next car!
|
||||||
|
|
||||||
|
Best regards,
|
||||||
|
{lead.dealer.name}
|
||||||
|
{lead.dealer.address}
|
||||||
|
{lead.dealer.phone_number}
|
||||||
|
|
||||||
|
"""
|
||||||
subject = ""
|
subject = ""
|
||||||
if email_pk:
|
if email_pk:
|
||||||
email = get_object_or_404(models.Email, pk=email_pk)
|
email = get_object_or_404(models.Email, pk=email_pk)
|
||||||
@ -6822,7 +6850,7 @@ class OpportunityCreateView(
|
|||||||
template_name = "crm/opportunities/opportunity_form.html"
|
template_name = "crm/opportunities/opportunity_form.html"
|
||||||
success_message = _("Opportunity created successfully.")
|
success_message = _("Opportunity created successfully.")
|
||||||
permission_required = ["inventory.add_opportunity"]
|
permission_required = ["inventory.add_opportunity"]
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
initial = super().get_initial()
|
initial = super().get_initial()
|
||||||
dealer = get_object_or_404(models.Dealer, slug=self.kwargs.get("dealer_slug"))
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs.get("dealer_slug"))
|
||||||
@ -6898,11 +6926,12 @@ class OpportunityUpdateView(
|
|||||||
template_name = "crm/opportunities/opportunity_form.html"
|
template_name = "crm/opportunities/opportunity_form.html"
|
||||||
success_message = _("Opportunity updated successfully.")
|
success_message = _("Opportunity updated successfully.")
|
||||||
permission_required = ["inventory.change_opportunity"]
|
permission_required = ["inventory.change_opportunity"]
|
||||||
|
|
||||||
|
|
||||||
def get_form(self, form_class=None):
|
def get_form(self, form_class=None):
|
||||||
form = super().get_form(form_class)
|
form = super().get_form(form_class)
|
||||||
dealer = get_object_or_404(models.Dealer, slug=self.kwargs.get("dealer_slug"))
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs.get("dealer_slug"))
|
||||||
staff = getattr(self.request.user.staffmember, "staff", None)
|
staff = getattr(self.request.user, "staff", None)
|
||||||
form.fields["car"].queryset = models.Car.objects.filter(
|
form.fields["car"].queryset = models.Car.objects.filter(
|
||||||
dealer=dealer, status="available", finances__marked_price__gt=0
|
dealer=dealer, status="available", finances__marked_price__gt=0
|
||||||
)
|
)
|
||||||
@ -6920,6 +6949,46 @@ class OpportunityUpdateView(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class OpportunityStageUpdateView(
|
||||||
|
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, UpdateView
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Handles the update functionality for Opportunity objects.
|
||||||
|
|
||||||
|
This class-based view is responsible for handling the update of existing
|
||||||
|
Opportunity instances. It uses a Django form that is specified by the
|
||||||
|
`form_class` attribute and renders a template to display and process the
|
||||||
|
update form. Access to this view is restricted to authenticated users, as
|
||||||
|
it inherits from `LoginRequiredMixin`.
|
||||||
|
|
||||||
|
It defines the model to be updated and the form template to be used. Upon
|
||||||
|
successful update, it redirects the user to the detail page of the updated
|
||||||
|
opportunity instance.
|
||||||
|
|
||||||
|
:ivar model: The model associated with this view. Represents the Opportunity model.
|
||||||
|
:type model: django.db.models.Model
|
||||||
|
:ivar form_class: The form class used to manage the Opportunity update process.
|
||||||
|
:type form_class: django.forms.ModelForm
|
||||||
|
:ivar template_name: The path to the template used to render the opportunity
|
||||||
|
update form.
|
||||||
|
:type template_name: str
|
||||||
|
"""
|
||||||
|
|
||||||
|
model = models.Opportunity
|
||||||
|
form_class = forms.OpportunityStageForm
|
||||||
|
success_message = _("Opportunity Stage updated successfully.")
|
||||||
|
permission_required = ["inventory.change_opportunity"]
|
||||||
|
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse_lazy(
|
||||||
|
"opportunity_detail",
|
||||||
|
kwargs={
|
||||||
|
"dealer_slug": self.kwargs.get("dealer_slug"),
|
||||||
|
"slug": self.object.slug,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class OpportunityDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
class OpportunityDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
@ -6942,7 +7011,7 @@ class OpportunityDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailV
|
|||||||
template_name = "crm/opportunities/opportunity_detail.html"
|
template_name = "crm/opportunities/opportunity_detail.html"
|
||||||
context_object_name = "opportunity"
|
context_object_name = "opportunity"
|
||||||
permission_required = ["inventory.view_opportunity"]
|
permission_required = ["inventory.view_opportunity"]
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
dealer = get_object_or_404(models.Dealer, slug=self.kwargs.get("dealer_slug"))
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs.get("dealer_slug"))
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
@ -7014,6 +7083,7 @@ class OpportunityDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailV
|
|||||||
"schedules": qs.filter(scheduled_at__gt=timezone.now())[:5],
|
"schedules": qs.filter(scheduled_at__gt=timezone.now())[:5],
|
||||||
}
|
}
|
||||||
context["schedule_form"] = forms.ScheduleForm()
|
context["schedule_form"] = forms.ScheduleForm()
|
||||||
|
context["stage_form"] = forms.OpportunityStageForm()
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@ -7034,17 +7104,11 @@ class OpportunityListView(LoginRequiredMixin, PermissionRequiredMixin, ListView)
|
|||||||
staff = self.request.staff
|
staff = self.request.staff
|
||||||
queryset = models.Opportunity.objects.filter(dealer=dealer, lead__staff=staff)
|
queryset = models.Opportunity.objects.filter(dealer=dealer, lead__staff=staff)
|
||||||
|
|
||||||
# Search filter
|
|
||||||
search = self.request.GET.get("q")
|
|
||||||
if search:
|
|
||||||
queryset = queryset.filter(
|
|
||||||
Q(customer__first_name__icontains=search)
|
|
||||||
| Q(customer__last_name__icontains=search)
|
|
||||||
| Q(customer__email__icontains=search)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Stage filter
|
# Stage filter
|
||||||
stage = self.request.GET.get("stage")
|
stage = self.request.GET.get("stage")
|
||||||
|
print(stage)
|
||||||
if stage:
|
if stage:
|
||||||
queryset = queryset.filter(stage=stage)
|
queryset = queryset.filter(stage=stage)
|
||||||
|
|
||||||
@ -7055,7 +7119,16 @@ class OpportunityListView(LoginRequiredMixin, PermissionRequiredMixin, ListView)
|
|||||||
elif sort == "highest":
|
elif sort == "highest":
|
||||||
queryset = queryset.order_by("-expected_revenue")
|
queryset = queryset.order_by("-expected_revenue")
|
||||||
elif sort == "closing":
|
elif sort == "closing":
|
||||||
queryset = queryset.order_by("closing_date")
|
queryset = queryset.order_by("expected_close_date")
|
||||||
|
|
||||||
|
# Search filter
|
||||||
|
search = self.request.GET.get("q")
|
||||||
|
if search:
|
||||||
|
queryset = queryset.filter(
|
||||||
|
Q(customer__first_name__icontains=search)
|
||||||
|
| Q(customer__last_name__icontains=search)
|
||||||
|
| Q(customer__email__icontains=search)
|
||||||
|
)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
@ -7303,10 +7376,15 @@ class ItemServiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView)
|
|||||||
query = self.request.GET.get("q")
|
query = self.request.GET.get("q")
|
||||||
qs = models.AdditionalServices.objects.filter(dealer=dealer).all()
|
qs = models.AdditionalServices.objects.filter(dealer=dealer).all()
|
||||||
if query:
|
if query:
|
||||||
qs = apply_search_filters(qs, query)
|
qs = qs.filter(Q(name__icontains=query)|
|
||||||
|
Q(id__icontains=query)|
|
||||||
|
Q(uom__icontains=query)
|
||||||
|
)
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ItemExpenseCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView):
|
class ItemExpenseCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, CreateView):
|
||||||
"""
|
"""
|
||||||
Represents a view for creating item expense entries.
|
Represents a view for creating item expense entries.
|
||||||
@ -7398,6 +7476,9 @@ class ItemExpenseUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateV
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ItemExpenseListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
class ItemExpenseListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||||
"""
|
"""
|
||||||
Handles the display of a list of item expenses.
|
Handles the display of a list of item expenses.
|
||||||
@ -7799,27 +7880,44 @@ def send_email_view(request, dealer_slug, pk):
|
|||||||
)
|
)
|
||||||
|
|
||||||
msg = f"""
|
msg = f"""
|
||||||
السلام عليكم
|
السلام عليكم،
|
||||||
Dear {estimate.customer.customer_name},
|
|
||||||
|
|
||||||
أود أن أشارككم عرض السعر.
|
عزيزي {estimate.customer.customer_name}،
|
||||||
|
|
||||||
I wanted to share with you the quotation.
|
يسعدني أن أشارككم عرض السعر الذي طلبتموه. يرجى الاطلاع على التفاصيل الكاملة والأسعار من خلال الرابط أدناه.
|
||||||
|
|
||||||
يرجى مراجعة عرض السعر وإعلامي إذا كانت لديك أي استفسارات أو ملاحظات. إذا كان كل شيء على ما يرام، يمكننا المتابعة في الإجراءات.
|
حرصنا على أن يكون عرضنا مناسباً وشفافاً. إذا كانت لديكم أي استفسارات أو ملاحظات، فلا تترددوا في التواصل معنا.
|
||||||
|
|
||||||
Please review the quotation and let me know if you have any questions or concerns. If everything looks good, we can proceed with the process.
|
رابط عرض السعر:
|
||||||
|
{link}
|
||||||
|
|
||||||
رابط عرض السعر:
|
نأمل أن ينال العرض إعجابكم ونتطلع إلى بدء العمل قريباً!
|
||||||
{link}
|
|
||||||
|
|
||||||
|
تحياتي،
|
||||||
|
|
||||||
تحياتي,
|
{dealer.get_local_name}
|
||||||
Best regards,
|
{dealer.phone_number}
|
||||||
{dealer.get_local_name}
|
Haikal | هيكل
|
||||||
{dealer.phone_number}
|
-----
|
||||||
هيكل | Haikal
|
Dear {estimate.customer.customer_name},
|
||||||
"""
|
|
||||||
|
I hope this email finds you well.
|
||||||
|
|
||||||
|
Following up on our conversation, I'm excited to share the quotation for your review. Please find the detailed pricing and information by clicking on the link below.
|
||||||
|
|
||||||
|
We've done our best to provide you with a fair and competitive offer. If you have any questions or would like to discuss it further, please don't hesitate to reach out.
|
||||||
|
|
||||||
|
Quotation Link:
|
||||||
|
{link}
|
||||||
|
|
||||||
|
We look forward to hearing from you and hopefully moving forward with your project!
|
||||||
|
|
||||||
|
Best regards,
|
||||||
|
|
||||||
|
{dealer.get_local_name}
|
||||||
|
{dealer.phone_number}
|
||||||
|
Haikal
|
||||||
|
"""
|
||||||
# subject = _("Quotation")
|
# subject = _("Quotation")
|
||||||
|
|
||||||
send_email(
|
send_email(
|
||||||
@ -9764,12 +9862,12 @@ def management_view(request, dealer_slug):
|
|||||||
@login_required
|
@login_required
|
||||||
@permission_required("inventory.change_dealer", raise_exception=True)
|
@permission_required("inventory.change_dealer", raise_exception=True)
|
||||||
def user_management(request, dealer_slug):
|
def user_management(request, dealer_slug):
|
||||||
get_object_or_404(models.Dealer, slug=dealer_slug)
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
||||||
context = {
|
context = {
|
||||||
"customers": models.Customer.objects.filter(active=False),
|
"customers": models.Customer.objects.filter(active=False,dealer=dealer),
|
||||||
"organizations": models.Organization.objects.filter(active=False),
|
"organizations": models.Organization.objects.filter(active=False,dealer=dealer),
|
||||||
"vendors": models.Vendor.objects.filter(active=False),
|
"vendors": models.Vendor.objects.filter(active=False,dealer=dealer),
|
||||||
"staff": models.Staff.objects.filter(active=False),
|
"staff": models.Staff.objects.filter(active=False,dealer=dealer),
|
||||||
}
|
}
|
||||||
return render(request, "admin_management/user_management.html", context)
|
return render(request, "admin_management/user_management.html", context)
|
||||||
|
|
||||||
@ -10242,8 +10340,11 @@ class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListVie
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
dealer = get_user_type(self.request)
|
dealer = get_user_type(self.request)
|
||||||
|
vendors=models.Vendor.objects.filter(dealer=dealer)
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context["entity_slug"] = dealer.entity.slug
|
context["entity_slug"] = dealer.entity.slug
|
||||||
|
context["vendors"] = vendors
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@ -10373,9 +10474,9 @@ def upload_cars(request, dealer_slug, pk=None):
|
|||||||
f"User {user_username} retrieved ItemTransactionModel PK: {pk} for car upload."
|
f"User {user_username} retrieved ItemTransactionModel PK: {pk} for car upload."
|
||||||
)
|
)
|
||||||
item = get_object_or_404(ItemTransactionModel, pk=pk)
|
item = get_object_or_404(ItemTransactionModel, pk=pk)
|
||||||
|
|
||||||
po_item = models.PoItemsUploaded.objects.get(dealer=dealer, item=item)
|
po_item = models.PoItemsUploaded.objects.get(dealer=dealer, item=item)
|
||||||
|
|
||||||
response = redirect("upload_cars", dealer_slug=dealer_slug, pk=pk)
|
response = redirect("upload_cars", dealer_slug=dealer_slug, pk=pk)
|
||||||
if po_item.status == "uploaded":
|
if po_item.status == "uploaded":
|
||||||
messages.add_message(request, messages.ERROR, "Item already uploaded.")
|
messages.add_message(request, messages.ERROR, "Item already uploaded.")
|
||||||
@ -10613,7 +10714,7 @@ def purchase_report_view(request,dealer_slug):
|
|||||||
po_quantity=0
|
po_quantity=0
|
||||||
for item in items:
|
for item in items:
|
||||||
po_amount+=item["total"]
|
po_amount+=item["total"]
|
||||||
po_quantity+=item["q"]
|
po_quantity+=item["q"]
|
||||||
|
|
||||||
total_po_amount+=po_amount
|
total_po_amount+=po_amount
|
||||||
total_po_cars+=po_quantity
|
total_po_cars+=po_quantity
|
||||||
@ -10687,13 +10788,69 @@ def purchase_report_csv_export(request,dealer_slug):
|
|||||||
])
|
])
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
# @login_required
|
||||||
|
# def car_sale_report_view(request,dealer_slug):
|
||||||
|
# dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
||||||
|
# cars_sold = models.Car.objects.filter(dealer=dealer,status='sold')
|
||||||
|
# current_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
# context={'cars_sold':cars_sold,'current_time':current_time }
|
||||||
|
# return render(request,'ledger/reports/car_sale_report.html',context)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def car_sale_report_view(request,dealer_slug):
|
def car_sale_report_view(request, dealer_slug):
|
||||||
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
||||||
cars_sold = models.Car.objects.filter(dealer=dealer,status='sold')
|
cars_sold = models.Car.objects.filter(dealer=dealer, status='sold')
|
||||||
current_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
context={'cars_sold':cars_sold,'current_time':current_time }
|
# Get filter parameters from the request
|
||||||
return render(request,'ledger/reports/car_sale_report.html',context)
|
selected_make = request.GET.get('make')
|
||||||
|
selected_model = request.GET.get('model')
|
||||||
|
selected_serie = request.GET.get('serie')
|
||||||
|
selected_year = request.GET.get('year')
|
||||||
|
|
||||||
|
# Apply filters to the queryset
|
||||||
|
if selected_make:
|
||||||
|
cars_sold = cars_sold.filter(id_car_make__name=selected_make)
|
||||||
|
if selected_model:
|
||||||
|
cars_sold = cars_sold.filter(id_car_model__name=selected_model)
|
||||||
|
if selected_serie:
|
||||||
|
cars_sold = cars_sold.filter(id_car_serie__name=selected_serie)
|
||||||
|
if selected_year:
|
||||||
|
cars_sold = cars_sold.filter(year=selected_year)
|
||||||
|
|
||||||
|
# Get distinct values for filter dropdowns
|
||||||
|
makes = models.Car.objects.filter(dealer=dealer, status='sold').values_list('id_car_make__name', flat=True).distinct()
|
||||||
|
models_qs = models.Car.objects.filter(dealer=dealer, status='sold').values_list('id_car_model__name', flat=True).distinct()
|
||||||
|
series = models.Car.objects.filter(dealer=dealer, status='sold').values_list('id_car_serie__name', flat=True).distinct()
|
||||||
|
years = models.Car.objects.filter(dealer=dealer, status='sold').values_list('year', flat=True).distinct().order_by('-year')
|
||||||
|
|
||||||
|
# # Calculate summary data for the filtered results
|
||||||
|
|
||||||
|
total_revenue = cars_sold.aggregate(total_revenue=Sum('finances__marked_price'))['total_revenue'] or 0
|
||||||
|
# total_vat = cars_sold.aggregate(total_vat=Sum('finances__vat_amount'))['total_vat'] or 0
|
||||||
|
total_discount = cars_sold.aggregate(total_discount=Sum('finances__discount_amount'))['total_discount'] or 0
|
||||||
|
|
||||||
|
current_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'cars_sold': cars_sold,
|
||||||
|
'current_time': current_time,
|
||||||
|
'dealer': dealer,
|
||||||
|
'total_revenue': total_revenue,
|
||||||
|
# 'total_vat': total_vat,
|
||||||
|
'total_discount': total_discount,
|
||||||
|
'makes': makes,
|
||||||
|
'models': models_qs,
|
||||||
|
'series': series,
|
||||||
|
'years': years,
|
||||||
|
'selected_make': selected_make,
|
||||||
|
'selected_model': selected_model,
|
||||||
|
'selected_serie': selected_serie,
|
||||||
|
'selected_year': selected_year,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, 'ledger/reports/car_sale_report.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def car_sale_report_csv_export(request,dealer_slug):
|
def car_sale_report_csv_export(request,dealer_slug):
|
||||||
@ -10737,10 +10894,10 @@ def car_sale_report_csv_export(request,dealer_slug):
|
|||||||
car.year,
|
car.year,
|
||||||
car.id_car_serie.name,
|
car.id_car_serie.name,
|
||||||
car.id_car_trim.name,
|
car.id_car_trim.name,
|
||||||
car.mileage,
|
car.mileage if car.mileage else '0',
|
||||||
car.stock_type,
|
car.stock_type,
|
||||||
car.created_at.strftime("%Y-%m-%d %H:%M:%S") if car.created_at else '',
|
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.created_at else '',
|
car.sold_date.strftime("%Y-%m-%d %H:%M:%S") if car.sold_date else '',
|
||||||
car.finances.cost_price,
|
car.finances.cost_price,
|
||||||
car.finances.marked_price,
|
car.finances.marked_price,
|
||||||
car.finances.discount_amount,
|
car.finances.discount_amount,
|
||||||
@ -10909,4 +11066,16 @@ class RecallCreateView(FormView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
class RecallSuccessView(TemplateView):
|
class RecallSuccessView(TemplateView):
|
||||||
template_name = 'recalls/recall_success.html'
|
template_name = 'recalls/recall_success.html'
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def schedule_calendar(request,dealer_slug):
|
||||||
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
||||||
|
user_schedules = models.Schedule.objects.filter(dealer=dealer,scheduled_by=request.user).order_by('scheduled_at')
|
||||||
|
upcoming_schedules = user_schedules.filter(scheduled_at__gte=timezone.now()).order_by('scheduled_at')
|
||||||
|
context = {
|
||||||
|
'schedules': user_schedules,
|
||||||
|
'upcoming_schedules':upcoming_schedules
|
||||||
|
}
|
||||||
|
return render(request, 'schedule_calendar.html', context)
|
||||||
18
pg-compose.yml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:16
|
||||||
|
container_name: dev_db
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: dev_database
|
||||||
|
POSTGRES_USER: dev_user
|
||||||
|
POSTGRES_PASSWORD: dev_password
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- dev_postgres_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
dev_postgres_data:
|
||||||
105
static/css/calendar_dark.css
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
|
||||||
|
/* Card and container styling */
|
||||||
|
.card {
|
||||||
|
background-color: #2d3748;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FullCalendar header */
|
||||||
|
.fc .fc-toolbar.fc-header-toolbar {
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-toolbar-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #edf2f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar buttons */
|
||||||
|
.fc .fc-button-group > .fc-button {
|
||||||
|
background-color: #4a5568;
|
||||||
|
border-color: #4a5568;
|
||||||
|
color: #e2e8f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-button-group > .fc-button:hover {
|
||||||
|
background-color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-button-primary:not(:disabled).fc-button-active,
|
||||||
|
.fc .fc-button-primary:not(:disabled):active {
|
||||||
|
background-color: #4299e1;
|
||||||
|
border-color: #4299e1;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Day cells */
|
||||||
|
.fc-daygrid-day {
|
||||||
|
background-color: #2d3748;
|
||||||
|
border: 1px solid #4a5568;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-day-other {
|
||||||
|
background-color: #202c3c !important;
|
||||||
|
color: #718096;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-day-today {
|
||||||
|
background-color: #38a169 !important;
|
||||||
|
border-color: #38a169 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-daygrid-day-number {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Event styling */
|
||||||
|
.fc-event {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ffffff !important;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Event colors (you can adjust these in your Django template) */
|
||||||
|
/* .fc-event-completed { background-color: #38a169; border-color: #38a169; } */
|
||||||
|
/* .fc-event-canceled { background-color: #e53e3e; border-color: #e53e3e; } */
|
||||||
|
/* .fc-event-scheduled { background-color: #4299e1; border-color: #4299e1; } */
|
||||||
|
|
||||||
|
|
||||||
|
/* List group styling */
|
||||||
|
.list-group-item {
|
||||||
|
border-color: #4a5568;
|
||||||
|
background-color: #2d3748;
|
||||||
|
color: #e2e8f0;
|
||||||
|
transition: background-color 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item:hover {
|
||||||
|
background-color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: #2d3748;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header .close {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.fc .fc-toolbar-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
99
static/css/calendar_light.css
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
/* static/css/light_theme.css */
|
||||||
|
|
||||||
|
/* Card and container styling */
|
||||||
|
.card {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
border: 1px solid #e0e6ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FullCalendar header */
|
||||||
|
.fc .fc-toolbar.fc-header-toolbar {
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-toolbar-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar buttons */
|
||||||
|
.fc .fc-button-group > .fc-button {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
border-color: #e9ecef;
|
||||||
|
color: #495057;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-button-group > .fc-button:hover {
|
||||||
|
background-color: #e2e6ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-button-primary:not(:disabled).fc-button-active,
|
||||||
|
.fc .fc-button-primary:not(:disabled):active {
|
||||||
|
background-color: #007bff;
|
||||||
|
border-color: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Day cells */
|
||||||
|
.fc-daygrid-day {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #e0e6ed;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-day-other {
|
||||||
|
background-color: #f8f9fa !important;
|
||||||
|
color: #ced4da;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-day-today {
|
||||||
|
background-color: #fff3cd !important;
|
||||||
|
border-color: #ffeeba !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-daygrid-day-number {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Event styling */
|
||||||
|
.fc-event {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ffffff !important;
|
||||||
|
text-shadow: 0 1px 1px rgba(0,0,0,0.2);
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* List group styling */
|
||||||
|
.list-group-item {
|
||||||
|
border-color: #e0e6ed;
|
||||||
|
background-color: #ffffff;
|
||||||
|
transition: background-color 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #34495e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header .close {
|
||||||
|
color: #adb5bd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.fc .fc-toolbar-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 100 KiB |
BIN
static/images/logos/vendors/logo1.jpg
vendored
Normal file
|
After Width: | Height: | Size: 19 KiB |
105
staticfiles/css/calendar_dark.css
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
|
||||||
|
/* Card and container styling */
|
||||||
|
.card {
|
||||||
|
background-color: #2d3748;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FullCalendar header */
|
||||||
|
.fc .fc-toolbar.fc-header-toolbar {
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-toolbar-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #edf2f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar buttons */
|
||||||
|
.fc .fc-button-group > .fc-button {
|
||||||
|
background-color: #4a5568;
|
||||||
|
border-color: #4a5568;
|
||||||
|
color: #e2e8f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-button-group > .fc-button:hover {
|
||||||
|
background-color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-button-primary:not(:disabled).fc-button-active,
|
||||||
|
.fc .fc-button-primary:not(:disabled):active {
|
||||||
|
background-color: #4299e1;
|
||||||
|
border-color: #4299e1;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Day cells */
|
||||||
|
.fc-daygrid-day {
|
||||||
|
background-color: #2d3748;
|
||||||
|
border: 1px solid #4a5568;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-day-other {
|
||||||
|
background-color: #202c3c !important;
|
||||||
|
color: #718096;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-day-today {
|
||||||
|
background-color: #38a169 !important;
|
||||||
|
border-color: #38a169 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-daygrid-day-number {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Event styling */
|
||||||
|
.fc-event {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ffffff !important;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Event colors (you can adjust these in your Django template) */
|
||||||
|
/* .fc-event-completed { background-color: #38a169; border-color: #38a169; } */
|
||||||
|
/* .fc-event-canceled { background-color: #e53e3e; border-color: #e53e3e; } */
|
||||||
|
/* .fc-event-scheduled { background-color: #4299e1; border-color: #4299e1; } */
|
||||||
|
|
||||||
|
|
||||||
|
/* List group styling */
|
||||||
|
.list-group-item {
|
||||||
|
border-color: #4a5568;
|
||||||
|
background-color: #2d3748;
|
||||||
|
color: #e2e8f0;
|
||||||
|
transition: background-color 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item:hover {
|
||||||
|
background-color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: #2d3748;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header .close {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.fc .fc-toolbar-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
99
staticfiles/css/calendar_light.css
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
/* static/css/light_theme.css */
|
||||||
|
|
||||||
|
/* Card and container styling */
|
||||||
|
.card {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
border: 1px solid #e0e6ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FullCalendar header */
|
||||||
|
.fc .fc-toolbar.fc-header-toolbar {
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-toolbar-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar buttons */
|
||||||
|
.fc .fc-button-group > .fc-button {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
border-color: #e9ecef;
|
||||||
|
color: #495057;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-button-group > .fc-button:hover {
|
||||||
|
background-color: #e2e6ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-button-primary:not(:disabled).fc-button-active,
|
||||||
|
.fc .fc-button-primary:not(:disabled):active {
|
||||||
|
background-color: #007bff;
|
||||||
|
border-color: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Day cells */
|
||||||
|
.fc-daygrid-day {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #e0e6ed;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-day-other {
|
||||||
|
background-color: #f8f9fa !important;
|
||||||
|
color: #ced4da;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-day-today {
|
||||||
|
background-color: #fff3cd !important;
|
||||||
|
border-color: #ffeeba !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-daygrid-day-number {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Event styling */
|
||||||
|
.fc-event {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ffffff !important;
|
||||||
|
text-shadow: 0 1px 1px rgba(0,0,0,0.2);
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* List group styling */
|
||||||
|
.list-group-item {
|
||||||
|
border-color: #e0e6ed;
|
||||||
|
background-color: #ffffff;
|
||||||
|
transition: background-color 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #34495e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header .close {
|
||||||
|
color: #adb5bd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.fc .fc-toolbar-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -132,3 +132,45 @@ html[dir="rtl"] .form-icon-container .form-control {
|
|||||||
to { transform: rotate(360deg); }
|
to { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#spinner {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
#spinner-bg {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(255, 255, 255, 0.7);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 500ms ease-in;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#spinner-bg.htmx-request {
|
||||||
|
opacity: .8;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* .fade-me-in.htmx-added {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.fade-me-in {
|
||||||
|
opacity: .9;
|
||||||
|
transition: opacity 300ms ease-out;
|
||||||
|
} */
|
||||||
|
|
||||||
|
#main_content.fade-me-in:not(.modal):not(.modal *) {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 300ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main_content.fade-me-in.htmx-added:not(.modal):not(.modal *) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 570 B |
|
After Width: | Height: | Size: 100 KiB |
BIN
staticfiles/images/default-image/user.jpg
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
staticfiles/images/logos/no-content-new.jpg
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
staticfiles/images/logos/no-content-new1.jpg
Normal file
|
After Width: | Height: | Size: 150 KiB |
BIN
staticfiles/images/logos/no-content-new2.jpg
Normal file
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 573 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 506 KiB |
|
After Width: | Height: | Size: 812 KiB |
|
After Width: | Height: | Size: 522 KiB |
|
After Width: | Height: | Size: 709 KiB |
BIN
staticfiles/images/logos/users/customer2.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
staticfiles/images/logos/users/dealer.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
staticfiles/images/logos/users/dealer_default.png
Normal file
|
After Width: | Height: | Size: 3.2 MiB |
BIN
staticfiles/images/logos/users/employee.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
staticfiles/images/logos/users/new_dealer.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
staticfiles/images/logos/users/output_2.jpg
Normal file
|
After Width: | Height: | Size: 238 KiB |
BIN
staticfiles/images/logos/users/salesperson.png
Normal file
|
After Width: | Height: | Size: 601 KiB |
BIN
staticfiles/images/logos/users/user-logo.jpg
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
staticfiles/images/logos/vendors/Gemini_Generated_Image_6tpm9i6tpm9i6tpm_1.png
vendored
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
staticfiles/images/logos/vendors/Gemini_Generated_Image_6tpm9i6tpm9i6tpm_1_PcQIcmG.png
vendored
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
staticfiles/images/logos/vendors/Gemini_Generated_Image_6tpm9i6tpm9i6tpm_2.png
vendored
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
staticfiles/images/logos/vendors/logo1.jpg
vendored
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
staticfiles/images/logos/vendors/output_2.jpg
vendored
Normal file
|
After Width: | Height: | Size: 238 KiB |
BIN
staticfiles/images/logos/vendors/output_2_tiU2l8C.jpg
vendored
Normal file
|
After Width: | Height: | Size: 238 KiB |
BIN
staticfiles/images/logos/vendors/output_4.jpg
vendored
Normal file
|
After Width: | Height: | Size: 290 KiB |
BIN
staticfiles/images/logos/vendors/output_4_owyivsr.jpg
vendored
Normal file
|
After Width: | Height: | Size: 290 KiB |
BIN
staticfiles/images/logos/vendors/salesperson.png
vendored
Normal file
|
After Width: | Height: | Size: 601 KiB |
BIN
staticfiles/images/logos/vendors/vendor.png
vendored
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
staticfiles/images/logos/vendors/vnd.png
vendored
Normal file
|
After Width: | Height: | Size: 619 KiB |
BIN
staticfiles/images/no_content/no_car.png
Normal file
|
After Width: | Height: | Size: 574 KiB |
BIN
staticfiles/images/no_content/no_estimate.jpg
Normal file
|
After Width: | Height: | Size: 573 KiB |
BIN
staticfiles/images/no_content/no_item.jpg
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
staticfiles/images/no_content/no_plan.jpg
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
staticfiles/images/no_content/no_search_results.png
Normal file
|
After Width: | Height: | Size: 7.8 MiB |
BIN
staticfiles/images/no_content/no_user.png
Normal file
|
After Width: | Height: | Size: 4.3 MiB |
BIN
staticfiles/images/no_content/no_vendor.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
@ -249,3 +249,46 @@ const getDataTableInit = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Register delete modal initializer
|
||||||
|
htmxInitializer.register(function initDeleteModals() {
|
||||||
|
const deleteModal = document.getElementById("deleteModal");
|
||||||
|
const confirmDeleteBtn = document.getElementById("deleteModalConfirm");
|
||||||
|
const deleteModalMessage = document.getElementById("deleteModalText");
|
||||||
|
|
||||||
|
// Clean up old listeners
|
||||||
|
document.querySelectorAll(".delete-btn").forEach(btn => {
|
||||||
|
btn.removeEventListener("click", handleDeleteClick);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add new listeners
|
||||||
|
document.querySelectorAll(".delete-btn").forEach(button => {
|
||||||
|
button.addEventListener("click", handleDeleteClick);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleDeleteClick() {
|
||||||
|
if (!deleteModal || !confirmDeleteBtn || !deleteModalMessage) return;
|
||||||
|
|
||||||
|
const deleteUrl = this.getAttribute("data-url");
|
||||||
|
const deleteMessage = this.getAttribute("data-message") || "Are you sure?";
|
||||||
|
|
||||||
|
confirmDeleteBtn.setAttribute("href", deleteUrl);
|
||||||
|
deleteModalMessage.textContent = deleteMessage;
|
||||||
|
|
||||||
|
if (typeof htmx !== 'undefined') htmx.process(confirmDeleteBtn);
|
||||||
|
if (typeof bootstrap !== 'undefined') new bootstrap.Modal(deleteModal).show();
|
||||||
|
}
|
||||||
|
}, "delete_modals");
|
||||||
|
|
||||||
|
// Register custom selects initializer
|
||||||
|
htmxInitializer.register(function initCustomSelects() {
|
||||||
|
// Your custom select initialization code
|
||||||
|
}, "custom_selects");
|
||||||
|
|
||||||
|
// Register form submission initializer
|
||||||
|
htmxInitializer.register(function initForms() {
|
||||||
|
// Your form handling code
|
||||||
|
}, "forms");
|
||||||
|
*/
|
||||||
1
staticfiles/spinner.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'><rect fill='#09577F' stroke='#09577F' stroke-width='15' width='30' height='30' x='25' y='85'><animate attributeName='opacity' calcMode='spline' dur='2' values='1;0;1;' keySplines='.5 0 .5 1;.5 0 .5 1' repeatCount='indefinite' begin='-.4'></animate></rect><rect fill='#09577F' stroke='#09577F' stroke-width='15' width='30' height='30' x='85' y='85'><animate attributeName='opacity' calcMode='spline' dur='2' values='1;0;1;' keySplines='.5 0 .5 1;.5 0 .5 1' repeatCount='indefinite' begin='-.2'></animate></rect><rect fill='#09577F' stroke='#09577F' stroke-width='15' width='30' height='30' x='145' y='85'><animate attributeName='opacity' calcMode='spline' dur='2' values='1;0;1;' keySplines='.5 0 .5 1;.5 0 .5 1' repeatCount='indefinite' begin='0'></animate></rect></svg>
|
||||||
BIN
staticfiles/user-logo.jpg
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
staticfiles/user-logo1.png
Normal file
|
After Width: | Height: | Size: 814 B |
@ -3,6 +3,7 @@
|
|||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans 'Admin Management' %} {% endblock %}
|
{% trans 'Admin Management' %} {% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<h3 class="my-4">{% trans "Admin Management" %}<li class="fa fa-user-cog ms-2 text-primary"></li></h3>
|
||||||
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-4 g-4 mt-10">
|
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-4 g-4 mt-10">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<a href="{% url 'user_management' request.dealer.slug %}">
|
<a href="{% url 'user_management' request.dealer.slug %}">
|
||||||
|
|||||||
@ -84,7 +84,7 @@
|
|||||||
{% include "plans/expiration_messages.html" %}
|
{% include "plans/expiration_messages.html" %}
|
||||||
{% block period_navigation %}
|
{% block period_navigation %}
|
||||||
{% endblock period_navigation %}
|
{% endblock period_navigation %}
|
||||||
<div id="main_content" class="fade-me-in" hx-boost="true" hx-target="#main_content" hx-select="#main_content" hx-swap="outerHTML transition:true" hx-select-oob="#toast-container" hx-history-elt>
|
<div id="main_content" class="fade-me-in" hx-boost="false" hx-target="#main_content" hx-select="#main_content" hx-swap="outerHTML transition:true" hx-select-oob="#toast-container" hx-history-elt>
|
||||||
<div id="spinner" class="htmx-indicator spinner-bg">
|
<div id="spinner" class="htmx-indicator spinner-bg">
|
||||||
<img src="{% static 'spinner.svg' %}" width="100" height="100" alt="">
|
<img src="{% static 'spinner.svg' %}" width="100" height="100" alt="">
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -201,32 +201,32 @@
|
|||||||
<div class="d-flex flex-wrap gap-2 mt-2">
|
<div class="d-flex flex-wrap gap-2 mt-2">
|
||||||
<!-- Update Button -->
|
<!-- Update Button -->
|
||||||
{% if perms.django_ledger.change_billmodel %}
|
{% if perms.django_ledger.change_billmodel %}
|
||||||
<a hx-boost="true" href="{% url 'bill-update' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill.uuid %}">
|
{% if "update" not in request.path %}
|
||||||
<button class="btn btn-phoenix-primary"
|
<a hx-boost="true" href="{% url 'bill-update' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill.uuid %}">
|
||||||
{% if not request.is_accountant %}disabled{% endif %}>
|
<button class="btn btn-phoenix-primary">
|
||||||
<i class="fas fa-edit me-2"></i>{% trans 'Update' %}
|
<i class="fas fa-edit me-2"></i>{% trans 'Update' %}
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
{% if "detail" not in request.path %}
|
{% if "detail" not in request.path %}
|
||||||
<!-- Mark as Draft -->
|
<!-- Mark as Draft -->
|
||||||
{% if bill.can_draft %}
|
{% if bill.can_draft %}
|
||||||
<button class="btn btn-phoenix-success"
|
<button class="btn btn-phoenix-success"
|
||||||
{% if not request.is_accountant %}disabled{% endif %}
|
|
||||||
onclick="showPOModal('Mark as Draft', '{% url 'bill-action-mark-as-draft' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Draft')">
|
onclick="showPOModal('Mark as Draft', '{% url 'bill-action-mark-as-draft' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Draft')">
|
||||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Draft' %}
|
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Draft' %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- Mark as Review -->
|
<!-- Mark as Review -->
|
||||||
{% if bill.can_review %}
|
{% if bill.can_review %}
|
||||||
|
{{request.is_accountant}}
|
||||||
<button class="btn btn-phoenix-warning"
|
<button class="btn btn-phoenix-warning"
|
||||||
{% if not request.is_accountant %}disabled{% endif %}
|
|
||||||
onclick="showPOModal('Mark as Review', '{% url 'bill-action-mark-as-review' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Review')">
|
onclick="showPOModal('Mark as Review', '{% url 'bill-action-mark-as-review' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Review')">
|
||||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Review' %}
|
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Review' %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- Mark as Approved -->
|
<!-- Mark as Approved -->
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if bill.can_approve and not request.is_manager or not request.is_dealer %}
|
{% if bill.can_approve and request.is_accountant %}
|
||||||
<button class="btn btn-phoenix-warning" disabled>
|
<button class="btn btn-phoenix-warning" disabled>
|
||||||
<i class="fas fa-hourglass-start me-2"></i><span class="text-warning">{% trans 'Waiting for Manager Approval' %}</span>
|
<i class="fas fa-hourglass-start me-2"></i><span class="text-warning">{% trans 'Waiting for Manager Approval' %}</span>
|
||||||
</button>
|
</button>
|
||||||
@ -239,7 +239,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- Mark as Paid -->
|
<!-- Mark as Paid -->
|
||||||
{% if "detail" not in request.path %}
|
|
||||||
{% if bill.can_pay %}
|
{% if bill.can_pay %}
|
||||||
<button class="btn btn-phoenix-success"
|
<button class="btn btn-phoenix-success"
|
||||||
onclick="showPOModal('Mark as Paid', '{% url 'bill-action-mark-as-paid' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Paid')">
|
onclick="showPOModal('Mark as Paid', '{% url 'bill-action-mark-as-paid' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Paid')">
|
||||||
@ -256,13 +255,12 @@
|
|||||||
<!-- Cancel Button -->
|
<!-- Cancel Button -->
|
||||||
{% if bill.can_cancel %}
|
{% if bill.can_cancel %}
|
||||||
<button class="btn btn-phoenix-danger"
|
<button class="btn btn-phoenix-danger"
|
||||||
{% if not request.is_accountant %}disabled{% endif %}
|
|
||||||
onclick="showPOModal('Mark as Canceled', '{% url 'bill-action-mark-as-canceled' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Canceled')">
|
onclick="showPOModal('Mark as Canceled', '{% url 'bill-action-mark-as-canceled' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Canceled')">
|
||||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Canceled' %}
|
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Canceled' %}
|
||||||
</button>
|
</button>
|
||||||
{% modal_action_v2 bill bill.get_mark_as_canceled_url bill.get_mark_as_canceled_message bill.get_mark_as_canceled_html_id %}
|
{% modal_action_v2 bill bill.get_mark_as_canceled_url bill.get_mark_as_canceled_message bill.get_mark_as_canceled_html_id %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -30,19 +30,11 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pagination">
|
{% if page_obj.paginator.num_pages > 1 %}
|
||||||
<span class="step-links">
|
<div class="d-flex justify-content-end mt-3">
|
||||||
{% if notifications.has_previous %}
|
<div class="d-flex">{% include 'partials/pagination.html' %}</div>
|
||||||
<a href="?page=1">« first</a>
|
</div>
|
||||||
<a href="?page={{ notifications.previous_page_number }}">previous</a>
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
<span class="current">Page {{ notifications.number }} of {{ notifications.paginator.num_pages }}.</span>
|
|
||||||
{% if notifications.has_next %}
|
|
||||||
<a href="?page={{ notifications.next_page_number }}">next</a>
|
|
||||||
<a href="?page={{ notifications.paginator.num_pages }}">last »</a>
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>No notifications found.</p>
|
<p>No notifications found.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load i18n static humanize %}
|
{% load i18n static humanize %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{{ _("Opportunity Detail") }}
|
{{ _("Opportunity Detail") }}
|
||||||
{% endblock title %}
|
{% endblock title %}
|
||||||
@ -39,8 +40,9 @@
|
|||||||
href="{% url 'update_opportunity' request.dealer.slug opportunity.slug %}">Update Opportunity</a>
|
href="{% url 'update_opportunity' request.dealer.slug opportunity.slug %}">Update Opportunity</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item"
|
<a class="dropdown-item" type="button"
|
||||||
href="{% url 'update_opportunity' request.dealer.slug opportunity.slug %}">Update Stage</a>
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#updateStageModal">Update Stage</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.inventory.delete_opportunity %}
|
{% if perms.inventory.delete_opportunity %}
|
||||||
@ -1095,6 +1097,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal fade" id="updateStageModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form method="post"
|
||||||
|
action="{% url 'update_opportunity_stage' request.dealer.slug opportunity.slug %}"
|
||||||
|
hx-post="{% url 'update_opportunity_stage' request.dealer.slug opportunity.slug %}"
|
||||||
|
hx-swap="none"
|
||||||
|
hx-on::after-request="location.reload()">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="updateStageModalLabel">{{ _("Update Opportunity Stage") }}</h5>
|
||||||
|
<button class="btn btn-close p-1"
|
||||||
|
type="button"
|
||||||
|
data-bs-dismiss="modal"
|
||||||
|
aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
{{ stage_form|crispy }}
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-phoenix-primary" type="submit">{{ _("Save") }}</button>
|
||||||
|
<button class="btn btn-phoenix-secondary"
|
||||||
|
type="button"
|
||||||
|
data-bs-dismiss="modal">{{ _("Cancel") }}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{% include 'modal/delete_modal.html' %}
|
{% include 'modal/delete_modal.html' %}
|
||||||
<!-- email Modal -->
|
<!-- email Modal -->
|
||||||
|
|||||||
@ -5,13 +5,28 @@
|
|||||||
{{ _("Opportunities") }}
|
{{ _("Opportunities") }}
|
||||||
{% endblock title %}
|
{% endblock title %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% if opportunities or request.GET.q%}
|
{% if opportunities or request.GET.q%}
|
||||||
<div class="row g-3 mt-4">
|
<div class="row g-3 mt-4">
|
||||||
<div class="col-12">
|
<div class="row g-3 justify-content-between mb-4">
|
||||||
<h2 class="mb-3">
|
<div class="col-auto">
|
||||||
{{ _("Opportunities") }}
|
<div class="d-md-flex justify-content-between">
|
||||||
<li class="fas fas fa-rocket text-primary ms-2"></li>
|
<h2 class="mb-3">
|
||||||
</h2>
|
{{ _("Opportunities") }}
|
||||||
|
<li class="fas fas fa-rocket text-primary ms-2"></li>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
{% if perms.inventory.add_opportunity %}
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<a class="btn btn-phoenix-primary btn-sm"
|
||||||
|
href="{% url 'opportunity_create' request.dealer.slug %}">
|
||||||
|
<span class="fas fa-plus me-2"></span>{{ _("Add Opportunity") }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4">
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4">
|
||||||
@ -19,29 +34,38 @@
|
|||||||
<div class="d-flex flex-column flex-lg-row align-items-start align-items-lg-center gap-3 w-100"
|
<div class="d-flex flex-column flex-lg-row align-items-start align-items-lg-center gap-3 w-100"
|
||||||
id="filter-container">
|
id="filter-container">
|
||||||
<!-- Search Input - Wider and properly aligned -->
|
<!-- Search Input - Wider and properly aligned -->
|
||||||
<div class="search-box position-relative flex-grow-1 me-2"
|
<div class="search-box position-relative flex-grow-1 me-2" style="min-width: 200px">
|
||||||
style="min-width: 200px">
|
<form class="position-relative show" id="search-form"
|
||||||
<form class="position-relative show" id="search-form">
|
hx-get=""
|
||||||
<input name="q"
|
hx-boost="false"
|
||||||
id="search-input"
|
hx-trigger="keyup changed delay:500ms, search">
|
||||||
class="form-control form-control-sm search-input search"
|
|
||||||
type="search"
|
<input name="q"
|
||||||
aria-label="Search"
|
id="search-input"
|
||||||
placeholder="{{ _("Search") }}"
|
class="form-control form-control-sm search-input search"
|
||||||
value="{{ request.GET.q }}" />
|
type="search"
|
||||||
<span class="fa fa-magnifying-glass search-box-icon"></span>
|
aria-label="Search"
|
||||||
{% if request.GET.q %}
|
placeholder="{{ _("Search") }}..."
|
||||||
<button type="button"
|
value="{{ request.GET.q}}" />
|
||||||
class="btn-close position-absolute end-0 top-50 translate-middle cursor-pointer shadow-none"
|
|
||||||
id="clear-search"
|
<span class="fa fa-magnifying-glass search-box-icon"></span>
|
||||||
aria-label="Close"></button>
|
|
||||||
{% endif %}
|
{% if request.GET.q %}
|
||||||
</form>
|
<button type="button"
|
||||||
|
class="btn-close position-absolute end-0 top-50 translate-middle cursor-pointer shadow-none"
|
||||||
|
id="clear-search"
|
||||||
|
aria-label="Clear Search"></button>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filter Dropdowns - Aligned in a row -->
|
<!-- Filter Dropdowns - Aligned in a row -->
|
||||||
<div class="d-flex flex-column flex-sm-row gap-3 w-100"
|
<div class="d-flex flex-column flex-sm-row gap-3 w-100"
|
||||||
style="max-width: 400px">
|
style="max-width: 400px">
|
||||||
<!-- Stage Filter -->
|
<!-- Stage Filter -->
|
||||||
|
<!-- Stage Filter -->
|
||||||
|
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<select class="form-select"
|
<select class="form-select"
|
||||||
name="stage"
|
name="stage"
|
||||||
@ -78,14 +102,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% if perms.inventory.add_opportunity %}
|
|
||||||
<div class="d-flex justify-content-between">
|
|
||||||
<a class="btn btn-phoenix-primary btn-sm"
|
|
||||||
href="{% url 'opportunity_create' request.dealer.slug %}">
|
|
||||||
<span class="fas fa-plus me-2"></span>{{ _("Add Opportunity") }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -103,22 +120,21 @@
|
|||||||
{% include "empty-illustration-page.html" with value="opportunity" url=create_opportunity_url %}
|
{% include "empty-illustration-page.html" with value="opportunity" url=create_opportunity_url %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% block customJS %}
|
{% block customJS %}
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
const searchInput = document.getElementById("search-input");
|
const searchInput = document.getElementById("search-input");
|
||||||
const clearButton = document.getElementById("clear-search");
|
const clearButton = document.getElementById("clear-search");
|
||||||
|
const searchForm = document.getElementById("search-form");
|
||||||
|
|
||||||
if (clearButton) {
|
if (clearButton) {
|
||||||
clearButton.addEventListener("click", function(event) {
|
clearButton.addEventListener("click", function() {
|
||||||
event.preventDefault();
|
searchInput.value = "";
|
||||||
searchInput.value = ""; // Clear input field
|
// This clears the search and triggers the htmx search
|
||||||
// Remove query parameter without reloading the page
|
// by submitting the form with an empty query.
|
||||||
const newUrl = window.location.pathname;
|
searchForm.submit();
|
||||||
history.replaceState(null, "", newUrl);
|
});
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -5,223 +5,222 @@
|
|||||||
{% endblock title %}
|
{% endblock title %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% include 'modal/delete_modal.html' %}
|
{% include 'modal/delete_modal.html' %}
|
||||||
|
{% include 'components/note_modal.html' with content_type="customer" slug=customer.slug %}
|
||||||
|
|
||||||
<!---->
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<!--heading -->
|
|
||||||
<div class="row align-items-center justify-content-between g-3 mb-4">
|
<div class="row align-items-center justify-content-between g-3 mb-4">
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<h3 class="mb-0">{% trans 'Customer details' %}<li class="fas fa-user ms-2 text-primary"></li></h3>
|
<h3 class="mb-0">{% trans 'Customer details' %}<i class="fas fa-user ms-2 text-primary"></i></h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto d-flex gap-2">
|
||||||
<div class="row g-3">
|
{% if perms.inventory.change_customer %}
|
||||||
<div class="col-auto">
|
<a href="{% url 'customer_update' request.dealer.slug customer.slug %}"
|
||||||
{% if perms.inventory.change_customer %}
|
class="btn btn-sm btn-phoenix-primary">
|
||||||
<a href="{% url 'customer_update' request.dealer.slug customer.slug %}"
|
<span class="fa-solid fa-pen-to-square me-2"></span>{{ _("Edit") }}
|
||||||
class="btn btn-sm btn-phoenix-primary"><span class="fa-solid fa-pen-to-square me-2"></span>{{ _("Update") }}</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
{% if perms.inventory.delete_customer %}
|
||||||
{% if perms.inventory.delete_customer %}
|
<button class="btn btn-sm btn-phoenix-danger delete-btn"
|
||||||
<div class="col-auto">
|
data-url="{% url 'customer_delete' request.dealer.slug customer.slug %}"
|
||||||
<button class="btn btn-phoenix-danger btn-sm delete-btn"
|
data-message="{% trans 'Are you sure you want to delete this customer?' %}"
|
||||||
data-url="{% url 'customer_delete' request.dealer.slug customer.slug %}"
|
data-bs-toggle="modal"
|
||||||
data-message="Are you sure you want to delete this customer?"
|
data-bs-target="#deleteModal">
|
||||||
data-bs-toggle="modal"
|
<i class="fas fa-trash me-1"></i>{{ _("Delete") }}
|
||||||
data-bs-target="#deleteModal">
|
</button>
|
||||||
<i class="fas fa-trash me-1"> </i>{{ _("Delete") }}
|
{% endif %}
|
||||||
</button>
|
</div>
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<div class="col-12 col-lg-4">
|
||||||
<!--cards-->
|
<div class="card h-100 shadow-sm">
|
||||||
<div class="row">
|
<div class="card-body d-flex flex-column justify-content-between pb-3">
|
||||||
|
<div class="row align-items-center g-5 mb-3 text-center text-sm-start">
|
||||||
<div class="col m-2">
|
<div class="col-12 col-sm-auto mb-sm-2">
|
||||||
<div class="card h-100">
|
<div class="avatar avatar-5xl">
|
||||||
<div class="card-body d-flex flex-column justify-content-between pb-3">
|
{% if customer.image %}
|
||||||
<div class="row align-items-center g-5 mb-3 text-center text-sm-start">
|
<img class="rounded-circle border border-2 border-primary" src="{{ customer.image.url }}" alt="{{ customer.full_name }}"/>
|
||||||
<div class="col-12 col-sm-auto mb-sm-2">
|
{% else %}
|
||||||
<div class="avatar avatar-5xl">
|
<div class="avatar-text rounded-circle bg-secondary text-white border border-2 border-primary">
|
||||||
{% if customer.image %}<img class="rounded-circle" src="{{ customer.image.url }}" alt="" />{% endif %}
|
<span class="fs-4">{{ customer.full_name|first|default:"?" }}</span>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-sm-auto flex-1">
|
|
||||||
<h3>{{ customer.full_name }}</h3>
|
|
||||||
<p class="text-body-secondary">{{ customer.created|timesince }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-content-between border-top border-dashed pt-4">
|
|
||||||
<div>
|
|
||||||
<h6>{% trans 'Invoices' %}</h6>
|
|
||||||
<p class="fs-7 text-body-secondary mb-0">{{ invoices.count }}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h6>{% trans 'Quotations' %}</h6>
|
|
||||||
<p class="fs-7 text-body-secondary mb-0">{{ estimates.count }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="col m-2">
|
|
||||||
<div class="card h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex align-items-center mb-3">
|
|
||||||
<h3 class="me-1">{% trans 'Default Address' %}</h3>
|
|
||||||
<button class="btn btn-link p-0">
|
|
||||||
<span class="fas fa-pen fs-8 ms-3 text-body-quaternary"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<h5 class="text-body-secondary">{{ _("Address") }}</h5>
|
|
||||||
<p class="text-body-secondary">{{ customer.address }}</p>
|
|
||||||
<div class="mb-3">
|
|
||||||
<h5 class="text-body-secondary">{% trans 'Email' %}</h5>
|
|
||||||
<a href="{{ customer.email }}">{{ customer.email }}</a>
|
|
||||||
</div>
|
|
||||||
<h5 class="text-body-secondary">{% trans 'Phone Number' %}</h5>
|
|
||||||
<a class="text-body-secondary" href="#">{{ customer.phone_number }}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="col m-2">
|
|
||||||
<div class="card h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
{% if perms.inventory.change_customer %}
|
|
||||||
<div class="d-flex align-items-center justify-content-end">
|
|
||||||
{% if perms.inventory.change_lead %}
|
|
||||||
<button class="btn btn-phoenix-primary btn-sm"
|
|
||||||
type="button"
|
|
||||||
onclick=""
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="#noteModal">
|
|
||||||
<span class="fas fa-plus me-1"></span>{{ _("Add Note") }}
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<table class="table fs-9 mb-0 table-responsive">
|
|
||||||
<tr>
|
|
||||||
<th class="align-middle pe-6 text-start" scope="col">{{ _("Note") }}</th>
|
|
||||||
<th class="align-middle pe-6 text-start" scope="col">{{ _("Date") }}</th>
|
|
||||||
</tr>
|
|
||||||
<tbody id="notesTable">
|
|
||||||
{% for note in notes %}
|
|
||||||
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
|
|
||||||
<td class="align-middle text-start fw-bold text-body-tertiary ps-1">{{ note.note }}</td>
|
|
||||||
<td class="align-middle text-body-tertiary text-start white-space-nowrap">{{ note.created }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-12 col-sm-auto flex-1">
|
||||||
<div class="col-12 mt-3">
|
<h3>{{ customer.full_name }}</h3>
|
||||||
<div class="mb-6">
|
<p class="text-body-secondary">{% trans "Member since:" %} {{ customer.created|date:"d M Y" }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div class="d-flex justify-content-between border-top border-dashed pt-4 mt-auto">
|
||||||
<div class="table-responsive scrollbar">
|
<div class="text-center">
|
||||||
<table class="table table-sm fs-9 mb-0">
|
<h6 class="mb-1 text-uppercase text-body-secondary fs-8">{% trans 'Invoices' %}</h6>
|
||||||
<thead class="bg-body-highlight">
|
<p class="fs-6 fw-bold mb-0">{{ invoices.count }}</p>
|
||||||
<tr>
|
</div>
|
||||||
<th class="sort align-middle" scope="col" >{% trans 'Leads'|upper %}</th>
|
<div class="text-center">
|
||||||
<th class="sort align-middle " scope="col" >{% trans 'Opportunities'|upper %}</th>
|
<h6 class="mb-1 text-uppercase text-body-secondary fs-8">{% trans 'Quotations' %}</h6>
|
||||||
<th class="sort align-middle " scope="col">{% trans 'Estimates'|upper %}</th>
|
<p class="fs-6 fw-bold mb-0">{{ estimates.count }}</p>
|
||||||
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="list" id="customer-order-table-body">
|
|
||||||
{% for lead in leads %}
|
|
||||||
<tr>
|
|
||||||
<td><a href="{% url 'lead_detail' request.dealer.slug lead.slug%}">{{lead}} ({{ forloop.counter }})<a></td>
|
|
||||||
<td><a href="{% url 'opportunity_detail' request.dealer.slug lead.opportunity.slug%}">{{lead.opportunity}} ({{ forloop.counter }})</a></td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
{% for estimate in lead.customer.customer_model.estimatemodel_set.all %}
|
|
||||||
<h4 class="me-2 my-1"><a href="{% url 'estimate_detail' request.dealer.slug estimate.pk %}"><span class="me-2">#{{forloop.counter }}</span>{{estimate}}</a></h4>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<table class="table table-sm">
|
|
||||||
<thead class="bg-body-highlight">
|
|
||||||
<tr>
|
|
||||||
<th class="sort align-middle " scope="col">{% trans 'Sale orders'|upper %}</th>
|
|
||||||
<th class="sort align-middle " scope="col">{% trans 'Invoices'|upper %}</th>
|
|
||||||
<th class="sort align-middle " scope="col">{% trans 'Car VIN'|upper %}</th>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
{% for sale_order in estimate.sale_orders.all %}
|
|
||||||
|
|
||||||
<div><a href={% url 'order_detail' request.dealer.slug sale_order.pk%}>{{estimate.sale_orders.first}}</a></div>
|
|
||||||
{% endfor %}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
|
|
||||||
<td>
|
|
||||||
{% for invoice in estimate.invoicemodel_set.all %}
|
|
||||||
|
|
||||||
{% if invoice.is_paid %}
|
|
||||||
<span class="badge badge-phoenix fs-10 badge-phoenix-success">
|
|
||||||
<div><a href="{% url 'invoice_detail' request.dealer.slug request.entity.slug invoice.pk %}">{{invoice}}</a></div>
|
|
||||||
</span>
|
|
||||||
{%else%}
|
|
||||||
<span class="badge badge-phoenix fs-10 badge-phoenix-info">
|
|
||||||
<div><a href="{% url 'invoice_detail' request.dealer.slug request.entity.slug invoice.pk %}">{{invoice}}</a></div>
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
|
|
||||||
<div><a href="#">{{estimate.itemtransactionmodel_set.first.item_model.name}}</a></div>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<br>
|
|
||||||
{% endfor %}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="col-12 col-lg-8">
|
||||||
|
<div class="card h-100 shadow-sm">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h5 class="card-title mb-0">{% trans 'Default Information' %}</h5>
|
||||||
|
</div>
|
||||||
|
<ul class="list-unstyled mb-0">
|
||||||
|
<li class="mb-2">
|
||||||
|
<strong class="text-body-secondary d-block">{% trans 'Address' %}:</strong>
|
||||||
|
<p class="mb-0">{{ customer.address|default:_("N/A") }}</p>
|
||||||
|
</li>
|
||||||
|
<li class="mb-2">
|
||||||
|
<strong class="text-body-secondary d-block">{% trans 'Email' %}:</strong>
|
||||||
|
<a href="mailto:{{ customer.email|default:"" }}" class="text-decoration-none">{{ customer.email|default:_("N/A") }}</a>
|
||||||
|
</li>
|
||||||
|
<li class="mb-0">
|
||||||
|
<strong class="text-body-secondary d-block">{% trans 'Phone Number' %}:</strong>
|
||||||
|
<a href="tel:{{ customer.phone_number|default:"" }}" class="text-decoration-none">{{ customer.phone_number|default:_("N/A") }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||||
|
<h5 class="card-title mb-0">{% trans 'Notes' %}</h5>
|
||||||
|
{% if perms.inventory.change_customer %}
|
||||||
|
<button class="btn btn-phoenix-primary btn-sm d-flex align-items-center"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#noteModal">
|
||||||
|
<i class="fas fa-plus me-1"></i>{{ _("Add Note") }}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover table-striped mb-0">
|
||||||
|
<thead class="bg-body-tertiary">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" style="width: 60%;">{% trans 'Note' %}</th>
|
||||||
|
<th scope="col" style="width: 15%;">{% trans 'Date' %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for note in notes %}
|
||||||
|
<tr class="align-middle">
|
||||||
|
<td class="text-body-secondary">{{ note.note|default_if_none:""|linebreaksbr }}</td>
|
||||||
|
<td class="text-body-secondary text-nowrap">{{ note.created|date:"d M Y" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="4" class="text-center text-body-secondary">
|
||||||
|
<i class="fas fa-info-circle me-2"></i>{% trans 'No notes found for this customer.' %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4 mb-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h5 class="card-title mb-3">{% trans 'Sales History' %}</h5>
|
||||||
|
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
||||||
|
<li class="nav-item me-6" role="presentation">
|
||||||
|
<button class="nav-link active" id="leads-tab" data-bs-toggle="tab" data-bs-target="#leads-tab-pane" type="button" role="tab" aria-controls="leads-tab-pane" aria-selected="true">{% trans 'Leads' %}</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item me-6" role="presentation">
|
||||||
|
<button class="nav-link" id="opportunities-tab" data-bs-toggle="tab" data-bs-target="#opportunities-tab-pane" type="button" role="tab" aria-controls="opportunities-tab-pane" aria-selected="false">{% trans 'Opportunities' %}</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="estimates-tab" data-bs-toggle="tab" data-bs-target="#estimates-tab-pane" type="button" role="tab" aria-controls="estimates-tab-pane" aria-selected="false">{% trans 'Estimates' %}</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="tab-content pt-3" id="myTabContent">
|
||||||
|
<div class="tab-pane fade show active" id="leads-tab-pane" role="tabpanel" aria-labelledby="leads-tab" tabindex="0">
|
||||||
|
{% for lead in leads %}
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<i class="fas fa-handshake me-2 text-primary"></i>
|
||||||
|
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}" class="fw-bold">{{ lead }}</a>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<p class="text-body-secondary">{% trans 'No leads found for this customer.' %}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane fade" id="opportunities-tab-pane" role="tabpanel" aria-labelledby="opportunities-tab" tabindex="0">
|
||||||
|
{% for lead in leads %}
|
||||||
|
{% if lead.opportunity %}
|
||||||
|
<div class="d-flex align-items-center mb-2">
|
||||||
|
<i class="fas fa-chart-line me-2 text-success"></i>
|
||||||
|
<a href="{% url 'opportunity_detail' request.dealer.slug lead.opportunity.slug %}" class="fw-bold">{{ lead.opportunity }}</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% empty %}
|
||||||
|
<p class="text-body-secondary">{% trans 'No opportunities found for this customer.' %}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane fade" id="estimates-tab-pane" role="tabpanel" aria-labelledby="estimates-tab" tabindex="0">
|
||||||
|
{% for estimate in estimates %}
|
||||||
|
<div class="card mb-3 shadow-sm">
|
||||||
|
<div class="card-body p-3">
|
||||||
|
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="fas fa-file-invoice me-2 text-info"></i>
|
||||||
|
<a href="{% url 'estimate_detail' request.dealer.slug estimate.pk %}" class="text-decoration-none">{{ estimate }}</a>
|
||||||
|
</h6>
|
||||||
|
<span class="badge bg-success">{{ estimate.created|date:"d M Y" }}</span>
|
||||||
|
</div>
|
||||||
|
<ul class="list-unstyled mb-0 ms-5">
|
||||||
|
{% for sale_order in estimate.sale_orders.all %}
|
||||||
|
<li class="mb-2">
|
||||||
|
<i class="fas fa-truck-moving me-2 text-success"></i>
|
||||||
|
<a href="{% url 'order_detail' request.dealer.slug sale_order.pk %}">{{ sale_order }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% for invoice in estimate.invoicemodel_set.all %}
|
||||||
|
<li class="mb-2">
|
||||||
|
<i class="fas fa-receipt me-2 {% if invoice.is_paid %}text-success{% else %}text-warning{% endif %}"></i>
|
||||||
|
<a href="{% url 'invoice_detail' request.dealer.slug request.entity.slug invoice.pk %}" class="text-decoration-none">{{ invoice }}</a>
|
||||||
|
<span class="badge rounded-pill {% if invoice.is_paid %}bg-success{% else %}bg-warning{% endif %} ms-2">{% if invoice.is_paid %}{% trans "Paid" %}{% else %}{% trans "Unpaid" %}{% endif %}</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% for item in estimate.itemtransactionmodel_set.all %}
|
||||||
|
<li>
|
||||||
|
<i class="fas fa-car me-2 text-primary"></i>
|
||||||
|
<a href="{% url 'car_detail' request.dealer.slug item.item_model.car.slug %}">{{ item.item_model.car.vin}} | {{item.item_model.car.id_car_make.name}} | {{item.item_model.car.id_car_model.name}}</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<p class="text-body-secondary">{% trans 'No estimates found for this customer.' %}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% include "components/note_modal.html" with content_type="customer" slug=customer.slug %}
|
{% include "components/note_modal.html" with content_type="customer" slug=customer.slug %}
|
||||||
|
|
||||||
|
|
||||||
@ -254,4 +253,5 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
|
||||||
|
{% endblock %}
|
||||||
@ -56,8 +56,8 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<h2>{{ _("Select Car Makes You Sell") }}</h2>
|
<h2 class="text-center text-primary">{{ _("Select Car Makes You Sell") }}</h2>
|
||||||
<form method="post"
|
<form method="post" class="mb-3"
|
||||||
action="{% url 'assign_car_makes' request.dealer.slug %}">
|
action="{% url 'assign_car_makes' request.dealer.slug %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="car-makes-grid">
|
<div class="car-makes-grid">
|
||||||
@ -83,7 +83,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
<button class="btn btn-phoenix-primary btn-lg" type="submit">
|
<button class="btn btn-outline-primary btn-lg" type="submit">
|
||||||
<i class="fa fa-save me-2"></i>{{ _("Save") }}
|
<i class="fa fa-save me-2"></i>{{ _("Save") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
32
templates/emails/schedule_reminder.html
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 0; }
|
||||||
|
.container { max-width: 600px; margin: 20px auto; background-color: #ffffff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); }
|
||||||
|
h2 { color: #333333; }
|
||||||
|
p { color: #555555; line-height: 1.6; }
|
||||||
|
.footer { text-align: center; margin-top: 20px; font-size: 0.8em; color: #888888; }
|
||||||
|
.highlight { font-weight: bold; color: #007bff; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h2>Hello {{ user_name }},</h2>
|
||||||
|
<p>This is a friendly reminder for your upcoming schedule:</p>
|
||||||
|
<p>
|
||||||
|
<span class="highlight">Purpose:</span> {{ schedule_purpose }}<br>
|
||||||
|
<span class="highlight">Scheduled At:</span> {{ scheduled_at }}<br>
|
||||||
|
<span class="highlight">Type:</span> {{ schedule_type }}<br>
|
||||||
|
{% if customer_name != 'N/A' %}<span class="highlight">Customer:</span> {{ customer_name }}<br>{% endif %}
|
||||||
|
{% if notes %}<span class="highlight">Notes:</span> {{ notes }}<br>{% endif %}
|
||||||
|
</p>
|
||||||
|
<p>Please be prepared for your schedule.</p>
|
||||||
|
<p>Thank you!</p>
|
||||||
|
<div class="footer">
|
||||||
|
<p>This is an automated reminder. Please do not reply to this email.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
16
templates/emails/schedule_reminder.txt
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
Hello {{ user_name }},
|
||||||
|
|
||||||
|
This is a friendly reminder for your upcoming schedule:
|
||||||
|
|
||||||
|
Purpose: {{ schedule_purpose }}
|
||||||
|
Scheduled At: {{ scheduled_at }}
|
||||||
|
Type: {{ schedule_type }}
|
||||||
|
{% if customer_name != 'N/A' %}Customer: {{ customer_name }}{% endif %}
|
||||||
|
{% if notes %}Notes: {{ notes }}{% endif %}
|
||||||
|
|
||||||
|
Please be prepared for your schedule.
|
||||||
|
|
||||||
|
Thank you!
|
||||||
|
|
||||||
|
---
|
||||||
|
This is an automated reminder. Please do not reply to this email.
|
||||||
@ -59,7 +59,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<p>
|
<p>
|
||||||
<strong>{{ _("Name") }}:</strong> {{ user.staffmember.staff }}
|
<strong>{{ _("Name") }}:</strong> {{ user.staff }}
|
||||||
</p>
|
</p>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@ -21,8 +21,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
<form method="post" novalidate>
|
<form method="post" novalidate>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
<li class="collapsed-nav-item-title d-none">{% trans "Inventory"|capfirst %}</li>
|
<li class="collapsed-nav-item-title d-none">{% trans "Inventory"|capfirst %}</li>
|
||||||
{% if perms.inventory.add_car %}
|
{% if perms.inventory.add_car %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a hx-boost="true" id="btn-add-car" class="nav-link btn-add-car" href="{% url 'car_add' request.dealer.slug %}">
|
<a hx-boost="false" id="btn-add-car" class="nav-link btn-add-car" href="{% url 'car_add' request.dealer.slug %}">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<span class="nav-link-icon"><span class="fas fa-plus-circle"></span></span><span class="nav-link-text">{% trans "add car"|capfirst %}</span>
|
<span class="nav-link-icon"><span class="fas fa-plus-circle"></span></span><span class="nav-link-text">{% trans "add car"|capfirst %}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -164,7 +164,7 @@
|
|||||||
<li class="collapsed-nav-item-title d-none">{% trans 'sales'|capfirst %}</li>
|
<li class="collapsed-nav-item-title d-none">{% trans 'sales'|capfirst %}</li>
|
||||||
{% if perms.django_ledger.add_estimatemodel %}
|
{% if perms.django_ledger.add_estimatemodel %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'estimate_create' request.dealer.slug %}">
|
<a hx-boost="false" class="nav-link" href="{% url 'estimate_create' request.dealer.slug %}">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<span class="nav-link-icon"><span class="fas fa-handshake"></span></span><span class="nav-link-text">{% trans "create quotation"|capfirst %}</span>
|
<span class="nav-link-icon"><span class="fas fa-handshake"></span></span><span class="nav-link-text">{% trans "create quotation"|capfirst %}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -352,7 +352,7 @@
|
|||||||
<a class="nav-link" href="#">
|
<a class="nav-link" href="#">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<i class="fas fa-shopping-cart"></i><span class="nav-link-text">{% trans 'Car purchase Report'|capfirst %}</span>
|
<i class="fas fa-chart-pie"></i><span class="nav-link-text">{% trans 'Car purchase Report'|capfirst %}</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@ -363,9 +363,9 @@
|
|||||||
<a class="nav-link" href="#">
|
<a class="nav-link" href="#">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<i class="fas fa-car"></i><span class="nav-link-text">{% trans 'Car Sale Report'|capfirst %}</span>
|
<i class="fas fa-chart-pie"></i><span class="nav-link-text">{% trans 'Car Sale Report'|capfirst %}</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@ -164,7 +164,7 @@
|
|||||||
<th>{% trans "Custom Card" %}</th>
|
<th>{% trans "Custom Card" %}</th>
|
||||||
<td>
|
<td>
|
||||||
{% if perms.inventory.add_customcard %}
|
{% if perms.inventory.add_customcard %}
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-sm btn-phoenix-success"
|
class="btn btn-sm btn-phoenix-success"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
data-bs-target="#mainModal"
|
data-bs-target="#mainModal"
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
<script src="{% static 'vendors/zxing/index.min.js' %}"></script>
|
<script src="{% static 'vendors/zxing/index.min.js' %}"></script>
|
||||||
<script src="{% static 'vendors/tesseract/tesseract.min.js' %}"></script>
|
<script src="{% static 'vendors/tesseract/tesseract.min.js' %}"></script>
|
||||||
{% if not vendor_exists %}
|
{% if not vendor_exists %}
|
||||||
{% url "user_create" request.dealer.slug as create_vendor_url %}
|
{% url "vendor_create" request.dealer.slug as create_vendor_url %}
|
||||||
{% include "empty-illustration-page.html" with value="Vendor" url=create_vendor_url %}
|
{% include "empty-illustration-page.html" with value="Vendor" url=create_vendor_url %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!---->
|
<!---->
|
||||||
|
|||||||
@ -18,9 +18,9 @@
|
|||||||
{% include "partials/search_box.html" %}
|
{% include "partials/search_box.html" %}
|
||||||
{% if page_obj.object_list or request.GET.q%}
|
{% if page_obj.object_list or request.GET.q%}
|
||||||
<div class="table-responsive px-1 scrollbar mt-3">
|
<div class="table-responsive px-1 scrollbar mt-3">
|
||||||
<table class="table align-items-center table-flush">
|
<table class="table align-items-center">
|
||||||
<thead>
|
<thead class="bg-body-highlight">
|
||||||
<tr class="bg-body-highlight">
|
<tr>
|
||||||
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Item Number" %}</th>
|
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Item Number" %}</th>
|
||||||
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Name" %}</th>
|
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Name" %}</th>
|
||||||
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Unit of Measure" %}</th>
|
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Unit of Measure" %}</th>
|
||||||
@ -35,12 +35,12 @@
|
|||||||
<td class="align-middle product white-space-nowrap">{{ expense.uom }}</td>
|
<td class="align-middle product white-space-nowrap">{{ expense.uom }}</td>
|
||||||
<td class="align-middle product white-space-nowrap">
|
<td class="align-middle product white-space-nowrap">
|
||||||
{% if perms.django_ledger.change_itemmodel %}
|
{% if perms.django_ledger.change_itemmodel %}
|
||||||
|
|
||||||
<a href="{% url 'item_expense_update' request.dealer.slug expense.pk %}"
|
<a href="{% url 'item_expense_update' request.dealer.slug expense.pk %}"
|
||||||
class="btn btn-sm btn-phoenix-success">{% trans "Update" %}</a>
|
class="btn btn-sm btn-phoenix-primary"><li class="fa fa-edit me-1"></li>{% trans "Update" %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle product white-space-nowrap"></td>
|
|
||||||
<td class="align-middle white-space-nowrap text-start"></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@ -16,10 +16,10 @@
|
|||||||
</div>
|
</div>
|
||||||
{% include "partials/search_box.html" %}
|
{% include "partials/search_box.html" %}
|
||||||
{% if page_obj.object_list or request.GET.q %}
|
{% if page_obj.object_list or request.GET.q %}
|
||||||
<div class="table-responsive px-1 scrollbar mt-3">
|
<div class="table-responsive px-1 scrollbar mt-3">
|
||||||
<table class="table align-items-center table-flush">
|
<table class="table align-items-center">
|
||||||
<thead>
|
<thead class="bg-body-highlight">
|
||||||
<tr class="bg-body-highlight">
|
<tr >
|
||||||
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Item Number" %}</th>
|
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Item Number" %}</th>
|
||||||
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Name" %}</th>
|
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Name" %}</th>
|
||||||
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Unit of Measure" %}</th>
|
<th class="sort white-space-nowrap align-middle" scope="col">{% trans "Unit of Measure" %}</th>
|
||||||
@ -39,7 +39,7 @@
|
|||||||
<td class="align-middle white-space-nowrap text-start">
|
<td class="align-middle white-space-nowrap text-start">
|
||||||
{% if perms.inventory.add_additionalservices %}
|
{% if perms.inventory.add_additionalservices %}
|
||||||
<a href="{% url 'item_service_update' request.dealer.slug service.pk %}"
|
<a href="{% url 'item_service_update' request.dealer.slug service.pk %}"
|
||||||
class="btn btn-sm btn-phoenix-success">{% trans "Update" %}</a>
|
class="btn btn-sm btn-phoenix-primary"><li class="fa fa-edit me-1"></li>{% trans "Update" %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@ -7,98 +7,177 @@
|
|||||||
|
|
||||||
{% block content%}
|
{% block content%}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
.summary-card {
|
||||||
|
border: 1px solid var(--card-border);
|
||||||
|
border-radius: .5rem;
|
||||||
|
box-shadow: var(--card-shadow);
|
||||||
|
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
.summary-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.summary-card .card-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.summary-card .card-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--secondary-color) !important;
|
||||||
|
}
|
||||||
|
.summary-card .card-text {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
<div class="container-fluid report-container">
|
<div class="container-fluid report-container">
|
||||||
<header class="report-header text-center">
|
<header class="report-header text-center">
|
||||||
<h1 class="display-4">{% trans 'Car Sale Report' %} <span class="fas fa-car mx-2 text-primary"></span><span class="fas fa-money-bill mx-2 text-primary"></span></h1>
|
<h1 class="display-4">{% trans 'Car Sale Report' %} <span class="fas fa-chart-line mx-2 text-primary"></span></h1>
|
||||||
<p class="lead text-muted"><strong>{{dealer}}</strong></p>
|
<p class="lead text-muted"><strong>{{dealer}}</strong></p>
|
||||||
<p class="text-muted">{% trans 'Report Date' %}: {{current_time}}</p>
|
<p class="text-muted">{% trans 'Report Date' %}: {{current_time}}</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<section id="summary" class="mb-1">
|
<section id="filters" class="mb-5 p-4 rounded border border-primary">
|
||||||
<h2 class="section-heading mb-2">{% trans 'Report Summary' %}</h2>
|
<h2 class="section-heading mb-4">{% trans 'Filters' %} <i class="fas fa-sliders-h ms-2"></i></h2>
|
||||||
<div class="row ">
|
<form method="GET" class="row g-3 align-items-end">
|
||||||
<div class="col-md-6 col-lg-4 mb-2">
|
<div class="col-md-3">
|
||||||
<div class="card summary-card">
|
<label for="make-select" class="form-label">{% trans 'Make' %}</label>
|
||||||
<div class="card-body">
|
<select id="make-select" name="make" class="form-select">
|
||||||
<h5 class="card-title text-primary">{% trans 'Total Revenue Amount' %}<span class="fas fa-solid fa-dollar-sign mx-1"></span><span class="fas fa-car"></span></h5>
|
<option value="">{% trans 'All Makes' %}</option>
|
||||||
<p class="card-text fs-4 fw-bold">120000000</p>
|
{% for make in makes %}
|
||||||
</div>
|
<option value="{{ make }}" {% if make == selected_make %}selected{% endif %}>{{ make }}</option>
|
||||||
</div>
|
{% endfor %}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-lg-4 mb-2">
|
<div class="col-md-3">
|
||||||
<div class="card summary-card">
|
<label for="model-select" class="form-label">{% trans 'Model' %}</label>
|
||||||
<div class="card-body">
|
<select id="model-select" name="model" class="form-select">
|
||||||
<h5 class="card-title text-primary">{% trans 'Total Vat Amount' %}<span class="fas fa-solid fa-percent mx-1"></span><span class="fas fa-car"></span></h5>
|
<option value="">{% trans 'All Models' %}</option>
|
||||||
<p class="card-text fs-4 fw-bold">12000</p>
|
{% for model in models %}
|
||||||
</div>
|
<option value="{{ model }}" {% if model == selected_model %}selected{% endif %}>{{ model }}</option>
|
||||||
</div>
|
{% endfor %}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-lg-4 mb-2">
|
<div class="col-md-3">
|
||||||
<div class="card summary-card">
|
<label for="serie-select" class="form-label">{% trans 'Serie' %}</label>
|
||||||
<div class="card-body">
|
<select id="serie-select" name="serie" class="form-select">
|
||||||
<h5 class="card-title text-primary">{% trans 'Total Discount Amount' %} <span class="fas fa-solid fa-tag mx-1"></span><span class="fas fa-car"></span></h5>
|
<option value="">{% trans 'All Series' %}</option>
|
||||||
<p class="card-text fs-4 fw-bold">12000</p>
|
{% for serie in series %}
|
||||||
</div>
|
<option value="{{ serie }}" {% if serie == selected_serie %}selected{% endif %}>{{ serie }}</option>
|
||||||
</div>
|
{% endfor %}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label for="year-select" class="form-label">{% trans 'Year' %}</label>
|
||||||
|
<select id="year-select" name="year" class="form-select">
|
||||||
</div>
|
<option value="">{% trans 'All Years' %}</option>
|
||||||
|
{% for year in years %}
|
||||||
|
<option value="{{ year }}" {% if year|stringformat:"s" == selected_year %}selected{% endif %}>{{ year }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-1">
|
||||||
|
<button type="submit" class="btn btn-primary w-100"><i class="fas fa-filter me-2"></i>{% trans 'Filter' %}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="purchase-details" class="mb-3">
|
<section id="summary" class="mb-5">
|
||||||
<h2 class="section-heading">{% trans 'Detailed Purchase List' %}</h2>
|
<h2 class="section-heading mb-4 border-start border-5 border-primary p-2">{% trans 'Report Summary' %}</h2>
|
||||||
|
<div class="row g-4">
|
||||||
<div class="d-flex justify-content-end mb-3 d-print-none">
|
<div class="col-md-6 col-lg-3">
|
||||||
<a href="{% url 'car-sale-report-csv-export' request.dealer.slug %}" class="btn btn-phoenix-primary">
|
<div class="card summary-card">
|
||||||
<i class="bi bi-download me-2"></i>{% trans 'Download as CSV' %}
|
<div class="card-body">
|
||||||
</a>
|
<h5 class="card-title">{% trans 'Total Revenue' %}<i class="fas fa-dollar-sign ms-2"></i></h5>
|
||||||
|
<p class="card-text">{{ total_revenue|floatformat:2 }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 col-lg-3">
|
||||||
|
<div class="card summary-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{% trans 'Total VAT Amount' %}<i class="fas fa-percent ms-2"></i></h5>
|
||||||
|
<p class="card-text">{{ 10000|floatformat:2 }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 col-lg-3">
|
||||||
|
<div class="card summary-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{% trans 'Total Discount Amount' %}<i class="fas fa-tag ms-2"></i></h5>
|
||||||
|
<p class="card-text">{{ total_discount|floatformat:2 }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 col-lg-3">
|
||||||
|
<div class="card summary-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{% trans 'Total Cars Sold' %}<i class="fas fa-car ms-2"></i></h5>
|
||||||
|
<p class="card-text">{{ cars_sold|length }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-responsive">
|
</section>
|
||||||
<table class="table table-striped table-hover table-bordered table-sm">
|
|
||||||
<thead >
|
<section id="sale-details" class="mb-3">
|
||||||
|
<h2 class="section-heading border-start border-5 border-primary p-2">{% trans 'Detailed Sale List' %}</h2>
|
||||||
|
<div class="d-flex justify-content-end mb-3 d-print-none">
|
||||||
|
<a href="{% url 'car-sale-report-csv-export' dealer.slug %}" class="btn btn-phoenix-primary">
|
||||||
|
<i class="bi bi-download me-2"></i>{% trans 'Download as CSV' %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive ">
|
||||||
|
<table class="table table-sm table-striped table-hover ">
|
||||||
|
<thead class="bg-body-highlight">
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans 'VIN' %}</th>
|
<th scope="col">{% trans 'VIN' %}</th>
|
||||||
<th>{% trans 'Make' %}</th>
|
<th scope="col">{% trans 'Make' %}</th>
|
||||||
<th>{% trans 'Model' %}</th>
|
<th scope="col">{% trans 'Model' %}</th>
|
||||||
<th>{% trans 'Year' %}</th>
|
<th scope="col">{% trans 'Year' %}</th>
|
||||||
<th>{% trans 'Serie' %}</th>
|
<th scope="col">{% trans 'Serie' %}</th>
|
||||||
<th>{% trans 'Trim' %}</th>
|
<th scope="col">{% trans 'Trim' %}</th>
|
||||||
<th>{% trans 'Mileage' %}</th>
|
<th scope="col">{% trans 'Mileage' %}</th>
|
||||||
<th>{% trans 'Stock Type' %}</th>
|
<th scope="col">{% trans 'Stock Type' %}</th>
|
||||||
<th>{% trans 'Created Date' %}</th>
|
<th scope="col">{% trans 'Created Date' %}</th>
|
||||||
<th>{% trans 'Sold Date' %}</th>
|
<th scope="col">{% trans 'Sold Date' %}</th>
|
||||||
<th>{% trans 'Cost Price' %}</th>
|
<th scope="col">{% trans 'Cost Price' %}</th>
|
||||||
<th>{% trans 'Marked Price' %}</th>
|
<th scope="col">{% trans 'Marked Price' %}</th>
|
||||||
<th>{% trans 'Discount Amount' %}</th>
|
<th scope="col">{% trans 'Discount Amount' %}</th>
|
||||||
<th>{% trans 'Selling Price' %}</th>
|
<th scope="col">{% trans 'Selling Price' %}</th>
|
||||||
<th>{% trans 'Tax Amount' %}</th>
|
<th scope="col">{% trans 'Tax Amount' %}</th>
|
||||||
<th>{% trans 'Invoice Number' %}</th>
|
<th scope="col">{% trans 'Invoice Number' %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
{% for car in cars_sold %}
|
||||||
{% for car in cars_sold%}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="ps-1">{{car.vin}}</td>
|
<td>{{ car.vin }}</td>
|
||||||
<td>{{car.id_car_make.name}}</td>
|
<td>{{ car.id_car_make.name }}</td>
|
||||||
<td>{{car.id_car_model.name}}</td>
|
<td>{{ car.id_car_model.name }}</td>
|
||||||
<td>{{car.year}}</td>
|
<td>{{ car.year }}</td>
|
||||||
<td>{{car.id_car_serie.name}}</td>
|
<td>{{ car.id_car_serie.name }}</td>
|
||||||
<td>{{car.id_car_trim.name}}</td>
|
<td>{{ car.id_car_trim.name }}</td>
|
||||||
<td>{{car.mileage}}</td>
|
<td>{{ car.mileage }}</td>
|
||||||
<td>{{car.stock_type}}</td>
|
<td>{{ car.stock_type }}</td>
|
||||||
<td>{{car.created_at}}</td>
|
<td>{{ car.created_at }}</td>
|
||||||
<td>{{car.sold_date}}</td>
|
<td>{{ car.sold_date }}</td>
|
||||||
<td>{{car.finances.cost_price}}</td>
|
<td>{{ car.finances.cost_price }}</td>
|
||||||
<td>{{car.finances.marked_price}}</td>
|
<td>{{ car.finances.marked_price }}</td>
|
||||||
<td>{{car.finances.discount_amount}}</td>
|
<td>{{ car.finances.discount_amount }}</td>
|
||||||
<td>{{car.finances.selling_price}}</td>
|
<td>{{ car.finances.selling_price }}</td>
|
||||||
<td>{{car.finances.vat_amount}}</td>
|
<td>{{ car.finances.vat_amount }}</td>
|
||||||
<td>{{car.item_model.invoicemodel_set.first.invoice_number}}</td>
|
<td>{{ car.item_model.invoicemodel_set.first.invoice_number }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -108,4 +187,4 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -8,73 +8,114 @@
|
|||||||
|
|
||||||
{% block content%}
|
{% block content%}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
.summary-card {
|
||||||
|
border: 1px solid var(--card-border);
|
||||||
|
border-radius: .5rem;
|
||||||
|
box-shadow: var(--card-shadow);
|
||||||
|
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
.summary-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.summary-card .card-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.summary-card .card-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--secondary-color) !important;
|
||||||
|
}
|
||||||
|
.summary-card .card-text {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
<div class="container-fluid report-container">
|
<div class="container-fluid report-container">
|
||||||
<header class="report-header text-center">
|
<header class="report-header text-center">
|
||||||
<h1 class="display-4">{% trans 'Car Purchase Report' %} <span class="fas fa-car mx-2 text-primary"></span><span class="fas fa-chart-bar mx-2 text-primary"></span></h1>
|
<h1 class="display-4">{% trans 'Car Purchase Report' %}<i class="fas fa-chart-pie ms-2 text-primary"></i></h1>
|
||||||
<p class="lead text-muted"><strong>{{dealer}}</strong></p>
|
<p class="lead text-muted"><strong>{{dealer}}</strong></p>
|
||||||
<p class="text-muted">Report Date: {{current_time}}</p>
|
<p class="text-muted">{% trans "Report Date" %}: {{current_time}}</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<section id="summary" class="mb-1">
|
<section id="summary" class="mb-5">
|
||||||
<h2 class="section-heading mb-2">{% trans 'Report Summary' %}</h2>
|
<h2 class="section-heading mb-4 border-start border-3 border-primary p-2">{% trans 'Report Summary' %}</h2>
|
||||||
<div class="row ">
|
<div class="row g-4">
|
||||||
<div class="col-md-6 col-lg-4 mb-2">
|
<div class="col-md-6 col-lg-4">
|
||||||
<div class="card summary-card">
|
<div class="card summary-card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title text-primary">{% trans 'Total Purchase Amount' %}<span class="fas fa-money-bill ms-1"><span></h5>
|
<h5 class="card-title">{% trans 'Total Purchase Amount' %}<span class="fas fa-money-bill ms-2"></span></h5>
|
||||||
<p class="card-text fs-4 fw-bold">{{total_po_amount}}</p>
|
<p class="card-text">{{total_po_amount}}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-lg-4 mb-2">
|
<div class="col-md-6 col-lg-4">
|
||||||
<div class="card summary-card">
|
<div class="card summary-card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title text-primary">{% trans 'Total Cars Purchased' %}<span class="fas fa-shopping-bag mx-1"></span><span class="fas fa-car"></span></h5>
|
<h5 class="card-title">{% trans 'Total Cars Purchased' %}<span class="fas fa-car ms-2"></span></h5>
|
||||||
<p class="card-text fs-4 fw-bold">{{total_po_cars}}</p>
|
<p class="card-text">{{total_po_cars}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card summary-card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{% trans 'Total Purchase Orders' %}<span class="fas fa-file-invoice ms-2"></span></h5>
|
||||||
|
<p class="card-text">{{data|length}}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="purchase-details" class="mb-3">
|
<section id="purchase-details" class="mb-3">
|
||||||
<h2 class="section-heading">{% trans 'Detailed Purchase List' %}</h2>
|
<h2 class="section-heading border-start border-3 border-primary p-2">{% trans 'Detailed Purchase List' %}</h2>
|
||||||
|
|
||||||
<div class="d-flex justify-content-end mb-3 d-print-none">
|
<div class="d-flex justify-content-end mb-3 d-print-none">
|
||||||
<a href="{% url 'purchase-report-csv-export' request.dealer.slug %}" class="btn btn-phoenix-primary">
|
<a href="{% url 'purchase-report-csv-export' request.dealer.slug %}" class="btn btn-phoenix-primary">
|
||||||
<i class="bi bi-download me-2"></i>{% trans 'Download as CSV' %}
|
<i class="bi bi-download me-2"></i>{% trans 'Download as CSV' %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped table-hover table-bordered table-sm">
|
<table class="table table-striped table-hover">
|
||||||
<thead>
|
<thead class="bg-body-highlight">
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans 'Purchase ID' %}</th>
|
<th scope="col">{% trans 'Purchase ID' %}</th>
|
||||||
<th>{% trans 'Date Created' %}</th>
|
<th scope="col">{% trans 'Date Created' %}</th>
|
||||||
<th>{% trans 'Status' %}</th>
|
<th scope="col">{% trans 'Status' %}</th>
|
||||||
<th>{% trans 'PO Amount' %}</th>
|
<th scope="col">{% trans 'PO Amount' %}</th>
|
||||||
<th>{% trans 'Date Fulfilled' %}</th>
|
<th scope="col">{% trans 'Date Fulfilled' %}</th>
|
||||||
<th>{% trans 'Created By' %}</th>
|
<th scope="col">{% trans 'Created By' %}</th>
|
||||||
<th>{% trans 'Cars Purchased' %}</th>
|
<th scope="col">{% trans 'Cars Purchased' %}</th>
|
||||||
<th>{% trans 'Vendor' %}</th>
|
<th scope="col">{% trans 'Vendor' %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
{% for po in data %}
|
{% for po in data %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="ps-1">{{po.po_number}}</td>
|
<td>{{po.po_number}}</td>
|
||||||
<td>{{po.po_created|date}}</td>
|
<td>{{po.po_created|date}}</td>
|
||||||
<td>{{po.po_status}}</td>
|
<td>{{po.po_status}}</td>
|
||||||
<td>{{po.po_amount}}</td>
|
<td>{{po.po_amount}}</td>
|
||||||
<td>{{po.po_fulfilled_date}}</td>
|
<td>
|
||||||
<td>staff</td>
|
{% if po.po_fulfilled_date%}
|
||||||
|
{{po.po_fulfilled_date}}
|
||||||
|
{%else%}
|
||||||
|
{% trans 'Not fulfilled'%}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{% firstof po.created_by.get_full_name 'staff' %}</td>
|
||||||
<td>{{po.po_quantity}}</td>
|
<td>{{po.po_quantity}}</td>
|
||||||
<td>
|
<td>
|
||||||
{{po.vendors_str}}
|
{{po.vendors_str}}
|
||||||
@ -82,16 +123,10 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
|
||||||
<tr>
|
|
||||||
<td colspan="6" class="text-end fw-bold">{% trans 'Total Purchase'%}:</td>
|
|
||||||
<td class="fw-bold text-primary">{{total_po_amount}}</td>
|
|
||||||
</tr>
|
|
||||||
</tfoot>
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -1,13 +1,25 @@
|
|||||||
{% load i18n %}{% autoescape off %}
|
{% load i18n %}{% autoescape off %}
|
||||||
{% trans "Hi" %} {% firstof user.get_full_name user.username %},
|
|
||||||
|
مرحباً {% firstof user.get_full_name user.username %}،
|
||||||
|
|
||||||
{% if userplan.expire != None %}
|
{% if userplan.expire != None %}
|
||||||
{% blocktrans with plan_name=plan.name expire=userplan.expire %}Your current plan is {{ plan_name }} and it will expire on {{ expire }}. {% endblocktrans %}
|
خطتك الحالية هي {{ plan.name }} وستنتهي صلاحيتها في {{ userplan.expire }}.
|
||||||
{% else %}
|
{% else %}
|
||||||
{% blocktrans with plan_name=plan.name %}Your current plan is {{ plan_name }}. {% endblocktrans %}
|
خطتك الحالية هي {{ plan.name }}.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% trans "Thank you" %}
|
شكراً لك،
|
||||||
--
|
فريق تنحل
|
||||||
{% blocktrans %}The Team at {{ site_name }}{% endblocktrans %}
|
---------------------
|
||||||
{% endautoescape %}
|
|
||||||
|
Hi {% firstof user.get_full_name user.username %},
|
||||||
|
|
||||||
|
{% if userplan.expire != None %}
|
||||||
|
Your current plan is {{ plan_name }} and it will expire on {{ expire }}. {% endblocktrans %}
|
||||||
|
{% else %}
|
||||||
|
Your current plan is {{ plan_name }}. {% endblocktrans %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
Thank you
|
||||||
|
The Team at Tenhal
|
||||||
|
{% endautoescape %}
|
||||||
@ -1 +1,3 @@
|
|||||||
{% load i18n %}{% blocktrans with user=user plan=plan.name %}Your account {{ user }} has new plan {{ plan }}{% endblocktrans %}
|
{% load i18n %}
|
||||||
|
Your account {{ user }} has new plan {{ plan }}
|
||||||
|
حسابك {{ user }} لديه خطة جديدة {{ plan }}
|
||||||
@ -1,14 +1,27 @@
|
|||||||
{% load i18n %}{% autoescape off %}
|
{% load i18n %}{% autoescape off %}
|
||||||
{% trans "Hi" %} {% firstof user.get_full_name user.username %},
|
مرحباً {% firstof user.get_full_name user.username %}،
|
||||||
|
|
||||||
{% blocktrans %}Your account has just expired.{% endblocktrans %}
|
لقد انتهت صلاحية حسابك للتو.
|
||||||
|
|
||||||
{% blocktrans with plan_name=userplan.plan.name %}You can restore your current plan {{ plan_name }} here:{% endblocktrans %}
|
يمكنك استعادة خطتك الحالية {{ userplan.plan.name }} من هنا:
|
||||||
http://{{ site_domain }}{% url 'current_plan' %}
|
http://{{ site_domain }}{% url 'current_plan' %}
|
||||||
{% blocktrans %}or you can upgrade your plan here:{% endblocktrans %}
|
أو يمكنك ترقية خطتك من هنا:
|
||||||
http://{{ site_domain }}{% url 'upgrade_plan' %}
|
http://{{ site_domain }}{% url 'upgrade_plan' %}
|
||||||
|
|
||||||
{% trans "Thank you" %}
|
شكراً لك،
|
||||||
--
|
--
|
||||||
{% blocktrans %}The Team at {{ site_name }}{% endblocktrans %}
|
فريق تنحل
|
||||||
{% endautoescape %}
|
---------------------
|
||||||
|
Hi {% firstof user.get_full_name user.username %},
|
||||||
|
|
||||||
|
Your account has just expired.
|
||||||
|
|
||||||
|
You can restore your current plan {{ userplan.plan.name }} here:
|
||||||
|
http://{{ site_domain }}{% url 'current_plan' %}
|
||||||
|
or you can upgrade your plan here:
|
||||||
|
http://{{ site_domain }}{% url 'upgrade_plan' %}
|
||||||
|
|
||||||
|
Thank you,
|
||||||
|
--
|
||||||
|
The Team at TENHAL
|
||||||
|
{% endautoescape %}
|
||||||
@ -1 +1,5 @@
|
|||||||
{% load i18n %}{% blocktrans %}Your account {{ user }} has just expired{% endblocktrans %}
|
{% load i18n %}{% autoescape off %}
|
||||||
|
لقد انتهت صلاحية حسابك {{ user }} للتو.
|
||||||
|
---------------------
|
||||||
|
Your account {{ user }} has just expired.
|
||||||
|
{% endautoescape %}
|
||||||
@ -1,11 +1,20 @@
|
|||||||
{% load i18n %}{% autoescape off %}
|
{% load i18n %}{% autoescape off %}
|
||||||
{% trans "Hi" %} {% firstof user.get_full_name user.username %},
|
مرحباً {% firstof user.get_full_name user.username %}،
|
||||||
|
|
||||||
{% blocktrans with days=pricing.period plan_name=plan.name expire=userplan.expire %}Your account has just been extended by {{ days }} days. Your current plan is {{ plan_name }} and it will expire on {{ expire }}. {% endblocktrans %}
|
تم تمديد صلاحية حسابك للتو لمدة {{ pricing.period }} يوم. خطتك الحالية هي {{plan.name}} وستنتهي في {{userplan.expire}}.
|
||||||
|
|
||||||
{% trans "An invoice will be sent with another e-mail, if billing data was provided." %}
|
سيتم إرسال فاتورة في رسالة بريد إلكتروني أخرى، في حال تم تقديم بيانات الفوترة.
|
||||||
|
شكراً لك
|
||||||
{% trans "Thank you" %}
|
|
||||||
--
|
--
|
||||||
{% blocktrans %}The Team at {{ site_name }}{% endblocktrans %}
|
فريق تنحل
|
||||||
{% endautoescape %}
|
---------------------
|
||||||
|
Hi {% firstof user.get_full_name user.username %},
|
||||||
|
|
||||||
|
Your account has just been extended by {{ pricing.period }} days. Your current plan is {{plan.name}} and it will expire on {{userplan.expire}}.
|
||||||
|
|
||||||
|
An invoice will be sent with another e-mail, if billing data was provided.
|
||||||
|
|
||||||
|
Thank you
|
||||||
|
--
|
||||||
|
The Team at Tenhal
|
||||||
|
{% endautoescape %}
|
||||||
@ -1 +1,14 @@
|
|||||||
{% load i18n %}{% blocktrans with user=user days=pricing.period %}Your account {{ user }} has been extended by {{ days }} days{% endblocktrans %}
|
{% load i18n %}
|
||||||
|
{% autoescape off %}
|
||||||
|
مرحباً {% firstof user.get_full_name user.username %}،
|
||||||
|
تم تمديد حسابك {{ user }} لمدة {{ pricing.period }} يوم
|
||||||
|
شكراً لك
|
||||||
|
--
|
||||||
|
فريق تنحل
|
||||||
|
--------
|
||||||
|
Hi {% firstof user.get_full_name user.username %},
|
||||||
|
Your account {{ user }} has been extended by {{ pricing.period }} days
|
||||||
|
Thank you
|
||||||
|
--
|
||||||
|
The Team at Tenhal
|
||||||
|
{% endautoescape %}
|
||||||
@ -1,14 +1,21 @@
|
|||||||
{% load i18n %}{% autoescape off %}
|
{% load i18n %}{% autoescape off %}
|
||||||
{% trans "Hi" %} {% firstof user.get_full_name user.username %},
|
مرحباً {% firstof user.get_full_name user.username %}،
|
||||||
|
نكتب إليك لإعلامك، أنه قد تم إصدار {{ invoice_type }} رقم {{ invoice_number }}. يمكنك الاطلاع عليها وطباعتها عبر الرابط:
|
||||||
{% blocktrans %}We are writing to inform you, that {{ invoice_type }} {{ invoice_number }} has been issued. You can view it and print it at:
|
|
||||||
http://{{ site_domain }}{{ url }}
|
http://{{ site_domain }}{{ url }}
|
||||||
{% endblocktrans %}
|
يمكنك الاطلاع على تفاصيل الطلب عبر الرابط:
|
||||||
|
|
||||||
{% trans "Details of the order can be see on:" %}:
|
|
||||||
http://{{ site_domain }}{% url 'order' pk=order %}
|
http://{{ site_domain }}{% url 'order' pk=order %}
|
||||||
|
|
||||||
{% trans "Thank you" %}
|
شكراً لك
|
||||||
--
|
--
|
||||||
{% blocktrans %}The Team at {{ site_name }}{% endblocktrans %}
|
فريق تنحل
|
||||||
{% endautoescape %}
|
---------------------
|
||||||
|
Hi {% firstof user.get_full_name user.username %},
|
||||||
|
We are writing to inform you, that {{ invoice_type }} {{ invoice_number }} has been issued. You can view it and print it at:
|
||||||
|
http://{{ site_domain }}{{ url }}
|
||||||
|
Details of the order can be see on:
|
||||||
|
http://{{ site_domain }}{% url 'order' pk=order %}
|
||||||
|
|
||||||
|
Thank you
|
||||||
|
--
|
||||||
|
The Team at Tenhal
|
||||||
|
{% endautoescape %}
|
||||||
@ -1 +1,3 @@
|
|||||||
{% load i18n %}{% trans 'Order' %} {{ order }} - {% blocktrans with invoice_type=invoice_type invoice_number=invoice_number user=user %}{{ invoice_type }} {{ invoice_number }} has been issued for {{ user }}{% endblocktrans %}
|
{% load i18n %}
|
||||||
|
Order {{ order }} - {{ invoice_type }} {{ invoice_number }} has been issued for {{ user }}
|
||||||
|
تم إصدار {{ invoice_type }} {{ invoice_number }} للأمر {{ order }} باسم {{ user }}
|
||||||
|
|||||||
@ -1,15 +1,29 @@
|
|||||||
{% load i18n %}{% autoescape off %}
|
{% load i18n %}{% autoescape off %}
|
||||||
{% trans "Hi" %} {% firstof user.get_full_name user.username %},
|
Hi {% firstof user.get_full_name user.username %},
|
||||||
|
|
||||||
{% blocktrans %}Your account will expire in {{ days }} days.{% endblocktrans %}
|
Your account will expire in {{ days }} days.
|
||||||
|
|
||||||
{% blocktrans with plan_name=userplan.plan.name %}You can extend your current plan {{ plan_name }} on page:{% endblocktrans %}
|
You can extend your current plan {{ userplan.plan.name }} on page:
|
||||||
http://{{ site_domain }}{% url 'current_plan' %}
|
http://{{ site_domain }}{% url 'current_plan' %}
|
||||||
|
|
||||||
{% blocktrans %}or you can upgrade your plan here:{% endblocktrans %}
|
or you can upgrade your plan here:
|
||||||
http://{{ site_domain }}{% url 'upgrade_plan' %}
|
http://{{ site_domain }}{% url 'upgrade_plan' %}
|
||||||
|
|
||||||
{% trans "Thank you" %}
|
Thank you
|
||||||
--
|
--
|
||||||
{% blocktrans %}The Team at {{ site_name }}{% endblocktrans %}
|
The Team at Tenhal
|
||||||
|
----------------
|
||||||
|
مرحباً {% firstof full_name username %}،
|
||||||
|
|
||||||
|
سينتهي حسابك في غضون {{ days }} يوم.
|
||||||
|
|
||||||
|
يمكنك تمديد خطتك الحالية {{ userplan.plan.name }} على الصفحة التالية:
|
||||||
|
http://{{ site_domain }}{% url 'current_plan' %}
|
||||||
|
|
||||||
|
أو يمكنك ترقية خطتك هنا:
|
||||||
|
http://{{ site_domain }}{% url 'upgrade_plan' %}
|
||||||
|
|
||||||
|
شكراً لك
|
||||||
|
--
|
||||||
|
فريق تنحل
|
||||||
{% endautoescape %}
|
{% endautoescape %}
|
||||||
|
|||||||
@ -1 +1,3 @@
|
|||||||
{% load i18n %}{% blocktrans count days as days %}Your account {{ user }} will expire in {{ days }} day{% plural %}Your account {{ user }} will expire in {{ days }} days{% endblocktrans %}
|
{% load i18n %}
|
||||||
|
Your account {{ user }} will expire in {{ days }} day.
|
||||||
|
سينتهي حسابك {{ user }} في غضون {{ days }} يوم.
|
||||||
@ -1,7 +1,7 @@
|
|||||||
<!-- po_list.html -->
|
<!-- po_list.html -->
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load i18n static %}
|
{% load i18n static %}
|
||||||
{% block title %}Purchase Orders - {{ block.super }}{% endblock %}
|
{% block title %}Purchase Orders{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
|
||||||
@ -123,7 +123,15 @@
|
|||||||
{% include 'modal/delete_modal.html' %}
|
{% include 'modal/delete_modal.html' %}
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
{% url "purchase_order_create" request.dealer.slug request.dealer.entity.slug as create_purchase_url %}
|
{% if vendors %}
|
||||||
{% include "empty-illustration-page.html" with value="purchase order" url=create_purchase_url %}
|
{% url "purchase_order_create" request.dealer.slug request.dealer.entity.slug as create_purchase_url %}
|
||||||
|
{% include "empty-illustration-page.html" with value="purchase order" url=create_purchase_url %}
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
{% url "vendor_create" request.dealer.slug as vendor_create_url %}
|
||||||
|
{% include "message-illustration.html" with value1=_("You don't have a Vendor, Please add a Vendor before creating a Purchase Order.") value2=_("Create New Vendor") url=vendor_create_url %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -191,7 +191,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
233
templates/schedule_calendar.html
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block customCSS %}
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/fullcalendar@5.10.1/main.min.css" rel="stylesheet">
|
||||||
|
<link id="theme-link" rel="stylesheet" href="{% static 'css/calendar_dark.css' %}">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.schedule-list {
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
{% endblock customCSS %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid mt-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div id="calendar"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4>Upcoming Schedules</h4>
|
||||||
|
{% comment %} <button class="btn btn-phoenix-primary btn-sm"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#scheduleModal">
|
||||||
|
<span class="fas fa-plus me-1"></span>{{ _("Add New Schedule") }}
|
||||||
|
</button> {% endcomment %}
|
||||||
|
</div>
|
||||||
|
<div class="card-body schedule-list">
|
||||||
|
<ul class="list-group">
|
||||||
|
{% for schedule in upcoming_schedules %}
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-1">{{ schedule.get_purpose }}</h6>
|
||||||
|
<p class="mb-1 text-muted">{{ schedule.scheduled_at|date:"F d, Y P" }}</p>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-info btn-sm view-schedule-btn" data-toggle="modal" data-target="#viewScheduleModal"
|
||||||
|
data-purpose="{{ schedule.get_purpose }}"
|
||||||
|
data-scheduled-type="{{ schedule.scheduled_type }}"
|
||||||
|
data-scheduled-at="{{ schedule.scheduled_at }}"
|
||||||
|
data-start-time="{{ schedule.start_time }}"
|
||||||
|
data-end-time="{{ schedule.end_time }}"
|
||||||
|
data-completed="{{ schedule.completed }}"
|
||||||
|
data-status="{{ schedule.status }}"
|
||||||
|
data-notes="{{ schedule.notes }}"
|
||||||
|
data-customer="{% if schedule.customer %}{{ schedule.customer.customer_name }}{% else %}N/A{% endif %}"
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{% empty %}
|
||||||
|
<li class="list-group-item text-center text-muted">No schedules found.</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="addScheduleModal" tabindex="-1" role="dialog" aria-labelledby="addScheduleModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="addScheduleModalLabel">Add New Schedule</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form action="{% url 'schedule_calendar' request.dealer.slug %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<button type="submit" class="btn btn-primary">Save Schedule</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="viewScheduleModal" tabindex="-1" role="dialog" aria-labelledby="viewScheduleModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="viewScheduleTitle">Schedule Details</h5>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p><strong>Purpose:</strong> <span id="modal-schedule-purpose"></span></p>
|
||||||
|
<p><strong>Scheduled Type:</strong> <span id="modal-schedule-type"></span></p>
|
||||||
|
<p><strong>Customer:</strong> <span id="modal-schedule-customer"></span></p>
|
||||||
|
<p><strong>Scheduled At:</strong> <span id="modal-schedule-at"></span></p>
|
||||||
|
<p><strong>Time:</strong> <span id="modal-schedule-time"></span></p>
|
||||||
|
<p><strong>Status:</strong> <span id="modal-schedule-status"></span></p>
|
||||||
|
<p><strong>Notes:</strong> <span id="modal-schedule-notes"></span></p>
|
||||||
|
<p><strong>Completed:</strong> <span id="modal-schedule-completed"></span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock content %}
|
||||||
|
{% block customJS %}
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.10.1/main.min.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const themeToggle = document.getElementById('themeControlToggleSm');
|
||||||
|
const themeLink = document.getElementById('theme-link');
|
||||||
|
|
||||||
|
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||||
|
|
||||||
|
function setTheme(theme) {
|
||||||
|
if (theme === 'dark') {
|
||||||
|
themeLink.href = '{% static "css/dark_theme.css" %}';
|
||||||
|
themeToggle.checked = true;
|
||||||
|
} else {
|
||||||
|
themeLink.href = '{% static "css/light_theme.css" %}';
|
||||||
|
themeToggle.checked = false;
|
||||||
|
}
|
||||||
|
localStorage.setItem('theme', theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the initial theme on page load
|
||||||
|
setTheme(savedTheme);
|
||||||
|
|
||||||
|
// Listen for changes on the toggle switch
|
||||||
|
themeToggle.addEventListener('change', function() {
|
||||||
|
if (this.checked) {
|
||||||
|
setTheme('dark');
|
||||||
|
} else {
|
||||||
|
setTheme('light');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var calendarEl = document.getElementById('calendar');
|
||||||
|
var calendar = new FullCalendar.Calendar(calendarEl, {
|
||||||
|
initialView: 'dayGridMonth',
|
||||||
|
themeSystem: 'bootstrap',
|
||||||
|
locale: '{{ LANGUAGE_CODE }}',
|
||||||
|
headerToolbar: {
|
||||||
|
left: 'prev,next today',
|
||||||
|
center: 'title',
|
||||||
|
right: 'dayGridMonth,timeGridWeek,timeGridDay'
|
||||||
|
},
|
||||||
|
events: [
|
||||||
|
{% for schedule in schedules %}
|
||||||
|
{
|
||||||
|
title: '{{ schedule.get_purpose|escapejs }}',
|
||||||
|
start: '{{ schedule.scheduled_at|date:"Y-m-d" }}{% if schedule.start_time %}T{{ schedule.start_time|time:"H:i:s" }}{% endif %}',
|
||||||
|
end: '{{ schedule.scheduled_at|date:"Y-m-d" }}{% if schedule.end_time %}T{{ schedule.end_time|time:"H:i:s" }}{% endif %}',
|
||||||
|
backgroundColor: '{% if schedule.status == "completed" %}#1abc9c{% elif schedule.status == "canceled" %}#e74c3c{% else %}#3498db{% endif %}',
|
||||||
|
borderColor: '{% if schedule.status == "completed" %}#1abc9c{% elif schedule.status == "canceled" %}#e74c3c{% else %}#3498db{% endif %}',
|
||||||
|
extendedProps: {
|
||||||
|
scheduledType: '{{ schedule.scheduled_type|escapejs }}',
|
||||||
|
notes: '{{ schedule.notes|escapejs }}',
|
||||||
|
completed: '{{ schedule.completed }}',
|
||||||
|
status: '{{ schedule.status|escapejs }}',
|
||||||
|
customer: '{{ schedule.customer.customer_name|default:"N/A"|escapejs }}'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{% endfor %}
|
||||||
|
],
|
||||||
|
eventClick: function(info) {
|
||||||
|
// Populate the view modal with schedule details
|
||||||
|
$('#viewScheduleTitle').text(info.event.title);
|
||||||
|
$('#modal-schedule-purpose').text(info.event.title);
|
||||||
|
$('#modal-schedule-type').text(info.event.extendedProps.scheduledType);
|
||||||
|
$('#modal-schedule-customer').text(info.event.extendedProps.customer);
|
||||||
|
$('#modal-schedule-at').text(info.event.startStr.split('T')[0]);
|
||||||
|
|
||||||
|
let startTime = info.event.startStr.split('T')[1];
|
||||||
|
let endTime = info.event.endStr ? info.event.endStr.split('T')[1] : '';
|
||||||
|
|
||||||
|
let timeString = '';
|
||||||
|
if (startTime) {
|
||||||
|
timeString += startTime.substring(0, 5);
|
||||||
|
}
|
||||||
|
if (endTime) {
|
||||||
|
timeString += ' - ' + endTime.substring(0, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#modal-schedule-time').text(timeString);
|
||||||
|
$('#modal-schedule-status').text(info.event.extendedProps.status);
|
||||||
|
$('#modal-schedule-notes').text(info.event.extendedProps.notes);
|
||||||
|
$('#modal-schedule-completed').text(info.event.extendedProps.completed === 'True' ? 'Yes' : 'No');
|
||||||
|
|
||||||
|
$('#viewScheduleModal').modal('show');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
calendar.render();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle view schedule buttons in the list
|
||||||
|
$('.view-schedule-btn').on('click', function() {
|
||||||
|
const purpose = $(this).data('purpose');
|
||||||
|
const scheduledType = $(this).data('scheduled-type');
|
||||||
|
const customer = $(this).data('customer');
|
||||||
|
const scheduledAt = $(this).data('scheduled-at');
|
||||||
|
const startTime = $(this).data('start-time');
|
||||||
|
const endTime = $(this).data('end-time');
|
||||||
|
const status = $(this).data('status');
|
||||||
|
const notes = $(this).data('notes');
|
||||||
|
const completed = $(this).data('completed');
|
||||||
|
|
||||||
|
$('#viewScheduleTitle').text(purpose);
|
||||||
|
$('#modal-schedule-purpose').text(purpose);
|
||||||
|
$('#modal-schedule-type').text(scheduledType);
|
||||||
|
$('#modal-schedule-customer').text(customer);
|
||||||
|
$('#modal-schedule-at').text(scheduledAt);
|
||||||
|
|
||||||
|
let timeString = '';
|
||||||
|
if (startTime) {
|
||||||
|
timeString += startTime.substring(0, 5);
|
||||||
|
}
|
||||||
|
if (endTime) {
|
||||||
|
if (timeString) {
|
||||||
|
timeString += ' - ';
|
||||||
|
}
|
||||||
|
timeString += endTime.substring(0, 5);
|
||||||
|
}
|
||||||
|
$('#modal-schedule-time').text(timeString);
|
||||||
|
$('#modal-schedule-status').text(status);
|
||||||
|
$('#modal-schedule-notes').text(notes);
|
||||||
|
$('#modal-schedule-completed').text(completed === true ? 'Yes' : 'No');
|
||||||
|
|
||||||
|
$('#viewScheduleModal').modal('show');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock customJS %}
|
||||||
@ -79,12 +79,12 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer d-flex ">
|
<div class="card-footer d-flex ">
|
||||||
<a class="btn btn-sm btn-phoenix-primary me-1"
|
<a class="btn btn-sm btn-phoenix-primary me-3"
|
||||||
href="{% url 'user_update' request.dealer.slug user_.slug %}">
|
href="{% url 'user_update' request.dealer.slug user_.slug %}">
|
||||||
{{ _("Edit") }}
|
{{ _("Edit") }}
|
||||||
<i class="fa-solid fa-pen-to-square"></i>
|
<i class="fa-solid fa-pen-to-square"></i>
|
||||||
</a>
|
</a>
|
||||||
<button class="btn btn-phoenix-danger btn-sm delete-btn me-1"
|
<button class="btn btn-phoenix-danger btn-sm delete-btn me-3"
|
||||||
data-url="{% url 'user_delete' request.dealer.slug user_.slug %}"
|
data-url="{% url 'user_delete' request.dealer.slug user_.slug %}"
|
||||||
data-message='{{ _("Are you sure you want to delete this user?") }}'
|
data-message='{{ _("Are you sure you want to delete this user?") }}'
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
@ -92,9 +92,9 @@
|
|||||||
{{ _("Delete") }}
|
{{ _("Delete") }}
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
<a class="btn btn-sm btn-phoenix-secondary me-1"
|
<a class="btn btn-sm btn-phoenix-secondary me-3"
|
||||||
href="{% url 'user_list' request.dealer.slug %}">
|
href="{% url 'user_list' request.dealer.slug %}">
|
||||||
{{ _("Back to List") }}
|
{{ _("Back to Staffs List") }}
|
||||||
<i class="fa-regular fa-circle-left"></i>
|
<i class="fa-regular fa-circle-left"></i>
|
||||||
</a>
|
</a>
|
||||||
<a class="btn btn-sm btn-phoenix-secondary"
|
<a class="btn btn-sm btn-phoenix-secondary"
|
||||||
|
|||||||
@ -88,7 +88,7 @@
|
|||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
{% url "pricing_page" request.dealer.slug as pricing_page_url %}
|
{% 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="Buy Plan" url=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 %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||