Compare commits
15 Commits
a1fb89e020
...
d18d4326b3
| Author | SHA1 | Date | |
|---|---|---|---|
| d18d4326b3 | |||
| ac9727dde4 | |||
| 85515bd272 | |||
| e83e412455 | |||
| 1ac8f76d8f | |||
| f8d38e6172 | |||
| 1fd00af6ac | |||
| 5719ddbf14 | |||
| e9ee330162 | |||
| 6c2b6b1588 | |||
| 4815ae4f6a | |||
| 8d82a954e8 | |||
| 14680585b9 | |||
| f8527795ae | |||
| 0b1bb10934 |
@ -1,15 +1,15 @@
|
||||
from inventory import views
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.conf.urls.static import static
|
||||
from django.conf import settings
|
||||
from django.conf.urls.i18n import i18n_patterns
|
||||
from inventory import views
|
||||
# from debug_toolbar.toolbar import debug_toolbar_urls
|
||||
|
||||
from inventory.notifications.sse import NotificationSSEApp
|
||||
# import debug_toolbar
|
||||
from schema_graph.views import Schema
|
||||
from django.conf.urls.static import static
|
||||
from django.conf.urls.i18n import i18n_patterns
|
||||
from inventory.notifications.sse import NotificationSSEApp
|
||||
|
||||
# import debug_toolbar
|
||||
# from two_factor.urls import urlpatterns as tf_urls
|
||||
# from debug_toolbar.toolbar import debug_toolbar_urls
|
||||
|
||||
urlpatterns = [
|
||||
# path('__debug__/', include(debug_toolbar.urls)),
|
||||
|
||||
@ -440,14 +440,14 @@ class CarFinanceForm(forms.ModelForm):
|
||||
model = CarFinance
|
||||
fields = ["cost_price","marked_price"]
|
||||
|
||||
def save(self, commit=True):
|
||||
instance = super().save()
|
||||
try:
|
||||
instance.additional_services.set(self.cleaned_data["additional_finances"])
|
||||
except KeyError:
|
||||
pass
|
||||
instance.save()
|
||||
return instance
|
||||
# def save(self, commit=True):
|
||||
# instance = super().save()
|
||||
# try:
|
||||
# instance.additional_services.set(self.cleaned_data["additional_finances"])
|
||||
# except KeyError:
|
||||
# pass
|
||||
# instance.save()
|
||||
# return instance
|
||||
|
||||
|
||||
class CarLocationForm(forms.ModelForm):
|
||||
@ -1603,7 +1603,7 @@ class PermissionForm(forms.ModelForm):
|
||||
"django_ledger.invoicemodel",
|
||||
"django_ledger.vendormodel",
|
||||
"django_ledger.journalentrymodel"
|
||||
"django_ledger.purchaseordermodel", # TODO add purchase order
|
||||
"django_ledger.purchaseordermodel",
|
||||
]
|
||||
|
||||
permissions = cache.get(
|
||||
|
||||
28
inventory/management/commands/p.py
Normal file
28
inventory/management/commands/p.py
Normal file
@ -0,0 +1,28 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.contrib.auth import get_user_model
|
||||
import datetime
|
||||
from inventory.models import Dealer
|
||||
from plans.models import Plan, Order,PlanPricing
|
||||
User = get_user_model()
|
||||
class Command(BaseCommand):
|
||||
help = ""
|
||||
|
||||
def handle(self, *args, **options):
|
||||
dealer = Dealer.objects.get(user__email="dealer6@example.com")
|
||||
user = dealer.user
|
||||
|
||||
user.userplan.expire = datetime.datetime.now().date()
|
||||
user.userplan.save()
|
||||
pp = PlanPricing.objects.get(plan__name="Basic")
|
||||
order = Order.objects.create(
|
||||
user=user,
|
||||
plan=pp.plan,
|
||||
pricing=pp.pricing,
|
||||
amount=pp.price,
|
||||
currency="SA",
|
||||
tax=15,
|
||||
status=1,
|
||||
)
|
||||
|
||||
order.complete_order()
|
||||
print(user.userplan)
|
||||
72
inventory/management/commands/plans_maintenance.py
Normal file
72
inventory/management/commands/plans_maintenance.py
Normal file
@ -0,0 +1,72 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.template.loader import render_to_string
|
||||
from plans.models import UserPlan, Order
|
||||
from datetime import timedelta
|
||||
from django.utils.translation import activate, get_language
|
||||
from django_q.tasks import async_task
|
||||
import logging
|
||||
from inventory.tasks import send_bilingual_reminder, handle_email_result
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Handles subscription plan maintenance tasks"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.stdout.write("Starting plans maintenance...")
|
||||
|
||||
# 1. Send expiration reminders
|
||||
self.send_expiration_reminders()
|
||||
|
||||
# 2. Deactivate expired plans
|
||||
self.deactivate_expired_plans()
|
||||
|
||||
# 3. Clean up old incomplete orders
|
||||
self.cleanup_old_orders()
|
||||
|
||||
self.stdout.write("Maintenance completed!")
|
||||
|
||||
def send_expiration_reminders(self):
|
||||
"""Queue email reminders for expiring plans"""
|
||||
reminder_days = getattr(settings, 'PLANS_EXPIRATION_REMIND', [3, 7, 14])
|
||||
today = timezone.now().date()
|
||||
|
||||
for days in reminder_days:
|
||||
target_date = today + timedelta(days=days)
|
||||
expiring_plans = UserPlan.objects.filter(
|
||||
active=True,
|
||||
expire=target_date
|
||||
).select_related('user', 'plan')
|
||||
|
||||
self.stdout.write(f"Queuing {days}-day reminders for {expiring_plans.count()} plans")
|
||||
|
||||
for user_plan in expiring_plans:
|
||||
# Queue email task
|
||||
async_task(
|
||||
send_bilingual_reminder,
|
||||
user_plan.user_id,
|
||||
user_plan.plan_id,
|
||||
user_plan.expire,
|
||||
days,
|
||||
hook=handle_email_result
|
||||
)
|
||||
|
||||
def deactivate_expired_plans(self):
|
||||
"""Deactivate plans that have expired (synchronous)"""
|
||||
expired_plans = UserPlan.objects.filter(
|
||||
active=True,
|
||||
expire__lt=timezone.now().date()
|
||||
)
|
||||
count = expired_plans.update(active=False)
|
||||
self.stdout.write(f"Deactivated {count} expired plans")
|
||||
|
||||
def cleanup_old_orders(self):
|
||||
"""Delete incomplete orders older than 30 days"""
|
||||
cutoff = timezone.now() - timedelta(days=30)
|
||||
count, _ = Order.objects.filter(
|
||||
created__lt=cutoff,
|
||||
status=Order.STATUS.NEW
|
||||
).delete()
|
||||
self.stdout.write(f"Cleaned up {count} old incomplete orders")
|
||||
@ -208,7 +208,7 @@ class Command(BaseCommand):
|
||||
last_name = fake.last_name()
|
||||
email = fake.email()
|
||||
staff = random.choice(Staff.objects.filter(dealer=dealer))
|
||||
|
||||
|
||||
make = random.choice(CarMake.objects.all())
|
||||
model = random.choice(make.carmodel_set.all())
|
||||
lead = Lead.objects.create(
|
||||
|
||||
@ -589,10 +589,11 @@ class AdditionalServices(models.Model, LocalizedNameMixin):
|
||||
verbose_name_plural = _("Additional Services")
|
||||
|
||||
def __str__(self):
|
||||
return self.name + " - " + str(self.price)
|
||||
return self.name + " - " + str(self.price_)
|
||||
|
||||
|
||||
class Car(Base):
|
||||
|
||||
item_model = models.OneToOneField(
|
||||
ItemModel,
|
||||
models.DO_NOTHING,
|
||||
|
||||
@ -321,7 +321,6 @@ class BasePurchaseOrderActionActionView(
|
||||
f"User {user_username} attempting to call action '{self.action_name}' "
|
||||
f"on Purchase Order ID: {po_model.pk} (Entity: {entity_slug})."
|
||||
)
|
||||
print(self.action_name)
|
||||
if self.action_name == "mark_as_fulfilled":
|
||||
try:
|
||||
if po_model.can_fulfill():
|
||||
|
||||
@ -24,6 +24,7 @@ from . import models
|
||||
from django.utils.timezone import now
|
||||
from django.db import transaction
|
||||
from django_q.tasks import async_task
|
||||
from plans.signals import order_completed, activate_user_plan
|
||||
|
||||
# logging
|
||||
import logging
|
||||
@ -370,8 +371,8 @@ def update_item_model_cost(sender, instance, created, **kwargs):
|
||||
)
|
||||
|
||||
instance.car.item_model.default_amount = instance.marked_price
|
||||
if not isinstance(instance.car.item_model.additional_info, dict):
|
||||
instance.car.item_model.additional_info = {}
|
||||
# if not isinstance(instance.car.item_model.additional_info, dict):
|
||||
# instance.car.item_model.additional_info = {}
|
||||
# instance.car.item_model.additional_info.update({"car_finance": instance.to_dict()})
|
||||
# instance.car.item_model.additional_info.update(
|
||||
# {
|
||||
@ -1153,3 +1154,10 @@ def bill_model_after_approve_notification(sender, instance, created, **kwargs):
|
||||
please complete the bill payment.
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
|
||||
def handle_upgrade(sender, order, **kwargs):
|
||||
logger.info(f"User {order.user} upgraded to {order.plan}")
|
||||
|
||||
order_completed.connect(handle_upgrade)
|
||||
@ -1,11 +1,18 @@
|
||||
|
||||
import logging
|
||||
from plans.models import Plan
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django_ledger.io import roles
|
||||
from django_q.tasks import async_task
|
||||
from django.core.mail import send_mail
|
||||
from appointment.models import StaffMember
|
||||
from django.utils.translation import activate
|
||||
from django.contrib.auth import get_user_model
|
||||
from allauth.account.models import EmailAddress
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from inventory.models import DealerSettings, Dealer
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.contrib.auth.models import User, Group, Permission
|
||||
|
||||
@ -1151,14 +1158,6 @@ def create_user_dealer(email, password, name, arabic_name, phone, crn, vrn, addr
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
|
||||
#TODO remove this later
|
||||
EmailAddress.objects.create(
|
||||
user=user,
|
||||
email=user.email,
|
||||
verified=True,
|
||||
primary=True
|
||||
)
|
||||
|
||||
group = Group.objects.create(name=f"{user.pk}-Admin")
|
||||
user.groups.add(group)
|
||||
for perm in Permission.objects.filter(
|
||||
@ -1195,3 +1194,64 @@ def create_user_dealer(email, password, name, arabic_name, phone, crn, vrn, addr
|
||||
# instance.user.groups.add(group)
|
||||
|
||||
# transaction.on_commit(run)
|
||||
|
||||
|
||||
|
||||
|
||||
def send_bilingual_reminder(user_id, plan_id, expiration_date, days_until_expire):
|
||||
"""Send bilingual email reminder using Django-Q"""
|
||||
try:
|
||||
user = User.objects.get(id=user_id)
|
||||
plan = Plan.objects.get(id=plan_id)
|
||||
|
||||
# Determine user language preference
|
||||
user_language = getattr(user, 'language', settings.LANGUAGE_CODE)
|
||||
activate(user_language)
|
||||
|
||||
# Context data
|
||||
context = {
|
||||
'user': user,
|
||||
'plan': plan,
|
||||
'expiration_date': expiration_date,
|
||||
'days_until_expire': days_until_expire,
|
||||
'SITE_NAME': settings.SITE_NAME,
|
||||
'RENEWAL_URL': "url" ,#settings.RENEWAL_URL,
|
||||
'direction': 'rtl' if user_language.startswith('ar') else 'ltr'
|
||||
}
|
||||
|
||||
# Subject with translation
|
||||
subject_en = f"Your {plan.name} subscription expires in {days_until_expire} days"
|
||||
subject_ar = f"اشتراكك في {plan.name} ينتهي خلال {days_until_expire} أيام"
|
||||
|
||||
# Render templates
|
||||
text_content = render_to_string([
|
||||
f'emails/expiration_reminder_{user_language}.txt',
|
||||
'emails/expiration_reminder.txt'
|
||||
], context)
|
||||
|
||||
html_content = render_to_string([
|
||||
f'emails/expiration_reminder_{user_language}.html',
|
||||
'emails/expiration_reminder.html'
|
||||
], context)
|
||||
|
||||
# Create email
|
||||
email = EmailMultiAlternatives(
|
||||
subject=subject_ar if user_language.startswith('ar') else subject_en,
|
||||
body=text_content,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
to=[user.email]
|
||||
)
|
||||
email.attach_alternative(html_content, "text/html")
|
||||
email.send()
|
||||
|
||||
return f"Sent to {user.email} in {user_language}"
|
||||
except Exception as e:
|
||||
logger.error(f"Email failed: {str(e)}")
|
||||
raise
|
||||
|
||||
def handle_email_result(task):
|
||||
"""Callback for email results"""
|
||||
if task.success:
|
||||
logger.info(f"Email task succeeded: {task.result}")
|
||||
else:
|
||||
logger.error(f"Email task failed: {task.result}")
|
||||
@ -701,7 +701,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"<slug:dealer_slug>/ledgers/<slug:entity_slug>/delete/<uuid:ledger_pk>/",
|
||||
views.LedgerModelDeleteView.as_view(),
|
||||
views.LedgerModelDeleteView,
|
||||
name="ledger-delete",
|
||||
),
|
||||
path(
|
||||
|
||||
@ -1515,7 +1515,6 @@ class CarFinanceCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMe
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["car"] = self.car
|
||||
print(self.car)
|
||||
return context
|
||||
|
||||
# def get_form(self, form_class=None):
|
||||
@ -1571,6 +1570,10 @@ class CarFinanceUpdateView(
|
||||
kwargs["instance"] = self.get_object()
|
||||
return kwargs
|
||||
|
||||
def get_context_data(self , **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["car"] = self.object.car
|
||||
return context
|
||||
# def get_initial(self):
|
||||
# initial = super().get_initial()
|
||||
# instance = self.get_object()
|
||||
@ -1617,7 +1620,7 @@ class CarUpdateView(
|
||||
permission_required = ["inventory.change_car"]
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("car_detail", kwargs={"slug": self.object.slug})
|
||||
return reverse("car_detail", kwargs={"dealer_slug": self.request.dealer.slug,"slug": self.object.slug})
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
@ -2263,6 +2266,49 @@ class CustomerListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
return context
|
||||
|
||||
|
||||
# class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
||||
# """
|
||||
# CustomerDetailView handles retrieving and presenting detailed information about
|
||||
# a specific customer. It ensures that the user is authenticated and has the
|
||||
# necessary permissions before accessing the customer's details. This view
|
||||
# provides context data including estimates and invoices related to the customer.
|
||||
|
||||
# :ivar model: The model associated with the view.
|
||||
# :type model: CustomerModel
|
||||
# :ivar template_name: The path to the template used for rendering the view.
|
||||
# :type template_name: str
|
||||
# :ivar context_object_name: The name of the variable in the template context
|
||||
# for the object being viewed.
|
||||
# :type context_object_name: str
|
||||
# :ivar permission_required: The list of permissions required to access this view.
|
||||
# :type permission_required: list[str]
|
||||
# """
|
||||
|
||||
# model = models.Customer
|
||||
# template_name = "customers/view_customer.html"
|
||||
# context_object_name = "customer"
|
||||
# permission_required = ["inventory.view_customer"]
|
||||
|
||||
# def get_context_data(self, **kwargs):
|
||||
# dealer = get_user_type(self.request)
|
||||
# entity = dealer.entity
|
||||
# context = super().get_context_data(**kwargs)
|
||||
|
||||
# context["notes"] = models.Notes.objects.filter(
|
||||
# dealer=dealer,
|
||||
# content_type__model="customer", object_id=self.object.id
|
||||
# )
|
||||
# estimates = entity.get_estimates().filter(customer=self.object.customer_model)
|
||||
# invoices = entity.get_invoices().filter(customer=self.object.customer_model)
|
||||
|
||||
# total = estimates.count() + invoices.count()
|
||||
|
||||
# context["estimates"] = estimates
|
||||
# context["invoices"] = invoices
|
||||
# context["total"] = total
|
||||
# context["note_form"] = forms.NoteForm()
|
||||
# return context
|
||||
|
||||
class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
||||
"""
|
||||
CustomerDetailView handles retrieving and presenting detailed information about
|
||||
@ -2290,17 +2336,22 @@ class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView
|
||||
dealer = get_user_type(self.request)
|
||||
entity = dealer.entity
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["customer_notes"] = models.Notes.objects.filter(
|
||||
object_id=self.object.pk
|
||||
|
||||
context["notes"] = models.Notes.objects.filter(
|
||||
dealer=dealer,
|
||||
content_type__model="customer", object_id=self.object.id
|
||||
)
|
||||
estimates = entity.get_estimates().filter(customer=self.object.customer_model)
|
||||
invoices = entity.get_invoices().filter(customer=self.object.customer_model)
|
||||
context['leads']=self.object.customer_leads.all()
|
||||
|
||||
|
||||
total = estimates.count() + invoices.count()
|
||||
|
||||
context["estimates"] = estimates
|
||||
context["invoices"] = invoices
|
||||
context["total"] = total
|
||||
context["note_form"] = forms.NoteForm()
|
||||
return context
|
||||
|
||||
|
||||
@ -3973,7 +4024,7 @@ class BankAccountDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailV
|
||||
def get_queryset(self):
|
||||
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
||||
query = self.request.GET.get("q")
|
||||
qs = self.model.objects.filter(entity=dealer.entity)
|
||||
qs = self.model.objects.filter(entity_model=dealer.entity)
|
||||
if query:
|
||||
qs = apply_search_filters(qs, query)
|
||||
return qs
|
||||
@ -4016,8 +4067,8 @@ class BankAccountUpdateView(
|
||||
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
||||
entity = dealer.entity
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs["entity_slug"] = entity.slug # Get entity_slug from URL
|
||||
kwargs["user_model"] = entity.admin # Get user_model from the request
|
||||
kwargs["entity_slug"] = entity.slug
|
||||
kwargs["user_model"] = entity.admin
|
||||
return kwargs
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
@ -4031,7 +4082,6 @@ class BankAccountUpdateView(
|
||||
]
|
||||
)
|
||||
form.fields["account_model"].queryset = account_qs
|
||||
|
||||
return form
|
||||
|
||||
def get_success_url(self):
|
||||
@ -4334,16 +4384,12 @@ def sales_list_view(request, dealer_slug):
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
# query = request.GET.get('q')
|
||||
# # if query:
|
||||
# # qs = qs.filter(
|
||||
# # Q(order_number__icontains=query) |
|
||||
# # Q(customer__name__icontains=query) |
|
||||
# # Q(item_details__icontains=query)
|
||||
|
||||
# # ).distinct()
|
||||
# for so in qs:
|
||||
# if query in so.customer_customer
|
||||
search_query = request.GET.get('q', None)
|
||||
if search_query:
|
||||
qs = qs.filter(
|
||||
Q(order_number__icontains=search_query)|
|
||||
Q(customer__customer_name__icontains=search_query)
|
||||
).distinct()
|
||||
|
||||
paginator = Paginator(qs, 30)
|
||||
page_number = request.GET.get("page")
|
||||
@ -4443,6 +4489,13 @@ class EstimateListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
related_content_type=ContentType.objects.get_for_model(models.Staff),
|
||||
related_object_id=self.request.staff.pk,
|
||||
)
|
||||
qs = EstimateModel.objects.filter(pk__in=[x.content_object.pk for x in qs])
|
||||
search_query = self.request.GET.get('q', None)
|
||||
if search_query:
|
||||
qs = qs.filter(
|
||||
Q(estimate_number__icontains=search_query)|
|
||||
Q(customer__customer_name__icontains=search_query)
|
||||
).distinct()
|
||||
context["staff_estimates"] = qs
|
||||
return context
|
||||
|
||||
@ -4450,19 +4503,16 @@ class EstimateListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
dealer = get_user_type(self.request)
|
||||
entity = dealer.entity
|
||||
status = self.request.GET.get("status")
|
||||
|
||||
queryset = entity.get_estimates()
|
||||
type(queryset)
|
||||
if status:
|
||||
queryset = queryset.filter(status=status)
|
||||
for f in queryset.first()._meta.get_fields():
|
||||
print(f)
|
||||
search_query = self.request.GET.get('q', '').strip()
|
||||
print(search_query)
|
||||
if search_query:
|
||||
print("inside")
|
||||
queryset = queryset.filter(
|
||||
Q(estimate_number__icontains=search_query)
|
||||
search_query = self.request.GET.get('q', None)
|
||||
|
||||
if search_query:
|
||||
queryset = queryset.filter(
|
||||
Q(estimate_number__icontains=search_query)|
|
||||
Q(customer__customer_name__icontains=search_query)
|
||||
).distinct()
|
||||
|
||||
return queryset
|
||||
@ -4774,9 +4824,10 @@ class EstimateDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView
|
||||
kwargs["data"] = finance_data
|
||||
kwargs["invoice"] = invoice_obj
|
||||
try:
|
||||
cf = estimate.get_itemtxs_data()[0].first().item_model.car.finances
|
||||
selected_items = cf.additional_services.filter(dealer=dealer)
|
||||
car_finances = estimate.get_itemtxs_data()[0].first().item_model.car.finances
|
||||
selected_items = car_finances.additional_services.filter(dealer=dealer)
|
||||
form = forms.AdditionalFinancesForm()
|
||||
form.fields["additional_finances"].queryset = form.fields["additional_finances"].queryset.filter(dealer=dealer)
|
||||
form.initial["additional_finances"] = selected_items
|
||||
kwargs["additionals_form"] = form
|
||||
except Exception as e:
|
||||
@ -4824,7 +4875,7 @@ def create_sale_order(request, dealer_slug, pk):
|
||||
estimate.save()
|
||||
for item in estimate.get_itemtxs_data()[0].all():
|
||||
try:
|
||||
item.item_model.additional_info["car_info"]["status"] = "sold"
|
||||
# item.item_model.additional_info["car_info"]["status"] = "sold"
|
||||
item.item_model.save()
|
||||
logger.debug(
|
||||
f"Car status updated to 'sold' for item.item_model PK: {getattr(item.item_model, 'pk', 'N/A')}."
|
||||
@ -5047,6 +5098,7 @@ def estimate_mark_as(request, dealer_slug, pk):
|
||||
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
||||
estimate = get_object_or_404(EstimateModel, pk=pk)
|
||||
mark = request.GET.get("mark")
|
||||
print(mark)
|
||||
if mark:
|
||||
if mark == "review":
|
||||
if not estimate.can_review():
|
||||
@ -5829,6 +5881,7 @@ class LeadListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
permission_required = ["inventory.view_lead"]
|
||||
|
||||
def get_queryset(self):
|
||||
# print(self.request.is_dealer)
|
||||
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
||||
query = self.request.GET.get("q")
|
||||
qs = models.Lead.objects.filter(dealer=dealer).exclude(status="converted")
|
||||
@ -6487,7 +6540,7 @@ def schedule_event(request, dealer_slug, content_type, slug):
|
||||
|
||||
if not request.is_staff:
|
||||
messages.error(request, _("You do not have permission to schedule."))
|
||||
return redirect(request.META.get("HTTP_REFERER"))
|
||||
return redirect(f"{content_type}_detail", dealer_slug=dealer_slug, slug=slug)
|
||||
|
||||
if request.method == "POST":
|
||||
form = forms.ScheduleForm(request.POST)
|
||||
@ -6994,8 +7047,12 @@ class OpportunityListView(LoginRequiredMixin, PermissionRequiredMixin, ListView)
|
||||
|
||||
def get_queryset(self):
|
||||
dealer = get_user_type(self.request)
|
||||
staff = getattr(self.request.user.staffmember, "staff", None)
|
||||
queryset = models.Opportunity.objects.filter(dealer=dealer, lead__staff=staff)
|
||||
|
||||
if self.request.is_dealer:
|
||||
queryset = models.Opportunity.objects.filter(dealer=dealer)
|
||||
elif self.request.is_staff:
|
||||
staff = self.request.staff
|
||||
queryset = models.Opportunity.objects.filter(dealer=dealer, lead__staff=staff)
|
||||
|
||||
# Search filter
|
||||
search = self.request.GET.get("q")
|
||||
@ -7168,8 +7225,8 @@ class ItemServiceCreateView(
|
||||
dealer = get_user_type(self.request)
|
||||
vat = models.VatRate.objects.get(dealer=dealer, is_active=True)
|
||||
form.instance.dealer = dealer
|
||||
if form.instance.taxable:
|
||||
form.instance.price = (form.instance.price * vat.rate) + form.instance.price
|
||||
# if form.instance.taxable:
|
||||
# form.instance.price = (form.instance.price * vat.rate) + form.instance.price
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
@ -7217,8 +7274,8 @@ class ItemServiceUpdateView(
|
||||
dealer = get_user_type(self.request)
|
||||
vat = models.VatRate.objects.get(dealer=dealer, is_active=True)
|
||||
form.instance.dealer = dealer
|
||||
if form.instance.taxable:
|
||||
form.instance.price = (form.instance.price * vat.rate) + form.instance.price
|
||||
# if form.instance.taxable:
|
||||
# form.instance.price = (form.instance.price * vat.rate) + form.instance.price
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
@ -8880,7 +8937,7 @@ class LedgerModelDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailV
|
||||
permission_required = "django_ledger.view_ledgermodel"
|
||||
|
||||
|
||||
class LedgerModelCreateView(LedgerModelCreateViewBase):
|
||||
class LedgerModelCreateView(LedgerModelCreateViewBase,SuccessMessageMixin):
|
||||
"""
|
||||
Handles the creation of LedgerModel entities.
|
||||
|
||||
@ -8896,6 +8953,7 @@ class LedgerModelCreateView(LedgerModelCreateViewBase):
|
||||
|
||||
template_name = "ledger/ledger/ledger_form.html"
|
||||
permission_required = ["django_ledger.add_ledgermodel"]
|
||||
success_message = _("Ledger created successfully")
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
return LedgerModelCreateForm(
|
||||
@ -8905,10 +8963,11 @@ class LedgerModelCreateView(LedgerModelCreateViewBase):
|
||||
)
|
||||
|
||||
def form_valid(self, form):
|
||||
form.field["entity"] = self.request.dealer.entity
|
||||
form.fields["entity"] = self.request.dealer.entity
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
messages.success(self.request, self.success_message)
|
||||
return reverse(
|
||||
"ledger_list",
|
||||
kwargs={
|
||||
@ -8942,34 +9001,47 @@ class LedgerModelModelActionView(LedgerModelModelActionViewBase):
|
||||
)
|
||||
|
||||
|
||||
class LedgerModelDeleteView(LedgerModelDeleteViewBase, SuccessMessageMixin):
|
||||
"""
|
||||
Handles the deletion of a Ledger model instance.
|
||||
|
||||
Provides functionality for rendering a confirmation template and deleting a
|
||||
ledger instance from the system. Extends functionality for managing success
|
||||
messages and redirections upon successful deletion.
|
||||
@login_required
|
||||
@permission_required("django_ledger.delete_ledgermodel", raise_exception=True)
|
||||
def LedgerModelDeleteView(request, dealer_slug,entity_slug,ledger_pk):
|
||||
ledger = LedgerModel.objects.filter(pk=ledger_pk).first()
|
||||
if request.method == "POST":
|
||||
ledger.delete()
|
||||
messages.success(request, _("Ledger deleted successfully"))
|
||||
return redirect("ledger_list", dealer_slug=dealer_slug, entity_slug=entity_slug)
|
||||
return render(request,"ledger/ledger/ledger_delete.html",{"ledger_model":ledger})
|
||||
# class LedgerModelDeleteView(DeleteView, SuccessMessageMixin):
|
||||
# """
|
||||
# Handles the deletion of a Ledger model instance.
|
||||
|
||||
:ivar template_name: Path to the template used for rendering the delete
|
||||
confirmation view.
|
||||
:type template_name: str
|
||||
:ivar success_message: Success message displayed upon successful deletion
|
||||
of the ledger instance.
|
||||
:type success_message: str
|
||||
"""
|
||||
# Provides functionality for rendering a confirmation template and deleting a
|
||||
# ledger instance from the system. Extends functionality for managing success
|
||||
# messages and redirections upon successful deletion.
|
||||
|
||||
template_name = "ledger/ledger/ledger_delete.html"
|
||||
success_message = _("Ledger deleted successfully")
|
||||
permission_required = ["django_ledger.delete_ledgermodel"]
|
||||
# :ivar template_name: Path to the template used for rendering the delete
|
||||
# confirmation view.
|
||||
# :type template_name: str
|
||||
# :ivar success_message: Success message displayed upon successful deletion
|
||||
# of the ledger instance.
|
||||
# :type success_message: str
|
||||
# """
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse(
|
||||
"ledger_list",
|
||||
kwargs={
|
||||
"dealer_slug": self.kwargs["dealer_slug"],
|
||||
"entity_slug": self.kwargs["entity_slug"],
|
||||
},
|
||||
)
|
||||
# template_name = "ledger/ledger/ledger_delete.html"
|
||||
# pk_url_kwarg = 'ledger_pk'
|
||||
# context_object_name = 'ledger_model'
|
||||
|
||||
# success_message = _("Ledger deleted successfully")
|
||||
# permission_required = ["django_ledger.delete_ledgermodel"]
|
||||
|
||||
# def get_success_url(self):
|
||||
# return reverse(
|
||||
# "ledger_list",
|
||||
# kwargs={
|
||||
# "dealer_slug": self.kwargs["dealer_slug"],
|
||||
# "entity_slug": self.kwargs["entity_slug"],
|
||||
# },
|
||||
# )
|
||||
|
||||
|
||||
class JournalEntryListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
@ -9063,7 +9135,7 @@ class JournalEntryCreateView(
|
||||
|
||||
@login_required
|
||||
@permission_required("django_ledger.delete_journalentrymodel", raise_exception=True)
|
||||
def JournalEntryDeleteView(request, pk):
|
||||
def JournalEntryDeleteView(request,dealer_slug, pk):
|
||||
"""
|
||||
Handles the deletion of a specific journal entry. This view facilitates
|
||||
the deletion of a journal entry identified by its primary key (pk). If the
|
||||
@ -9084,10 +9156,10 @@ def JournalEntryDeleteView(request, pk):
|
||||
ledger = journal_entry.ledger
|
||||
if not journal_entry.can_delete():
|
||||
messages.error(request, _("Journal Entry cannot be deleted"))
|
||||
return redirect("journalentry_list", pk=ledger.pk)
|
||||
return redirect("journalentry_list",dealer_slug=dealer_slug, pk=ledger.pk)
|
||||
journal_entry.delete()
|
||||
messages.success(request, "Journal Entry deleted")
|
||||
return redirect("journalentry_list", pk=ledger.pk)
|
||||
return redirect("journalentry_list",dealer_slug=dealer_slug, pk=ledger.pk)
|
||||
return render(
|
||||
request,
|
||||
"ledger/journal_entry/journal_entry_delete.html",
|
||||
@ -9317,39 +9389,91 @@ def payment_callback(request, dealer_slug):
|
||||
payment_id = request.GET.get("id")
|
||||
history = models.PaymentHistory.objects.filter(transaction_id=payment_id).first()
|
||||
payment_status = request.GET.get("status")
|
||||
order = Order.objects.filter(user=dealer.user, status=1).first()
|
||||
order = Order.objects.filter(user=dealer.user, status=1).first() # Status 1 = NEW
|
||||
|
||||
if payment_status == "paid":
|
||||
# Get or create billing info (optional step)
|
||||
billing_info, created = BillingInfo.objects.get_or_create(
|
||||
user=dealer.user,
|
||||
tax_number=dealer.vrn,
|
||||
name=dealer.arabic_name,
|
||||
street=dealer.address,
|
||||
zipcode=dealer.entity.zip_code if dealer.entity.zip_code else " ",
|
||||
city=dealer.entity.city if dealer.entity.city else " ",
|
||||
country=dealer.entity.country if dealer.entity.country else " ",
|
||||
defaults={
|
||||
'tax_number': dealer.vrn,
|
||||
'name': dealer.arabic_name,
|
||||
'street': dealer.address,
|
||||
'zipcode': dealer.entity.zip_code or " ",
|
||||
'city': dealer.entity.city or " ",
|
||||
'country': dealer.entity.country or " ",
|
||||
}
|
||||
)
|
||||
if created:
|
||||
userplan = UserPlan.objects.create(
|
||||
user=request.user,
|
||||
plan=order.plan,
|
||||
active=True,
|
||||
)
|
||||
userplan.initialize()
|
||||
|
||||
order.complete_order()
|
||||
history.status = "paid"
|
||||
history.save()
|
||||
invoice = order.get_invoices().first()
|
||||
return render(
|
||||
request, "payment_success.html", {"order": order, "invoice": invoice}
|
||||
)
|
||||
try:
|
||||
# COMPLETE THE ORDER - This handles plan activation/upgrade
|
||||
order.complete_order() # Critical step: activates the plan
|
||||
|
||||
# Update payment history
|
||||
history.status = "paid"
|
||||
history.save()
|
||||
|
||||
# Retrieve invoice
|
||||
invoice = order.get_invoices().first()
|
||||
return render(
|
||||
request,
|
||||
"payment_success.html",
|
||||
{"order": order, "invoice": invoice}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# Handle activation errors (log, notify admin, etc.)
|
||||
logger.error(f"Plan activation failed: {str(e)}")
|
||||
history.status = "failed"
|
||||
history.save()
|
||||
return render(request, "payment_failed.html", {"message": "Plan activation error"})
|
||||
|
||||
elif payment_status == "failed":
|
||||
history.status = "failed"
|
||||
history.save()
|
||||
return render(request, "payment_failed.html", {"message": message})
|
||||
|
||||
return render(request, "payment_failed.html", {"message": message})
|
||||
# Handle unexpected status
|
||||
return render(request, "payment_failed.html", {"message": "Unknown payment status"})
|
||||
# def payment_callback(request, dealer_slug):
|
||||
# message = request.GET.get("message")
|
||||
# dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
||||
# payment_id = request.GET.get("id")
|
||||
# history = models.PaymentHistory.objects.filter(transaction_id=payment_id).first()
|
||||
# payment_status = request.GET.get("status")
|
||||
# order = Order.objects.filter(user=dealer.user, status=1).first()
|
||||
|
||||
# if payment_status == "paid":
|
||||
# billing_info, created = BillingInfo.objects.get_or_create(
|
||||
# user=dealer.user,
|
||||
# tax_number=dealer.vrn,
|
||||
# name=dealer.arabic_name,
|
||||
# street=dealer.address,
|
||||
# zipcode=dealer.entity.zip_code if dealer.entity.zip_code else " ",
|
||||
# city=dealer.entity.city if dealer.entity.city else " ",
|
||||
# country=dealer.entity.country if dealer.entity.country else " ",
|
||||
# )
|
||||
# if created:
|
||||
# userplan = UserPlan.objects.create(
|
||||
# user=request.user,
|
||||
# plan=order.plan,
|
||||
# active=True,
|
||||
# )
|
||||
# userplan.initialize()
|
||||
|
||||
# order.complete_order()
|
||||
# history.status = "paid"
|
||||
# history.save()
|
||||
# invoice = order.get_invoices().first()
|
||||
# return render(
|
||||
# request, "payment_success.html", {"order": order, "invoice": invoice}
|
||||
# )
|
||||
|
||||
# elif payment_status == "failed":
|
||||
# history.status = "failed"
|
||||
# history.save()
|
||||
|
||||
# return render(request, "payment_failed.html", {"message": message})
|
||||
|
||||
|
||||
@login_required
|
||||
|
||||
@ -31,10 +31,7 @@ msgstr ""
|
||||
#: templates/inventory/transfer_details.html:89
|
||||
#: templates/sales/estimates/estimate_detail.html:234
|
||||
#: templates/sales/estimates/sale_order_form.html:123
|
||||
#: templates/sales/estimates/sale_order_preview.html:203
|
||||
#: templates/sales/invoices/invoice_detail.html:328
|
||||
#: templates/sales/orders/order_details.html:201
|
||||
#: templates/sales/orders/order_details.html:503
|
||||
#: templates/sales/estimates/sale_or ils.html:503
|
||||
msgid "VIN"
|
||||
msgstr "رقم الهيكل"
|
||||
|
||||
|
||||
BIN
static/images/logos/no-content-new.jpg
Normal file
BIN
static/images/logos/no-content-new.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 150 KiB |
@ -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");
|
||||
*/
|
||||
|
Before Width: | Height: | Size: 814 B After Width: | Height: | Size: 814 B |
@ -80,7 +80,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<main class="main" id="top">
|
||||
<div class="px-3">
|
||||
<div id="main_content" class="px-3">
|
||||
<div class="row min-vh-100 flex-center p-5">
|
||||
<div class="col-12 col-xl-10 col-xxl-8">
|
||||
<div class="row justify-content-center align-items-center g-5">
|
||||
|
||||
@ -1,9 +1,17 @@
|
||||
{% extends "account/email/base_message.txt" %}
|
||||
{% load account %}
|
||||
{% load i18n %}
|
||||
{% load account i18n %}
|
||||
|
||||
{% block content %}{% autoescape off %}{% user_display user as user_display %}{% blocktranslate with site_name=current_site.name site_domain=current_site.domain %}You're receiving this email because user {{ user_display }} has given your email address to register an account on {{ site_domain }}.{% endblocktranslate %}
|
||||
{% block content %}{% autoescape off %}{% user_display user as user_display %}
|
||||
{% blocktranslate with site_domain=current_site.domain %}تتلقى هذا البريد لأن {{ user_display }} استخدم بريدك للتسجيل في {{ site_domain }}.{% endblocktranslate %}
|
||||
|
||||
{% if code %}{% blocktranslate %}Your email verification code is listed below. Please enter it in your open browser window.{% endblocktranslate %}
|
||||
{% if code %}
|
||||
{% blocktranslate %}أدخل رمز التحقق في المتصفح:{% endblocktranslate %}
|
||||
|
||||
{{ code }}{% else %}{% blocktranslate %}To confirm this is correct, go to {{ activate_url }}{% endblocktranslate %}{% endif %}{% endautoescape %}{% endblock content %}
|
||||
{{ code }}
|
||||
|
||||
{% blocktranslate %}ينتهي صلاحية هذا الرمز خلال {{ code_expiration }} دقيقة.{% endblocktranslate %}
|
||||
{% else %}
|
||||
{% blocktranslate %}أكد هذا التسجيل بالزيارة:{% endblocktranslate %}
|
||||
|
||||
{{ activate_url }}
|
||||
{% endif %}{% endautoescape %}{% endblock content %}
|
||||
@ -27,6 +27,7 @@
|
||||
<h3 class="mb-4">{% trans "Change Password" %}</h3>
|
||||
</div>
|
||||
<form method="post"
|
||||
hx-boost="false"
|
||||
action="{% url 'account_change_password' %}"
|
||||
class="form needs-validation"
|
||||
novalidate>
|
||||
|
||||
@ -66,7 +66,8 @@
|
||||
rel="stylesheet"
|
||||
id="user-style-default">
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
<script src="{% static 'js/main.js' %}"></script>
|
||||
<script src="{% static 'js/jquery.min.js' %}"></script>
|
||||
{% comment %} <script src="{% static 'js/echarts.js' %}"></script> {% endcomment %}
|
||||
@ -82,7 +83,7 @@
|
||||
{% include "plans/expiration_messages.html" %}
|
||||
{% block period_navigation %}
|
||||
{% endblock period_navigation %}
|
||||
<div id="main_content" class="fade-me-in" hx-boost="true" hx-target="#main_content" hx-select="#main_content" hx-swap="outerHTML transition:false" hx-select-oob="#toast-container" hx-history-elt>
|
||||
<div id="main_content" class="fade-me-in" hx-boost="true" hx-target="#main_content" hx-select="#main_content" hx-swap="innerHTML transition:true" hx-select-oob="#toast-container" hx-history-elt>
|
||||
<div id="spinner" class="htmx-indicator spinner-bg">
|
||||
<img src="{% static 'spinner.svg' %}" width="100" height="100" alt="">
|
||||
</div>
|
||||
@ -93,8 +94,8 @@
|
||||
{% comment %} <script src="{% static 'vendors/feather-icons/feather.min.js' %}"></script>
|
||||
<script src="{% static 'vendors/fontawesome/all.min.js' %}"></script>
|
||||
<script src="{% static 'vendors/popper/popper.min.js' %}"></script>
|
||||
<script src="{% static 'vendors/bootstrap/bootstrap.min.js' %}"></script>
|
||||
<script src="{% static 'js/phoenix.js' %}"></script> {% endcomment %}
|
||||
<script src="{% static 'vendors/bootstrap/bootstrap.min.js' %}"></script> {% endcomment %}
|
||||
<script src="{% static 'js/phoenix.js' %}"></script>
|
||||
|
||||
</div>
|
||||
{% block body %}
|
||||
@ -154,8 +155,57 @@
|
||||
document.getElementById('global-indicator')
|
||||
];
|
||||
});*/
|
||||
|
||||
|
||||
let Toast = Swal.mixin({
|
||||
toast: true,
|
||||
position: "top-end",
|
||||
showConfirmButton: false,
|
||||
timer: 3000,
|
||||
timerProgressBar: true,
|
||||
didOpen: (toast) => {
|
||||
toast.onmouseenter = Swal.stopTimer;
|
||||
toast.onmouseleave = Swal.resumeTimer;
|
||||
}
|
||||
});
|
||||
function notify(tag, msg) {
|
||||
Toast.fire({
|
||||
icon: tag,
|
||||
titleText: msg
|
||||
});
|
||||
}
|
||||
document.addEventListener('htmx:afterRequest', function(evt) {
|
||||
if(evt.detail.xhr.status == 403){
|
||||
/* Notify the user of a 404 Not Found response */
|
||||
notify("error", "You do not have permission to view this page");
|
||||
}
|
||||
if(evt.detail.xhr.status == 404){
|
||||
/* Notify the user of a 404 Not Found response */
|
||||
return alert("Error: Could Not Find Resource");
|
||||
}
|
||||
if (evt.detail.successful != true) {
|
||||
console.log(evt.detail.xhr.statusText)
|
||||
/* Notify of an unexpected error, & print error to console */
|
||||
notify("error", `Unexpected Error ,${evt.detail.xhr.statusText}`);
|
||||
}
|
||||
});
|
||||
document.body.addEventListener('htmx:beforeSwap', function(evt) {
|
||||
if (evt.detail.target.id === 'main_content') {
|
||||
var backdrops = document.querySelectorAll('.modal-backdrop');
|
||||
backdrops.forEach(function(backdrop) {
|
||||
backdrop.remove();
|
||||
});
|
||||
}
|
||||
});
|
||||
// Close modal after successful form submission
|
||||
document.body.addEventListener('htmx:afterSwap', function(evt) {
|
||||
if (evt.detail.target.id === 'main_content') {
|
||||
document.querySelectorAll('.modal').forEach(function(m) {
|
||||
var modal = bootstrap.Modal.getInstance(m);
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% comment %} {% block customJS %}{% endblock %} {% endcomment %}
|
||||
</body>
|
||||
|
||||
@ -40,6 +40,5 @@
|
||||
document.querySelector('#id_note').value = note
|
||||
let form = document.querySelector('.add_note_form')
|
||||
form.action = url
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -16,6 +16,13 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form action="{% url 'schedule_event' request.dealer.slug content_type slug %}"
|
||||
hx-select=".taskTable"
|
||||
hx-target=".taskTable"
|
||||
hx-on::after-request="{
|
||||
resetSubmitButton(document.querySelector('.add_schedule_form button[type=submit]'));
|
||||
$('#scheduleModal').modal('hide');
|
||||
}"
|
||||
hx-swap="outerHTML"
|
||||
method="post"
|
||||
class="add_schedule_form">
|
||||
{% csrf_token %}
|
||||
|
||||
@ -23,14 +23,12 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form action="{% url 'add_task' request.dealer.slug content_type slug %}"
|
||||
hx-select=".taskTable"
|
||||
hx-target=".taskTable"
|
||||
hx-on::after-request="{
|
||||
resetSubmitButton(document.querySelector('.add_task_form button[type=submit]'));
|
||||
$('#taskModal').modal('hide');
|
||||
}"
|
||||
method="post"
|
||||
class="add_task_form">
|
||||
method="post"
|
||||
class="add_task_form"
|
||||
hx-post="{% url 'add_task' request.dealer.slug content_type slug %}"
|
||||
hx-target="#your-content-container"
|
||||
hx-swap="innerHTML"
|
||||
hx-boost="false">
|
||||
{% csrf_token %}
|
||||
{{ staff_task_form|crispy }}
|
||||
<button type="submit" class="btn btn-phoenix-success w-100">{% trans 'Save' %}</button>
|
||||
|
||||
@ -37,17 +37,9 @@
|
||||
{% include "crm/leads/partials/update_action.html" %}
|
||||
<div class="row align-items-center justify-content-between g-3 mb-3">
|
||||
<div class="col-12 col-md-auto">
|
||||
<h4 class="mb-0">{{ _("Lead Details") }}</h4>
|
||||
</div>
|
||||
<div class="col-12 col-md-auto">
|
||||
<div class="d-flex">
|
||||
<div class="flex-1 d-md-none">
|
||||
<button class="btn px-3 btn-phoenix-secondary text-body-tertiary me-2">
|
||||
<span class="fa-solid fa-bars"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="mb-0">{{ _("Lead Details") }}</h3>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -55,12 +47,7 @@
|
||||
<div class="col-md-5 col-lg-5 col-xl-4">
|
||||
<div class="sticky-leads-sidebar">
|
||||
<div class="lead-details" data-breakpoint="md">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 d-md-none">
|
||||
<h3 class="mb-0">{{ _("Lead Details") }}</h3>
|
||||
<button class="btn p-0">
|
||||
<span class="uil uil-times fs-7"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card mb-2">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center g-3 text-center text-xxl-start">
|
||||
@ -494,7 +481,8 @@
|
||||
data-url="{% url 'update_note' request.dealer.slug note.pk %}"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#noteModal"
|
||||
data-note-title="{{ _("Update") }}<i class='fas fa-pen-square text-primary ms-2'></i>">
|
||||
data-note-title="{{ _('Update') }}">
|
||||
<i class='fas fa-pen-square text-primary ms-2'></i>
|
||||
{{ _("Update") }}
|
||||
</a>
|
||||
<button class="btn btn-phoenix-danger btn-sm delete-btn"
|
||||
@ -806,24 +794,13 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="row align-items-center justify-content-between py-2 pe-0 fs-9">
|
||||
<div class="row align-items-center justify-content-between py-2 pe-0 fs-9 mt-3">
|
||||
<div class="col-auto d-flex">
|
||||
<p class="mb-0 d-none d-sm-block me-3 fw-semibold text-body"
|
||||
data-list-info="data-list-info"></p>
|
||||
<a class="nav-link px-3 d-block"
|
||||
href="{% url 'appointment:get_user_appointments' %}"> <span class="me-2 text-body align-bottom" data-feather="calendar"></span>{{ _("View in Calendar") }}
|
||||
<span class="fas fa-angle-right ms-1" data-fa-transform="down-1"></span></a>
|
||||
</div>
|
||||
<div class="col-auto d-flex">
|
||||
<button class="page-link" data-list-pagination="prev">
|
||||
<span class="fas fa-chevron-left"></span>
|
||||
</button>
|
||||
<ul class="mb-0 pagination">
|
||||
</ul>
|
||||
<button class="page-link pe-0" data-list-pagination="next">
|
||||
<span class="fas fa-chevron-right"></span>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -837,9 +814,9 @@
|
||||
{% include "components/note_modal.html" with content_type="lead" slug=lead.slug %}
|
||||
<!-- schedule Modal -->
|
||||
{% include "components/schedule_modal.html" with content_type="lead" slug=lead.slug %}
|
||||
{% endblock content %}
|
||||
{% block customJS %}
|
||||
<script>
|
||||
{% endblock content %}
|
||||
{% block customJS %}
|
||||
<script>
|
||||
function reset_form() {
|
||||
document.querySelector('#id_note').value = ""
|
||||
let form = document.querySelector('.add_note_form')
|
||||
|
||||
@ -1,16 +1,19 @@
|
||||
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n static humanize %}
|
||||
{% block title %}
|
||||
{{ _("Leads") |capfirst }}
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
{% if page_obj.object_list %}
|
||||
<div class="row g-3 mt-4 mb-4">
|
||||
<h2 class="mb-2">
|
||||
{{ _("Leads") |capfirst }}
|
||||
<li class="fas fa-bullhorn text-primary ms-2"></li>
|
||||
</h2>
|
||||
<!-- Action Tracking Modal -->
|
||||
{% include "crm/leads/partials/update_action.html" %}
|
||||
{% comment %} {% include "crm/leads/partials/update_action.html" %} {% endcomment %}
|
||||
|
||||
<div class="row g-3 justify-content-between mb-4">
|
||||
<div class="col-auto">
|
||||
<div class="d-md-flex justify-content-between">
|
||||
@ -26,6 +29,7 @@
|
||||
<div class="d-flex">{% include 'partials/search_box.html' %}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
{% if page_obj.object_list %}
|
||||
@ -201,26 +205,7 @@
|
||||
</small>
|
||||
</div>
|
||||
</td>
|
||||
{% comment %} <td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">
|
||||
{% if lead.opportunity.stage == "prospect" %}
|
||||
<span class="badge text-bg-primary">{{ lead.opportunity.stage|upper }}</span>
|
||||
{% elif lead.opportunity.stage == "proposal" %}
|
||||
<span class="badge text-bg-info">{{ lead.opportunity.stage|upper }}</span>
|
||||
{% elif lead.opportunity.stage == "negotiation" %}
|
||||
<span class="badge text-bg-warning">{{ lead.opportunity.stage|upper }}</span>
|
||||
{% elif lead.opportunity.stage == "closed_won" %}
|
||||
<span class="badge text-bg-success">{{ lead.opportunity.stage|upper }}</span>
|
||||
{% elif lead.opportunity.stage == "closed_lost" %}
|
||||
<span class="badge text-bg-danger">{{ lead.opportunity.stage|upper }}</span>
|
||||
{% endif %}
|
||||
</td> {% endcomment %}
|
||||
{% comment %} <td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">
|
||||
{% if lead.opportunity %}
|
||||
<a href="{% url 'opportunity_detail' request.dealer.slug lead.opportunity.slug %}">
|
||||
<span class="badge badge-phoenix badge-phoenix-success">Opportunity {{ lead.opportunity.lead}} <i class="fa-solid fa-arrow-up-right-from-square"></i></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td> {% endcomment %}
|
||||
|
||||
<td class="align-middle white-space-nowrap text-end">
|
||||
{% if user == lead.staff.user or request.is_dealer %}
|
||||
<div class="btn-reveal-trigger position-static">
|
||||
@ -268,14 +253,15 @@
|
||||
<div class="d-flex">{% include 'partials/pagination.html' %}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">{% trans "No Lead Yet" %}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{% url 'lead_create' request.dealer.slug as create_lead_url %}
|
||||
{% include "empty-illustration-page.html" with value="lead" url=create_lead_url %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block customJS %}
|
||||
<script>
|
||||
@ -341,3 +327,5 @@
|
||||
}
|
||||
</script>
|
||||
{% endblock customJS %}
|
||||
|
||||
|
||||
|
||||
@ -168,6 +168,11 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% if not new and not follow_up and not negotiation %}
|
||||
{% url 'lead_create' request.dealer.slug as create_lead_url %}
|
||||
{% include "empty-illustration-page.html" with value="lead" url=create_lead_url %}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
<form id="actionTrackingForm"
|
||||
action="{% url 'update_lead_actions' lead.dealer.slug %}"
|
||||
action="{% url 'update_lead_actions' request.dealer.slug %}"
|
||||
hx-select-oob="#currentStage:outerHTML,#leadStatus:outerHTML,#toast-container:outerHTML"
|
||||
hx-swap="none"
|
||||
hx-on::after-request="{
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<div class="content">
|
||||
<h2 class="mb-5">{{ _("Notifications") }}</h2>
|
||||
<div class="d-flex justify-content-end mb-3">
|
||||
<a href="{% url 'mark_all_notifications_as_read' %}" class="btn btn-phoenix-primary"><i class="far fa-envelope fs-8 me-2"></i>{{ _("Mark all as read") }}</a>
|
||||
<a href="{% url 'mark_all_notifications_as_read' %}" hx-select-oob="#notification-counter:outerHTML" class="btn btn-phoenix-primary"><i class="far fa-envelope fs-8 me-2"></i>{{ _("Mark all as read") }}</a>
|
||||
</div>
|
||||
{% if notifications %}
|
||||
<div class="mx-n4 mx-lg-n6 mb-5 border-bottom">
|
||||
|
||||
@ -578,7 +578,7 @@
|
||||
style="min-width:165px">Completed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="list" id="all-tasks-table-body">
|
||||
<tbody class="list taskTable" id="all-tasks-table-body">
|
||||
{% for task in schedules %}
|
||||
{% include "partials/task.html" %}
|
||||
{% endfor %}
|
||||
@ -639,7 +639,7 @@
|
||||
<th class="align-middle pe-0 text-end" scope="col" style="width:10%;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody id="notesTable">
|
||||
{% for note in opportunity.get_notes %}
|
||||
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
|
||||
<td class="align-middle text-start fw-bold text-body-tertiary ps-1">{{ note.note }}</td>
|
||||
@ -1091,6 +1091,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'modal/delete_modal.html' %}
|
||||
<!-- email Modal -->
|
||||
{% include "components/email_modal.html" %}
|
||||
<!-- task Modal -->
|
||||
@ -1100,3 +1101,28 @@
|
||||
<!-- schedule Modal -->
|
||||
{% include "components/schedule_modal.html" with content_type="opportunity" slug=opportunity.slug %}
|
||||
{% endblock %}
|
||||
|
||||
{% block customJS %}
|
||||
<script>
|
||||
|
||||
document.body.addEventListener('htmx:afterSwap', function(evt) {
|
||||
if (evt.detail.target.id === 'main_content') {
|
||||
var modal = bootstrap.Modal.getInstance(document.getElementById('exampleModal'));
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup modal backdrop if needed
|
||||
document.body.addEventListener('htmx:beforeSwap', function(evt) {
|
||||
if (evt.detail.target.id === 'main_content') {
|
||||
var backdrops = document.querySelectorAll('.modal-backdrop');
|
||||
backdrops.forEach(function(backdrop) {
|
||||
backdrop.remove();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock customJS %}
|
||||
@ -5,6 +5,7 @@
|
||||
{{ _("Opportunities") }}
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
{% if opportunities %}
|
||||
<div class="row g-3 mt-4">
|
||||
<div class="col-12">
|
||||
<h2 class="mb-3">
|
||||
@ -87,6 +88,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div id="opportunities-grid" class="row g-4 px-2 px-lg-4 mt-1 mb-4">
|
||||
{% include 'crm/opportunities/partials/opportunity_grid.html' %}
|
||||
@ -96,6 +98,10 @@
|
||||
<div class="d-flex">{% include 'partials/pagination.html' %}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% url 'opportunity_create' request.dealer.slug as create_opportunity_url %}
|
||||
{% include "empty-illustration-page.html" with value="opportunity" url=create_opportunity_url %}
|
||||
{% endif %}
|
||||
{% block customJS %}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
{% endblock title %}
|
||||
{% block vendors %}<a class="nav-link active">{{ _("Customers") |capfirst }}</a>{% endblock %}
|
||||
{% block content %}
|
||||
{% if customers %}
|
||||
<div class="row g-3 mt-4">
|
||||
<h2 class="mb-2">
|
||||
{{ _("Customers") |capfirst }}
|
||||
@ -168,4 +169,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% include 'modal/delete_modal.html' %}
|
||||
{% else %}
|
||||
{% url "customer_create" request.dealer.slug as create_customer_url %}
|
||||
{% include "empty-illustration-page.html" with value="customer" url=create_customer_url %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@ -5,9 +5,11 @@
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
{% include 'modal/delete_modal.html' %}
|
||||
<div class="row">
|
||||
<div class="mb-9">
|
||||
<div class="row align-items-center justify-content-between g-3 mb-4">
|
||||
|
||||
<!---->
|
||||
<div class="mt-4">
|
||||
<!--heading -->
|
||||
<div class="row align-items-center justify-content-between g-3 mb-4">
|
||||
<div class="col-auto">
|
||||
<h3 class="mb-0">{% trans 'Customer details' %}</h3>
|
||||
</div>
|
||||
@ -32,12 +34,15 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-5">
|
||||
<div class="col-12 col-xxl-4">
|
||||
<div class="row g-3 h-100">
|
||||
<div class="col-12 col-md-7 col-xxl-12">
|
||||
<div class="card h-100 h-xxl-auto">
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!--cards-->
|
||||
<div class="row">
|
||||
|
||||
<div class="col m-2">
|
||||
<div class="card h-100">
|
||||
<div class="card-body d-flex flex-column justify-content-between pb-3">
|
||||
<div class="row align-items-center g-5 mb-3 text-center text-sm-start">
|
||||
<div class="col-12 col-sm-auto mb-sm-2">
|
||||
@ -62,9 +67,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-5 col-xxl-12">
|
||||
<div class="card">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col m-2">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<h3 class="me-1">{% trans 'Default Address' %}</h3>
|
||||
@ -82,22 +89,23 @@
|
||||
<a class="text-body-secondary" href="#">{{ customer.phone_number }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col m-2">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
{% if perms.inventory.change_customer %}
|
||||
<div class="d-flex align-items-center justify-content-end">
|
||||
<a id="addBtn"
|
||||
href="#"
|
||||
class="btn btn-sm btn-phoenix-primary mb-3"
|
||||
data-url="{% url 'add_note_to_customer' request.dealer.slug customer.slug %}"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#noteModal"
|
||||
data-note-title="{{ _("Add") }}<i class='fa fa-plus-circle text-success ms-2'></i>">
|
||||
<span class="fas fa-plus me-1"></span>
|
||||
{% trans 'Add Note' %}
|
||||
</a>
|
||||
{% if perms.inventory.change_lead %}
|
||||
<button class="btn btn-phoenix-primary btn-sm"
|
||||
type="button"
|
||||
onclick=""
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#noteModal">
|
||||
<span class="fas fa-plus me-1"></span>{{ _("Add Note") }}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<table class="table fs-9 mb-0 table-responsive">
|
||||
@ -105,8 +113,8 @@
|
||||
<th class="align-middle pe-6 text-start" scope="col">{{ _("Note") }}</th>
|
||||
<th class="align-middle pe-6 text-start" scope="col">{{ _("Date") }}</th>
|
||||
</tr>
|
||||
<tbody>
|
||||
{% for note in customer_notes %}
|
||||
<tbody id="notesTable">
|
||||
{% for note in notes %}
|
||||
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
|
||||
<td class="align-middle text-start fw-bold text-body-tertiary ps-1">{{ note.note }}</td>
|
||||
<td class="align-middle text-body-tertiary text-start white-space-nowrap">{{ note.created }}</td>
|
||||
@ -116,14 +124,10 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-xxl-8">
|
||||
</div>
|
||||
<div class="col-12 mt-3">
|
||||
<div class="mb-6">
|
||||
<h3 class="mb-4">
|
||||
{{ _("Related") }} <span class="text-body-tertiary fw-normal">({{ total }})</span>
|
||||
</h3>
|
||||
|
||||
<div class="border-top border-bottom border-translucent"
|
||||
id="customerOrdersTable"
|
||||
data-list='{"valueNames":["order","total","payment_status","fulfilment_status","delivery_type","date"],"page":6,"pagination":true}'>
|
||||
@ -131,120 +135,78 @@
|
||||
<table class="table table-sm fs-9 mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sort white-space-nowrap align-middle ps-0 pe-3"
|
||||
scope="col"
|
||||
data-sort="order">{% trans 'Type'|upper %}</th>
|
||||
<th class="sort align-middle text-end pe-7" scope="col" data-sort="total">{% trans 'Total'|upper %}</th>
|
||||
<th class="sort align-middle white-space-nowrap pe-3"
|
||||
scope="col"
|
||||
data-sort="payment_status">{% trans 'Payment Status'|upper %}</th>
|
||||
<th class="sort align-middle text-end pe-0" scope="col" data-sort="date">{% trans 'Date'|upper %}</th>
|
||||
<th class="sort text-end align-middle pe-0 ps-5" scope="col"></th>
|
||||
<th class="sort white-space-nowrap align-middle" scope="col" data-sort="leads">{% trans 'Leads'|upper %}</th>
|
||||
<th class="sort align-middle " scope="col" data-sort="opportunities">{% trans 'Opportunities'|upper %}</th>
|
||||
<th class="sort align-middle " scope="col" data-sort="estimates">{% trans 'Estimates'|upper %}</th>
|
||||
<th class="sort align-middle " scope="col" data-sort="sale_orders">{% trans 'Sale orders'|upper %}</th>
|
||||
<th class="sort align-middle " scope="col" data-sort="invoices">{% trans 'Invoices'|upper %}</th>
|
||||
<th class="sort align-middle " scope="col" data-sort="car">{% trans 'Car'|upper %}</th>
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="list" id="customer-order-table-body">
|
||||
{% for estimate in estimates %}
|
||||
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
|
||||
<td class="order align-middle white-space-nowrap ps-0">
|
||||
<p class="fw-semibold mb-0">
|
||||
{{ _("Quotation") }}-<span class="fs-10 fw-medium">{{ estimate.estimate_number }}</span>
|
||||
</p>
|
||||
{% for lead in leads %}
|
||||
<tr>
|
||||
<td><a href="#">{{lead}} ({{ forloop.counter }})<a></td>
|
||||
<td>{{lead.opportunity}} ({{ forloop.counter }})</td>
|
||||
|
||||
<td>
|
||||
|
||||
{% for estimate in lead.customer.customer_model.estimatemodel_set.all %}
|
||||
<div class="me-2">{{estimate}}</div>
|
||||
<hr>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td class="total align-middle text-end fw-semibold pe-7 text-body-highlight">
|
||||
{{ estimate.revenue_estimate|currency_format }} <span class="icon-saudi_riyal"></span>
|
||||
|
||||
<td>
|
||||
{% for estimate in lead.customer.customer_model.estimatemodel_set.all %}
|
||||
<div>{{estimate.sale_orders.first}}</div>
|
||||
<hr>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td class="payment_status align-middle white-space-nowrap text-start fw-bold text-body-tertiary"></td>
|
||||
<td class="date align-middle white-space-nowrap text-body-tertiary fs-9 ps-4 text-end">{{ estimate.created }}</td>
|
||||
<td class="align-middle white-space-nowrap text-end pe-0 ps-5">
|
||||
<div class="btn-reveal-trigger position-static">
|
||||
<button class="btn btn-sm dropdown-toggle dropdown-caret-none transition-none btn-reveal fs-10"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown"
|
||||
data-boundary="window"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
data-bs-reference="parent">
|
||||
<span class="fas fa-ellipsis-h fs-10"></span>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-end py-2">
|
||||
<a class="dropdown-item" href="#!">View</a><a class="dropdown-item" href="#!">Export</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger" href="#!">Remove</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% for invoice in invoices %}
|
||||
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
|
||||
<td class="order align-middle white-space-nowrap ps-0">
|
||||
<p class="fw-semibold mb-0">
|
||||
{{ _("Invoice") }}-<span class="fs-10 fw-medium">{{ invoice.invoice_number }}</span>
|
||||
</p>
|
||||
</td>
|
||||
<td class="total align-middle text-end fw-semibold pe-7 text-body-highlight">
|
||||
{{ invoice.amount_paid|currency_format }} <span class="icon-saudi_riyal"></span>
|
||||
</td>
|
||||
<td class="payment_status align-middle white-space-nowrap text-start fw-bold text-body-tertiary">
|
||||
{% if invoice.is_paid %}
|
||||
<td>
|
||||
{% for invoice in lead.customer.customer_model.invoicemodel_set.all %}
|
||||
|
||||
{% if invoice.is_paid %}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-success">
|
||||
<span class="badge-label">{{ _("Paid") }}</span>
|
||||
<span class="ms-1" data-feather="check" style="height:12.8px;width:12.8px;"></span>
|
||||
<div>{{invoice}}</div>
|
||||
</span>
|
||||
{% endif %}
|
||||
{%else%}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-info">
|
||||
<div>{{invoice}}</div>
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td class="date align-middle white-space-nowrap text-body-tertiary fs-9 ps-4 text-end">{{ invoice.created }}</td>
|
||||
<td class="align-middle white-space-nowrap text-end pe-0 ps-5">
|
||||
<div class="btn-reveal-trigger position-static">
|
||||
<button class="btn btn-sm dropdown-toggle dropdown-caret-none transition-none btn-reveal fs-10"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown"
|
||||
data-boundary="window"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
data-bs-reference="parent">
|
||||
<span class="fas fa-ellipsis-h fs-10"></span>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-end py-2">
|
||||
<a class="dropdown-item" href="#!">View</a><a class="dropdown-item" href="#!">Export</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger" href="#!">Remove</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<td>
|
||||
{% for estimate in lead.customer.customer_model.invoicemodel_set.all %}
|
||||
<div>{{estimate.itemtransactionmodel_set.first.item_model.name}}</div>
|
||||
<hr>
|
||||
{% endfor %}
|
||||
<td>
|
||||
|
||||
<tr>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade"
|
||||
id="noteModal"
|
||||
tabindex="-1"
|
||||
aria-labelledby="noteModalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header justify-content-between align-items-start gap-5 px-4 pt-4 pb-3 border-0">
|
||||
<h4 class="modal-title" id="noteModalLabel">{% trans 'Notes' %}</h4>
|
||||
<button class="btn p-0 text-body-quaternary fs-6"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close">
|
||||
<span class="fas fa-times"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Content will be loaded here via AJAX -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{% include "components/note_modal.html" with content_type="customer" slug=customer.slug %}
|
||||
|
||||
|
||||
|
||||
<!---->
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const noteModal = document.getElementById("noteModal");
|
||||
@ -269,5 +231,6 @@
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
18
templates/emails/expiration_reminder_ar.html
Normal file
18
templates/emails/expiration_reminder_ar.html
Normal file
@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body style="font-family: 'Segoe UI', Tahoma, sans-serif; direction: rtl;">
|
||||
<p>مرحباً {{ user.get_full_name }}،</p>
|
||||
|
||||
<p>
|
||||
اشتراكك في <strong>{{ plan.name }}</strong> سينتهي خلال
|
||||
{{ days_until_expire }} يوم في {{ expiration_date|date:"j F Y" }}.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="{{ RENEWAL_URL }}">جدد اشتراكك الآن</a> لمواصلة الخدمة.
|
||||
</p>
|
||||
|
||||
<p>مع أطيب التحيات،<br>
|
||||
فريق {{ SITE_NAME }}</p>
|
||||
</body>
|
||||
</html>
|
||||
8
templates/emails/expiration_reminder_ar.txt
Normal file
8
templates/emails/expiration_reminder_ar.txt
Normal file
@ -0,0 +1,8 @@
|
||||
مرحباً {{ user.get_full_name }}،
|
||||
|
||||
اشتراكك في {{ plan.name }} سينتهي خلال {{ days_until_expire }} يوم في {{ expiration_date|date:"j F Y" }}.
|
||||
|
||||
جدد اشتراكك الآن: {{ RENEWAL_URL }}
|
||||
|
||||
مع أطيب التحيات،
|
||||
فريق {{ SITE_NAME }}
|
||||
14
templates/emails/expiration_reminder_en.html
Normal file
14
templates/emails/expiration_reminder_en.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; direction: {{ direction }};">
|
||||
<p>Hello {{ user.get_full_name }},</p>
|
||||
|
||||
<p>Your <strong>{{ plan.name }}</strong> subscription will expire
|
||||
in {{ days_until_expire }} days on {{ expiration_date|date:"F j, Y" }}.</p>
|
||||
|
||||
<p><a href="{{ RENEWAL_URL }}">Renew now</a> to continue service.</p>
|
||||
|
||||
<p>Best regards,<br>
|
||||
The {{ SITE_NAME }} Team</p>
|
||||
</body>
|
||||
</html>
|
||||
8
templates/emails/expiration_reminder_en.txt
Normal file
8
templates/emails/expiration_reminder_en.txt
Normal file
@ -0,0 +1,8 @@
|
||||
Hello {{ user.get_full_name }},
|
||||
|
||||
Your {{ plan.name }} subscription will expire in {{ days_until_expire }} days on {{ expiration_date|date:"F j, Y" }}.
|
||||
|
||||
Renew now: {{ RENEWAL_URL }}
|
||||
|
||||
Best regards,
|
||||
{{ SITE_NAME }} Team
|
||||
55
templates/empty-illustration-page.html
Normal file
55
templates/empty-illustration-page.html
Normal file
@ -0,0 +1,55 @@
|
||||
{% load static %}
|
||||
|
||||
|
||||
<style>
|
||||
|
||||
.empty-state-container {
|
||||
background-color: #ffffff;
|
||||
padding: 40px;
|
||||
border-radius: 15px; /* Rounded corners */
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); /* Subtle shadow */
|
||||
text-align: center;
|
||||
max-width: 80rem; /* Max width for content - made wider */
|
||||
width: 90%; /* Fluid width */
|
||||
margin: 10px auto; /* Added margin-top and auto for horizontal centering */
|
||||
max-height: auto; /* Added min-height to control the height */
|
||||
display: flex; /* Use flexbox for vertical centering of content */
|
||||
flex-direction: column; /* Stack children vertically */
|
||||
justify-content: center; /* Center content vertically */
|
||||
align-items: center; /* Center content horizontally */
|
||||
}
|
||||
.empty-state-image {
|
||||
max-width: 50%; /* Responsive image size */
|
||||
height: auto;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 10px; /* Rounded corners for image */
|
||||
}
|
||||
.empty-state-title {
|
||||
color: #343a40; /* Dark text for title */
|
||||
font-weight: 600;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.empty-state-text {
|
||||
color: #6c757d; /* Muted text for description */
|
||||
margin-bottom: 30px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
/* No specific styles for .btn-add-new or .message-box are needed here as per previous updates */
|
||||
</style>
|
||||
|
||||
<div class="empty-state-container">
|
||||
<!-- Empty State Illustration -->
|
||||
<img src="{% static 'images/logos/no-content-new.jpg' %}" alt="No-empty-state-image" class="empty-state-image">
|
||||
|
||||
<!-- Title -->
|
||||
<h3 class="empty-state-title">No {{ value|capfirst }} Yet</h3>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="empty-state-text">
|
||||
It looks like you haven't added any {{ value }} to your account.
|
||||
Click the button below to get started and add your first {{ value }}!
|
||||
</p>
|
||||
|
||||
<!-- Call to Action Button -->
|
||||
<a class="btn btn-lg btn-primary" href="{{ url }}">Create New {{ value|capfirst }} </a>
|
||||
</div>
|
||||
@ -6,6 +6,8 @@
|
||||
{% trans "Groups" %}
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
|
||||
{% if groups %}
|
||||
<section class="">
|
||||
<div class="row mt-4">
|
||||
<div class="col-auto">
|
||||
@ -57,4 +59,9 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% else %}
|
||||
{% url "group_create" request.dealer.slug as create_group_url %}
|
||||
{% include "empty-illustration-page.html" with value="group" url=create_group_url %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
<li class="collapsed-nav-item-title d-none">{% trans "Inventory"|capfirst %}</li>
|
||||
{% if perms.inventory.add_car %}
|
||||
<li class="nav-item">
|
||||
<a id="btn-add-car" class="nav-link btn-add-car" href="{% url 'car_add' request.dealer.slug %}">
|
||||
<a hx-boost="true" id="btn-add-car" class="nav-link btn-add-car" href="{% url 'car_add' request.dealer.slug %}">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="nav-link-icon"><span class="fas fa-plus-circle"></span></span><span class="nav-link-text">{% trans "add car"|capfirst %}</span>
|
||||
</div>
|
||||
|
||||
@ -611,12 +611,14 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block customJS %}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const csrftoken = getCookie("csrftoken");
|
||||
const ajaxUrl = "{% url 'ajax_handler' request.dealer.slug %}";
|
||||
|
||||
|
||||
const modalBody = customCardModal.querySelector(".modal-body");
|
||||
|
||||
const showSpecificationButton = document.getElementById("specification-btn");
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body bg-light-subtle">
|
||||
|
||||
|
||||
<form method="post" class="needs-validation" novalidate>
|
||||
<div class="row g-1">
|
||||
<div class="col-lg-4 col-xl-12">
|
||||
@ -35,16 +35,13 @@
|
||||
{{ form|crispy }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<hr class="my-2">
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
|
||||
<button class="btn btn-lg btn-phoenix-primary md-me-2" type="submit"><i class="fa-solid fa-floppy-disk me-1"></i>{{ _("Save") }}</button>
|
||||
<a href="{% url 'car_detail' request.dealer.slug car.slug %}" class="btn btn-lg btn-phoenix-secondary"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
{% load i18n static custom_filters %}
|
||||
{% block title %}
|
||||
{% trans 'Add New Car' %} {% endblock %}
|
||||
{% block content %}
|
||||
{% block content %}
|
||||
<style>
|
||||
#video {
|
||||
width: 100%;
|
||||
@ -339,336 +339,435 @@
|
||||
</div>
|
||||
</div>
|
||||
<!---->
|
||||
<script>
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== "") {
|
||||
const cookies = document.cookie.split(";");
|
||||
for (let cookie of cookies) {
|
||||
cookie = cookie.trim();
|
||||
if (cookie.substring(0, name.length + 1) === name + "=") {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
{% endblock content %}
|
||||
{% block customJS %}
|
||||
<script>
|
||||
// Global variables
|
||||
let codeReader;
|
||||
let currentStream = null;
|
||||
const csrfToken = getCookie("csrftoken");
|
||||
const ajaxUrl = "{% url 'ajax_handler' request.dealer.slug %}";
|
||||
|
||||
// Initialize when page loads and after HTMX swaps
|
||||
document.addEventListener('DOMContentLoaded', initPage);
|
||||
document.addEventListener('htmx:afterSwap', initPage);
|
||||
|
||||
function initPage() {
|
||||
// Get DOM elements
|
||||
const elements = {
|
||||
vinInput: document.getElementById("{{ form.vin.id_for_label }}"),
|
||||
decodeVinBtn: document.getElementById("decodeVinBtn"),
|
||||
makeSelect: document.getElementById("{{ form.id_car_make.id_for_label }}"),
|
||||
modelSelect: document.getElementById("{{ form.id_car_model.id_for_label }}"),
|
||||
yearSelect: document.getElementById("{{ form.year.id_for_label }}"),
|
||||
serieSelect: document.getElementById("{{ form.id_car_serie.id_for_label }}"),
|
||||
trimSelect: document.getElementById("{{ form.id_car_trim.id_for_label }}"),
|
||||
equipmentSelect: document.getElementById("equipment_id"),
|
||||
showSpecificationButton: document.getElementById("specification-btn"),
|
||||
showEquipmentButton: document.getElementById("options-btn"),
|
||||
specificationsContent: document.getElementById("specificationsContent"),
|
||||
optionsContent: document.getElementById("optionsContent"),
|
||||
generationContainer: document.getElementById("generation-div"),
|
||||
closeButton: document.querySelector(".btn-close"),
|
||||
scanVinBtn: document.getElementById("scan-vin-btn"),
|
||||
videoElement: document.getElementById("video"),
|
||||
resultDisplay: document.getElementById("result"),
|
||||
fallbackButton: document.getElementById("ocr-fallback-btn")
|
||||
};
|
||||
|
||||
// Initialize scanner if available
|
||||
if (typeof ZXing !== 'undefined' && !codeReader) {
|
||||
codeReader = new ZXing.BrowserMultiFormatReader();
|
||||
}
|
||||
|
||||
// Add event listeners
|
||||
setupEventListeners(elements);
|
||||
}
|
||||
|
||||
function setupEventListeners(elements) {
|
||||
// Remove existing listeners first to prevent duplicates
|
||||
removeEventListeners(elements);
|
||||
|
||||
// Add new listeners
|
||||
if (elements.decodeVinBtn) {
|
||||
elements.decodeVinBtn.addEventListener("click", decodeVin);
|
||||
}
|
||||
|
||||
if (elements.scanVinBtn) {
|
||||
elements.scanVinBtn.addEventListener("click", () => {
|
||||
elements.resultDisplay.textContent = "";
|
||||
startScanner(elements.videoElement);
|
||||
});
|
||||
}
|
||||
|
||||
if (elements.fallbackButton) {
|
||||
elements.fallbackButton.addEventListener("click", () => {
|
||||
captureAndOCR(elements.videoElement, elements.vinInput);
|
||||
});
|
||||
}
|
||||
|
||||
if (elements.serieSelect) {
|
||||
elements.serieSelect.addEventListener("change", () => {
|
||||
const serie_id = elements.serieSelect.value;
|
||||
const model_id = elements.modelSelect.value;
|
||||
if (serie_id && model_id) loadTrims(serie_id, model_id, elements);
|
||||
});
|
||||
}
|
||||
|
||||
if (elements.trimSelect) {
|
||||
elements.trimSelect.addEventListener("change", () => {
|
||||
const trimId = elements.trimSelect.value;
|
||||
elements.showSpecificationButton.disabled = !trimId;
|
||||
elements.showEquipmentButton.disabled = !trimId;
|
||||
if (trimId) loadSpecifications(trimId, elements);
|
||||
loadEquipment(trimId, elements);
|
||||
});
|
||||
}
|
||||
|
||||
if (elements.equipmentSelect) {
|
||||
elements.equipmentSelect.addEventListener("change", () => {
|
||||
const equipmentId = elements.equipmentSelect.value;
|
||||
if (equipmentId) loadOptions(equipmentId, elements);
|
||||
});
|
||||
}
|
||||
|
||||
if (elements.closeButton) {
|
||||
elements.closeButton.addEventListener("click", () => closeModal(elements.scanVinBtn));
|
||||
}
|
||||
|
||||
if (elements.makeSelect) {
|
||||
elements.makeSelect.addEventListener("change", (e) => {
|
||||
loadModels(e.target.value, elements.modelSelect.value, elements);
|
||||
});
|
||||
}
|
||||
|
||||
if (elements.modelSelect) {
|
||||
elements.modelSelect.addEventListener("change", (e) => {
|
||||
loadSeries(e.target.value, elements.yearSelect.value, elements);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function removeEventListeners(elements) {
|
||||
// Remove all event listeners to prevent duplicates
|
||||
const events = [
|
||||
{ element: elements.decodeVinBtn, event: "click", func: decodeVin },
|
||||
{ element: elements.scanVinBtn, event: "click" },
|
||||
{ element: elements.fallbackButton, event: "click" },
|
||||
{ element: elements.serieSelect, event: "change" },
|
||||
{ element: elements.trimSelect, event: "change" },
|
||||
{ element: elements.equipmentSelect, event: "change" },
|
||||
{ element: elements.closeButton, event: "click" },
|
||||
{ element: elements.makeSelect, event: "change" },
|
||||
{ element: elements.modelSelect, event: "change" }
|
||||
];
|
||||
|
||||
events.forEach(item => {
|
||||
if (item.element) {
|
||||
item.element.removeEventListener(item.event, item.func || null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const csrfToken = getCookie("csrftoken");
|
||||
|
||||
const vinInput = document.getElementById("{{ form.vin.id_for_label }}");
|
||||
const decodeVinBtn = document.getElementById("decodeVinBtn");
|
||||
const makeSelect = document.getElementById("{{ form.id_car_make.id_for_label }}");
|
||||
const modelSelect = document.getElementById("{{ form.id_car_model.id_for_label }}");
|
||||
const yearSelect = document.getElementById("{{ form.year.id_for_label }}");
|
||||
const serieSelect = document.getElementById("{{ form.id_car_serie.id_for_label }}");
|
||||
const trimSelect = document.getElementById("{{ form.id_car_trim.id_for_label }}");
|
||||
const equipmentSelect = document.getElementById("equipment_id")
|
||||
const showSpecificationButton = document.getElementById("specification-btn");
|
||||
const showEquipmentButton = document.getElementById("options-btn")
|
||||
const specificationsContent = document.getElementById("specificationsContent");
|
||||
const optionsContent = document.getElementById("optionsContent")
|
||||
const generationContainer = document.getElementById("generation-div")
|
||||
|
||||
const ajaxUrl = "{% url 'ajax_handler' request.dealer.slug %}";
|
||||
|
||||
const closeButton = document.querySelector(".btn-close");
|
||||
const scanVinBtn = document.getElementById("scan-vin-btn");
|
||||
const videoElement = document.getElementById("video");
|
||||
const resultDisplay = document.getElementById("result");
|
||||
const fallbackButton = document.getElementById("ocr-fallback-btn");
|
||||
let codeReader;
|
||||
codeReader = new ZXing.BrowserMultiFormatReader();
|
||||
let currentStream = null;
|
||||
|
||||
function closeModal() {
|
||||
stopScanner();
|
||||
try {
|
||||
const scannerModal = document.getElementById("scannerModal");
|
||||
if (scannerModal) {
|
||||
document.activeElement.blur();
|
||||
scannerModal.setAttribute("inert", "true");
|
||||
const modalInstance = bootstrap.Modal.getInstance(scannerModal);
|
||||
if (modalInstance) modalInstance.hide();
|
||||
if (scanVinBtn) scanVinBtn.focus();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error closing scanner modal:", err);
|
||||
}
|
||||
// Cookie helper function
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== "") {
|
||||
const cookies = document.cookie.split(";");
|
||||
for (let cookie of cookies) {
|
||||
cookie = cookie.trim();
|
||||
if (cookie.substring(0, name.length + 1) === name + "=") {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
async function decodeVin() {
|
||||
const vinNumber = vinInput.value.trim();
|
||||
if (vinNumber.length !== 17) {
|
||||
Swal.fire("error", "{% trans 'Please enter a valid VIN.' %}");
|
||||
/*alert("{% trans 'Please enter a valid VIN.' %}");*/
|
||||
return;
|
||||
}
|
||||
showLoading();
|
||||
try {
|
||||
const response = await fetch(`${ajaxUrl}?action=decode_vin&vin_no=${vinNumber}`, {
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"X-CSRFToken": csrfToken,
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
hideLoading();
|
||||
await updateFields(data.data);
|
||||
} else {
|
||||
hideLoading();
|
||||
Swal.fire("{% trans 'error' %}", data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error decoding VIN:", error);
|
||||
hideLoading();
|
||||
Swal.fire("error", "{% trans 'An error occurred while decoding the VIN.' %}");
|
||||
}
|
||||
// VIN Decoding functions
|
||||
async function decodeVin() {
|
||||
const vinInput = document.getElementById("{{ form.vin.id_for_label }}");
|
||||
const vinNumber = vinInput.value.trim();
|
||||
|
||||
if (vinNumber.length !== 17) {
|
||||
Swal.fire("error", "{% trans 'Please enter a valid VIN.' %}");
|
||||
return;
|
||||
}
|
||||
|
||||
showLoading();
|
||||
try {
|
||||
const response = await fetch(`${ajaxUrl}?action=decode_vin&vin_no=${vinNumber}`, {
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"X-CSRFToken": csrfToken,
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
hideLoading();
|
||||
await updateFields(data.data);
|
||||
} else {
|
||||
hideLoading();
|
||||
Swal.fire("{% trans 'error' %}", data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error decoding VIN:", error);
|
||||
hideLoading();
|
||||
Swal.fire("error", "{% trans 'An error occurred while decoding the VIN.' %}");
|
||||
}
|
||||
}
|
||||
|
||||
async function updateFields(vinData) {
|
||||
const elements = {
|
||||
makeSelect: document.getElementById("{{ form.id_car_make.id_for_label }}"),
|
||||
modelSelect: document.getElementById("{{ form.id_car_model.id_for_label }}"),
|
||||
yearSelect: document.getElementById("{{ form.year.id_for_label }}"),
|
||||
generationContainer: document.getElementById("generation-div")
|
||||
};
|
||||
|
||||
console.log(vinData);
|
||||
if (vinData.make_id && elements.makeSelect) {
|
||||
elements.makeSelect.value = vinData.make_id;
|
||||
document.getElementById("make-check").innerHTML = "✓";
|
||||
await loadModels(vinData.make_id);
|
||||
}
|
||||
if (vinData.model_id && elements.modelSelect) {
|
||||
elements.modelSelect.value = vinData.model_id;
|
||||
document.getElementById("model-check").innerHTML = "✓";
|
||||
await loadSeries(vinData.model_id, vinData.year);
|
||||
}
|
||||
if (vinData.year && elements.yearSelect) {
|
||||
elements.yearSelect.value = vinData.year;
|
||||
document.getElementById("year-check").innerHTML = "✓";
|
||||
}
|
||||
}
|
||||
|
||||
// Scanner functions
|
||||
async function startScanner(videoElement) {
|
||||
if (!codeReader) return;
|
||||
|
||||
codeReader
|
||||
.decodeFromVideoDevice(null, videoElement, async (result, err) => {
|
||||
if (result) {
|
||||
document.getElementById("{{ form.vin.id_for_label }}").value = result.text;
|
||||
closeModal();
|
||||
await decodeVin();
|
||||
}
|
||||
|
||||
async function updateFields(vinData) {
|
||||
console.log(vinData);
|
||||
if (vinData.make_id) {
|
||||
makeSelect.value = vinData.make_id;
|
||||
document.getElementById("make-check").innerHTML = "✓";
|
||||
await loadModels(vinData.make_id);
|
||||
}
|
||||
if (vinData.model_id) {
|
||||
modelSelect.value = vinData.model_id;
|
||||
document.getElementById("model-check").innerHTML = "✓";
|
||||
|
||||
await loadSeries(vinData.model_id, vinData.year);
|
||||
}
|
||||
if (vinData.year) {
|
||||
yearSelect.value = vinData.year;
|
||||
document.getElementById("year-check").innerHTML = "✓";
|
||||
}
|
||||
}
|
||||
|
||||
// Start the scanner
|
||||
async function startScanner() {
|
||||
codeReader
|
||||
.decodeFromVideoDevice(null, videoElement, async (result, err) => {
|
||||
let res = await result;
|
||||
if (result) {
|
||||
vinInput.value = result.text;
|
||||
closeModal();
|
||||
await decodeVin();
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
function captureAndOCR() {
|
||||
const canvas = document.createElement("canvas");
|
||||
const context = canvas.getContext("2d");
|
||||
canvas.width = videoElement.videoWidth;
|
||||
canvas.height = videoElement.videoHeight;
|
||||
context.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
|
||||
Tesseract.recognize(canvas.toDataURL("image/png"), "eng")
|
||||
.then(({ data: { text } }) => {
|
||||
const vin = text.match(/[A-HJ-NPR-Z0-9]{17}/);
|
||||
if (vin) vinInput.value = vin[0];
|
||||
closeModal();
|
||||
decodeVin();
|
||||
})
|
||||
.catch((err) => console.error("OCR Error:", err));
|
||||
}
|
||||
|
||||
function stopScanner() {
|
||||
if (currentStream) {
|
||||
currentStream.getTracks().forEach((track) => track.stop());
|
||||
currentStream = null;
|
||||
}
|
||||
codeReader.reset();
|
||||
}
|
||||
|
||||
function resetDropdown(dropdown, placeholder) {
|
||||
dropdown.innerHTML = `<option value="">${placeholder}</option>`;
|
||||
}
|
||||
|
||||
async function loadModels(makeId) {
|
||||
resetDropdown(modelSelect, '{% trans "Select" %}');
|
||||
const response = await fetch(`${ajaxUrl}?action=get_models&make_id=${makeId}`, {
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"X-CSRFToken": csrfToken,
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
data.forEach((model) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = model.id_car_model;
|
||||
option.textContent = document.documentElement.lang === "en" ? model.name : model.arabic_name;
|
||||
modelSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadSeries(modelId, year) {
|
||||
resetDropdown(serieSelect, '{% trans "Select" %}');
|
||||
resetDropdown(trimSelect, '{% trans "Select" %}');
|
||||
specificationsContent.innerHTML = "";
|
||||
|
||||
const response = await fetch(`${ajaxUrl}?action=get_series&model_id=${modelId}&year=${year}`, {
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"X-CSRFToken": csrfToken,
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
console.log(data)
|
||||
data.forEach((serie) => {
|
||||
|
||||
const option = document.createElement("option");
|
||||
option.value = serie.id_car_serie;
|
||||
option.textContent = document.documentElement.lang === "en" ? serie.name : serie.name;
|
||||
generationContainer.innerHTML = serie.generation_name
|
||||
serieSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadTrims(serie_id, model_id) {
|
||||
resetDropdown(trimSelect, '{% trans "Select" %}');
|
||||
specificationsContent.innerHTML = "";
|
||||
const response = await fetch(`${ajaxUrl}?action=get_trims&serie_id=${serie_id}&model_id=${model_id}`, {
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"X-CSRFToken": csrfToken,
|
||||
}
|
||||
});
|
||||
const data = await response.json();
|
||||
data.forEach((trim) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = trim.id_car_trim;
|
||||
option.textContent = document.documentElement.lang === "en" ? trim.name : trim.name;
|
||||
trimSelect.appendChild(option);
|
||||
});
|
||||
showSpecificationButton.disabled = !this.value;
|
||||
}
|
||||
|
||||
async function loadEquipment(trimId){
|
||||
resetDropdown(equipmentSelect, '{% trans "Select" %}');
|
||||
optionsContent.innerHTML = "";
|
||||
const response = await fetch(`${ajaxUrl}?action=get_equipments&trim_id=${trimId}`, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRFToken': csrfToken
|
||||
}
|
||||
});
|
||||
const data = await response.json();
|
||||
data.forEach((equipment) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = equipment.id_car_equipment;
|
||||
option.textContent = equipment.name;
|
||||
equipmentSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadSpecifications(trimId) {
|
||||
specificationsContent.innerHTML = "";
|
||||
const response = await fetch(`${ajaxUrl}?action=get_specifications&trim_id=${trimId}`, {
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"X-CSRFToken": csrfToken,
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
data.forEach((spec) => {
|
||||
const parentDiv = document.createElement("div");
|
||||
parentDiv.innerHTML = `<strong>${spec.parent_name}</strong>`;
|
||||
spec.specifications.forEach((s) => {
|
||||
const specDiv = document.createElement("div");
|
||||
specDiv.innerHTML = `• ${s.s_name}: ${s.s_value} ${s.s_unit}`;
|
||||
parentDiv.appendChild(specDiv);
|
||||
});
|
||||
specificationsContent.appendChild(parentDiv);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadOptions(equipmentId) {
|
||||
optionsContent.innerHTML = "";
|
||||
const response = await fetch(`${ajaxUrl}?action=get_options&equipment_id=${equipmentId}`, {
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"X-CSRFToken": csrfToken,
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
data.forEach((parent) => {
|
||||
const parentDiv = document.createElement("div");
|
||||
parentDiv.innerHTML = `<strong>${parent.parent_name}</strong>`;
|
||||
parent.options.forEach((option) => {
|
||||
const optDiv = document.createElement("div");
|
||||
optDiv.innerHTML = `• ${option.option_name}`;
|
||||
parentDiv.appendChild(optDiv);
|
||||
});
|
||||
optionsContent.appendChild(parentDiv);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
scanVinBtn.addEventListener("click", () => {
|
||||
resultDisplay.textContent = "";
|
||||
startScanner();
|
||||
});
|
||||
|
||||
fallbackButton.addEventListener("click", () => {
|
||||
captureAndOCR();
|
||||
})
|
||||
|
||||
serieSelect.addEventListener("change", () => {
|
||||
const serie_id = serieSelect.value;
|
||||
const model_id = modelSelect.value;
|
||||
if (serie_id && model_id) loadTrims(serie_id, model_id);
|
||||
})
|
||||
|
||||
trimSelect.addEventListener("change", () => {
|
||||
const trimId = trimSelect.value
|
||||
showSpecificationButton.disabled = !trimId
|
||||
showEquipmentButton.disabled = !trimId
|
||||
if (trimId) loadSpecifications(trimId)
|
||||
loadEquipment(trimId)
|
||||
})
|
||||
|
||||
equipmentSelect.addEventListener("change", () => {
|
||||
const equipmentId = equipmentSelect.value
|
||||
if (equipmentId) loadOptions(equipmentId)
|
||||
})
|
||||
|
||||
closeButton.addEventListener("click", closeModal);
|
||||
makeSelect.addEventListener("change", (e) => {
|
||||
loadModels(e.target.value, modelSelect.value);
|
||||
})
|
||||
modelSelect.addEventListener("change", (e) => {
|
||||
loadSeries(e.target.value, yearSelect.value);
|
||||
})
|
||||
decodeVinBtn.addEventListener("click", decodeVin);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
function captureAndOCR(videoElement, vinInput) {
|
||||
const canvas = document.createElement("canvas");
|
||||
const context = canvas.getContext("2d");
|
||||
canvas.width = videoElement.videoWidth;
|
||||
canvas.height = videoElement.videoHeight;
|
||||
context.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
function showLoading() {
|
||||
Swal.fire({
|
||||
title: "{% trans 'Please Wait' %}",
|
||||
text: "{% trans 'Loading' %}...",
|
||||
allowOutsideClick: false,
|
||||
didOpen: () => {
|
||||
Swal.showLoading();
|
||||
},
|
||||
});
|
||||
if (typeof Tesseract !== 'undefined') {
|
||||
Tesseract.recognize(canvas.toDataURL("image/png"), "eng")
|
||||
.then(({ data: { text } }) => {
|
||||
const vin = text.match(/[A-HJ-NPR-Z0-9]{17}/);
|
||||
if (vin) vinInput.value = vin[0];
|
||||
closeModal();
|
||||
decodeVin();
|
||||
})
|
||||
.catch((err) => console.error("OCR Error:", err));
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal(scanVinBtn) {
|
||||
stopScanner();
|
||||
try {
|
||||
const scannerModal = document.getElementById("scannerModal");
|
||||
if (scannerModal) {
|
||||
document.activeElement.blur();
|
||||
scannerModal.setAttribute("inert", "true");
|
||||
const modalInstance = bootstrap.Modal.getInstance(scannerModal);
|
||||
if (modalInstance) modalInstance.hide();
|
||||
if (scanVinBtn) scanVinBtn.focus();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error closing scanner modal:", err);
|
||||
}
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
Swal.close();
|
||||
}
|
||||
function stopScanner() {
|
||||
if (currentStream) {
|
||||
currentStream.getTracks().forEach((track) => track.stop());
|
||||
currentStream = null;
|
||||
}
|
||||
if (codeReader) codeReader.reset();
|
||||
}
|
||||
|
||||
function notify(tag, msg) {
|
||||
Swal.fire({
|
||||
icon: tag,
|
||||
titleText: msg,
|
||||
});
|
||||
// Data loading functions
|
||||
function resetDropdown(dropdown, placeholder) {
|
||||
if (dropdown) dropdown.innerHTML = `<option value="">${placeholder}</option>`;
|
||||
}
|
||||
|
||||
async function loadModels(makeId, modelSelect, elements) {
|
||||
if (!modelSelect) return;
|
||||
|
||||
resetDropdown(modelSelect, '{% trans "Select" %}');
|
||||
const response = await fetch(`${ajaxUrl}?action=get_models&make_id=${makeId}`, {
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"X-CSRFToken": csrfToken,
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
data.forEach((model) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = model.id_car_model;
|
||||
option.textContent = document.documentElement.lang === "en" ? model.name : model.arabic_name;
|
||||
modelSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadSeries(modelId, year, elements) {
|
||||
if (!elements?.serieSelect) return;
|
||||
|
||||
resetDropdown(elements.serieSelect, '{% trans "Select" %}');
|
||||
resetDropdown(elements.trimSelect, '{% trans "Select" %}');
|
||||
if (elements.specificationsContent) elements.specificationsContent.innerHTML = "";
|
||||
|
||||
const response = await fetch(`${ajaxUrl}?action=get_series&model_id=${modelId}&year=${year}`, {
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"X-CSRFToken": csrfToken,
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
|
||||
data.forEach((serie) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = serie.id_car_serie;
|
||||
option.textContent = document.documentElement.lang === "en" ? serie.name : serie.name;
|
||||
if (elements.generationContainer) elements.generationContainer.innerHTML = serie.generation_name;
|
||||
elements.serieSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadTrims(serie_id, model_id, elements) {
|
||||
if (!elements?.trimSelect) return;
|
||||
|
||||
resetDropdown(elements.trimSelect, '{% trans "Select" %}');
|
||||
if (elements.specificationsContent) elements.specificationsContent.innerHTML = "";
|
||||
|
||||
const response = await fetch(`${ajaxUrl}?action=get_trims&serie_id=${serie_id}&model_id=${model_id}`, {
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"X-CSRFToken": csrfToken,
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
data.forEach((trim) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = trim.id_car_trim;
|
||||
option.textContent = document.documentElement.lang === "en" ? trim.name : trim.name;
|
||||
elements.trimSelect.appendChild(option);
|
||||
});
|
||||
|
||||
if (elements.showSpecificationButton) {
|
||||
elements.showSpecificationButton.disabled = !elements.trimSelect.value;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadEquipment(trimId, elements) {
|
||||
if (!elements?.equipmentSelect) return;
|
||||
|
||||
resetDropdown(elements.equipmentSelect, '{% trans "Select" %}');
|
||||
if (elements.optionsContent) elements.optionsContent.innerHTML = "";
|
||||
|
||||
const response = await fetch(`${ajaxUrl}?action=get_equipments&trim_id=${trimId}`, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRFToken': csrfToken
|
||||
}
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
data.forEach((equipment) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = equipment.id_car_equipment;
|
||||
option.textContent = equipment.name;
|
||||
elements.equipmentSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadSpecifications(trimId, elements) {
|
||||
if (!elements?.specificationsContent) return;
|
||||
|
||||
elements.specificationsContent.innerHTML = "";
|
||||
const response = await fetch(`${ajaxUrl}?action=get_specifications&trim_id=${trimId}`, {
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"X-CSRFToken": csrfToken,
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
data.forEach((spec) => {
|
||||
const parentDiv = document.createElement("div");
|
||||
parentDiv.innerHTML = `<strong>${spec.parent_name}</strong>`;
|
||||
spec.specifications.forEach((s) => {
|
||||
const specDiv = document.createElement("div");
|
||||
specDiv.innerHTML = `• ${s.s_name}: ${s.s_value} ${s.s_unit}`;
|
||||
parentDiv.appendChild(specDiv);
|
||||
});
|
||||
elements.specificationsContent.appendChild(parentDiv);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadOptions(equipmentId, elements) {
|
||||
if (!elements?.optionsContent) return;
|
||||
|
||||
elements.optionsContent.innerHTML = "";
|
||||
const response = await fetch(`${ajaxUrl}?action=get_options&equipment_id=${equipmentId}`, {
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"X-CSRFToken": csrfToken,
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
data.forEach((parent) => {
|
||||
const parentDiv = document.createElement("div");
|
||||
parentDiv.innerHTML = `<strong>${parent.parent_name}</strong>`;
|
||||
parent.options.forEach((option) => {
|
||||
const optDiv = document.createElement("div");
|
||||
optDiv.innerHTML = `• ${option.option_name}`;
|
||||
parentDiv.appendChild(optDiv);
|
||||
});
|
||||
elements.optionsContent.appendChild(parentDiv);
|
||||
});
|
||||
}
|
||||
|
||||
// UI Helper functions
|
||||
function showLoading() {
|
||||
Swal.fire({
|
||||
title: "{% trans 'Please Wait' %}",
|
||||
text: "{% trans 'Loading' %}...",
|
||||
allowOutsideClick: false,
|
||||
didOpen: () => {
|
||||
Swal.showLoading();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
Swal.close();
|
||||
}
|
||||
|
||||
function notify(tag, msg) {
|
||||
Swal.fire({
|
||||
icon: tag,
|
||||
titleText: msg,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock customJS %}
|
||||
@ -24,6 +24,8 @@
|
||||
</style>
|
||||
{% endblock customCSS %}
|
||||
{% block content %}
|
||||
|
||||
{% if cars%}
|
||||
<div class="container-fluid" id="projectSummary">
|
||||
<div class="row g-3 justify-content-between align-items-end mb-4">
|
||||
<div class="col-12 col-sm-auto">
|
||||
@ -312,6 +314,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
{% url "car_add" request.dealer.slug as create_car_url %}
|
||||
{% include "empty-illustration-page.html" with value="car" url=create_car_url %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% endblock %}
|
||||
{% block customJS %}
|
||||
<script>
|
||||
|
||||
@ -4,6 +4,8 @@
|
||||
{% trans "Inventory Stats"|capfirst %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
{% if inventory %}
|
||||
<div class="row justify-content-between">
|
||||
<div class="col-sm-12 ">
|
||||
<div class="card border h-100 w-100 p-lg-10">
|
||||
@ -92,4 +94,9 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
{% url "car_add" request.dealer.slug as create_car_url %}
|
||||
{% include "empty-illustration-page.html" with value="car" url=create_car_url %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
{{ _("Add New Expense") }}
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
|
||||
|
||||
<!---->
|
||||
<div class="row justify-content-center mt-5 mb-3">
|
||||
|
||||
@ -19,7 +19,6 @@
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body bg-light-subtle">
|
||||
|
||||
<form method="post" action="">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
@ -29,11 +28,7 @@
|
||||
<button class="btn btn-lg btn-phoenix-primary md-me-2" type="submit"><i class="fa-solid fa-floppy-disk me-1"></i>{{ _("Save") }}</button>
|
||||
<a href="{% url 'item_expense_list' request.dealer.slug %}" class="btn btn-lg btn-phoenix-secondary"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -4,6 +4,9 @@
|
||||
{{ _("Expenses") }}
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
|
||||
|
||||
{% if expenses %}
|
||||
<div class="row mt-4">
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<h3 class="">{% trans "Expenses" %} <span class="fas fa-money-bill-wave ms-2 text-primary"></span></h3>
|
||||
@ -54,4 +57,9 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
{% url "item_expense_create" request.dealer.slug as create_expense_url %}
|
||||
{% include "empty-illustration-page.html" with value="expense" url=create_expense_url %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@ -4,6 +4,8 @@
|
||||
{{ _("Services") }}
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
|
||||
{% if services %}
|
||||
<div class="row mt-4">
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<h3 class="">{% trans "Services" %}<span class="fas fa-tools text-primary ms-2"></span></h3>
|
||||
@ -56,4 +58,10 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
{% url 'item_service_create' request.dealer.slug as create_services_url %}
|
||||
{% include "empty-illustration-page.html" with value="service" url=create_services_url %}
|
||||
{%endif%}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
{{ _("Bank Accounts") }}
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
{% if bank_accounts %}
|
||||
<div class="row mt-4">
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<h3 class="">{% trans "Bank Accounts" %}<span class="fas fa-bank ms-2 text-primary"></span></h3>
|
||||
@ -53,4 +54,9 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
{% url 'bank_account_create' request.dealer.slug as create_bank_account_url %}
|
||||
{% include "empty-illustration-page.html" with value="bank account" url=create_bank_account_url%}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
{{ _("Create Bill") }}
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
<div class="row mt-4">
|
||||
<div class="row mt-4" hx-boost="true">
|
||||
<h3 class="text-center">{% trans "Create Bill" %}<span class="fas fa-money-bills ms-2 text-primary"></span></h3>
|
||||
<form id="mainForm" method="post" class="needs-validation">
|
||||
{% csrf_token %}
|
||||
@ -15,7 +15,7 @@
|
||||
<button class="btn btn-sm btn-phoenix-success me-2" type="submit">
|
||||
<i class="fa-solid fa-floppy-disk me-1"></i>{{ _("Save") }}
|
||||
</button>
|
||||
<a href="{{ request.META.HTTP_REFERER }}"
|
||||
<a href="{% url 'bill_list' request.dealer.slug %}"
|
||||
class="btn btn-sm btn-phoenix-danger"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -10,6 +10,8 @@
|
||||
</a>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
{% if bills %}
|
||||
<div class="row mt-4">
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<h3 class="">{% trans "Bills" %}<span class="fas fa-money-bills ms-2 text-primary"></span></h3>
|
||||
@ -85,4 +87,10 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% else %}
|
||||
{% url "bill-create" request.dealer.slug as create_bill_url %}
|
||||
{% include "empty-illustration-page.html" with value="bill" url=create_bill_url %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@ -10,6 +10,9 @@
|
||||
</a>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
|
||||
{% if accounts%}
|
||||
<div class="row mt-4">
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<h3 class=""> {% trans "Accounts" %}<i class="fa-solid fa-book ms-2 text-primary"></i></h3>
|
||||
@ -196,6 +199,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
|
||||
{% endif %}
|
||||
{% url "account_create" request.dealer.slug as create_account_url %}
|
||||
{% include "empty-illustration-page.html" with value="account" url=create_account_url %}
|
||||
{% endblock %}
|
||||
{% block customerJS %}
|
||||
<script>
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<form action="{% url 'journalentry_delete' journal_entry.pk %}"
|
||||
<form action="{% url 'journalentry_delete' request.dealer.slug journal_entry.pk %}"
|
||||
method="post">
|
||||
{% csrf_token %}
|
||||
<div class="card">
|
||||
@ -13,7 +13,7 @@
|
||||
<h5 class="card-title fw-light">Are you sure you want to delete?</h5>
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<a href="{% url 'journalentry_list' journal_entry.ledger.pk %}"
|
||||
<a href="{% url 'journalentry_list' request.dealer.slug journal_entry.ledger.pk %}"
|
||||
class="btn btn-phoenix-primary me-2">{% trans 'Go Back' %}</a>
|
||||
<button type="submit" class="btn btn-phoenix-danger">{% trans 'Delete' %}</button>
|
||||
</div>
|
||||
|
||||
@ -4,6 +4,8 @@
|
||||
{{ _("Journal Entries") }}
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
|
||||
{% if journal_entries %}
|
||||
<div class="modal fade"
|
||||
id="confirmModal"
|
||||
tabindex="-1"
|
||||
@ -130,5 +132,10 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!--test-->
|
||||
|
||||
{% else %}
|
||||
{% url 'journalentry_create' request.dealer.slug ledger.pk as create_je_url %}
|
||||
{% include "empty-illustration-page.html" with value="journal entry" url=create_je_url %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
{% for transaction in transactions %}
|
||||
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
|
||||
<td>{{ forloop.counter }}</td>
|
||||
<td class="align-middle product white-space-nowrap py-0">{{ transaction.created|date }}</td>
|
||||
<td class="align-middle product white-space-nowrap py-0">{{ transaction.created|date }}</td>
|
||||
<td class="align-middle product white-space-nowrap">{{ transaction.account.name }}</td>
|
||||
<td class="align-middle product white-space-nowrap">{{ transaction.account.code }}</td>
|
||||
<td class="align-middle product white-space-nowrap text-success">
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<form action="{% url 'ledger-delete' entity_slug=view.kwargs.entity_slug ledger_pk=ledger_model.uuid %}"
|
||||
<form action="{% url 'ledger-delete' dealer_slug=request.dealer.slug entity_slug=request.dealer.entity.slug ledger_pk=ledger_model.uuid %}"
|
||||
method="post">
|
||||
{% csrf_token %}
|
||||
<div class="card">
|
||||
@ -13,7 +13,7 @@
|
||||
<h5 class="card-title fw-light">{{ ledger_model.get_delete_message }}</h5>
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<a href="{% url 'ledger_list' %}" class="btn btn-phoenix-primary me-2">{% trans 'Go Back' %}</a>
|
||||
<a href="{% url 'ledger_list' request.dealer.slug request.dealer.entity.slug %}" class="btn btn-phoenix-primary me-2">{% trans 'Go Back' %}</a>
|
||||
<button type="submit" class="btn btn-phoenix-danger">{% trans 'Delete' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -4,6 +4,8 @@
|
||||
{{ _("Ledger") }}
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
|
||||
{% if ledgers %}
|
||||
<div class="row mt-4">
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<h3 class="">{% trans "Ledger" %} <span class="fas fa-book-open ms-2 text-primary"></span></h3>
|
||||
@ -127,4 +129,9 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
{% url 'ledger_create' request.dealer.slug as create_ledger_url %}
|
||||
{% include "empty-illustration-page.html" with value="ledger" url=create_ledger_url %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@ -22,8 +22,10 @@
|
||||
<div class="modal-body p-4">
|
||||
<p id="deleteModalText"></p>
|
||||
</div>
|
||||
<div class="modal-footer flex justify-content-center border-top-0">
|
||||
<div class="modal-footer flex justify-content-center border-top-0" >
|
||||
<a id="deleteModalConfirm"
|
||||
hx-select-oob="#notesTable:outerHTML,#toast-container:outerHTML"
|
||||
hx-swap="none"
|
||||
type="button"
|
||||
class="btn btn-sm btn-phoenix-danger w-100"
|
||||
href="">{{ _("Delete") }}</a>
|
||||
@ -31,24 +33,47 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const deleteModal = document.getElementById("deleteModal");
|
||||
const confirmDeleteBtn = document.getElementById("deleteModalConfirm");
|
||||
const deleteModalMessage = document.getElementById("deleteModalText");
|
||||
|
||||
document.querySelectorAll(".delete-btn").forEach(button => {
|
||||
button.addEventListener("click", function () {
|
||||
let deleteUrl = this.getAttribute("data-url");
|
||||
let deleteMessage = this.getAttribute("data-message");
|
||||
<script>
|
||||
// Initialize when page loads and after HTMX swaps
|
||||
document.addEventListener('DOMContentLoaded', initDeleteModals);
|
||||
document.addEventListener('htmx:afterSwap', initDeleteModals);
|
||||
|
||||
confirmDeleteBtn.setAttribute("href", deleteUrl);
|
||||
confirmDeleteBtn.setAttribute("hx-boost", "true");
|
||||
confirmDeleteBtn.setAttribute("hx-select-oob", "#notesTable:outerHTML,#toast-container:outerHTML");
|
||||
confirmDeleteBtn.setAttribute("hx-swap", "none");
|
||||
confirmDeleteBtn.setAttribute("hx-on::after-request", "$('#deleteModal').modal('hide');");
|
||||
deleteModalMessage.innerHTML = deleteMessage;
|
||||
});
|
||||
});
|
||||
function initDeleteModals() {
|
||||
const deleteModal = document.getElementById("deleteModal");
|
||||
const confirmDeleteBtn = document.getElementById("deleteModalConfirm");
|
||||
const deleteModalMessage = document.getElementById("deleteModalText");
|
||||
|
||||
// Clean up previous listeners if any
|
||||
document.querySelectorAll(".delete-btn").forEach(btn => {
|
||||
btn.removeEventListener("click", handleDeleteClick);
|
||||
});
|
||||
|
||||
// Add new listeners to all delete buttons
|
||||
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 you want to delete this item?";
|
||||
|
||||
// Update modal content
|
||||
confirmDeleteBtn.setAttribute("href", deleteUrl);
|
||||
deleteModalMessage.textContent = deleteMessage; // Use textContent instead of innerHTML for security
|
||||
|
||||
// Process with HTMX if available
|
||||
if (typeof htmx !== 'undefined') {
|
||||
htmx.process(confirmDeleteBtn);
|
||||
}
|
||||
|
||||
// Show the modal
|
||||
/*if (typeof bootstrap !== 'undefined') {
|
||||
const modal = new bootstrap.Modal(deleteModal);
|
||||
modal.show();
|
||||
}*/
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
<div class="row my-4">
|
||||
<h2>{{ organization.get_local_name }}</h2>
|
||||
<h2 class="mb-2">{{ organization.get_local_name }}</h2>
|
||||
<ul class="list-group mb-4">
|
||||
<li class="list-group-item">
|
||||
<strong>{% trans "CRN" %}:</strong> {{ organization.crn }}
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
<a class="nav-link active">{% trans 'Organizations' %}</a>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
{% if organizations%}
|
||||
<section class="pt-5 pb-9 ">
|
||||
<div class="row overflow-x-auto whitespace-nowrap -mx-2 sm:mx-0">
|
||||
<h2 class="mb-4">
|
||||
@ -182,4 +183,10 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% else %}
|
||||
{% url 'organization_create' request.dealer.slug as create_organization_url %}
|
||||
{% include "empty-illustration-page.html" with value="organization" url=create_organization_url %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@ -1,35 +1,16 @@
|
||||
<div class="search-box me-2">
|
||||
<form class="position-relative show" id="search-form">
|
||||
<input name="q"
|
||||
hx-get=""
|
||||
hx-boost="true"
|
||||
hx-trigger="keyup delay:500ms"
|
||||
id="search-input"
|
||||
class="form-control form-control-sm search-input search"
|
||||
type="search"
|
||||
aria-label="Search"
|
||||
placeholder="{{ _("Search") }}..."
|
||||
placeholder="{{ _('Search') }}..."
|
||||
value="{{ request.GET.q }}" />
|
||||
<span class="fa fa-magnifying-glass search-box-icon"></span>
|
||||
{% if request.GET.q %}
|
||||
<button type="button"
|
||||
class="btn-close position-absolute end-0 top-50 translate-middle cursor-pointer shadow-none"
|
||||
id="clear-search"
|
||||
aria-label="Close"></button>
|
||||
{% endif %}
|
||||
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const searchInput = document.getElementById("search-input");
|
||||
const clearButton = document.getElementById("clear-search");
|
||||
|
||||
if (clearButton) {
|
||||
clearButton.addEventListener("click", function(event) {
|
||||
event.preventDefault();
|
||||
searchInput.value = ""; // Clear input field
|
||||
// Remove query parameter without reloading the page
|
||||
const newUrl = window.location.pathname;
|
||||
history.replaceState(null, "", newUrl);
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -3,6 +3,9 @@
|
||||
{% load i18n static %}
|
||||
{% block title %}Purchase Orders - {{ block.super }}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
|
||||
{% if purchase_orders %}
|
||||
<div class="row mt-4">
|
||||
<!-- Success Message -->
|
||||
{% if messages %}
|
||||
@ -118,4 +121,9 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'modal/delete_modal.html' %}
|
||||
|
||||
{% else %}
|
||||
{% url "purchase_order_create" request.dealer.slug request.dealer.entity.slug as create_purchase_url %}
|
||||
{% include "empty-illustration-page.html" with value="purchase order" url=create_purchase_url %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@ -250,12 +250,7 @@
|
||||
<td class="align-middle text-body-tertiary fw-semibold">{{ item.total }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr class="bg-body-secondary total-sum">
|
||||
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="7">{% trans "Vat" %} ({{ data.vat }})</td>
|
||||
<td class="align-middle text-start fw-semibold">
|
||||
<span id="grand-total">+ {{ data.total_vat_amount|floatformat }}<span class="icon-saudi_riyal"></span></span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr class="bg-body-secondary total-sum">
|
||||
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="7">{% trans "Discount Amount" %}</td>
|
||||
<td class="align-middle text-start text-danger fw-semibold">
|
||||
@ -275,6 +270,12 @@
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="bg-body-secondary total-sum">
|
||||
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="7">{% trans "Vat" %} ({{ data.vat }})</td>
|
||||
<td class="align-middle text-start fw-semibold">
|
||||
<span id="grand-total">+ {{ data.total_vat_amount|floatformat }}<span class="icon-saudi_riyal"></span></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="bg-body-secondary total-sum">
|
||||
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="7">{% trans "Additional Services" %}</td>
|
||||
<td class="align-middle text-start fw-semibold">
|
||||
@ -334,39 +335,90 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block customJS %}
|
||||
<script>
|
||||
function calculateTotals() {
|
||||
const table = document.getElementById('estimate-table');
|
||||
const rows = table.getElementsByTagName('tbody')[0].rows;
|
||||
let grandTotal = 0;
|
||||
<script>
|
||||
// Initialize when page loads and after HTMX swaps
|
||||
document.addEventListener('DOMContentLoaded', initEstimateFunctions);
|
||||
document.addEventListener('htmx:afterSwap', initEstimateFunctions);
|
||||
|
||||
for (let row of rows) {
|
||||
// Ensure the row has the expected number of cells
|
||||
if (row.cells.length >= 5) {
|
||||
const quantity = parseFloat(row.cells[2].textContent); // Quantity column
|
||||
const unitPrice = parseFloat(row.cells[3].textContent); // Unit Price column
|
||||
function initEstimateFunctions() {
|
||||
// Initialize calculateTotals if estimate table exists
|
||||
const estimateTable = document.getElementById('estimate-table');
|
||||
if (estimateTable) {
|
||||
calculateTotals();
|
||||
|
||||
if (!isNaN(quantity) && !isNaN(unitPrice)) {
|
||||
const total = quantity * unitPrice;
|
||||
row.cells[4].textContent = total.toFixed(2); // Populate Total column
|
||||
grandTotal += total; // Add to grand total
|
||||
}
|
||||
// Optional: If you need to recalculate when table content changes
|
||||
estimateTable.addEventListener('change', calculateTotals);
|
||||
}
|
||||
|
||||
// Initialize form action setter if form exists
|
||||
const confirmForm = document.getElementById('confirmForm');
|
||||
if (confirmForm) {
|
||||
// Remove old event listeners if any
|
||||
document.querySelectorAll('[data-set-form-action]').forEach(button => {
|
||||
button.removeEventListener('click', setFormActionHandler);
|
||||
button.addEventListener('click', setFormActionHandler);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function calculateTotals() {
|
||||
const table = document.getElementById('estimate-table');
|
||||
if (!table) return;
|
||||
|
||||
try {
|
||||
const tbody = table.getElementsByTagName('tbody')[0];
|
||||
if (!tbody) return;
|
||||
|
||||
const rows = tbody.rows;
|
||||
let grandTotal = 0;
|
||||
|
||||
for (let row of rows) {
|
||||
// Skip rows that don't have enough cells
|
||||
if (row.cells.length < 5) continue;
|
||||
|
||||
// Get quantity and unit price
|
||||
const quantityText = row.cells[2]?.textContent?.trim() || '0';
|
||||
const unitPriceText = row.cells[3]?.textContent?.trim() || '0';
|
||||
|
||||
// Parse values, handling any formatting
|
||||
const quantity = parseFloat(quantityText.replace(/[^0-9.-]/g, ''));
|
||||
const unitPrice = parseFloat(unitPriceText.replace(/[^0-9.-]/g, ''));
|
||||
|
||||
if (!isNaN(quantity) && !isNaN(unitPrice)) {
|
||||
const total = quantity * unitPrice;
|
||||
if (row.cells[4]) {
|
||||
row.cells[4].textContent = total.toFixed(2);
|
||||
}
|
||||
grandTotal += total;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Display the grand total
|
||||
document.getElementById('grand-total').textContent = grandTotal.toFixed(2);
|
||||
// Update grand total display
|
||||
const grandTotalElement = document.getElementById('grand-total');
|
||||
if (grandTotalElement) {
|
||||
grandTotalElement.textContent = grandTotal.toFixed(2);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error calculating totals:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Run the function on page load
|
||||
//window.onload = calculateTotals;
|
||||
|
||||
function setFormAction(action) {
|
||||
// Get the form element
|
||||
const form = document.getElementById('confirmForm');
|
||||
// Set the form action with the query parameter
|
||||
form.action = "{% url 'estimate_mark_as' request.dealer.slug estimate.pk %}?mark=" + action;
|
||||
function setFormActionHandler(event) {
|
||||
const action = event.currentTarget.getAttribute('data-set-form-action');
|
||||
if (action) {
|
||||
setFormAction(action);
|
||||
}
|
||||
</script>
|
||||
}
|
||||
|
||||
function setFormAction(action) {
|
||||
const form = document.getElementById('confirmForm');
|
||||
if (!form) return;
|
||||
|
||||
const baseUrl = "{% url 'estimate_mark_as' request.dealer.slug estimate.pk %}";
|
||||
form.action = `${baseUrl}?mark=${encodeURIComponent(action)}`;
|
||||
|
||||
// Optional: Submit form immediately after setting action
|
||||
// form.submit();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -154,14 +154,14 @@
|
||||
<div class="card shadow-sm border-0 rounded-3">
|
||||
<div class="card-header bg-gray-200 py-3 border-0 rounded-top-3">
|
||||
<h3 class="mb-0 fs-4 text-center">
|
||||
{% trans "Create Quotation" %}<i class="fa-regular fa-file-lines text-primary me-2"></i>
|
||||
{% trans "Create Quotation" %}<i class="fa-regular fa-file-lines text-primary me-2"></i>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body bg-light-subtle">
|
||||
|
||||
|
||||
|
||||
<form id="mainForm" method="post" class="needs-validation {% if not items or not customer_count %}disabled{% endif %}">
|
||||
|
||||
|
||||
{% csrf_token %}
|
||||
<div class="row g-3 col-12">
|
||||
{{ form|crispy }}
|
||||
@ -202,165 +202,246 @@
|
||||
<a href="{% url 'estimate_list' request.dealer.slug%}" class="btn btn-lg btn-phoenix-secondary"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% block customJS %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const Toast = Swal.mixin({
|
||||
toast: true,
|
||||
position: "top-end",
|
||||
showConfirmButton: false,
|
||||
timer: 2000,
|
||||
timerProgressBar: false,
|
||||
didOpen: (toast) => {
|
||||
toast.onmouseenter = Swal.stopTimer;
|
||||
toast.onmouseleave = Swal.resumeTimer;
|
||||
}
|
||||
});
|
||||
function notify(tag,msg){Toast.fire({icon: tag,titleText: msg});}
|
||||
// Global variables
|
||||
let Toast;
|
||||
let customSelectsInitialized = false;
|
||||
let formInitialized = false;
|
||||
|
||||
const customSelects = document.querySelectorAll('.custom-select');
|
||||
// Initialize when page loads and after HTMX swaps
|
||||
document.addEventListener('DOMContentLoaded', initPage);
|
||||
document.addEventListener('htmx:afterSwap', initPage);
|
||||
|
||||
customSelects.forEach(select => {
|
||||
const trigger = select.querySelector('.select-trigger');
|
||||
const optionsContainer = select.querySelector('.options-container');
|
||||
const options = select.querySelectorAll('.option');
|
||||
const nativeSelect = select.querySelector('.native-select');
|
||||
const selectedValue = select.querySelector('.selected-value');
|
||||
|
||||
// Toggle dropdown
|
||||
trigger.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
select.classList.toggle('open');
|
||||
trigger.classList.toggle('active');
|
||||
|
||||
// Close other open selects
|
||||
document.querySelectorAll('.custom-select').forEach(otherSelect => {
|
||||
if (otherSelect !== select) {
|
||||
otherSelect.classList.remove('open');
|
||||
otherSelect.querySelector('.select-trigger').classList.remove('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Handle option selection
|
||||
options.forEach(option => {
|
||||
option.addEventListener('click', () => {
|
||||
const value = option.getAttribute('data-value');
|
||||
const image = option.getAttribute('data-image');
|
||||
const text = option.querySelector('span').textContent;
|
||||
|
||||
// Update selected display
|
||||
selectedValue.innerHTML = `
|
||||
<img src="${image}" alt="${text}">
|
||||
<span>${text}</span>
|
||||
`;
|
||||
|
||||
// Update native select value
|
||||
nativeSelect.value = value;
|
||||
|
||||
// Mark as selected
|
||||
options.forEach(opt => opt.classList.remove('selected'));
|
||||
option.classList.add('selected');
|
||||
|
||||
// Close dropdown
|
||||
select.classList.remove('open');
|
||||
trigger.classList.remove('active');
|
||||
|
||||
// Trigger change event
|
||||
const event = new Event('change');
|
||||
nativeSelect.dispatchEvent(event);
|
||||
});
|
||||
});
|
||||
|
||||
// Close when clicking outside
|
||||
document.addEventListener('click', () => {
|
||||
select.classList.remove('open');
|
||||
trigger.classList.remove('active');
|
||||
});
|
||||
|
||||
// Initialize with native select value
|
||||
if (nativeSelect.value) {
|
||||
const selectedOption = select.querySelector(`.option[data-value="${nativeSelect.value}"]`);
|
||||
if (selectedOption) {
|
||||
selectedOption.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Form submission
|
||||
/*const form = document.getElementById('demo-form');
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(form);
|
||||
alert(`Selected value: ${formData.get('car')}`);
|
||||
});*/
|
||||
|
||||
document.getElementById('mainForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const titleInput = document.querySelector('[name="title"]');
|
||||
if (titleInput.value.length < 5) {
|
||||
notify("error", "Customer Estimate Title must be at least 5 characters long.");
|
||||
return; // Stop form submission
|
||||
}
|
||||
|
||||
// Collect all form data
|
||||
const formData = {
|
||||
csrfmiddlewaretoken: document.querySelector('[name=csrfmiddlewaretoken]').value,
|
||||
title: document.querySelector('[name=title]').value,
|
||||
customer: document.querySelector('[name=customer]').value,
|
||||
item: [],
|
||||
quantity: [1],
|
||||
opportunity_id: "{{opportunity_id}}"
|
||||
};
|
||||
|
||||
|
||||
// Collect multi-value fields (e.g., item[], quantity[])
|
||||
document.querySelectorAll('[name="item"]').forEach(input => {
|
||||
formData.item.push(input.value);
|
||||
});
|
||||
|
||||
console.log(formData);
|
||||
|
||||
try {
|
||||
// Send data to the server using fetch
|
||||
const response = await fetch("{% url 'estimate_create' request.dealer.slug %}", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': formData.csrfmiddlewaretoken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
// Parse the JSON response
|
||||
const data = await response.json();
|
||||
|
||||
// Handle the response
|
||||
if (data.status === "error") {
|
||||
notify("error", data.message); // Display an error message
|
||||
} else if (data.status === "success") {
|
||||
notify("success","Estimate created successfully");
|
||||
setTimeout(() => {
|
||||
window.location.assign(data.url); // Redirect to the provided URL
|
||||
}, 1000);
|
||||
} else {
|
||||
notify("error","Unexpected response from the server");
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
notify("error", error);
|
||||
}
|
||||
});
|
||||
function initPage() {
|
||||
initToast();
|
||||
initCustomSelects();
|
||||
initFormSubmission();
|
||||
}
|
||||
|
||||
function initToast() {
|
||||
if (typeof Swal !== 'undefined') {
|
||||
Toast = Swal.mixin({
|
||||
toast: true,
|
||||
position: "top-end",
|
||||
showConfirmButton: false,
|
||||
timer: 2000,
|
||||
timerProgressBar: false,
|
||||
didOpen: (toast) => {
|
||||
toast.onmouseenter = Swal.stopTimer;
|
||||
toast.onmouseleave = Swal.resumeTimer;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
}
|
||||
}
|
||||
|
||||
function notify(tag, msg) {
|
||||
if (Toast) {
|
||||
Toast.fire({icon: tag, titleText: msg});
|
||||
} else if (typeof Swal !== 'undefined') {
|
||||
Swal.fire({icon: tag, titleText: msg});
|
||||
} else {
|
||||
console.log(`${tag}: ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
function initCustomSelects() {
|
||||
// Clean up previous listeners if any
|
||||
if (customSelectsInitialized) {
|
||||
document.removeEventListener('click', handleDocumentClickForSelects);
|
||||
}
|
||||
|
||||
const customSelects = document.querySelectorAll('.custom-select');
|
||||
if (!customSelects.length) return;
|
||||
|
||||
customSelects.forEach(select => {
|
||||
const trigger = select.querySelector('.select-trigger');
|
||||
const options = select.querySelectorAll('.option');
|
||||
const nativeSelect = select.querySelector('.native-select');
|
||||
const selectedValue = select.querySelector('.selected-value');
|
||||
|
||||
// Clone elements to remove old event listeners
|
||||
if (trigger) trigger.replaceWith(trigger.cloneNode(true));
|
||||
options.forEach(option => option.replaceWith(option.cloneNode(true)));
|
||||
|
||||
// Get fresh references
|
||||
const newTrigger = select.querySelector('.select-trigger');
|
||||
const newOptions = select.querySelectorAll('.option');
|
||||
const newNativeSelect = select.querySelector('.native-select');
|
||||
const newSelectedValue = select.querySelector('.selected-value');
|
||||
|
||||
// Add new listeners
|
||||
if (newTrigger) {
|
||||
newTrigger.addEventListener('click', (e) => handleSelectTriggerClick(e, select));
|
||||
}
|
||||
|
||||
newOptions.forEach(option => {
|
||||
option.addEventListener('click', () => handleOptionClick(option, select));
|
||||
});
|
||||
|
||||
// Initialize with current value
|
||||
if (newNativeSelect?.value && newSelectedValue) {
|
||||
initializeSelectedOption(select);
|
||||
}
|
||||
});
|
||||
|
||||
// Add document click handler
|
||||
document.addEventListener('click', handleDocumentClickForSelects);
|
||||
customSelectsInitialized = true;
|
||||
}
|
||||
|
||||
function handleSelectTriggerClick(e, select) {
|
||||
e.stopPropagation();
|
||||
const trigger = select.querySelector('.select-trigger');
|
||||
|
||||
// Toggle current select
|
||||
select.classList.toggle('open');
|
||||
trigger.classList.toggle('active');
|
||||
|
||||
// Close other open selects
|
||||
document.querySelectorAll('.custom-select').forEach(otherSelect => {
|
||||
if (otherSelect !== select) {
|
||||
otherSelect.classList.remove('open');
|
||||
const otherTrigger = otherSelect.querySelector('.select-trigger');
|
||||
if (otherTrigger) otherTrigger.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleOptionClick(option, select) {
|
||||
const value = option.getAttribute('data-value');
|
||||
const image = option.getAttribute('data-image');
|
||||
const text = option.querySelector('span')?.textContent || '';
|
||||
const nativeSelect = select.querySelector('.native-select');
|
||||
const selectedValue = select.querySelector('.selected-value');
|
||||
const options = select.querySelectorAll('.option');
|
||||
|
||||
// Update display
|
||||
if (selectedValue) {
|
||||
selectedValue.innerHTML = `
|
||||
<img src="${image}" alt="${text}">
|
||||
<span>${text}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
// Update native select
|
||||
if (nativeSelect) {
|
||||
nativeSelect.value = value;
|
||||
// Trigger change event
|
||||
const event = new Event('change');
|
||||
nativeSelect.dispatchEvent(event);
|
||||
}
|
||||
|
||||
// Update selected state
|
||||
options.forEach(opt => opt.classList.remove('selected'));
|
||||
option.classList.add('selected');
|
||||
|
||||
// Close dropdown
|
||||
select.classList.remove('open');
|
||||
const trigger = select.querySelector('.select-trigger');
|
||||
if (trigger) trigger.classList.remove('active');
|
||||
}
|
||||
|
||||
function handleDocumentClickForSelects() {
|
||||
document.querySelectorAll('.custom-select').forEach(select => {
|
||||
select.classList.remove('open');
|
||||
const trigger = select.querySelector('.select-trigger');
|
||||
if (trigger) trigger.classList.remove('active');
|
||||
});
|
||||
}
|
||||
|
||||
function initializeSelectedOption(select) {
|
||||
const nativeSelect = select.querySelector('.native-select');
|
||||
const selectedOption = select.querySelector(`.option[data-value="${nativeSelect.value}"]`);
|
||||
if (selectedOption) {
|
||||
handleOptionClick(selectedOption, select);
|
||||
}
|
||||
}
|
||||
|
||||
function initFormSubmission() {
|
||||
const form = document.getElementById('mainForm');
|
||||
if (!form) return;
|
||||
|
||||
// Remove old listener if exists
|
||||
if (formInitialized) {
|
||||
form.removeEventListener('submit', handleFormSubmit);
|
||||
}
|
||||
|
||||
form.addEventListener('submit', handleFormSubmit);
|
||||
formInitialized = true;
|
||||
}
|
||||
|
||||
async function handleFormSubmit(e) {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
|
||||
// Validate title
|
||||
const titleInput = form.querySelector('[name="title"]');
|
||||
if (titleInput && titleInput.value.length < 5) {
|
||||
notify("error", "Customer Estimate Title must be at least 5 characters long.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare form data
|
||||
const formData = {
|
||||
csrfmiddlewaretoken: document.querySelector('[name=csrfmiddlewaretoken]')?.value,
|
||||
title: form.querySelector('[name=title]')?.value,
|
||||
customer: form.querySelector('[name=customer]')?.value,
|
||||
item: [],
|
||||
quantity: [1],
|
||||
opportunity_id: "{{opportunity_id}}"
|
||||
};
|
||||
|
||||
// Collect items
|
||||
form.querySelectorAll('[name="item"]').forEach(input => {
|
||||
if (input.value) formData.item.push(input.value);
|
||||
});
|
||||
|
||||
try {
|
||||
// Submit form
|
||||
const response = await fetch("{% url 'estimate_create' request.dealer.slug %}", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': formData.csrfmiddlewaretoken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
// Handle response
|
||||
const data = await response.json();
|
||||
handleFormResponse(data);
|
||||
} catch (error) {
|
||||
console.error("Form submission error:", error);
|
||||
notify("error", "An error occurred while submitting the form");
|
||||
}
|
||||
}
|
||||
|
||||
function handleFormResponse(data) {
|
||||
if (!data) {
|
||||
notify("error", "No response from server");
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.status === "error") {
|
||||
notify("error", data.message || "An error occurred");
|
||||
} else if (data.status === "success") {
|
||||
notify("success", data.message || "Estimate created successfully");
|
||||
if (data.url) {
|
||||
setTimeout(() => window.location.assign(data.url), 1000);
|
||||
}
|
||||
} else {
|
||||
notify("error", "Unexpected response from the server");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
{{ _("Quotations") }}
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
{% if estimates %}
|
||||
<div class="row g-3 mt-4 mb-4">
|
||||
<div class="row g-3 justify-content-between mb-4">
|
||||
<div class="col-auto">
|
||||
@ -30,31 +31,31 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="list">
|
||||
{% for extra in staff_estimates %}
|
||||
{% for estimate in staff_estimates %}
|
||||
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
|
||||
<td class="align-middle product white-space-nowrap py-0 px-1">{{ extra.content_object.estimate_number }}</td>
|
||||
<td class="align-middle product white-space-nowrap">{{ extra.content_object.customer.customer_name }}</td>
|
||||
<td class="align-middle product white-space-nowrap py-0 px-1">{{ estimate.estimate_number }}</td>
|
||||
<td class="align-middle product white-space-nowrap">{{ estimate.customer.customer_name }}</td>
|
||||
<td class="align-middle product white-space-nowrap">
|
||||
{% if extra.content_object.status == 'draft' %}
|
||||
{% if estimate.status == 'draft' %}
|
||||
<span class="badge badge-phoenix badge-phoenix-warning">{% trans "Draft" %}</span>
|
||||
{% elif extra.content_object.status == 'in_review' %}
|
||||
{% elif estimate.status == 'in_review' %}
|
||||
<span class="badge badge-phoenix badge-phoenix-info">{% trans "In Review" %}</span>
|
||||
{% elif extra.content_object.status == 'approved' %}
|
||||
{% elif estimate.status == 'approved' %}
|
||||
<span class="badge badge-phoenix badge-phoenix-success">{% trans "Approved" %}</span>
|
||||
{% elif extra.content_object.status == 'declined' %}
|
||||
{% elif estimate.status == 'declined' %}
|
||||
<span class="badge badge-phoenix badge-phoenix-danger">{% trans "Declined" %}</span>
|
||||
{% elif extra.content_object.status == 'canceled' %}
|
||||
{% elif estimate.status == 'canceled' %}
|
||||
<span class="badge badge-phoenix badge-phoenix-danger">{% trans "Canceled" %}</span>
|
||||
{% elif extra.content_object.status == 'completed' %}
|
||||
{% elif estimate.status == 'completed' %}
|
||||
<span class="badge badge-phoenix badge-phoenix-success">{% trans "Completed" %}</span>
|
||||
{% elif extra.content_object.status == 'void' %}
|
||||
{% elif estimate.status == 'void' %}
|
||||
<span class="badge badge-phoenix badge-phoenix-secondary">{% trans "Void" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="align-middle product white-space-nowrap">{{ extra.content_object.get_status_action_date }}</td>
|
||||
<td class="align-middle product white-space-nowrap">{{ extra.content_object.created }}</td>
|
||||
<td class="align-middle product white-space-nowrap">{{ estimate.get_status_action_date }}</td>
|
||||
<td class="align-middle product white-space-nowrap">{{ estimate.created }}</td>
|
||||
<td class="align-middle product white-space-nowrap">
|
||||
<a href="{% url 'estimate_detail' request.dealer.slug extra.content_object.pk %}"
|
||||
<a href="{% url 'estimate_detail' request.dealer.slug estimate.pk %}"
|
||||
class="btn btn-sm btn-phoenix-success">
|
||||
<i class="fa-regular fa-eye me-1"></i>
|
||||
{% trans "view"|capfirst %}
|
||||
@ -75,4 +76,10 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
{% url "estimate_create" request.dealer.slug as create_estimate_url %}
|
||||
{% include "empty-illustration-page.html" with value="customer" url=create_estimate_url %}
|
||||
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@ -3,206 +3,23 @@
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% block title %}
|
||||
<h1>{% trans 'Sale Order' %}</h1>
|
||||
{% endblock %}
|
||||
{{ _("Sale Order") }}
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
<link rel="stylesheet" href="{% static 'flags/sprite.css' %}" />
|
||||
<div class="row">
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-6 col-md-8">
|
||||
<div class="d-sm-flex justify-content-between">
|
||||
<h3 class="mb-3">
|
||||
{% if customer.created %}
|
||||
{{ _('Edit Sale Order') }}
|
||||
{% else %}
|
||||
{{ _('Add Sale Order') }}
|
||||
{% endif %}
|
||||
<span class="fas fa-shopping-cart text-primary ms-2"></span>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-xl-12 col-xxl-12">
|
||||
<div class="px-xl-12">
|
||||
<div class="row mx-0 mx-sm-3 mx-lg-0 px-lg-0">
|
||||
<div class="col-sm-12 col-xxl-6 py-3">
|
||||
<table class="w-100 table-stats ">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-2">
|
||||
<div class="d-inline-flex align-items-center">
|
||||
<div class="d-flex bg-success-subtle rounded-circle flex-center me-3" style="width:24px; height:24px">
|
||||
<span class="text-success-dark" data-feather="user" style="width:16px; height:16px"></span>
|
||||
</div>
|
||||
<p class="fw-bold mb-0">{{ _("Customer Name")}}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-2 d-none d-sm-block pe-sm-2">:</td>
|
||||
<td class="py-2">
|
||||
<p class="ps-6 ps-sm-0 fw-semibold mb-0 mb-0 pb-3 pb-sm-0">{{ estimate.customer.customer_name }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-2">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="d-flex bg-success-subtle rounded-circle flex-center me-3" style="width:24px; height:24px">
|
||||
<span class="text-success-dark" data-feather="mail" style="width:16px; height:16px"></span>
|
||||
</div>
|
||||
<p class="fw-bold mb-0">{{ _("Email") }}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-2 d-none d-sm-block pe-sm-2">:</td>
|
||||
<td class="py-2">
|
||||
<p class="ps-6 ps-sm-0 fw-semibold mb-0">{{ estimate.customer.email }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-2">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="d-flex bg-success-subtle rounded-circle flex-center me-3" style="width:24px; height:24px">
|
||||
<span class="text-success-dark" data-feather="map-pin" style="width:16px; height:16px"></span>
|
||||
</div>
|
||||
<p class="fw-bold mb-0">{{ _("Address") }}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-2 d-none d-sm-block pe-sm-2">:</td>
|
||||
<td class="py-2">
|
||||
<p class="ps-6 ps-sm-0 fw-semibold mb-0">{{ estimate.customer.address_1 }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-2">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="d-flex bg-success-subtle rounded-circle flex-center me-3" style="width:24px; height:24px">
|
||||
<span class="text-success-dark" data-feather="trending-down" style="width:16px; height:16px"></span>
|
||||
</div>
|
||||
<p class="fw-bold mb-0">{{ _("Total Discount")}}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-2 d-none d-sm-block pe-sm-2">:</td>
|
||||
<td class="py-2">
|
||||
<p class="ps-6 ps-sm-0 fw-semibold mb-0">{{ data.total_discount }} <span class="icon-saudi_riyal"></span></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-2">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="d-flex bg-success-subtle rounded-circle flex-center me-3" style="width:24px; height:24px">
|
||||
<span class="text-success-dark" data-feather="briefcase" style="width:16px; height:16px"></span>
|
||||
</div>
|
||||
<p class="fw-bold mb-0">{{ _("Total Amount")}}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-2 d-none d-sm-block pe-sm-2">:</td>
|
||||
<td class="py-2">
|
||||
<p class="ps-6 ps-sm-0 fw-semibold mb-0">{{ data.grand_total }} <span class="icon-saudi_riyal"></span></p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="border-top border-bottom border-translucent mt-10" id="leadDetailsTable">
|
||||
<div class="table-responsive scrollbar mx-n1 px-1">
|
||||
<table class="table fs-9 mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="white-space-nowrap fs-9 align-middle ps-0" style="width:26px;">
|
||||
<div class="form-check mb-0 fs-8">
|
||||
<input class="form-check-input" type="checkbox" />
|
||||
</div>
|
||||
</th>
|
||||
<th class="sort white-space-nowrap align-middle pe-3 ps-0 text-uppercase" scope="col" data-sort="name" style="width:20%; min-width:100px">
|
||||
{{ _("VIN") }}</th>
|
||||
<th class="sort align-middle pe-6 text-uppercase" scope="col" data-sort="description" style="width:20%; max-width:60px">
|
||||
{{ _("Make") }}</th>
|
||||
<th class="sort align-middle text-start text-uppercase" scope="col" data-sort="create_date" style="width:20%; min-width:115px">
|
||||
{{ _("Model") }}</th>
|
||||
<th class="sort align-middle text-start text-uppercase" scope="col" data-sort="create_by" style="width:20%; min-width:150px">
|
||||
{{ _("Year") }}</th>
|
||||
<th class="sort align-middle text-start text-uppercase" scope="col" data-sort="create_by" style="width:20%; min-width:150px">
|
||||
{{ _("Unit Price")}}</th>
|
||||
<th class="align-middle pe-0 text-end" scope="col" style="width:15%;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="list" id="lead-details-table-body">
|
||||
{% for car in data.cars %}
|
||||
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
|
||||
<td class="fs-9 align-middle px-0 py-3">
|
||||
<div class="form-check mb-0 fs-8">
|
||||
<input class="form-check-input" type="checkbox" />
|
||||
</div>
|
||||
</td>
|
||||
<td class="name align-middle white-space-nowrap py-2 ps-0">
|
||||
<a class="d-flex align-items-center text-body-highlight" href="#!">
|
||||
{% comment %} <div class="avatar avatar-m me-3 status-online">
|
||||
<img class="rounded-circle" src="" alt="" />
|
||||
</div> {% endcomment %}
|
||||
<h6 class="mb-0 text-body-highlight fw-bold">{{ car.vin }}</h6>
|
||||
</a>
|
||||
</td>
|
||||
<td class="description align-middle white-space-nowrap text-start fw-bold text-body-tertiary py-2 pe-6">
|
||||
{{ car.make }}
|
||||
</td>
|
||||
<td class="create_by align-middle white-space-nowrap fw-semibold text-body-highlight">{{ car.model }}</td>
|
||||
<td class="create_by align-middle white-space-nowrap fw-semibold text-body-highlight">{{ car.year }}</td>
|
||||
<td class="last_activity align-middle text-center py-2">
|
||||
<div class="d-flex align-items-center flex-1">
|
||||
<span class="fw-bold fs-9 text-body">{{ car.total }} <span class="icon-saudi_riyal"></span></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mx-0">
|
||||
<form method="post" class="form row g-3 needs-validation" novalidate>
|
||||
<div class="row">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">{{ _("Sale Order") }}</div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="{% url 'create_sale_order' request.dealer.slug estimate.pk %}">
|
||||
{% csrf_token %}
|
||||
<div class="col-md-6">
|
||||
<label for="id_estimate" class="form-label">{% trans "Quotation" %}</label>
|
||||
<input type="text"
|
||||
class="form-control form-control-sm"
|
||||
id="id_estimate"
|
||||
name="estimate"
|
||||
value="{{ form.estimate.value|default_if_none:'' }}"
|
||||
readonly>
|
||||
<div class="invalid-feedback">{% trans "Please provide an estimate." %}</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="id_payment_method" class="form-label">{% trans "Payment Method" %}</label>
|
||||
<select class="form-select form-select-sm"
|
||||
id="id_payment_method"
|
||||
name="payment_method"
|
||||
required>
|
||||
{% for value, label in form.payment_method.field.choices %}
|
||||
<option value="{{ value }}"
|
||||
{% if form.payment_method.value == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="invalid-feedback">{% trans "Please select a payment method." %}</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="id_comments" class="form-label">{% trans "Comments" %}</label>
|
||||
<textarea class="form-control" id="id_comments" name="comments" rows="3">{{ form.comments.value|default_if_none:'' }}</textarea>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button class="btn btn-phoenix-primary" type="submit">{% trans 'Save' %}</button>
|
||||
</div>
|
||||
{{ form|crispy }}
|
||||
<button type="submit" class="btn btn-phoenix-primary">{% trans 'Save' %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!---->
|
||||
{% endblock content %}
|
||||
{% endblock content %}
|
||||
@ -4,6 +4,8 @@
|
||||
{{ _("Invoices") }}
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
|
||||
{% if invoices %}
|
||||
<div class="row mt-4">
|
||||
<div class="row g-3 justify-content-between mb-4">
|
||||
<div class="col-auto">
|
||||
@ -85,4 +87,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
{% include "empty-illustration-page.html" with value="invoice" url="#" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
{%block title%} {%trans 'Sale Orders'%} {%endblock%}
|
||||
|
||||
{% block content %}
|
||||
{% if txs %}
|
||||
<section class="mt-2">
|
||||
<div class="row overflow-x-auto whitespace-nowrap -mx-2 sm:mx-0">
|
||||
|
||||
@ -112,4 +113,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% else %}
|
||||
{% include "empty-illustration-page.html" with value="sale order" url='#' %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@ -5,6 +5,9 @@
|
||||
{% trans "Staffs" %}
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
|
||||
|
||||
{%if users %}
|
||||
<section class="">
|
||||
<div class="row mt-4">
|
||||
<div class="col-auto">
|
||||
@ -21,7 +24,7 @@
|
||||
<i class="fa-solid fa-circle-info fs-6"></i>
|
||||
<p class="mb-0 flex-1">
|
||||
{{ _("No Active Subscription,please activate your subscription.") }}<a href="{% url 'pricing_page' request.dealer.slug %}"
|
||||
class="ms-3 text-body-primary fs-9">Manage Subscription</a>
|
||||
class="ms-3 text-body-primary fs-9">Manage Subscription</a>
|
||||
</p>
|
||||
<button class="btn-close"
|
||||
type="button"
|
||||
@ -86,4 +89,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% else %}
|
||||
{% url "user_create" request.dealer.slug as create_staff_url %}
|
||||
{% include "empty-illustration-page.html" with value="staff" url=create_staff_url %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
7
templates/vendors/vendors_list.html
vendored
7
templates/vendors/vendors_list.html
vendored
@ -6,6 +6,8 @@
|
||||
{% endblock title %}
|
||||
{% block vendors %}<a class="nav-link active">{{ _("Vendors") |capfirst }}</a>{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
{% if vendors %}
|
||||
<div class="row mt-4">
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<h3 class="">
|
||||
@ -161,4 +163,9 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% include 'modal/delete_modal.html' %}
|
||||
|
||||
{% else %}
|
||||
{% url "vendor_create" request.dealer.slug as create_vendor_url %}
|
||||
{% include "empty-illustration-page.html" with value="vendor" url=create_vendor_url %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user