This commit is contained in:
Faheedkhan 2025-08-05 19:47:14 +03:00
commit d9d1a8c376
68 changed files with 1157 additions and 234 deletions

View File

@ -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):
""" """

View File

@ -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))

View File

@ -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:

View File

@ -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,

View File

@ -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},
), # ),
), # ),
) # )

View File

@ -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}")

View File

@ -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"

View File

@ -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:
# """ # """

View File

@ -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
View 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:

View 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;
}
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View 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;
}
}

View 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;
}
}

View File

@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 709 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
staticfiles/images/logos/vendors/vnd.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 619 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
staticfiles/user-logo1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 B

View File

@ -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>

View 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>

View 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.

View File

@ -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>

View File

@ -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"

View File

@ -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 %}
<!----> <!---->

View 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">&times;</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 %}