changes
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -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
|
||||||
@ -1277,8 +1277,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 +1341,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 +1391,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 +1513,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 +1555,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 +1581,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 +1625,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 +2076,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 +2093,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 +2345,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,
|
||||||
|
|||||||
@ -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},
|
||||||
@ -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)
|
||||||
@ -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}")
|
||||||
@ -1287,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
|
||||||
|
|
||||||
|
|
||||||
@ -1110,16 +1110,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:
|
||||||
# """
|
# """
|
||||||
|
|||||||
@ -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", [])
|
||||||
@ -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(
|
||||||
@ -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)
|
||||||
@ -6913,7 +6931,7 @@ class OpportunityUpdateView(
|
|||||||
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
|
||||||
)
|
)
|
||||||
@ -9844,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)
|
||||||
|
|
||||||
@ -10456,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.")
|
||||||
@ -10696,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
|
||||||
@ -11048,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;
|
||||||
|
}
|
||||||
|
}
|
||||||
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 |
@ -201,25 +201,25 @@
|
|||||||
<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>
|
||||||
@ -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>
|
||||||
|
|||||||
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>
|
||||||
|
|||||||
@ -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 %}
|
||||||
<!---->
|
<!---->
|
||||||
|
|||||||
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 %}
|
||||||