Compare commits

...

6 Commits

Author SHA1 Message Date
669e6b445d distiict 2025-07-08 11:27:29 +03:00
62356a997e add 2025-07-08 11:24:17 +03:00
b23e84331f update the task and integrate it with appointment calendar 2025-07-08 11:18:06 +03:00
4bfb533448 update 2025-07-07 14:00:52 +03:00
b33d928bcc update 2025-07-07 13:58:55 +03:00
25d17efa11 testing and fixing 2025-07-07 13:56:46 +03:00
30 changed files with 1761 additions and 835 deletions

View File

@ -12,21 +12,21 @@ class Command(BaseCommand):
Service.objects.create( Service.objects.create(
name="call", name="call",
price=0, price=0,
duration=datetime.timedelta(minutes=10), duration=datetime.timedelta(minutes=60),
currency="SAR", currency="SAR",
description="15 min call", description="15 min call",
) )
Service.objects.create( Service.objects.create(
name="meeting", name="meeting",
price=0, price=0,
duration=datetime.timedelta(minutes=30), duration=datetime.timedelta(minutes=90),
currency="SAR", currency="SAR",
description="30 min meeting", description="30 min meeting",
) )
Service.objects.create( Service.objects.create(
name="email", name="email",
price=0, price=0,
duration=datetime.timedelta(minutes=30), duration=datetime.timedelta(minutes=90),
currency="SAR", currency="SAR",
description="30 min visit", description="30 min visit",
) )

View File

@ -1,16 +1,16 @@
from inventory import models from inventory.models import Lead,Car
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django_ledger.models import EstimateModel,BillModel,AccountModel,LedgerModel from django_ledger.models import EstimateModel,BillModel,AccountModel,LedgerModel
class Command(BaseCommand): class Command(BaseCommand):
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
Permission.objects.get_or_create(name="Can view crm",codename="can_view_crm",content_type=ContentType.objects.get_for_model(models.Lead)) Permission.objects.get_or_create(name="Can view crm",codename="can_view_crm",content_type=ContentType.objects.get_for_model(Lead))
Permission.objects.get_or_create(name="Can reassign lead",codename="can_reassign_lead",content_type=ContentType.objects.get_for_model(Lead))
Permission.objects.get_or_create(name="Can view sales",codename="can_view_sales",content_type=ContentType.objects.get_for_model(EstimateModel)) Permission.objects.get_or_create(name="Can view sales",codename="can_view_sales",content_type=ContentType.objects.get_for_model(EstimateModel))
Permission.objects.get_or_create(name="Can view reports",codename="can_view_reports",content_type=ContentType.objects.get_for_model(LedgerModel)) Permission.objects.get_or_create(name="Can view reports",codename="can_view_reports",content_type=ContentType.objects.get_for_model(LedgerModel))
Permission.objects.get_or_create(name="Can view inventory",codename="can_view_inventory",content_type=ContentType.objects.get_for_model(models.Car)) Permission.objects.get_or_create(name="Can view inventory",codename="can_view_inventory",content_type=ContentType.objects.get_for_model(Car))
Permission.objects.get_or_create(name="Can approve bill",codename="can_approve_billmodel",content_type=ContentType.objects.get_for_model(BillModel)) Permission.objects.get_or_create(name="Can approve bill",codename="can_approve_billmodel",content_type=ContentType.objects.get_for_model(BillModel))
Permission.objects.get_or_create(name="Can view financials",codename="can_view_financials",content_type=ContentType.objects.get_for_model(AccountModel)) Permission.objects.get_or_create(name="Can view financials",codename="can_view_financials",content_type=ContentType.objects.get_for_model(AccountModel))
Permission.objects.get_or_create(name="Can approve estimate",codename="can_approve_estimatemodel",content_type=ContentType.objects.get_for_model(EstimateModel)) Permission.objects.get_or_create(name="Can approve estimate",codename="can_approve_estimatemodel",content_type=ContentType.objects.get_for_model(EstimateModel))

View File

@ -123,13 +123,18 @@ class DealerSlugMiddleware:
response = self.get_response(request) response = self.get_response(request)
return response return response
def process_view(self, request, view_func, view_args, view_kwargs): def process_view(self, request, view_func, view_args, view_kwargs):
if request.path_info.startswith('/en/signup/') or \ if request.path_info.startswith('/ar/signup/') or \
request.path_info.startswith('/en/signup/') or \
request.path_info.startswith('/ar/login/') or \
request.path_info.startswith('/en/login/') or \ request.path_info.startswith('/en/login/') or \
request.path_info.startswith('/ar/logout/') or \
request.path_info.startswith('/en/logout/') or \ request.path_info.startswith('/en/logout/') or \
request.path_info.startswith('/en/ledger/') or \ request.path_info.startswith('/en/ledger/') or \
request.path_info.startswith('/ar/ledger/') or \ request.path_info.startswith('/ar/ledger/') or \
request.path_info.startswith('/en/notifications/') or \ request.path_info.startswith('/en/notifications/') or \
request.path_info.startswith('/ar/notifications/'): request.path_info.startswith('/ar/notifications/') or \
request.path_info.startswith('/en/appointment/') or \
request.path_info.startswith('/ar/appointment/'):
return None return None
if not request.user.is_authenticated: if not request.user.is_authenticated:

View File

@ -1836,7 +1836,10 @@ class Schedule(models.Model):
("completed", _("Completed")), ("completed", _("Completed")),
("canceled", _("Canceled")), ("canceled", _("Canceled")),
] ]
lead = models.ForeignKey(Lead, on_delete=models.CASCADE, related_name="schedules") dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
customer = models.ForeignKey( customer = models.ForeignKey(
CustomerModel, CustomerModel,
on_delete=models.CASCADE, on_delete=models.CASCADE,
@ -1850,6 +1853,7 @@ class Schedule(models.Model):
scheduled_type = models.CharField( scheduled_type = models.CharField(
max_length=200, choices=ScheduledType, default="Call" max_length=200, choices=ScheduledType, default="Call"
) )
completed = models.BooleanField(default=False, verbose_name=_("Completed"))
duration = models.DurationField(default=timedelta(minutes=5)) duration = models.DurationField(default=timedelta(minutes=5))
notes = models.TextField(blank=True, null=True) notes = models.TextField(blank=True, null=True)
status = models.CharField( status = models.CharField(
@ -1859,7 +1863,7 @@ class Schedule(models.Model):
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
def __str__(self): def __str__(self):
return f"Scheduled {self.purpose} with {self.lead.full_name} on {self.scheduled_at}" return f"Scheduled {self.purpose} on {self.scheduled_at}"
@property @property
def schedule_past_date(self): def schedule_past_date(self):
@ -2634,6 +2638,7 @@ class CustomGroup(models.Model):
"notes", "notes",
"tasks", "tasks",
"activity", "activity",
"poitemsuploaded"
], ],
) )
self.set_permissions( self.set_permissions(
@ -2641,7 +2646,7 @@ class CustomGroup(models.Model):
allowed_models=[], allowed_models=[],
other_perms=[ other_perms=[
"view_purchaseordermodel", "view_purchaseordermodel",
"can_view_financials",
] ]
) )
###################################### ######################################
@ -2652,7 +2657,7 @@ class CustomGroup(models.Model):
elif self.name == "Sales": elif self.name == "Sales":
self.set_permissions( self.set_permissions(
app="django_ledger", app="django_ledger",
allowed_models=["estimatemodel", "invoicemodel", "customermodel"], allowed_models=["invoicemodel", "customermodel"],
) )
self.set_permissions( self.set_permissions(
app="inventory", app="inventory",
@ -2662,11 +2667,13 @@ class CustomGroup(models.Model):
"staff", "staff",
"schedule", "schedule",
"activity", "activity",
"lead",
"opportunity", "opportunity",
"customer", "customer",
"organization", "organization",
"notes", "notes",
"taska", "tasks",
"lead"
"activity", "activity",
], ],
other_perms=[ other_perms=[
@ -2678,6 +2685,10 @@ class CustomGroup(models.Model):
"can_view_inventory", "can_view_inventory",
"can_view_sales", "can_view_sales",
"can_view_crm", "can_view_crm",
"view_estimatemodel",
"add_estimatemodel",
"change_estimatemodel",
"delete_estimatemodel",
], ],
) )
###################################### ######################################
@ -2709,19 +2720,18 @@ class CustomGroup(models.Model):
"bankaccountmodel", "bankaccountmodel",
"accountmodel", "accountmodel",
"chartofaccountmodel", "chartofaccountmodel",
"billmodel",
"itemmodel", "itemmodel",
"invoicemodel", "invoicemodel",
"vendormodel", "vendormodel",
"journalentrymodel", "journalentrymodel",
"purchaseordermodel", "purchaseordermodel",
"estimatemodel", "estimatemodel",
"customermodel", "customermodel",
"vendormodel",
"ledgermodel", "ledgermodel",
"transactionmodel" "transactionmodel"
], ],
other_perms=["view_customermodel", "view_estimatemodel","can_view_inventory","can_view_sales","can_view_crm","can_view_financials","can_view_reports"], other_perms=["view_billmodel","add_billmodel","change_billmodel","delete_billmodel","view_customermodel", "view_estimatemodel","can_view_inventory","can_view_sales","can_view_crm","can_view_financials","can_view_reports"],
) )
@ -2917,6 +2927,8 @@ class PoItemsUploaded(models.Model):
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
def get_name(self):
return self.item.item.name.split('||')
class ExtraInfo(models.Model): class ExtraInfo(models.Model):
""" """
Stores additional information for any model with: Stores additional information for any model with:

644
inventory/override.py Normal file
View File

@ -0,0 +1,644 @@
import logging
from django.core.exceptions import ImproperlyConfigured,ValidationError
from django.contrib.auth.mixins import LoginRequiredMixin,PermissionRequiredMixin
from django_ledger.forms.bill import (
BillModelCreateForm,
BaseBillModelUpdateForm,
DraftBillModelUpdateForm,
get_bill_itemtxs_formset_class,
BillModelConfigureForm,
InReviewBillModelUpdateForm,
ApprovedBillModelUpdateForm,
AccruedAndApprovedBillModelUpdateForm,
PaidBillModelUpdateForm
)
from django.http import HttpResponseForbidden
from django.utils.html import format_html
from django.contrib import messages
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django_ledger.models import ItemTransactionModel
from django.views.generic.detail import DetailView
from django_ledger.forms.purchase_order import (ApprovedPurchaseOrderModelUpdateForm,
BasePurchaseOrderModelUpdateForm,
DraftPurchaseOrderModelUpdateForm,
ReviewPurchaseOrderModelUpdateForm,
get_po_itemtxs_formset_class)
from django_ledger.views.purchase_order import PurchaseOrderModelModelViewQuerySetMixIn
from django_ledger.models import PurchaseOrderModel,EstimateModel,BillModel
from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import UpdateView
from django.views.generic.base import RedirectView
from .models import Dealer
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
class PurchaseOrderModelUpdateView(LoginRequiredMixin,
PermissionRequiredMixin,
UpdateView):
slug_url_kwarg = 'po_pk'
slug_field = 'uuid'
context_object_name = 'po_model'
template_name = "purchase_orders/po_update.html"
context_object_name = "po_model"
permission_required = "django_ledger.change_purchaseordermodel"
extra_context = {
'header_subtitle_icon': 'uil:bill'
}
action_update_items = False
queryset = None
def get_context_data(self, itemtxs_formset=None, **kwargs):
dealer = get_object_or_404(Dealer, slug=self.kwargs["dealer_slug"])
context = super().get_context_data(**kwargs)
context["entity_slug"] = dealer.entity.slug
po_model: PurchaseOrderModel = self.object
if not itemtxs_formset:
itemtxs_qs = self.get_po_itemtxs_qs(po_model)
itemtxs_qs, itemtxs_agg = po_model.get_itemtxs_data(queryset=itemtxs_qs)
po_itemtxs_formset_class = get_po_itemtxs_formset_class(po_model)
itemtxs_formset = po_itemtxs_formset_class(
entity_slug=dealer.entity.slug,
user_model=dealer.entity.admin,
po_model=po_model,
queryset=itemtxs_qs,
)
else:
itemtxs_qs, itemtxs_agg = po_model.get_itemtxs_data()
context["itemtxs_qs"] = itemtxs_qs
context["itemtxs_formset"] = itemtxs_formset
return context
def get_queryset(self):
dealer = get_object_or_404(Dealer, slug=self.kwargs["dealer_slug"])
if self.queryset is None:
self.queryset = PurchaseOrderModel.objects.for_entity(
entity_slug=self.kwargs['entity_slug'],
user_model=dealer.entity.admin
).select_related('entity', 'ce_model')
return super().get_queryset()
def get_success_url(self):
return reverse(
"purchase_order_update",
kwargs={
"dealer_slug": self.kwargs["dealer_slug"],
"entity_slug": self.kwargs["entity_slug"],
"po_pk": self.kwargs["po_pk"],
},
)
def get(self, request, dealer_slug, entity_slug, po_pk, *args, **kwargs):
if self.action_update_items:
return HttpResponseRedirect(
redirect_to=reverse(
"purchase_order_update",
kwargs={
"dealer_slug": dealer_slug,
"entity_slug": entity_slug,
"po_pk": po_pk,
},
)
)
return super(PurchaseOrderModelUpdateView, self).get(
request, dealer_slug, entity_slug, po_pk, *args, **kwargs
)
def post(self, request, dealer_slug, entity_slug, *args, **kwargs):
if self.action_update_items:
if not request.user.is_authenticated:
return HttpResponseForbidden()
queryset = self.get_queryset()
po_model: PurchaseOrderModel = self.get_object(queryset=queryset)
self.object = po_model
po_itemtxs_formset_class = get_po_itemtxs_formset_class(po_model)
itemtxs_formset = po_itemtxs_formset_class(
request.POST,
user_model=request.dealer.entity.admin,
po_model=po_model,
entity_slug=entity_slug,
)
if itemtxs_formset.has_changed():
if itemtxs_formset.is_valid():
itemtxs_list = itemtxs_formset.save(commit=False)
create_bill_uuids = [
str(i["uuid"].uuid)
for i in itemtxs_formset.cleaned_data
if i and i["create_bill"] is True
]
if create_bill_uuids:
item_uuids = ",".join(create_bill_uuids)
redirect_url = reverse(
"bill-create-po",
kwargs={
"dealer_slug": self.kwargs["dealer_slug"],
"entity_slug": self.kwargs["entity_slug"],
"po_pk": po_model.uuid,
},
)
redirect_url += f"?item_uuids={item_uuids}"
return HttpResponseRedirect(redirect_url)
for itemtxs in itemtxs_list:
if not itemtxs.po_model_id:
itemtxs.po_model_id = po_model.uuid
itemtxs.clean()
itemtxs_list = itemtxs_formset.save()
po_model.update_state()
po_model.clean()
po_model.save(
update_fields=["po_amount", "po_amount_received", "updated"]
)
# if valid get saved formset from DB
messages.add_message(
request, messages.SUCCESS, "PO items updated successfully."
)
return self.render_to_response(context=self.get_context_data())
# if not valid, return formset with errors...
return self.render_to_response(
context=self.get_context_data(itemtxs_formset=itemtxs_formset)
)
return super(PurchaseOrderModelUpdateView, self).post(
request, dealer_slug, entity_slug, *args, **kwargs
)
def get_form(self, form_class=None):
po_model: PurchaseOrderModel = self.object
dealer = get_object_or_404(Dealer, slug=self.kwargs["dealer_slug"])
if po_model.is_draft():
return DraftPurchaseOrderModelUpdateForm(
entity_slug=self.kwargs["entity_slug"],
user_model=dealer.entity.admin,
**self.get_form_kwargs(),
)
elif po_model.is_review():
return ReviewPurchaseOrderModelUpdateForm(
entity_slug=self.kwargs["entity_slug"],
user_model=dealer.entity.admin,
**self.get_form_kwargs(),
)
elif po_model.is_approved():
return ApprovedPurchaseOrderModelUpdateForm(
entity_slug=self.kwargs["entity_slug"],
user_model=dealer.entity.admin,
**self.get_form_kwargs(),
)
return BasePurchaseOrderModelUpdateForm(
entity_slug=self.kwargs["entity_slug"],
user_model=dealer.entity.admin,
**self.get_form_kwargs(),
)
def get_form_kwargs(self):
if self.action_update_items:
return {
'initial': self.get_initial(),
'prefix': self.get_prefix(),
'instance': self.object
}
return super(PurchaseOrderModelUpdateView, self).get_form_kwargs()
def get_po_itemtxs_qs(self, po_model: PurchaseOrderModel):
return po_model.itemtransactionmodel_set.select_related('bill_model', 'po_model').order_by('created')
def form_valid(self, form: BasePurchaseOrderModelUpdateForm):
po_model: PurchaseOrderModel = form.save(commit=False)
if form.has_changed():
po_items_qs = ItemTransactionModel.objects.for_po(
entity_slug=self.kwargs['entity_slug'],
user_model=dealer.entity.admin,
po_pk=po_model.uuid,
).select_related('bill_model')
if all(['po_status' in form.changed_data,
po_model.po_status == po_model.PO_STATUS_APPROVED]):
po_items_qs.update(po_item_status=ItemTransactionModel.STATUS_NOT_ORDERED)
if 'fulfilled' in form.changed_data:
if not all([i.bill_model for i in po_items_qs]):
messages.add_message(self.request,
messages.ERROR,
f'All PO items must be billed before marking'
f' PO: {po_model.po_number} as fulfilled.',
extra_tags='is-danger')
return self.get(self.request)
else:
if not all([i.bill_model.is_paid() for i in po_items_qs]):
messages.add_message(self.request,
messages.SUCCESS,
f'All bills must be paid before marking'
f' PO: {po_model.po_number} as fulfilled.',
extra_tags='is-success')
return self.get(self.request)
po_items_qs.update(po_item_status=ItemTransactionModel.STATUS_RECEIVED)
messages.add_message(self.request,
messages.SUCCESS,
f'{self.object.po_number} successfully updated.',
extra_tags='is-success')
return super().form_valid(form)
class BasePurchaseOrderActionActionView(LoginRequiredMixin,
PermissionRequiredMixin,
RedirectView,
SingleObjectMixin):
http_method_names = ['get']
pk_url_kwarg = 'po_pk'
action_name = None
commit = True
permission_required = None
queryset = None
def get_queryset(self):
dealer = get_object_or_404(Dealer, slug=self.kwargs['dealer_slug'])
if self.queryset is None:
self.queryset = PurchaseOrderModel.objects.for_entity(
entity_slug=self.kwargs['entity_slug'],
user_model=dealer.entity.admin
).select_related('entity', 'ce_model')
return super().get_queryset()
def get_redirect_url(self, dealer_slug, entity_slug, po_pk, *args, **kwargs):
return reverse(
"purchase_order_update",
kwargs={
"dealer_slug": dealer_slug,
"entity_slug": entity_slug,
"po_pk": po_pk,
},
)
def get(self, request, dealer_slug, entity_slug, po_pk, *args, **kwargs):
# kwargs["user_model"] = dealer.entity.admin
# Get user information for logging
user_username = request.user.username if request.user.is_authenticated else 'anonymous'
dealer = get_object_or_404(Dealer, slug=dealer_slug)
kwargs["user_model"] = dealer.entity.admin
if not self.action_name:
raise ImproperlyConfigured("View attribute action_name is required.")
response = super(BasePurchaseOrderActionActionView, self).get(
request, dealer_slug, entity_slug, po_pk, *args, **kwargs
)
po_model: PurchaseOrderModel = self.get_object()
# Log the attempt to perform the action
logger.debug(
f"User {user_username} attempting to call action '{self.action_name}' "
f"on Purchase Order ID: {po_model.pk} (Entity: {entity_slug})."
)
try:
getattr(po_model, self.action_name)(commit=self.commit, **kwargs)
# --- Single-line log for successful action ---
logger.info(
f"User {user_username} successfully executed action '{self.action_name}' "
f"on Purchase Order ID: {po_model.pk}."
)
messages.add_message(
request,
message="PO updated successfully.",
level=messages.SUCCESS,
)
except ValidationError as e:
# --- Single-line log for ValidationError ---
logger.warning(
f"User {user_username} encountered a validation error "
f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. "
f"Error: {e}"
)
print(e)
return response
class BillModelDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
slug_url_kwarg = 'bill_pk'
slug_field = 'uuid'
context_object_name = 'bill'
template_name = "bill/bill_detail.html"
extra_context = {
'header_subtitle_icon': 'uil:bill',
'hide_menu': True
}
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=object_list, **kwargs)
context["dealer"] = self.request.dealer
bill_model: BillModel = self.object
title = f'Bill {bill_model.bill_number}'
context['page_title'] = title
context['header_title'] = title
bill_model: BillModel = self.object
bill_items_qs, item_data = bill_model.get_itemtxs_data()
context['itemtxs_qs'] = bill_items_qs
context['total_amount__sum'] = item_data['total_amount__sum']
if not bill_model.is_configured():
link = format_html(f"""
<a href="{reverse("bill-update", kwargs={
'dealer_slug': self.kwargs['dealer_slug'],
'entity_slug': self.kwargs['entity_slug'],
'bill_pk': bill_model.uuid
})}">here</a>
""")
msg = f'Bill {bill_model.bill_number} has not been fully set up. ' + \
f'Please update or assign associated accounts {link}.'
messages.add_message(self.request,
message=msg,
level=messages.WARNING,
extra_tags='is-danger')
return context
def get_queryset(self):
dealer = get_object_or_404(Dealer,slug=self.kwargs['dealer_slug'])
if self.queryset is None:
entity_model = dealer.entity
qs = entity_model.get_bills()
self.queryset = qs
return super().get_queryset()
######################################################3
#BILL
class BillModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
slug_url_kwarg = 'bill_pk'
slug_field = 'uuid'
context_object_name = 'bill_model'
template_name = "bill/bill_update.html"
extra_context = {
'header_subtitle_icon': 'uil:bill'
}
http_method_names = ['get', 'post']
action_update_items = False
queryset = None
def get_queryset(self):
dealer = get_object_or_404(Dealer,slug=self.kwargs['dealer_slug'])
if self.queryset is None:
entity_model = dealer.entity
qs = entity_model.get_bills()
self.queryset = qs
return super().get_queryset().select_related(
'ledger',
'ledger__entity',
'vendor',
'cash_account',
'prepaid_account',
'unearned_account',
'cash_account__coa_model',
'prepaid_account__coa_model',
'unearned_account__coa_model'
)
def get_form(self, form_class=None):
form_class = self.get_form_class()
dealer = get_object_or_404(Dealer,slug=self.kwargs['dealer_slug'])
entity_model = dealer.entity
if self.request.method == 'POST' and self.action_update_items:
return form_class(
entity_model=entity_model,
user_model=dealer.entity.admin,
instance=self.object
)
return form_class(
entity_model=entity_model,
user_model=dealer.entity.admin,
**self.get_form_kwargs()
)
def get_form_class(self):
bill_model: BillModel = self.object
if not bill_model.is_configured():
return BillModelConfigureForm
if bill_model.is_draft():
return DraftBillModelUpdateForm
elif bill_model.is_review():
return InReviewBillModelUpdateForm
elif bill_model.is_approved() and not bill_model.accrue:
return ApprovedBillModelUpdateForm
elif bill_model.is_approved() and bill_model.accrue:
return AccruedAndApprovedBillModelUpdateForm
elif bill_model.is_paid():
return PaidBillModelUpdateForm
return BaseBillModelUpdateForm
def get_context_data(self,
*,
object_list=None,
itemtxs_formset=None,
**kwargs):
context = super().get_context_data(object_list=object_list, **kwargs)
dealer = get_object_or_404(Dealer,slug=self.kwargs['dealer_slug'])
entity_model = dealer.entity
bill_model: BillModel = self.object
ledger_model = bill_model.ledger
title = f'Bill {bill_model.bill_number}'
context['page_title'] = title
context['header_title'] = title
context['header_subtitle'] = bill_model.get_bill_status_display()
if not bill_model.is_configured():
messages.add_message(
request=self.request,
message=f'Bill {bill_model.bill_number} must have all accounts configured.',
level=messages.ERROR,
extra_tags='is-danger'
)
if not bill_model.is_paid():
if ledger_model.locked:
messages.add_message(self.request,
messages.ERROR,
f'Warning! This bill is locked. Must unlock before making any changes.',
extra_tags='is-danger')
if ledger_model.locked:
messages.add_message(self.request,
messages.ERROR,
f'Warning! This bill is locked. Must unlock before making any changes.',
extra_tags='is-danger')
if not ledger_model.is_posted():
messages.add_message(self.request,
messages.INFO,
f'This bill has not been posted. Must post to see ledger changes.',
extra_tags='is-info')
itemtxs_qs = itemtxs_formset.get_queryset() if itemtxs_formset else None
if not itemtxs_formset:
itemtxs_formset_class = get_bill_itemtxs_formset_class(bill_model)
itemtxs_formset = itemtxs_formset_class(entity_model=entity_model, bill_model=bill_model)
itemtxs_qs, itemtxs_agg = bill_model.get_itemtxs_data(queryset=itemtxs_qs)
has_po = any(i.po_model_id for i in itemtxs_qs)
if has_po:
itemtxs_formset.can_delete = False
itemtxs_formset.has_po = has_po
context['itemtxs_formset'] = itemtxs_formset
context['total_amount__sum'] = itemtxs_agg['total_amount__sum']
context['has_po'] = has_po
return context
def get_success_url(self):
return reverse(
"bill-update",
kwargs={
"dealer_slug": self.kwargs["dealer_slug"],
"entity_slug": self.kwargs["entity_slug"],
"bill_pk": self.kwargs["bill_pk"],
},
)
def form_valid(self, form):
form.save(commit=False)
messages.add_message(self.request,
messages.SUCCESS,
f'Bill {self.object.bill_number} successfully updated.',
extra_tags='is-success')
return super().form_valid(form)
def get(self, request,dealer_slug,entity_slug,bill_pk, *args, **kwargs):
if self.action_update_items:
return HttpResponseRedirect(
redirect_to=reverse('bill-update',
kwargs={
'dealer_slug': dealer_slug,
'entity_slug': entity_slug,
'bill_pk': bill_pk
})
)
return super(BillModelUpdateView, self).get(request, *args, **kwargs)
def post(self, request, dealer_slug, entity_slug, bill_pk, *args, **kwargs):
if self.action_update_items:
if not request.user.is_authenticated:
return HttpResponseForbidden()
queryset = self.get_queryset()
dealer = get_object_or_404(Dealer, slug=dealer_slug)
entity_model = dealer.entity
bill_model: BillModel = self.get_object(queryset=queryset)
bill_pk = bill_model.uuid
self.object = bill_model
bill_itemtxs_formset_class = get_bill_itemtxs_formset_class(bill_model)
itemtxs_formset = bill_itemtxs_formset_class(
request.POST, bill_model=bill_model, entity_model=entity_model
)
if itemtxs_formset.has_changed():
if itemtxs_formset.is_valid():
itemtxs_list = itemtxs_formset.save(commit=False)
for itemtxs in itemtxs_list:
itemtxs.bill_model_id = bill_model.uuid
itemtxs.clean()
itemtxs_formset.save()
itemtxs_qs = bill_model.update_amount_due()
bill_model.get_state(commit=True)
bill_model.clean()
bill_model.save(
update_fields=[
"amount_due",
"amount_receivable",
"amount_unearned",
"amount_earned",
"updated",
]
)
bill_model.migrate_state(
entity_slug=self.kwargs["entity_slug"],
user_model=self.request.user,
itemtxs_qs=itemtxs_qs,
raise_exception=False,
)
messages.add_message(
request,
message=f"Items for Invoice {bill_model.bill_number} saved.",
level=messages.SUCCESS,
)
# if valid get saved formset from DB
return HttpResponseRedirect(
redirect_to=reverse(
"bill-update",
kwargs={
"dealer_slug": dealer_slug,
"entity_slug": entity_model.slug,
"bill_pk": bill_pk,
},
)
)
context = self.get_context_data(itemtxs_formset=itemtxs_formset)
return self.render_to_response(context=context)
return super(BillModelUpdateView, self).post(
request, dealer_slug, entity_slug, bill_pk, **kwargs
)
class BaseBillActionView(LoginRequiredMixin,PermissionRequiredMixin, RedirectView, SingleObjectMixin):
http_method_names = ['get']
pk_url_kwarg = 'bill_pk'
action_name = None
commit = True
permission_required = "django_ledger.change_billmodel"
queryset = None
def get_queryset(self):
if self.queryset is None:
dealer = get_object_or_404(Dealer, slug=self.kwargs["dealer_slug"])
entity_model = dealer.entity
qs = entity_model.get_bills()
self.queryset = qs
return super().get_queryset()
def get_redirect_url(self, dealer_slug, entity_slug, bill_pk, *args, **kwargs):
return reverse(
"bill-update",
kwargs={
"dealer_slug": dealer_slug,
"entity_slug": entity_slug,
"bill_pk": bill_pk,
},
)
def get(self, request, *args, **kwargs):
dealer = get_object_or_404(Dealer, slug=self.kwargs["dealer_slug"])
kwargs['user_model'] = dealer.entity.admin
if not self.action_name:
raise ImproperlyConfigured('View attribute action_name is required.')
response = super(BaseBillActionView, self).get(request, *args, **kwargs)
bill_model: BillModel = self.get_object()
try:
getattr(bill_model, self.action_name)(commit=self.commit, **kwargs)
except ValidationError as e:
messages.add_message(request,
message=e.message,
level=messages.ERROR,
extra_tags='is-danger')
return response

View File

@ -17,7 +17,8 @@ from django_ledger.models import (
LedgerModel, LedgerModel,
AccountModel, AccountModel,
PurchaseOrderModel, PurchaseOrderModel,
EstimateModel EstimateModel,
BillModel
) )
from . import models from . import models
from django.utils.timezone import now from django.utils.timezone import now
@ -949,12 +950,12 @@ def create_po_item_upload(sender,instance,created,**kwargs):
def create_po_fulfilled_notification(sender,instance,created,**kwargs): def create_po_fulfilled_notification(sender,instance,created,**kwargs):
if instance.po_status == "fulfilled": if instance.po_status == "fulfilled":
dealer = models.Dealer.objects.get(entity=instance.entity) dealer = models.Dealer.objects.get(entity=instance.entity)
accountants = models.CustomGroup.objects.filter(dealer=dealer,name="Inventory").first().group.user_set.exclude(email=dealer.user.email) accountants = models.CustomGroup.objects.filter(dealer=dealer,name="Inventory").first().group.user_set.exclude(email=dealer.user.email).distinct()
for accountant in accountants: for accountant in accountants:
models.Notification.objects.create( models.Notification.objects.create(
user=accountant, user=accountant,
message=f""" message=f"""
New Purchase Order {instance.po_number} has been added to dealer {instance.dealer.name}. New Purchase Order {instance.po_number} has been added to dealer {dealer.name}.
<a href="{instance.get_absolute_url()}" target="_blank">View</a> <a href="{instance.get_absolute_url()}" target="_blank">View</a>
""", """,
) )
@ -962,7 +963,11 @@ def create_po_fulfilled_notification(sender,instance,created,**kwargs):
@receiver(post_save, sender=models.Car) @receiver(post_save, sender=models.Car)
def car_created_notification(sender, instance, created, **kwargs): def car_created_notification(sender, instance, created, **kwargs):
if created: if created:
accountants = models.CustomGroup.objects.filter(dealer=instance.dealer,name__in=["Manager","Accountant"]).first().group.user_set.all() <<<<<<< HEAD
accountants = models.CustomGroup.objects.filter(dealer=instance.dealer,name__in=["Manager","Accountant","Inventory"]).first().group.user_set.all().distinct()
=======
accountants = models.CustomGroup.objects.filter(dealer=instance.dealer,name__in=["Manager","Accountant"]).first().group.user_set.all().distinct()
>>>>>>> b23e84331fbb3a8ce0814a04b68cd3380b93d3a2
for accountant in accountants: for accountant in accountants:
models.Notification.objects.create( models.Notification.objects.create(
user=accountant, user=accountant,
@ -971,11 +976,17 @@ def car_created_notification(sender, instance, created, **kwargs):
<a href="{instance.get_absolute_url()}" target="_blank">View</a> <a href="{instance.get_absolute_url()}" target="_blank">View</a>
""", """,
) )
@receiver(post_save, sender=PurchaseOrderModel) @receiver(post_save, sender=PurchaseOrderModel)
def po_fullfilled_notification(sender, instance, created, **kwargs): def po_fullfilled_notification(sender, instance, created, **kwargs):
if instance.is_fulfilled(): if instance.is_fulfilled():
dealer = models.Dealer.objects.get(entity=instance.entity) dealer = models.Dealer.objects.get(entity=instance.entity)
recipients = models.CustomGroup.objects.filter(dealer=instance.dealer,name="Accountant").first().group.user_set.all() recipients = User.objects.filter(
groups__customgroup__dealer=instance.dealer,
groups__customgroup__name__in=["Manager", "Inventory"]
).distinct()
for recipient in recipients: for recipient in recipients:
models.Notification.objects.create( models.Notification.objects.create(
user=recipient, user=recipient,
@ -987,21 +998,23 @@ def po_fullfilled_notification(sender, instance, created, **kwargs):
@receiver(post_save, sender=models.Vendor) @receiver(post_save, sender=models.Vendor)
def vendor_created_notification(sender, instance, created, **kwargs): def vendor_created_notification(sender, instance, created, **kwargs):
if created: if created:
recipients = models.CustomGroup.objects.filter(dealer=instance.dealer,name="Inventory").first().group.user_set.all() recipients = User.objects.filter(
groups__customgroup__dealer=instance.dealer,
groups__customgroup__name__in=["Manager", "Inventory"]
).distinct()
for recipient in recipients: for recipient in recipients:
models.Notification.objects.create( models.Notification.objects.create(
user=recipient, user=recipient,
message=f""" message=f"""
New Vendor {instance.name} has been added to dealer {instance.dealer.name}. New Vendor {instance.name} has been added to dealer {instance.dealer.name}.
<a href="{instance.get_absolute_url()}" target="_blank">View</a>
""", """,
) )
@receiver(post_save, sender=models.SaleOrder) @receiver(post_save, sender=models.SaleOrder)
def sale_order_created_notification(sender, instance, created, **kwargs): def sale_order_created_notification(sender, instance, created, **kwargs):
if created: if created:
recipients = models.CustomGroup.objects.filter(dealer=instance.dealer,name="Accountant").first().group.user_set.exclude(email=instance.dealer.user.email) recipients = models.CustomGroup.objects.filter(dealer=instance.dealer,name="Accountant").first().group.user_set.exclude(email=instance.dealer.user.email).distinct()
for recipient in recipients: for recipient in recipients:
models.Notification.objects.create( models.Notification.objects.create(
@ -1026,7 +1039,7 @@ def lead_created_notification(sender, instance, created, **kwargs):
def estimate_in_review_notification(sender, instance, created, **kwargs): def estimate_in_review_notification(sender, instance, created, **kwargs):
if instance.is_review(): if instance.is_review():
dealer = models.Dealer.objects.get(entity=instance.entity) dealer = models.Dealer.objects.get(entity=instance.entity)
recipients = models.CustomGroup.objects.filter(dealer=dealer,name="Manager").first().group.user_set.exclude(email=dealer.user.email) recipients = models.CustomGroup.objects.filter(dealer=dealer,name="Manager").first().group.user_set.exclude(email=dealer.user.email).distinct()
for recipient in recipients: for recipient in recipients:
models.Notification.objects.create( models.Notification.objects.create(
user=recipient, user=recipient,
@ -1055,19 +1068,33 @@ def estimate_in_approve_notification(sender, instance, created, **kwargs):
""" """
) )
@receiver(post_save, sender=BillModel)
def bill_model_in_approve_notification(sender, instance, created, **kwargs):
if instance.is_review():
dealer = models.Dealer.objects.get(entity=instance.ledger.entity)
recipients = models.CustomGroup.objects.filter(dealer=dealer,name="Manager").first().group.user_set.exclude(email=dealer.user.email).distinct()
# @receiver(post_save, sender=models.Lead) for recipient in recipients:
# def lead_created_notification(sender, instance, created, **kwargs): models.Notification.objects.create(
# if created: user=recipient,
# models.Notification.objects.create( message=f"""
# user=instance.staff.user, Bill {instance.bill_number} is in review,please review and approve it
# message=f""" <a href="{reverse('bill-detail', kwargs={'dealer_slug': dealer.slug, 'entity_slug':dealer.entity.slug, 'bill_pk': instance.pk})}" target="_blank">View</a>.
# New Lead has been added. """
# <a href="{reverse('lead_detail',kwargs={'dealer_slug':instance.dealer.slug,'slug':instance.slug})}" target="_blank">View</a> )
# """,
# )
@receiver(post_save, sender=BillModel)
def bill_model_after_approve_notification(sender, instance, created, **kwargs):
if instance.is_approved():
dealer = models.Dealer.objects.get(entity=instance.ledger.entity)
recipients = models.CustomGroup.objects.filter(dealer=dealer,name="Accountant").first().group.user_set.exclude(email=dealer.user.email).distinct()
# send notification after car is sold {manager,dealer} for recipient in recipients:
# after po review send notification to {manager} to approve po models.Notification.objects.create(
# after estimate review send notification to {manager} to approve estimate user=recipient,
message=f"""
Bill {instance.bill_number} has been approved.
<a href="{reverse('bill-detail', kwargs={'dealer_slug': dealer.slug, 'entity_slug':dealer.entity.slug, 'bill_pk': instance.pk})}" target="_blank">View</a>.
please comlete the bill payment.
"""
)

View File

@ -454,14 +454,16 @@ def po_item_table1(context, queryset):
@register.inclusion_tag( @register.inclusion_tag(
"purchase_orders/includes/po_item_formset.html", takes_context=True "purchase_orders/includes/po_item_formset.html", takes_context=True
) )
def po_item_formset_table(context, po_model, itemtxs_formset): def po_item_formset_table(context, po_model, itemtxs_formset,user):
# print(len(itemtxs_formset.forms)) # print(len(itemtxs_formset.forms))
for form in itemtxs_formset.forms: for form in itemtxs_formset.forms:
form.fields["item_model"].queryset = form.fields["item_model"].queryset.filter( form.fields["item_model"].queryset = form.fields["item_model"].queryset.filter(
item_role="inventory" item_role="inventory"
) )
return { return {
"can_add_bill": user.has_perm("django_ledger.add_billmodel"),
"can_view_bill": user.has_perm("django_ledger.view_billmodel"),
"dealer_slug": context["view"].kwargs["dealer_slug"], "dealer_slug": context["view"].kwargs["dealer_slug"],
"entity_slug": context["view"].kwargs["entity_slug"], "entity_slug": context["view"].kwargs["entity_slug"],
"po_model": po_model, "po_model": po_model,

View File

@ -120,6 +120,11 @@ urlpatterns = [
views.update_task, views.update_task,
name="update_task", name="update_task",
), ),
path(
"<slug:dealer_slug>/<int:pk>/update-schedule/",
views.update_schedule,
name="update_schedule",
),
path( path(
"<slug:dealer_slug>/crm/<str:content_type>/<slug:slug>/add-task/", "<slug:dealer_slug>/crm/<str:content_type>/<slug:slug>/add-task/",
views.add_task, views.add_task,
@ -141,9 +146,9 @@ urlpatterns = [
name="send_lead_email_with_template", name="send_lead_email_with_template",
), ),
path( path(
"<slug:dealer_slug>/crm/leads/<slug:slug>/schedule/", "<slug:dealer_slug>/crm/<str:content_type>/<slug:slug>/schedule/",
views.schedule_lead, views.schedule_event,
name="schedule_lead", name="schedule_event",
), ),
path( path(
"<slug:dealer_slug>/crm/leads/schedule/<int:pk>/cancel/", "<slug:dealer_slug>/crm/leads/schedule/<int:pk>/cancel/",
@ -802,17 +807,17 @@ urlpatterns = [
), ),
path( path(
"<slug:dealer_slug>/items/bills/<slug:entity_slug>/detail/<uuid:bill_pk>/", "<slug:dealer_slug>/items/bills/<slug:entity_slug>/detail/<uuid:bill_pk>/",
views.BillModelDetailViewView.as_view(), views.BillModelDetailView.as_view(),
name="bill-detail", name="bill-detail",
), ),
path( path(
"<slug:dealer_slug>/items/bills/<slug:entity_slug>/update/<uuid:bill_pk>/", "<slug:dealer_slug>/items/bills/<slug:entity_slug>/update/<uuid:bill_pk>/",
views.BillModelUpdateViewView.as_view(), views.BillModelUpdateView.as_view(),
name="bill-update", name="bill-update",
), ),
path( path(
"<slug:dealer_slug>/items/bills/<slug:entity_slug>/update/<uuid:bill_pk>/items/", "<slug:dealer_slug>/items/bills/<slug:entity_slug>/update/<uuid:bill_pk>/items/",
views.BillModelUpdateViewView.as_view(action_update_items=True), views.BillModelUpdateView.as_view(action_update_items=True),
name="bill-update-items", name="bill-update-items",
), ),
############################################################ ############################################################

View File

@ -1419,12 +1419,23 @@ def handle_payment(request, order):
headers = {"Content-Type": "application/json", "Accept": "application/json"} headers = {"Content-Type": "application/json", "Accept": "application/json"}
auth = (settings.MOYASAR_SECRET_KEY, "") auth = (settings.MOYASAR_SECRET_KEY, "")
response = requests.request("POST", url, auth=auth, headers=headers, data=payload) response = requests.request("POST", url, auth=auth, headers=headers, data=payload)
if response.status_code == 400:
data = response.json()
if data["type"] == "validation_error":
errors = data.get("errors", {})
if "source.year" in errors:
raise Exception("Invalid expiry year")
else:
raise Exception("Validation Error: ", errors)
else:
print("Failed to process payment:", data)
# #
order.status = AbstractOrder.STATUS.NEW order.status = AbstractOrder.STATUS.NEW
order.save() order.save()
# #
data = response.json() data = response.json()
amount = Decimal("{0:.2f}".format(Decimal(total) / Decimal(100))) amount = Decimal("{0:.2f}".format(Decimal(total) / Decimal(100)))
print(data)
models.PaymentHistory.objects.create( models.PaymentHistory.objects.create(
user=request.user, user=request.user,
user_data=user_data, user_data=user_data,

File diff suppressed because it is too large Load Diff

View File

@ -184,10 +184,12 @@
</td> </td>
<td class="align-items-start white-space-nowrap pe-2"> <td class="align-items-start white-space-nowrap pe-2">
{% if bill_item.po_model_id %} {% if bill_item.po_model_id %}
{% if perms.django_ledger.view_purchaseordermodel%}
<a class="btn btn-sm btn-phoenix-primary" <a class="btn btn-sm btn-phoenix-primary"
href="{% url 'purchase_order_detail' request.dealer.slug request.dealer.entity.slug bill_item.po_model_id %}"> href="{% url 'purchase_order_detail' request.dealer.slug request.dealer.entity.slug bill_item.po_model_id %}">
{% trans 'View PO' %} {% trans 'View PO' %}
</a> </a>
{% endif %}
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
@ -231,9 +233,11 @@
<h5 class="mb-0">{% trans 'Bill Notes' %}</h5> <h5 class="mb-0">{% trans 'Bill Notes' %}</h5>
</div> </div>
</div> </div>
{% if perms.django_ledger.change_billmodel%}
<div class="card-body"> <div class="card-body">
{% include 'bill/includes/card_markdown.html' with style='card_1' title='' notes_html=bill.notes_html %} {% include 'bill/includes/card_markdown.html' with style='card_1' title='' notes_html=bill.notes_html %}
</div> </div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>

View File

@ -207,7 +207,7 @@
{% if perms.django_ledger.change_billmodel%} {% if perms.django_ledger.change_billmodel%}
<a href="{% url 'bill-update' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill.uuid %}" class="btn btn-phoenix-primary"> <a href="{% url 'bill-update' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill.uuid %}" class="btn btn-phoenix-primary">
<i class="fas fa-edit me-2"></i>{% trans 'Update' %} <i class="fas fa-edit me-2"></i>{% trans 'Update' %}
</a>
<!-- Mark as Draft --> <!-- Mark as Draft -->
{% if bill.can_draft %} {% if bill.can_draft %}
<button class="btn btn-phoenix-success" <button class="btn btn-phoenix-success"
@ -223,7 +223,7 @@
</button> </button>
{% endif %} {% endif %}
<!-- Mark as Approved --> <!-- Mark as Approved -->
{% if bill.can_approve and perms.django_ledger.can_approve_bill %} {% if bill.can_approve and perms.django_ledger.can_approve_billmodel %}
<button class="btn btn-phoenix-success" <button class="btn btn-phoenix-success"
onclick="showPOModal('Mark as Approved', '{% url 'bill-action-mark-as-approved' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Approved')"> onclick="showPOModal('Mark as Approved', '{% url 'bill-action-mark-as-approved' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Approved')">
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Approved' %} <i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Approved' %}

View File

@ -0,0 +1,30 @@
{% load i18n crispy_forms_tags %}
<div class="modal fade" id="noteModal" tabindex="-1" aria-labelledby="noteModalLabel" aria-hidden="true">
<div class="modal-dialog modal-md">
<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 'Note' %}</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">
<form action="{% url 'add_note' request.dealer.slug content_type slug %}" method="post" class="add_note_form">
{% csrf_token %}
{{ note_form|crispy }}
<button type="submit" class="btn btn-phoenix-success w-100">{% trans 'Save' %}</button>
</form>
</div>
</div>
</div>
</div>
<script>
function updateNote(e) {
let url = e.getAttribute('data-url')
let note = e.getAttribute('data-note')
document.querySelector('#id_note').value = note
let form = document.querySelector('.add_note_form')
form.action = url
}
</script>

View File

@ -0,0 +1,20 @@
{% load i18n crispy_forms_filters %}
<div class="modal fade" id="scheduleModal" tabindex="-1" aria-labelledby="taskModalLabel" aria-hidden="true">
<div class="modal-dialog modal-md">
<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="taskModalLabel">{% trans 'Schedule' %}</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">
<form action="{% url 'schedule_event' request.dealer.slug content_type slug %}" method="post" class="add_schedule_form">
{% csrf_token %}
{{ schedule_form|crispy }}
<button type="submit" class="btn btn-phoenix-success w-100">{% trans 'Save' %}</button>
</form>
</div>
</div>
</div>
</div>

View File

@ -1,5 +1,11 @@
{% load static i18n crispy_forms_tags %} {% load static i18n crispy_forms_tags %}
<!-- task Modal --> <!-- task Modal -->
<style>
.completed-task {
text-decoration: line-through;
opacity: 0.7;
}
</style>
<div class="modal fade" id="taskModal" tabindex="-1" aria-labelledby="taskModalLabel" aria-hidden="true"> <div class="modal fade" id="taskModal" tabindex="-1" aria-labelledby="taskModalLabel" aria-hidden="true">
<div class="modal-dialog modal-md"> <div class="modal-dialog modal-md">
<div class="modal-content"> <div class="modal-content">

View File

@ -7,10 +7,6 @@
.main-tab li:last-child { .main-tab li:last-child {
margin-left: auto; margin-left: auto;
} }
.completed-task {
text-decoration: line-through;
opacity: 0.7;
}
.kanban-header { .kanban-header {
position: relative; position: relative;
background-color:rgb(237, 241, 245); background-color:rgb(237, 241, 245);
@ -60,8 +56,8 @@
<div class="d-flex justify-content-between align-items-center mb-2 d-md-none"> <div class="d-flex justify-content-between align-items-center mb-2 d-md-none">
<h3 class="mb-0">{{ _("Lead Details")}}</h3> <h3 class="mb-0">{{ _("Lead Details")}}</h3>
<button class="btn p-0" ><span class="uil uil-times fs-7"></span></button> <button class="btn p-0" ><span class="uil uil-times fs-7"></span></button>
</div> </div>
<div class="card mb-2"> <div class="card mb-2">
<div class="card-body"> <div class="card-body">
<div class="row align-items-center g-3 text-center text-xxl-start"> <div class="row align-items-center g-3 text-center text-xxl-start">
<div class="col-6 col-sm-auto flex-1"> <div class="col-6 col-sm-auto flex-1">
@ -109,7 +105,7 @@
<h5 class="fw-bolder mb-2 text-body-highlight">{{ _("Related Records") }}</h5> <h5 class="fw-bolder mb-2 text-body-highlight">{{ _("Related Records") }}</h5>
<h6 class="fw-bolder mb-2 text-body-highlight">{{ _("Opportunity") }}</h6> <h6 class="fw-bolder mb-2 text-body-highlight">{{ _("Opportunity") }}</h6>
{% if lead.opportunity %} {% if lead.opportunity %}
<a href="{% url 'opportunity_detail' lead.opportunity.slug %}" class="">{{ lead.opportunity }}</a> <a href="{% url 'opportunity_detail' request.dealer.slug lead.opportunity.slug %}" class="">{{ lead.opportunity }}</a>
{% else %} {% else %}
<p>{{ _("No Opportunity") }}</p> <p>{{ _("No Opportunity") }}</p>
{% endif %} {% endif %}
@ -167,14 +163,16 @@
<div class="kanban-header bg-secondary w-50 text-white fw-bold"><i class="fa-solid fa-circle-info me-2"></i>{{lead.next_action|capfirst}} <br> &nbsp; <small>{% trans "Next Action" %} :</small>&nbsp; <small>{{lead.next_action_date|naturalday|capfirst}}</small></div> <div class="kanban-header bg-secondary w-50 text-white fw-bold"><i class="fa-solid fa-circle-info me-2"></i>{{lead.next_action|capfirst}} <br> &nbsp; <small>{% trans "Next Action" %} :</small>&nbsp; <small>{{lead.next_action_date|naturalday|capfirst}}</small></div>
</div> </div>
<ul class="nav main-tab nav-underline fs-9 deal-details scrollbar flex-nowrap w-100 pb-1 mb-6 justify-content-end mt-5" id="myTab" role="tablist" style="overflow-y: hidden;"> <ul class="nav main-tab nav-underline fs-9 deal-details scrollbar flex-nowrap w-100 pb-1 mb-6 justify-content-end mt-5" id="myTab" role="tablist" style="overflow-y: hidden;">
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link active" id="opportunity-tab" data-bs-toggle="tab" href="#tab-opportunity" role="tab" aria-controls="tab-opportunity" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-chart-line me-2 tab-icon-color fs-8"></span>{{ _("Opportunities") }}</a></li> <li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link active" id="tasks-tab" data-bs-toggle="tab" href="#tab-tasks" role="tab" aria-controls="tab-tasks" aria-selected="true"> <span class="fa-solid fa-envelope me-2 tab-icon-color fs-8"></span>{{ _("Tasks") }}</a></li>
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="activity-tab" data-bs-toggle="tab" href="#tab-activity" role="tab" aria-controls="tab-activity" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-chart-line me-2 tab-icon-color fs-8"></span>{{ _("Activity") }}</a></li> <li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="notes-tab" data-bs-toggle="tab" href="#tab-notes" role="tab" aria-controls="tab-notes" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-clipboard me-2 tab-icon-color fs-8"></span>{{ _("Notes") }}</a></li>
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="notes-tab" data-bs-toggle="tab" href="#tab-notes" role="tab" aria-controls="tab-notes" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-clipboard me-2 tab-icon-color fs-8"></span>{{ _("Notes") }}</a></li> <li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="emails-tab" data-bs-toggle="tab" href="#tab-emails" role="tab" aria-controls="tab-emails" aria-selected="true"> <span class="fa-solid fa-envelope me-2 tab-icon-color fs-8"></span>{{ _("Emails") }}</a></li>
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="emails-tab" data-bs-toggle="tab" href="#tab-emails" role="tab" aria-controls="tab-emails" aria-selected="true"> <span class="fa-solid fa-envelope me-2 tab-icon-color fs-8"></span>{{ _("Emails") }}</a></li> <li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="activity-tab" data-bs-toggle="tab" href="#tab-activity" role="tab" aria-controls="tab-activity" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-chart-line me-2 tab-icon-color fs-8"></span>{{ _("Activity") }}</a></li>
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="tasks-tab" data-bs-toggle="tab" href="#tab-tasks" role="tab" aria-controls="tab-tasks" aria-selected="true"> <span class="fa-solid fa-envelope me-2 tab-icon-color fs-8"></span>{{ _("Tasks") }}</a></li> <li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="opportunity-tab" data-bs-toggle="tab" href="#tab-opportunity" role="tab" aria-controls="tab-opportunity" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-chart-line me-2 tab-icon-color fs-8"></span>{{ _("Opportunities") }}</a></li>
{% if perms.inventory.change_lead%} {% if perms.inventory.change_lead%}
<li class="nav-item text-nowrap ml-auto" role="presentation"> <li class="nav-item text-nowrap ml-auto" role="presentation">
<button class="btn btn-phoenix-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#exampleModal"> <i class="fa-solid fa-user-plus me-2"></i> Reassign Lead</button> {% if perms.inventory.can_reassign_lead %}
<button class="btn btn-phoenix-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#exampleModal"> <i class="fa-solid fa-user-plus me-2"></i> Reassign Lead</button>
{% endif %}
<button class="btn btn-phoenix-primary btn-sm" onclick="openActionModal('{{ lead.id }}', '{{ lead.action }}', '{{ lead.next_action }}', '{{ lead.next_action_date|date:"Y-m-d\TH:i" }}')"> <button class="btn btn-phoenix-primary btn-sm" onclick="openActionModal('{{ lead.id }}', '{{ lead.action }}', '{{ lead.next_action }}', '{{ lead.next_action_date|date:"Y-m-d\TH:i" }}')">
<i class="fa-solid fa-user-plus me-2"></i> <i class="fa-solid fa-user-plus me-2"></i>
{% trans "Update Actions" %} {% trans "Update Actions" %}
@ -207,7 +205,6 @@
<div class="mb-1 d-flex justify-content-between align-items-center"> <div class="mb-1 d-flex justify-content-between align-items-center">
<h3 class="mb-4" id="s crollspyTask">{{ _("Activities") }} <span class="fw-light fs-7">({{ activities.count}})</span></h3> <h3 class="mb-4" id="s crollspyTask">{{ _("Activities") }} <span class="fw-light fs-7">({{ activities.count}})</span></h3>
{% if perms.inventory.change_lead%} {% if perms.inventory.change_lead%}
<button class="btn btn-phoenix-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#activityModal"><span class="fas fa-plus me-1"></span>{{ _("Add Activity") }}</button>
{% endif %} {% endif %}
</div> </div>
<div class="row justify-content-between align-items-md-center hover-actions-trigger btn-reveal-trigger border-translucent py-3 gx-0 border-top"> <div class="row justify-content-between align-items-md-center hover-actions-trigger btn-reveal-trigger border-translucent py-3 gx-0 border-top">
@ -221,6 +218,10 @@
<div class="icon-item icon-item-md rounded-7 border border-translucent"> <div class="icon-item icon-item-md rounded-7 border border-translucent">
{% if activity.activity_type == "call" %} {% if activity.activity_type == "call" %}
<span class="fa-solid fa-phone text-warning fs-8"></span> <span class="fa-solid fa-phone text-warning fs-8"></span>
{% elif activity.activity_type == "note" %}
<span class="fa-regular fa-sticky-note text-info-light fs-8"></span>
{% elif activity.activity_type == "task" %}
<span class="fa-solid fa-list-check text-success fs-8"></span>
{% elif activity.activity_type == "email" %} {% elif activity.activity_type == "email" %}
<span class="fa-solid fa-envelope text-info-light fs-8"></span> <span class="fa-solid fa-envelope text-info-light fs-8"></span>
{% elif activity.activity_type == "visit" %} {% elif activity.activity_type == "visit" %}
@ -253,7 +254,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="tab-pane fade active show" id="tab-opportunity" role="tabpanel" aria-labelledby="opportunity-tab"> <div class="tab-pane fade" id="tab-opportunity" role="tabpanel" aria-labelledby="opportunity-tab">
<div class="mb-1 d-flex justify-content-between align-items-center"> <div class="mb-1 d-flex justify-content-between align-items-center">
<h3 class="mb-4" id="scrollspyTask">{{ _("Opportunities") }} <span class="fw-light fs-7">({{ lead.get_opportunities.count}})</span></h3> <h3 class="mb-4" id="scrollspyTask">{{ _("Opportunities") }} <span class="fw-light fs-7">({{ lead.get_opportunities.count}})</span></h3>
{% if perms.inventory.add_opportunity%} {% if perms.inventory.add_opportunity%}
@ -290,7 +291,7 @@
<div class="tab-pane fade" id="tab-notes" role="tabpanel" aria-labelledby="notes-tab"> <div class="tab-pane fade" id="tab-notes" role="tabpanel" aria-labelledby="notes-tab">
<div class="mb-1 d-flex align-items-center justify-content-between"> <div class="mb-1 d-flex align-items-center justify-content-between">
<h3 class="mb-4" id="scrollspyNotes">{{ _("Notes") }}</h3> <h3 class="mb-4" id="scrollspyNotes">{{ _("Notes") }}</h3>
{% if perms.inventory.change_lead%} {% if perms.inventory.change_lead %}
<button class="btn btn-phoenix-primary btn-sm" type="button" onclick="reset_form()" data-bs-toggle="modal" data-bs-target="#noteModal"><span class="fas fa-plus me-1"></span>{{ _("Add Note") }}</button> <button class="btn btn-phoenix-primary btn-sm" type="button" onclick="reset_form()" data-bs-toggle="modal" data-bs-target="#noteModal"><span class="fas fa-plus me-1"></span>{{ _("Add Note") }}</button>
{% endif %} {% endif %}
</div> </div>
@ -301,8 +302,8 @@
<thead> <thead>
<tr> <tr>
<th class="align-middle pe-6 text-uppercase text-start" scope="col" style="width:40%;">{{ _("Note") }}</th> <th class="align-middle pe-6 text-uppercase text-start" scope="col" style="width:40%;">{{ _("Note") }}</th>
<th class="align-middle text-start text-uppercase" scope="col" style="width:20%;">{{ _("Created By")}}</th> <th class="align-middle text-start text-uppercase white-space-nowrap" scope="col" style="width:40%;">{{ _("Created On")}}</th>
<th class="align-middle text-start text-uppercase white-space-nowrap" scope="col" style="width:20%;">{{ _("Created On")}}</th> <th class="align-middle text-start text-uppercase white-space-nowrap" scope="col" style="width:40%;">{{ _("Last Updated")}}</th>
<th class="align-middle pe-0 text-end" scope="col" style="width:10%;"> </th> <th class="align-middle pe-0 text-end" scope="col" style="width:10%;"> </th>
</tr> </tr>
</thead> </thead>
@ -310,12 +311,8 @@
{% for note in notes %} {% for note in notes %}
<tr class="hover-actions-trigger btn-reveal-trigger position-static"> <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-start fw-bold text-body-tertiary ps-1">{{note.note}}</td>
{% if note.created_by.staff %}
<td class="align-middle white-space-nowrap text-start white-space-nowrap">{{ note.created_by.staff.name }}</td>
{% else %}
<td class="align-middle white-space-nowrap text-start white-space-nowrap">{{ note.created_by.dealer.get_local_name|default:note.created_by.dealer.name }}</td>
{% endif %}
<td class="align-middle text-body-tertiary text-start white-space-nowrap">{{ note.created|naturalday|capfirst }}</td> <td class="align-middle text-body-tertiary text-start white-space-nowrap">{{ note.created|naturalday|capfirst }}</td>
<td class="align-middle text-body-tertiary text-start white-space-nowrap">{{ note.updated|naturalday|capfirst }}</td>
<td class="align-middle text-end white-space-nowrap pe-0 action py-2"> <td class="align-middle text-end white-space-nowrap pe-0 action py-2">
{% if note.created_by == request.user %} {% if note.created_by == request.user %}
<a id="updateBtn" <a id="updateBtn"
@ -446,7 +443,7 @@
</td> </td>
<td class="sent align-middle white-space-nowrap text-start fw-bold text-body-tertiary py-2">{{email.from_email}}</td> <td class="sent align-middle white-space-nowrap text-start fw-bold text-body-tertiary py-2">{{email.from_email}}</td>
<td class="date align-middle white-space-nowrap text-body py-2">{{email.created}}</td> <td class="date align-middle white-space-nowrap text-body py-2">{{email.created}}</td>
<td class="align-middle white-space-nowrap ps-3"><a class="text-body" href="{% url 'send_lead_email_with_template' lead.slug email.pk %}"><span class="fa-solid fa-email text-primary me-2"></span>Send</a></td> <td class="align-middle white-space-nowrap ps-3"><a class="text-body" href="{% url 'send_lead_email_with_template' request.dealer.slug lead.slug email.pk %}"><span class="fa-solid fa-email text-primary me-2"></span>Send</a></td>
<td class="status align-middle fw-semibold text-end py-2"><span class="badge badge-phoenix fs-10 badge-phoenix-warning">draft</span></td> <td class="status align-middle fw-semibold text-end py-2"><span class="badge badge-phoenix fs-10 badge-phoenix-warning">draft</span></td>
</tr> </tr>
{% endfor %} {% endfor %}
@ -469,11 +466,12 @@
</div> </div>
</div> </div>
{% comment %} {% endcomment %} {% comment %} {% endcomment %}
<div class="tab-pane fade" id="tab-tasks" role="tabpanel" aria-labelledby="tasks-tab"> <div class="tab-pane fade active show" id="tab-tasks" role="tabpanel" aria-labelledby="tasks-tab">
<div class="mb-1 d-flex justify-content-between align-items-center"> <div class="mb-1 d-flex justify-content-between align-items-center">
<h3 class="mb-0" id="scrollspyEmails">{{ _("Tasks") }}</h3> <h3 class="mb-0" id="scrollspyEmails">{{ _("Tasks") }}</h3>
{% if perms.inventory.change_lead%} {% if perms.inventory.change_lead%}
<button class="btn btn-phoenix-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#taskModal"><span class="fas fa-plus me-1"></span>{{ _("Add Task") }}</button> {% comment %} <button class="btn btn-phoenix-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#taskModal"><span class="fas fa-plus me-1"></span>{{ _("Add Task") }}</button> {% endcomment %}
<button class="btn btn-phoenix-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#scheduleModal"><span class="fas fa-plus me-1"></span>{{ _("Add Task") }}</button>
{% endif %} {% endif %}
</div> </div>
<div> <div>
@ -495,7 +493,7 @@
</tr> </tr>
</thead> </thead>
<tbody class="list" id="all-tasks-table-body"> <tbody class="list" id="all-tasks-table-body">
{% for task in tasks %} {% for task in schedules %}
{% include "partials/task.html" %} {% include "partials/task.html" %}
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -533,60 +531,16 @@
</div> </div>
</div> {% endcomment %} </div> {% endcomment %}
<!-- activity Modal -->
{% include "components/activity_modal.html" with content_type="lead" slug=lead.slug %}
<!-- task Modal --> <!-- task Modal -->
<div class="modal fade" id="taskModal" tabindex="-1" aria-labelledby="taskModalLabel" aria-hidden="true"> {% include "components/task_modal.html" with content_type="lead" slug=lead.slug %}
<div class="modal-dialog modal-md">
<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="taskModalLabel">{% trans 'Task' %}</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">
<form action="{% url 'add_task' request.dealer.slug 'lead' lead.slug %}" method="post" class="add_task_form">
{% csrf_token %}
{{ staff_task_form|crispy }}
<button type="submit" class="btn btn-phoenix-success w-100">{% trans 'Save' %}</button>
</form>
</div>
</div>
</div>
</div>
<!-- note Modal --> <!-- note Modal -->
<div class="modal fade" id="noteModal" tabindex="-1" aria-labelledby="noteModalLabel" aria-hidden="true"> {% include "components/note_modal.html" with content_type="lead" slug=lead.slug %}
<div class="modal-dialog modal-md"> <!-- schedule Modal -->
<div class="modal-content"> {% include "components/schedule_modal.html" with content_type="lead" slug=lead.slug %}
<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 'Note' %}</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">
<form action="{% url 'add_note' request.dealer.slug 'lead' lead.slug %}" method="post" class="add_note_form">
{% csrf_token %}
{{ note_form|crispy }}
<button type="submit" class="btn btn-phoenix-success w-100">{% trans 'Save' %}</button>
</form>
</div>
</div>
</div>
</div>
{% endblock content %} {% endblock content %}
{% block customJS %} {% block customJS %}
<script> <script>
function updateNote(e) {
let url = e.getAttribute('data-url')
let note = e.getAttribute('data-note')
document.querySelector('#id_note').value = note
let form = document.querySelector('.add_note_form')
form.action = url
}
function reset_form() { function reset_form() {
document.querySelector('#id_note').value = "" document.querySelector('#id_note').value = ""
let form = document.querySelector('.add_note_form') let form = document.querySelector('.add_note_form')

View File

@ -129,50 +129,7 @@
<td class="align-middle white-space-nowrap fw-semibold"><a class="text-body-highlight" href="">{{ lead.id_car_make.get_local_name }} - {{ lead.id_car_model.get_local_name }} {{ lead.year }}</a></td> <td class="align-middle white-space-nowrap fw-semibold"><a class="text-body-highlight" href="">{{ lead.id_car_make.get_local_name }} - {{ lead.id_car_model.get_local_name }} {{ lead.year }}</a></td>
<td class="align-middle white-space-nowrap fw-semibold"><a class="text-body-highlight" href="">{{ lead.email }}</a></td> <td class="align-middle white-space-nowrap fw-semibold"><a class="text-body-highlight" href="">{{ lead.email }}</a></td>
<td class="align-middle white-space-nowrap fw-semibold"><a class="text-body-highlight" href="tel:{{ lead.phone_number }}">{{ lead.phone_number }}</a></td> <td class="align-middle white-space-nowrap fw-semibold"><a class="text-body-highlight" href="tel:{{ lead.phone_number }}">{{ lead.phone_number }}</a></td>
<td class="align-middle white-space-nowrap fw-semibold">
{% if request.user.staffmember.staff %}
<div class="accordion" id="accordionExample">
<div class="accordion-item">
<h2 class="accordion-header" id="headingTwo">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse{{lead.slug}}" aria-expanded="false" aria-controls="collapseTwo">
{{ _("View Schedules")}} ({{lead.get_latest_schedules.count}})
</button>
</h2>
<div class="accordion-collapse collapse" id="collapse{{lead.slug}}" aria-labelledby="headingTwo" data-bs-parent="#accordionExample">
<div class="accordion-body pt-0">
<div class="d-flex flex-column gap-2">
<table><tbody>
{% for schedule in lead.get_latest_schedules %}
<tr class="schedule-{{ schedule.pk }}">
<td class="align-middle white-space-nowrap">
{% if schedule.scheduled_type == "call" %}
<a href="{% url 'appointment:get_user_appointments' %}">
<span class="badge badge-phoenix badge-phoenix-primary text-primary {% if schedule.schedule_past_date %}badge-phoenix-danger text-danger{% endif %} fw-semibold"><span class="text-primary {% if schedule.schedule_past_date %}text-danger{% endif %}" data-feather="phone"></span>
{{ schedule.scheduled_at|naturaltime|capfirst }}</span></a>
{% elif schedule.scheduled_type == "meeting" %}
<a href="{% url 'appointment:get_user_appointments' %}">
<span class="badge badge-phoenix badge-phoenix-success text-success fw-semibold"><span class="text-success" data-feather="calendar"></span>
{{ schedule.scheduled_at|naturaltime|capfirst }}</span></a>
{% elif schedule.scheduled_type == "email" %}
<a href="{% url 'appointment:get_user_appointments' %}">
<span class="badge badge-phoenix badge-phoenix-warning text-warning fw-semibold"><span class="text-warning" data-feather="email"></span>
{{ schedule.scheduled_at|naturaltime|capfirst }}</span></a>
{% endif %}
</td>
<td>
<a style="cursor: pointer;" hx-delete="{% url 'schedule_cancel' schedule.pk %}" hx-target=".schedule-{{ schedule.pk }}" hx-confirm="Are you sure you want to cancel this schedule?"><i class="fa-solid fa-ban text-danger"></i></a>
</td>
{% endfor %}
</tr>
<tr><td><small><a href="{% url 'appointment:get_user_appointments' %}">View All ...</a></small></td></tr>
</tbody></table>
</div>
</div>
</div>
</div>
</div>
{% endif %}
</td>
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">{{ lead.get_status|upper }}</td> <td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">{{ lead.get_status|upper }}</td>
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">{{ lead.staff|upper }}</td> <td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">{{ lead.staff|upper }}</td>
{% comment %} <td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight"> {% comment %} <td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">
@ -216,8 +173,6 @@
<button class="dropdown-item text-primary" onclick="openActionModal('{{ lead.pk }}', '{{ lead.action }}', '{{ lead.next_action }}', '{{ lead.next_action_date|date:"Y-m-d\TH:i" }}')"> <button class="dropdown-item text-primary" onclick="openActionModal('{{ lead.pk }}', '{{ lead.action }}', '{{ lead.next_action }}', '{{ lead.next_action_date|date:"Y-m-d\TH:i" }}')">
{% trans "Update Actions" %} {% trans "Update Actions" %}
</button> </button>
<a href="{% url 'send_lead_email' request.dealer.slug lead.slug %}" class="dropdown-item text-success-dark">{% trans "Send Email" %}</a>
<a href="{% url 'schedule_lead' request.dealer.slug lead.slug %}" class="dropdown-item text-success-dark">{% trans "Schedule Event" %}</a>
{% endif %} {% endif %}
{% if not lead.opportunity %} {% if not lead.opportunity %}
{% if perms.inventory.add_opportunity%} {% if perms.inventory.add_opportunity%}
@ -257,8 +212,6 @@
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block customJS %} {% block customJS %}

View File

@ -93,7 +93,7 @@
<div class="kanban-column bg-body"> <div class="kanban-column bg-body">
<div class="kanban-header opacity-75"><span class="text-body">{{ _("Follow Ups")}} ({{follow_up|length}})</span></div> <div class="kanban-header opacity-75"><span class="text-body">{{ _("Follow Ups")}} ({{follow_up|length}})</span></div>
{% for lead in follow_up %} {% for lead in follow_up %}
<a href="{% url 'lead_detail' lead.slug %}"> <a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
<div class="lead-card"> <div class="lead-card">
<strong>{{lead.full_name|capfirst}}</strong><br> <strong>{{lead.full_name|capfirst}}</strong><br>
<small>{{lead.email}}</small><br> <small>{{lead.email}}</small><br>
@ -109,7 +109,7 @@
<div class="kanban-column bg-body"> <div class="kanban-column bg-body">
<div class="kanban-header opacity-75"><span class="text-body">{{ _("Negotiation Ups")}} ({{follow_up|length}})</span></div> <div class="kanban-header opacity-75"><span class="text-body">{{ _("Negotiation Ups")}} ({{follow_up|length}})</span></div>
{% for lead in negotiation %} {% for lead in negotiation %}
<a href="{% url 'lead_detail' lead.slug %}"> <a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
<div class="lead-card"> <div class="lead-card">
<strong>{{lead.full_name|capfirst}}</strong><br> <strong>{{lead.full_name|capfirst}}</strong><br>
<small>{{lead.email}}</small><br> <small>{{lead.email}}</small><br>
@ -125,7 +125,7 @@
<div class="kanban-column bg-body"> <div class="kanban-column bg-body">
<div class="kanban-header bg-success-light opacity-75"><span class="text-body">{{ _("Won") }} ({{won|length}}) ({{follow_up|length}})</span></div> <div class="kanban-header bg-success-light opacity-75"><span class="text-body">{{ _("Won") }} ({{won|length}}) ({{follow_up|length}})</span></div>
{% for lead in won %} {% for lead in won %}
<a href="{% url 'lead_detail' lead.slug %}"> <a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
<div class="lead-card"> <div class="lead-card">
<strong>{{lead.full_name|capfirst}}</strong><br> <strong>{{lead.full_name|capfirst}}</strong><br>
<small>{{lead.email}}</small><br> <small>{{lead.email}}</small><br>
@ -141,7 +141,7 @@
<div class="kanban-column bg-body"> <div class="kanban-column bg-body">
<div class="kanban-header bg-danger-light opacity-75">{{ _("Lost") }} ({{lose|length}})</div> <div class="kanban-header bg-danger-light opacity-75">{{ _("Lost") }} ({{lose|length}})</div>
{% for lead in lose %} {% for lead in lose %}
<a href="{% url 'lead_detail' lead.slug %}"> <a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
<div class="lead-card"> <div class="lead-card">
<strong>{{lead.full_name|capfirst}}</strong><br> <strong>{{lead.full_name|capfirst}}</strong><br>
<small>{{lead.email}}</small><br> <small>{{lead.email}}</small><br>
@ -151,7 +151,6 @@
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -333,74 +333,118 @@
</div> </div>
</div> </div>
<ul class="nav nav-underline fs-9 deal-details scrollbar flex-nowrap w-100 pb-1 mb-6" id="myTab" role="tablist" style="overflow-y: hidden;"> <ul class="nav nav-underline fs-9 deal-details scrollbar flex-nowrap w-100 pb-1 mb-6" id="myTab" role="tablist" style="overflow-y: hidden;">
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link active" id="activity-tab" data-bs-toggle="tab" href="#tab-activity" role="tab" aria-controls="tab-activity" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-chart-line me-2 tab-icon-color"></span>{{ _("Activity") }}</a></li> <li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link active" id="tasks-tab" data-bs-toggle="tab" href="#tab-tasks" role="tab" aria-controls="tab-tasks" aria-selected="true"> <span class="fa-solid fa-envelope me-2 tab-icon-color fs-8"></span>{{ _("Tasks") }}</a></li>
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="notes-tab" data-bs-toggle="tab" href="#tab-notes" role="tab" aria-controls="tab-notes" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-clipboard me-2 tab-icon-color"></span>{{ _("Notes") }}</a></li> <li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="notes-tab" data-bs-toggle="tab" href="#tab-notes" role="tab" aria-controls="tab-notes" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-clipboard me-2 tab-icon-color"></span>{{ _("Notes") }}</a></li>
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="meeting-tab" data-bs-toggle="tab" href="#tab-meeting" role="tab" aria-controls="tab-meeting" aria-selected="true"> <span class="fa-solid fa-video me-2 tab-icon-color"></span>{{ _("Meetings") }}</a></li> {% comment %} <li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="meeting-tab" data-bs-toggle="tab" href="#tab-meeting" role="tab" aria-controls="tab-meeting" aria-selected="true"> <span class="fa-solid fa-video me-2 tab-icon-color"></span>{{ _("Meetings") }}</a></li> {% endcomment %}
{% comment %} <li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="task-tab" data-bs-toggle="tab" href="#tab-task" role="tab" aria-controls="tab-task" aria-selected="true"> <span class="fa-solid fa-square-check me-2 tab-icon-color"></span>Task</a></li> {% endcomment %} {% comment %} <li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="task-tab" data-bs-toggle="tab" href="#tab-task" role="tab" aria-controls="tab-task" aria-selected="true"> <span class="fa-solid fa-square-check me-2 tab-icon-color"></span>Task</a></li> {% endcomment %}
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="call-tab" data-bs-toggle="tab" href="#tab-call" role="tab" aria-controls="tab-call" aria-selected="true"> <span class="fa-solid fa-phone me-2 tab-icon-color"></span>{{ _("Calls") }}</a></li> {% comment %} <li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="call-tab" data-bs-toggle="tab" href="#tab-call" role="tab" aria-controls="tab-call" aria-selected="true"> <span class="fa-solid fa-phone me-2 tab-icon-color"></span>{{ _("Calls") }}</a></li> {% endcomment %}
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="emails-tab" data-bs-toggle="tab" href="#tab-emails" role="tab" aria-controls="tab-emails" aria-selected="true"> <span class="fa-solid fa-envelope me-2 tab-icon-color"></span>{{ _("Emails")}} </a></li> <li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="emails-tab" data-bs-toggle="tab" href="#tab-emails" role="tab" aria-controls="tab-emails" aria-selected="true"> <span class="fa-solid fa-envelope me-2 tab-icon-color"></span>{{ _("Emails")}} </a></li>
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="tasks-tab" data-bs-toggle="tab" href="#tab-tasks" role="tab" aria-controls="tab-tasks" aria-selected="true"> <span class="fa-solid fa-envelope me-2 tab-icon-color fs-8"></span>{{ _("Tasks") }}</a></li> <li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="activity-tab" data-bs-toggle="tab" href="#tab-activity" role="tab" aria-controls="tab-activity" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-chart-line me-2 tab-icon-color"></span>{{ _("Activity") }}</a></li>
{% comment %} <li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="attachments-tab" data-bs-toggle="tab" href="#tab-attachments" role="tab" aria-controls="tab-attachments" aria-selected="true"> <span class="fa-solid fa-paperclip me-2 tab-icon-color"></span>Attachments</a></li> {% endcomment %} {% comment %} <li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="attachments-tab" data-bs-toggle="tab" href="#tab-attachments" role="tab" aria-controls="tab-attachments" aria-selected="true"> <span class="fa-solid fa-paperclip me-2 tab-icon-color"></span>Attachments</a></li> {% endcomment %}
</ul> </ul>
<div class="tab-content" id="myTabContent"> <div class="tab-content" id="myTabContent">
<div class="tab-pane fade active show" id="tab-activity" role="tabpanel" aria-labelledby="activity-tab"> <div class="tab-pane fade active show" id="tab-tasks" role="tabpanel" aria-labelledby="tasks-tab">
<h2 class="mb-4">Activity</h2> <div class="mb-1 d-flex justify-content-between align-items-center">
<div class="row align-items-center g-3 justify-content-between justify-content-start"> <h3 class="mb-0" id="scrollspyEmails">{{ _("Tasks") }}</h3>
<div class="col-12 col-sm-auto"> {% if perms.inventory.change_opportunity%}
<div class="search-box mb-2 mb-sm-0"> <button class="btn btn-phoenix-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#taskModal"><span class="fas fa-plus me-1"></span>{{ _("Add Task") }}</button>
<form class="position-relative"> {% endif %}
<input class="form-control search-input search" type="search" placeholder="Search Activity" aria-label="Search" />
<span class="fas fa-search search-box-icon"></span>
</form>
</div>
</div>
{%if perms.inventory.change_opportunity%}
<div class="col-auto">
<button class="btn btn-phoenix-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#activityModal"><span class="fas fa-plus me-1"></span>{{ _("Add Activity") }}</button>
</div>
{% endif %}
</div> </div>
{% for activity in opportunity.get_activities %} <div>
<div class="border-bottom border-translucent py-4">
<div class="d-flex"> <div class="border-top border-bottom border-translucent" id="allEmailsTable" data-list='{"valueNames":["subject","sent","date","source","status"],"page":7,"pagination":true}'>
<div class="d-flex bg-primary-subtle rounded-circle flex-center me-3 bg-primary-subtle" style="width:25px; height:25px"> <div class="table-responsive scrollbar mx-n1 px-1">
{% if activity.activity_type == "call" %} <table class="table fs-9 mb-0">
<span class="fa-solid fa-phone text-warning fs-8"></span> <thead>
{% elif activity.activity_type == "email" %} <tr>
<span class="fa-solid fa-envelope text-info-light fs-8"></span> <th class="white-space-nowrap fs-9 align-middle ps-0" style="width:26px;">
{% elif activity.activity_type == "meeting" %} <div class="form-check mb-0 fs-8">
<span class="fa-solid fa-users text-danger fs-8"></span> <input class="form-check-input" type="checkbox" data-bulk-select='{"body":"all-email-table-body"}' />
{% elif activity.activity_type == "whatsapp" %} </div>
<span class="fab fa-whatsapp text-success-dark fs-7"></span> </th>
{% endif %} <th class="sort white-space-nowrap align-middle pe-3 ps-0 text-uppercase" scope="col" data-sort="subject" style="width:31%; min-width:350px">Title</th>
<th class="sort align-middle pe-3 text-uppercase" scope="col" data-sort="sent" style="width:15%; min-width:130px">Assigned to</th>
<th class="sort align-middle text-start text-uppercase" scope="col" data-sort="date" style="min-width:165px">Due Date</th>
<th class="sort align-middle text-start text-uppercase" scope="col" data-sort="date" style="min-width:165px">Completed</th>
</tr>
</thead>
<tbody class="list" id="all-tasks-table-body">
{% for task in opportunity.get_tasks %}
{% include "partials/task.html" %}
{% endfor %}
</tbody>
</table>
</div>
<div class="row align-items-center justify-content-between py-2 pe-0 fs-9">
<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="fw-semibold" href="" data-list-view="*">View all<span class="fas fa-angle-right ms-1" data-fa-transform="down-1"></span></a><a class="fw-semibold d-none" href="" data-list-view="less">View Less<span class="fas fa-angle-right ms-1" data-fa-transform="down-1"></span></a>
</div> </div>
<div class="flex-1"> <div class="col-auto d-flex">
<div class="d-flex justify-content-between flex-column flex-xl-row mb-2 mb-sm-0"> <button class="page-link" data-list-pagination="prev"><span class="fas fa-chevron-left"></span></button>
<div class="flex-1 me-2"> <ul class="mb-0 pagination"></ul>
<h5 class="text-body-highlight lh-sm"></h5> <button class="page-link pe-0" data-list-pagination="next"><span class="fas fa-chevron-right"></span></button>
<p class="fs-9 mb-0">{{activity.notes}}</p>
</div>
</div>
<div class="d-flex justify-content-between flex-column flex-xl-row mb-2 mb-sm-0">
<div class="flex-1 me-2">
<h5 class="text-body-highlight lh-sm"></h5>
{% if request.user.email == activity.created_by %}
<p class="fs-9 mb-0">by <a class="ms-1" href="#!">You</a></p>
{% else %}
<p class="fs-9 mb-0">by<a class="ms-1" href="#!">{{activity.created_by}}</a></p>
{% endif %}
</div>
<div class="fs-9"><span class="fa-regular fa-calendar-days text-primary me-2"></span><span class="fw-semibold">{{activity.created|naturalday|capfirst}}</span></div>
</div>
<p class="fs-9 mb-0"></p>
</div> </div>
</div> </div>
</div> </div>
{% endfor %} </div>
</div> </div>
<div class="tab-pane fade" id="tab-notes" role="tabpanel" aria-labelledby="notes-tab"> <div class="tab-pane fade" id="tab-notes" role="tabpanel" aria-labelledby="notes-tab">
<div class="mb-1 d-flex align-items-center justify-content-between">
<h3 class="mb-4" id="scrollspyNotes">{{ _("Notes") }}</h3>
{% if perms.inventory.change_lead%}
<button class="btn btn-phoenix-primary btn-sm" type="button" onclick="reset_form()" data-bs-toggle="modal" data-bs-target="#noteModal"><span class="fas fa-plus me-1"></span>{{ _("Add Note") }}</button>
{% endif %}
</div>
<div class="border-top border-bottom border-translucent" id="leadDetailsTable">
<div class="table-responsive scrollbar mx-n1 px-1">
<table class="table fs-9 mb-0">
<thead>
<tr>
<th class="align-middle pe-6 text-uppercase text-start" scope="col" style="width:40%;">{{ _("Note") }}</th>
<th class="align-middle text-start text-uppercase white-space-nowrap" scope="col" style="width:40%;">{{ _("Created On")}}</th>
<th class="align-middle text-start text-uppercase white-space-nowrap" scope="col" style="width:40%;">{{ _("Last Updated")}}</th>
<th class="align-middle pe-0 text-end" scope="col" style="width:10%;"> </th>
</tr>
</thead>
<tbody >
{% 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>
<td class="align-middle text-body-tertiary text-start white-space-nowrap">{{ note.created|naturalday|capfirst }}</td>
<td class="align-middle text-body-tertiary text-start white-space-nowrap">{{ note.updated|naturalday|capfirst }}</td>
<td class="align-middle text-end white-space-nowrap pe-0 action py-2">
{% if note.created_by == request.user %}
<a id="updateBtn"
href="#"
onclick="updateNote(this)"
class="btn btn-sm btn-phoenix-primary me-2"
data-pk="{{ note.pk }}"
data-note="{{ note.note|escapejs }}"
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>">
{{ _("Update") }}
</a>
<button class="btn btn-phoenix-danger btn-sm delete-btn"
data-url="{% url 'delete_note_to_lead' request.dealer.slug note.pk %}"
data-message="Are you sure you want to delete this note?"
data-bs-toggle="modal" data-bs-target="#deleteModal">
<i class="fas fa-trash"></i>
</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% comment %} <div class="tab-pane fade" id="tab-notes" role="tabpanel" aria-labelledby="notes-tab">
<h2 class="mb-4">Notes</h2> <h2 class="mb-4">Notes</h2>
{%if perms.inventory.change_opportunity%} {%if perms.inventory.change_opportunity%}
<form action="{% url 'add_note_to_opportunity' request.dealer.slug opportunity.slug %}" method="post"> <form action="{% url 'add_note_to_opportunity' request.dealer.slug opportunity.slug %}" method="post">
@ -422,8 +466,8 @@
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
</div> </div> {% endcomment %}
<div class="tab-pane fade" id="tab-meeting" role="tabpanel" aria-labelledby="meeting-tab"> {% comment %} <div class="tab-pane fade" id="tab-meeting" role="tabpanel" aria-labelledby="meeting-tab">
<h2 class="mb-4">Meeting</h2> <h2 class="mb-4">Meeting</h2>
<div class="row align-items-center g-2 flex-wrap justify-content-start mb-3"> <div class="row align-items-center g-2 flex-wrap justify-content-start mb-3">
<div class="col-12 col-sm-auto"> <div class="col-12 col-sm-auto">
@ -461,9 +505,9 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div> {% endcomment %}
<div class="tab-pane fade" id="tab-call" role="tabpanel" aria-labelledby="call-tab"> {% comment %} <div class="tab-pane fade" id="tab-call" role="tabpanel" aria-labelledby="call-tab">
<div class="row align-items-center gx-4 gy-3 flex-wrap mb-3"> <div class="row align-items-center gx-4 gy-3 flex-wrap mb-3">
<div class="col-auto d-flex flex-1"> <div class="col-auto d-flex flex-1">
<h2 class="mb-0">Call</h2> <h2 class="mb-0">Call</h2>
@ -516,7 +560,7 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div> {% endcomment %}
<div class="tab-pane fade" id="tab-emails" role="tabpanel" aria-labelledby="emails-tab"> <div class="tab-pane fade" id="tab-emails" role="tabpanel" aria-labelledby="emails-tab">
<h2 class="mb-4">Emails</h2> <h2 class="mb-4">Emails</h2>
{% if perms.inventory.change_opportunity%} {% if perms.inventory.change_opportunity%}
@ -595,52 +639,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="tab-pane fade" id="tab-tasks" role="tabpanel" aria-labelledby="tasks-tab">
<div class="mb-1 d-flex justify-content-between align-items-center">
<h3 class="mb-0" id="scrollspyEmails">{{ _("Tasks") }}</h3>
{% if perms.inventory.change_opportunity%}
<button class="btn btn-phoenix-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#taskModal"><span class="fas fa-plus me-1"></span>{{ _("Add Task") }}</button>
{% endif %}
</div>
<div>
<div class="border-top border-bottom border-translucent" id="allEmailsTable" data-list='{"valueNames":["subject","sent","date","source","status"],"page":7,"pagination":true}'>
<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" data-bulk-select='{"body":"all-email-table-body"}' />
</div>
</th>
<th class="sort white-space-nowrap align-middle pe-3 ps-0 text-uppercase" scope="col" data-sort="subject" style="width:31%; min-width:350px">Title</th>
<th class="sort align-middle pe-3 text-uppercase" scope="col" data-sort="sent" style="width:15%; min-width:130px">Assigned to</th>
<th class="sort align-middle text-start text-uppercase" scope="col" data-sort="date" style="min-width:165px">Due Date</th>
<th class="sort align-middle text-start text-uppercase" scope="col" data-sort="date" style="min-width:165px">Completed</th>
</tr>
</thead>
<tbody class="list" id="all-tasks-table-body">
{% for task in opportunity.get_tasks %}
{% include "partials/task.html" %}
{% endfor %}
</tbody>
</table>
</div>
<div class="row align-items-center justify-content-between py-2 pe-0 fs-9">
<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="fw-semibold" href="" data-list-view="*">View all<span class="fas fa-angle-right ms-1" data-fa-transform="down-1"></span></a><a class="fw-semibold d-none" href="" data-list-view="less">View Less<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>
</div>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="tab-attachments" role="tabpanel" aria-labelledby="attachments-tab"> <div class="tab-pane fade" id="tab-attachments" role="tabpanel" aria-labelledby="attachments-tab">
<h2 class="mb-3">Attachments</h2> <h2 class="mb-3">Attachments</h2>
<div class="border-top border-dashed pt-3 pb-4"> <div class="border-top border-dashed pt-3 pb-4">
@ -684,9 +683,73 @@
</div> </div>
</div> </div>
</div> </div>
<div class="tab-pane fade" id="tab-activity" role="tabpanel" aria-labelledby="activity-tab">
<h2 class="mb-4">Activity</h2>
<div class="row align-items-center g-3 justify-content-between justify-content-start">
<div class="col-12 col-sm-auto">
<div class="search-box mb-2 mb-sm-0">
<form class="position-relative">
<input class="form-control search-input search" type="search" placeholder="Search Activity" aria-label="Search" />
<span class="fas fa-search search-box-icon"></span>
</form>
</div>
</div>
{%if perms.inventory.change_opportunity%}
<div class="col-auto">
<button class="btn btn-phoenix-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#activityModal"><span class="fas fa-plus me-1"></span>{{ _("Add Activity") }}</button>
</div>
{% endif %}
</div>
{% for activity in opportunity.get_activities %}
<div class="border-bottom border-translucent py-4">
<div class="d-flex">
<div class="d-flex bg-primary-subtle rounded-circle flex-center me-3 bg-primary-subtle" style="width:25px; height:25px">
{% if activity.activity_type == "call" %}
<span class="fa-solid fa-phone text-warning fs-8"></span>
{% elif activity.activity_type == "email" %}
<span class="fa-solid fa-envelope text-info-light fs-8"></span>
{% elif activity.activity_type == "note" %}
<span class="fa-regular fa-sticky-note text-info-light fs-8"></span>
{% elif activity.activity_type == "task" %}
<span class="fa-solid fa-list-check text-success fs-8"></span>
{% elif activity.activity_type == "meeting" %}
<span class="fa-solid fa-users text-danger fs-8"></span>
{% elif activity.activity_type == "whatsapp" %}
<span class="fab fa-whatsapp text-success-dark fs-7"></span>
{% endif %}
</div>
<div class="flex-1">
<div class="d-flex justify-content-between flex-column flex-xl-row mb-2 mb-sm-0">
<div class="flex-1 me-2">
<h5 class="text-body-highlight lh-sm"></h5>
<p class="fs-9 mb-0">{{activity.notes}}</p>
</div>
</div>
<div class="d-flex justify-content-between flex-column flex-xl-row mb-2 mb-sm-0">
<div class="flex-1 me-2">
<h5 class="text-body-highlight lh-sm"></h5>
{% if request.user.email == activity.created_by %}
<p class="fs-9 mb-0">by <a class="ms-1" href="#!">You</a></p>
{% else %}
<p class="fs-9 mb-0">by<a class="ms-1" href="#!">{{activity.created_by}}</a></p>
{% endif %}
</div>
<div class="fs-9"><span class="fa-regular fa-calendar-days text-primary me-2"></span><span class="fw-semibold">{{activity.created|naturalday|capfirst}}</span></div>
</div>
<p class="fs-9 mb-0"></p>
</div>
</div>
</div>
{% endfor %}
</div>
</div> </div>
</div> </div>
</div> </div>
{% include "components/activity_modal.html" with content_type="opportunity" slug=opportunity.slug %}
{% include "components/task_modal.html" with content_type="opportunity" slug=opportunity.slug %}
<!-- task Modal -->
{% include "components/task_modal.html" with content_type="opportunity" slug=opportunity.slug %}
<!-- note Modal -->
{% include "components/note_modal.html" with content_type="opportunity" slug=opportunity.slug %}
{% endblock %} {% endblock %}

View File

@ -56,6 +56,16 @@
</a> </a>
</li> </li>
{% endif %} {% endif %}
{% if perms.django_ledger.view_purchaseordermodel %}
<li class="nav-item">
<a class="nav-link" href="{% url 'purchase_order_list' request.dealer.slug request.dealer.entity.slug %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span class="fas fa-warehouse"></span></span><span class="nav-link-text">{% trans "purchase Orders"|capfirst %}</span>
</div>
</a>
</li>
{% endif %}
</ul> </ul>
</div> </div>
</div> </div>
@ -183,16 +193,6 @@
</a> </a>
</li> </li>
{% endif %} {% endif %}
{% if perms.inventory.view_payment %}
<li class="nav-item">
<a class="nav-link" href="{% url 'payment_list' request.dealer.slug %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span class="fas fa-money-check"></span></span><span class="nav-link-text">{% trans "payments"|capfirst %}</span>
</div>
</a>
</li>
{% endif %}
</ul> </ul>
</div> </div>
</div> </div>
@ -275,15 +275,14 @@
</a> </a>
</li> </li>
{% endif %} {% endif %}
{% if perms.inventory.view_journalentrymodel %}
{% if perms.django_ledger.view_purchaseordermodel %} <li class="nav-item">
<li class="nav-item"> <a class="nav-link" href="{% url 'payment_list' request.dealer.slug %}">
<a class="nav-link" href="{% url 'purchase_order_list' request.dealer.slug request.dealer.entity.slug %}"> <div class="d-flex align-items-center">
<div class="d-flex align-items-center"> <span class="nav-link-icon"><span class="fas fa-money-check"></span></span><span class="nav-link-text">{% trans "payments"|capfirst %}</span>
<span class="nav-link-icon"><span class="fas fa-warehouse"></span></span><span class="nav-link-text">{% trans "purchase Orders"|capfirst %}</span> </div>
</div>
</a> </a>
</li> </li>
{% endif %} {% endif %}
</ul> </ul>
</div> </div>
@ -467,8 +466,13 @@
{% endif %} {% endif %}
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link px-3 d-block" href=""> <span class="me-2 text-body align-bottom" data-feather="help-circle"></span>Help Center</a> <a class="nav-link px-3 d-block" href=""> <span class="me-2 text-body align-bottom" data-feather="help-circle"></span>{{ _("Help Center") }}</a>
</li> </li>
{% if request.is_staff %}
<li class="nav-item">
<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>{{ _("Calendar") }}</a>
</li>
{% endif %}
<!--<li class="nav-item"><a class="nav-link px-3 d-block" href=""> Language</a></li>--> <!--<li class="nav-item"><a class="nav-link px-3 d-block" href=""> Language</a></li>-->
</ul> </ul>
</div> </div>

View File

@ -52,16 +52,20 @@
</div> </div>
</div> </div>
<div class="card-footer d-flex "> <div class="card-footer d-flex ">
{% if perms.django_ledger.change_bankaccountmodel%}
<a class="btn btn-sm btn-phoenix-primary me-1" href="{% url 'bank_account_update' request.dealer.slug bank_account.pk %}"> <a class="btn btn-sm btn-phoenix-primary me-1" href="{% url 'bank_account_update' request.dealer.slug bank_account.pk %}">
<!--<i class="bi bi-pencil-square"></i> --> <!--<i class="bi bi-pencil-square"></i> -->
{{ _("Edit") }} {{ _("Edit") }}
</a> </a>
{% endif %}
{% if perms.django_ledger.delete_bankaccountmodel%}
<a class="btn btn-sm btn-phoenix-danger me-1" <a class="btn btn-sm btn-phoenix-danger me-1"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#deleteModal"> data-bs-target="#deleteModal">
<!--<i class="bi bi-trash-fill"></i>--> <!--<i class="bi bi-trash-fill"></i>-->
{{ _("Delete") }} {{ _("Delete") }}
</a> </a>
{% endif %}
<a class="btn btn-sm btn-phoenix-secondary" <a class="btn btn-sm btn-phoenix-secondary"
href="{% url 'bank_account_list' request.dealer.slug %}"> href="{% url 'bank_account_list' request.dealer.slug %}">
<!--<i class="bi bi-arrow-left-square-fill"></i>--> <!--<i class="bi bi-arrow-left-square-fill"></i>-->

View File

@ -96,12 +96,14 @@
{% endif %} {% endif %}
</td> </td>
<td class="align-middle white-space-nowrap text-start"> <td class="align-middle white-space-nowrap text-start">
{% if perms.django_ledger.view_transactionmodel%}
<div class="btn-reveal-trigger position-static"> <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> <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"> <div class="dropdown-menu dropdown-menu-end py-2">
<a class="dropdown-item text-success-dark" href="{% url 'payment_details' request.dealer.slug tx.journal_entry.pk %}">{% trans 'view Transactions'|capfirst %}</a> <a class="dropdown-item text-success-dark" href="{% url 'payment_details' request.dealer.slug tx.journal_entry.pk %}">{% trans 'view Transactions'|capfirst %}</a>
</div> </div>
</div> </div>
{% endif %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@ -135,17 +137,21 @@
</div> </div>
<div class="mt-3 d-flex"> <div class="mt-3 d-flex">
{% if perms.django_ledger.change_chartofaccountmodel%}
<a class="btn btn-sm btn-phoenix-primary me-1" href="{% url 'account_update' request.dealer.slug account.pk %}"> <a class="btn btn-sm btn-phoenix-primary me-1" href="{% url 'account_update' request.dealer.slug account.pk %}">
<!-- <i class="bi bi-pencil-square"></i> --> <!-- <i class="bi bi-pencil-square"></i> -->
<i class="fa-solid fa-pen-to-square"></i> {{ _('Edit') }} <i class="fa-solid fa-pen-to-square"></i> {{ _('Edit') }}
</a> </a>
{% endif %}
{% if perms.django_ledger.delete_chartofaccountmodel%}
<a class="btn btn-sm btn-phoenix-danger me-1" data-bs-toggle="modal" data-bs-target="#deleteModal"> <a class="btn btn-sm btn-phoenix-danger me-1" data-bs-toggle="modal" data-bs-target="#deleteModal">
<!-- <i class="bi bi-trash-fill"></i> --> <!-- <i class="bi bi-trash-fill"></i> -->
<i class="fa-solid fa-trash"></i> {{ _('Delete') }} <i class="fa-solid fa-trash"></i> {{ _('Delete') }}
</a> </a>
{% endif%}
<a class="btn btn-sm btn-phoenix-secondary" href="{% url 'account_list' request.dealer.slug %}"> <a class="btn btn-sm btn-phoenix-secondary" href="{% url 'account_list' request.dealer.slug %}">
<!-- <i class="bi bi-arrow-left-square-fill"></i> --> <!-- <i class="bi bi-arrow-left-square-fill"></i> -->
<i class="fa-regular fa-circle-left"></i> {% trans 'Back to List' %} <i class="fa-regular fa-circle-left"></i> {% trans 'Back to COA List' %}
</a> </a>
</div> </div>

View File

@ -11,7 +11,9 @@
<div class="row mt-4"> <div class="row mt-4">
<div class="d-flex justify-content-between mb-2"> <div class="d-flex justify-content-between mb-2">
<h3 class=""><i class="fa-solid fa-book"></i> {% trans "Accounts" %}</h3> <h3 class=""><i class="fa-solid fa-book"></i> {% trans "Accounts" %}</h3>
{% if perms.django_ledger.create_chartofaccountmodel %}
<a href="{% url 'account_create' request.dealer.slug %}" class="btn btn-md btn-phoenix-primary"><i class="fa fa-plus me-2"></i>{% trans 'New Account' %}</a> <a href="{% url 'account_create' request.dealer.slug %}" class="btn btn-md btn-phoenix-primary"><i class="fa fa-plus me-2"></i>{% trans 'New Account' %}</a>
{% endif %}
</div> </div>
<!-- Account Type Tabs --> <!-- Account Type Tabs -->
@ -105,10 +107,12 @@
<div class="modal-dialog modal-sm"> <div class="modal-dialog modal-sm">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
{% if perms.django_ledger.delete_chartofaccountmodel %}
<h5 class="modal-title" id="deleteModalLabel"> <h5 class="modal-title" id="deleteModalLabel">
{% trans "Delete Account" %} {% trans "Delete Account" %}
<span data-feather="alert-circle"></span> <span data-feather="alert-circle"></span>
</h5> </h5>
{% endif %}
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body text-center"> <div class="modal-body text-center">

View File

@ -2,14 +2,14 @@
<tr id="task-{{task.pk}}" class="hover-actions-trigger btn-reveal-trigger position-static {% if task.completed %}completed-task{% endif %}"> <tr id="task-{{task.pk}}" class="hover-actions-trigger btn-reveal-trigger position-static {% if task.completed %}completed-task{% endif %}">
<td class="fs-9 align-middle px-0 py-3"> <td class="fs-9 align-middle px-0 py-3">
<div class="form-check mb-0 fs-8"> <div class="form-check mb-0 fs-8">
<input class="form-check-input" {% if task.completed %}checked{% endif %} type="checkbox" hx-post="{% url 'update_task' request.dealer.slug task.pk %}" hx-trigger="change" hx-swap="outerHTML" hx-target="#task-{{task.pk}}" /> <input class="form-check-input" {% if task.completed %}checked{% endif %} type="checkbox" hx-post="{% url 'update_schedule' request.dealer.slug task.pk %}" hx-trigger="change" hx-swap="outerHTML" hx-target="#task-{{task.pk}}" />
</div> </div>
</td> </td>
<td class="subject order align-middle white-space-nowrap py-2 ps-0"><a class="fw-semibold text-primary" href="">{{task.title}}</a> <td class="subject order align-middle white-space-nowrap py-2 ps-0"><a class="fw-semibold text-primary" href="">{{task.purpose|capfirst}}</a>
<div class="fs-10 d-block">{{task.description}}</div> <div class="fs-10 d-block">{{task.scheduled_type|capfirst}}</div>
</td> </td>
<td class="sent align-middle white-space-nowrap text-start fw-bold text-body-tertiary py-2">{{task.assigned_to}}</td> <td class="sent align-middle white-space-nowrap text-start fw-bold text-body-tertiary py-2">{{task.scheduled_by}}</td>
<td class="date align-middle white-space-nowrap text-body py-2">{{task.created|naturalday|capfirst}}</td> <td class="date align-middle white-space-nowrap text-body py-2">{{task.created_at|naturalday|capfirst}}</td>
<td class="date align-middle white-space-nowrap text-body py-2"> <td class="date align-middle white-space-nowrap text-body py-2">
{% if task.completed %} {% if task.completed %}
<span class="badge badge-phoenix fs-10 badge-phoenix-success"><i class="fa-solid fa-check"></i></span> <span class="badge badge-phoenix fs-10 badge-phoenix-success"><i class="fa-solid fa-check"></i></span>

View File

@ -71,19 +71,16 @@
{{ f.DELETE|add_class:"form-check-input" }} {{ f.DELETE|add_class:"form-check-input" }}
</td> </td>
{% endif %} {% endif %}
<td class="text-center"> <td class="text-center">
{% if f.instance.can_create_bill %} {% if f.instance.can_create_bill and can_add_bill %}
{% if perms.djagno_ledger.add_billmodel%}
{{ f.create_bill|add_class:"form-check-input" }} {{ f.create_bill|add_class:"form-check-input" }}
{% endif %} {% elif f.instance.bill_model and can_view_bill %}
{% elif f.instance.bill_model %}
{% if perms.djagno_ledger.view_billmodel%}
<a class="btn btn-sm btn-phoenix-secondary" <a class="btn btn-sm btn-phoenix-secondary"
href="{% url 'bill-detail' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=f.instance.bill_model_id %}"> href="{% url 'bill-detail' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=f.instance.bill_model_id %}">
{% trans 'View Bill' %} {% trans 'View Bill' %}
</a> </a>
{% endif %} {% endif %}
{% endif %}
</td> </td>
<td class="text-center"> <td class="text-center">
{% if f.instance.bill_model %} {% if f.instance.bill_model %}

View File

@ -8,45 +8,61 @@
<div class="container-fluid mt-4"> <div class="container-fluid mt-4">
<div class="row g-1"> <div class="row g-1">
<div class="col-lg-12">
<div class="d-flex flex-column gap-3"> <div class="col-lg-12 mb-3 ">
<div class="card"> <div class="row">
<div class="card-body">
{% include 'purchase_orders/includes/card_po.html' with dealer_slug=request.dealer.slug po_model=po_model entity_slug=entity_slug style='po-detail' %} <div class="col-9">
</div> <div class="card">
<div class="card-body">
{% include 'purchase_orders/includes/card_po.html' with dealer_slug=request.dealer.slug po_model=po_model entity_slug=entity_slug style='po-detail' %}
</div>
</div>
</div>
<div class='col-3'>
<div class="row">
<div class="col-12">
<div class="card mb-4">
<div class="card-body">
<div class="row text-center">
<div class="col-md-6 border-end">
<div class="p-2">
<h6 class="text-muted mb-2">{% trans 'PO Amount' %}</h6>
<h3 class="fw-light mb-0">
<span class="currency">{{CURRENCY}}</span>{{ po_model.po_amount | absolute | currency_format }}
</h3>
</div>
</div>
<div class="col-md-6">
<div class="p-2">
<h6 class="text-muted mb-2">{% trans 'Amount Received' %}</h6>
<h3 class="fw-light mb-0 text-success">
<span class="currency">{{CURRENCY}}</span>{{ po_model.po_amount_received | currency_format }}
</h3>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-12">
<a class="btn btn-phoenix-primary w-100 py-2"
href="{% url 'purchase_order_list' request.dealer.slug request.dealer.entity.slug %}">
<i class="fas fa-list me-2"></i>{% trans 'PO List' %}
</a>
</div>
</div>
</div> </div>
<a class="btn btn-phoenix-primary w-100 py-2" </div>
href="{% url 'purchase_order_list' request.dealer.slug request.dealer.entity.slug %}">
<i class="fas fa-list me-2"></i>{% trans 'PO List' %}
</a>
</div>
</div> </div>
<div class="col-lg-12">
<div class="card mb-4">
<div class="card-body">
<div class="row text-center">
<div class="col-md-6 border-end">
<div class="p-2">
<h6 class="text-muted mb-2">{% trans 'PO Amount' %}</h6>
<h3 class="fw-light mb-0">
<span class="currency">{{CURRENCY}}</span>{{ po_model.po_amount | absolute | currency_format }}
</h3>
</div>
</div>
<div class="col-md-6">
<div class="p-2">
<h6 class="text-muted mb-2">{% trans 'Amount Received' %}</h6>
<h3 class="fw-light mb-0 text-success">
<span class="currency">{{CURRENCY}}</span>{{ po_model.po_amount_received | currency_format }}
</h3>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-12"> <div class="col-lg-12">
<div class="table-responsive"> <div class="table-responsive">

View File

@ -83,7 +83,7 @@
<div class="col-12"> <div class="col-12">
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-body"> <div class="card-body">
{% po_item_formset_table po_model itemtxs_formset %} {% po_item_formset_table po_model itemtxs_formset request.user %}
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,195 @@
{% extends "base.html" %}
{% load crispy_forms_filters %}
{% load i18n static %}
{% block title %}{{ _("Create Quotation") }}{% endblock title %}
{% block customCSS %}
<style>
.disabled{
opacity: 0.5;
pointer-events: none;
}
</style>
{% endblock customCSS %}
{% block content %}
<div class="row mt-4">
{% if not items %}
<div class="alert alert-outline-warning d-flex align-items-center" role="alert">
<i class="fa-solid fa-circle-info fs-6"></i>
<p class="mb-0 flex-1">{{ _("Please add at least one car before creating a quotation.") }}<a class="ms-3 text-body-primary fs-9" href="{% url 'car_add' request.dealer.slug %}"> {{ _("Add Car") }} </a></p>
<button class="btn-close" type="button" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
{% if not customer_count %}
<div class="alert alert-outline-warning d-flex align-items-center" role="alert">
<i class="fa-solid fa-circle-info fs-6"></i>&nbsp;&nbsp;
<p class="mb-0 flex-1"> {{ _("Please add at least one customer before creating a quotation.") }}<a class="ms-3 text-body-primary fs-9" href="{% url 'customer_create' request.dealer.slug %}"> {{ _("Add Customer") }} </a></p>
<button class="btn-close" type="button" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
<form id="mainForm" method="post" class="needs-validation {% if not items or not customer_count %}disabled{% endif %}">
<h3 class="text-center"><i class="fa-regular fa-file-lines"></i> {% trans "Create Quotation" %}</h3>
{% csrf_token %}
<div class="row g-3 col-10">
{{ form|crispy }}
<div class="row mt-5">
<div id="formrow">
<h3 class="text-start"><i class="fa-solid fa-car-side"></i> {{ _("Cars") }}</h3>
<div class="form-row row g-3 mb-3 mt-5">
<div class="mb-2 col-sm-4 col-md-6 col-lg-4">
<select class="form-control item" name="item[]" required>
{% for item in items %}
<option style="background-color: rgb({{ item.color }});" value="{{ item.hash }}">{{ item.make }} {{item.model}} {{item.serie}} {{item.trim}} {{item.color_name}} ({{item.hash_count}})</option>
{% empty %}
<option disabled>{% trans "No Cars Found" %}</option>
{% endfor %}
</select>
</div>
<div class="mb-2 col-sm-4 col-md-3">
<input class="form-control quantity" type="number" placeholder="Quantity" name="quantity[]" required>
</div>
<div class="mb-2 col-sm-3 col-md-3">
<button class="btn btn-sm btn-phoenix-danger removeBtn"><i class="fa-solid fa-trash me-1"></i> {{ _("Remove") }}</button>
</div>
</div>
</div>
<div class="col-12">
<button id="addMoreBtn" class="btn btn-sm btn-phoenix-primary"><i class="fa-solid fa-plus me-2"></i> {{ _("Add More")}}</button>
</div>
</div>
</div>
<!-- Buttons -->
{% comment %} <div class="mt-5 text-center">
<button type="submit" class="btn btn-success me-2" {% if not items %}disabled{% endif %}><i class="fa-solid fa-floppy-disk me-1"></i> {% trans "Save" %}</button>
<a href="{% url 'estimate_list' %}" class="btn btn-danger"><i class="fa-solid fa-ban me-1"></i> {% trans "Cancel" %}</a>
</div> {% endcomment %}
<div class="d-flex justify-content-center">
<button class="btn btn-sm btn-phoenix-success me-2" type="submit"><i class="fa-solid fa-floppy-disk me-1"></i>
<!--<i class="bi bi-save"></i> -->
{{ _("Save") }}
</button>
<a href="{{request.META.HTTP_REFERER}}" class="btn btn-sm btn-phoenix-danger"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
</div>
</form>
</div>
{% endblock content %}
{% block customJS %}
<script>
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;
}
});
// Add new form fields
document.getElementById('addMoreBtn').addEventListener('click', function(e) {
e.preventDefault();
const formrow = document.getElementById('formrow');
const newForm = document.createElement('div');
newForm.className = 'form-row row g-3 mb-3 mt-5';
newForm.innerHTML = `
<div class="mb-2 col-sm-4">
<select class="form-control item" name="item[]" required>
{% for item in items %}
<option value="{{ item.hash }}">{{ item.make }} {{item.model}} {{item.serie}} {{item.trim}} {{item.color}}</option>
{% endfor %}
</select>
</div>
<div class="mb-2 col-sm-2">
<input class="form-control quantity" type="number" placeholder="Quantity" name="quantity[]" required>
</div>
<div class="mb-2 col-sm-1">
<button class="btn btn-phoenix-danger removeBtn"><i class="fa-solid fa-trash"></i> {{ _("Remove") }}</button>
</div>
`;
formrow.appendChild(newForm);
// Add remove button functionality
newForm.querySelector('.removeBtn').addEventListener('click', function() {
newForm.remove();
});
});
// Add remove button functionality to the initial form
document.querySelectorAll('.form-row').forEach(row => {
row.querySelector('.removeBtn').addEventListener('click', function() {
row.remove();
});
});
// Handle form submission
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: [],
opportunity_id: "{{opportunity_id}}"
};
// Collect multi-value fields (e.g., item[], quantity[])
document.querySelectorAll('[name="item[]"]').forEach(input => {
formData.item.push(input.value);
});
document.querySelectorAll('[name="quantity[]"]').forEach(input => {
formData.quantity.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);
}
});
</script>
{% endblock customJS %}

View File

@ -9,9 +9,118 @@
.disabled{ .disabled{
opacity: 0.5; opacity: 0.5;
pointer-events: none; pointer-events: none;
}
.color-box {
width: 20px;
height: 20px;
border-radius: 4px;
border: 1px solid #ccc;
} /* Custom select styles */
.custom-select {
position: relative;
width: 100%;
margin-bottom: 1.5rem;
}
.select-trigger {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.4rem 1rem;
border: 1px solid #ddd;
border-radius: 6px;
background-color: white;
cursor: pointer;
transition: all 0.2s ease;
}
.select-trigger:hover {
border-color: #aaa;
}
.select-trigger.active {
border-color: #4a90e2;
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2);
}
.selected-value {
display: flex;
align-items: center;
gap: 0.75rem;
}
.selected-value img {
width: 30px;
height: 20px;
object-fit: contain;
}
.dropdown-icon {
transition: transform 0.2s ease;
}
.custom-select.open .dropdown-icon {
transform: rotate(180deg);
}
.options-container {
position: absolute;
top: 100%;
left: 0;
width: 100%;
max-height: 300px;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 6px;
background-color: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 100;
display: none;
}
.custom-select.open .options-container {
display: block;
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.option {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
cursor: pointer;
transition: background-color 0.1s ease;
gap: 0.75rem;
}
.option:hover {
background-color: #f0f7ff;
}
.option.selected {
background-color: #e6f0ff;
}
.option img {
width: 30px;
height: 20px;
object-fit: contain;
}
/* Hidden native select for form submission */
.native-select {
position: absolute;
opacity: 0;
height: 0;
width: 0;
} }
</style> </style>
{% endblock customCSS %} {% endblock customCSS %}
{% block content %} {% block content %}
<div class="row mt-4"> <div class="row mt-4">
{% if not items %} {% if not items %}
@ -33,58 +142,53 @@
{% csrf_token %} {% csrf_token %}
<div class="row g-3 col-10"> <div class="row g-3 col-10">
{{ form|crispy }} {{ form|crispy }}
<div class="row mt-5"> <div class="custom-select">
<div id="formrow"> <!-- Hidden native select for form submission -->
<h3 class="text-start"><i class="fa-solid fa-car-side"></i> {{ _("Cars") }}</h3> <select class="native-select" name="item" required tabindex="-1">
<div class="form-row row g-3 mb-3 mt-5"> <option value="">Select a car</option>
<div class="mb-2 col-sm-4 col-md-6 col-lg-4"> {% for item in items %}
<select class="form-control item" name="item[]" required> <option value="{{ item.hash }}"></option>
{% for item in items %} {% endfor %}
<option style="background-color: rgb({{ item.color }});" value="{{ item.hash }}">{{ item.make }} {{item.model}} {{item.serie}} {{item.trim}} {{item.color_name}} ({{item.hash_count}})</option> </select>
{% empty %}
<option disabled>{% trans "No Cars Found" %}</option>
{% endfor %}
</select>
</div>
<div class="mb-2 col-sm-4 col-md-3">
<input class="form-control quantity" type="number" placeholder="Quantity" name="quantity[]" required>
</div>
<div class="mb-2 col-sm-3 col-md-3">
<button class="btn btn-sm btn-phoenix-danger removeBtn"><i class="fa-solid fa-trash me-1"></i> {{ _("Remove") }}</button>
</div>
</div>
</div>
<div class="col-12">
<button id="addMoreBtn" class="btn btn-sm btn-phoenix-primary"><i class="fa-solid fa-plus me-2"></i> {{ _("Add More")}}</button>
</div>
<!-- Custom select UI -->
<div class="select-trigger">
<div class="selected-value">
<span>Select a car</span>
</div>
<i class="fas fa-chevron-down dropdown-icon"></i>
</div>
<div class="options-container">
{% for item in items %}
<div class="option" data-value="{{ item.hash }}" data-image="{{item.logo}}">
<img src="{{item.logo}}" alt="{{item.model}}">
<span>{{item.make}} {{item.model}} {{item.serie}} {{item.trim}} {{item.color_name}}</span>
<div class="color-box" style="background-color: rgb({{ item.exterior_color }});"></div>
<div class="color-box" style="background-color: rgb({{ item.interior_color }});"></div>
</div>
{% endfor %}
</div> </div>
</div> </div>
<!-- Buttons --> </div>
{% comment %} <div class="mt-5 text-center">
<button type="submit" class="btn btn-success me-2" {% if not items %}disabled{% endif %}><i class="fa-solid fa-floppy-disk me-1"></i> {% trans "Save" %}</button>
<a href="{% url 'estimate_list' %}" class="btn btn-danger"><i class="fa-solid fa-ban me-1"></i> {% trans "Cancel" %}</a>
</div> {% endcomment %}
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
<button class="btn btn-sm btn-phoenix-success me-2" type="submit">
<button class="btn btn-sm btn-phoenix-success me-2" type="submit"><i class="fa-solid fa-floppy-disk me-1"></i> <i class="fa-solid fa-floppy-disk me-1"></i>
<!--<i class="bi bi-save"></i> -->
{{ _("Save") }} {{ _("Save") }}
</button> </button>
<a href="{{request.META.HTTP_REFERER}}" class="btn btn-sm btn-phoenix-danger">
<a href="{{request.META.HTTP_REFERER}}" class="btn btn-sm btn-phoenix-danger"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a> <i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}
</a>
</div> </div>
</form> </form>
</div> </div>
{% endblock content %} {% endblock content %}
{% block customJS %} {% block customJS %}
<script> <script>
const Toast = Swal.mixin({ document.addEventListener('DOMContentLoaded', function() {
const Toast = Swal.mixin({
toast: true, toast: true,
position: "top-end", position: "top-end",
showConfirmButton: false, showConfirmButton: false,
@ -95,44 +199,86 @@
toast.onmouseleave = Swal.resumeTimer; toast.onmouseleave = Swal.resumeTimer;
} }
}); });
// Add new form fields function notify(tag,msg){Toast.fire({icon: tag,titleText: msg});}
document.getElementById('addMoreBtn').addEventListener('click', function(e) {
e.preventDefault();
const formrow = document.getElementById('formrow');
const newForm = document.createElement('div');
newForm.className = 'form-row row g-3 mb-3 mt-5';
newForm.innerHTML = `
<div class="mb-2 col-sm-4">
<select class="form-control item" name="item[]" required>
{% for item in items %}
<option value="{{ item.hash }}">{{ item.make }} {{item.model}} {{item.serie}} {{item.trim}} {{item.color}}</option>
{% endfor %}
</select>
</div>
<div class="mb-2 col-sm-2">
<input class="form-control quantity" type="number" placeholder="Quantity" name="quantity[]" required>
</div>
<div class="mb-2 col-sm-1">
<button class="btn btn-phoenix-danger removeBtn"><i class="fa-solid fa-trash"></i> {{ _("Remove") }}</button>
</div>
`;
formrow.appendChild(newForm);
// Add remove button functionality const customSelects = document.querySelectorAll('.custom-select');
newForm.querySelector('.removeBtn').addEventListener('click', function() {
newForm.remove(); 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();
}
}
}); });
});
// Add remove button functionality to the initial form // Form submission
document.querySelectorAll('.form-row').forEach(row => { /*const form = document.getElementById('demo-form');
row.querySelector('.removeBtn').addEventListener('click', function() { form.addEventListener('submit', function(e) {
row.remove(); e.preventDefault();
}); const formData = new FormData(form);
}); alert(`Selected value: ${formData.get('car')}`);
});*/
// Handle form submission document.getElementById('mainForm').addEventListener('submit', async function(e) {
document.getElementById('mainForm').addEventListener('submit', async function(e) {
e.preventDefault(); e.preventDefault();
const titleInput = document.querySelector('[name="title"]'); const titleInput = document.querySelector('[name="title"]');
@ -147,18 +293,16 @@
title: document.querySelector('[name=title]').value, title: document.querySelector('[name=title]').value,
customer: document.querySelector('[name=customer]').value, customer: document.querySelector('[name=customer]').value,
item: [], item: [],
quantity: [], quantity: [1],
opportunity_id: "{{opportunity_id}}" opportunity_id: "{{opportunity_id}}"
}; };
// Collect multi-value fields (e.g., item[], quantity[]) // Collect multi-value fields (e.g., item[], quantity[])
document.querySelectorAll('[name="item[]"]').forEach(input => { document.querySelectorAll('[name="item"]').forEach(input => {
formData.item.push(input.value); formData.item.push(input.value);
}); });
document.querySelectorAll('[name="quantity[]"]').forEach(input => {
formData.quantity.push(input.value);
});
console.log(formData); console.log(formData);
try { try {
@ -191,5 +335,7 @@
notify("error", error); notify("error", error);
} }
}); });
});
</script> </script>
{% endblock customJS %} {% endblock customJS %}

View File

@ -9,22 +9,22 @@
<style> <style>
/* Custom styling */ /* Custom styling */
.form-section { .form-section {
background-color: #f8f9fa;
border-radius: 0.5rem; border-radius: 0.5rem;
padding: 1.5rem; padding: 1.5rem;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
.form-section-header { .form-section-header {
border-bottom: 1px solid #dee2e6;
padding-bottom: 0.75rem; padding-bottom: 0.75rem;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
color: #0d6efd;
} }
.required-field::after { .required-field::after {
content: " *"; content: " *";
color: #dc3545;
} }
.search-select { .search-select {
@ -40,7 +40,7 @@
right: 1rem; right: 1rem;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
color: #6c757d;
} }
.currency-input { .currency-input {
@ -52,7 +52,7 @@
left: 1rem; left: 1rem;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
color: #6c757d;
} }
.currency-input input { .currency-input input {
@ -60,12 +60,12 @@
} }
.form-actions { .form-actions {
background-color: #f8f9fa;
padding: 1rem; padding: 1rem;
border-radius: 0.5rem; border-radius: 0.5rem;
position: sticky; position:static;
bottom: 1rem; bottom: 1rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1);
} }
</style> </style>
{% endblock customCSS %} {% endblock customCSS %}
@ -75,7 +75,7 @@
<div class="row justify-content-center mb-4"> <div class="row justify-content-center mb-4">
<div class="col-lg-10"> <div class="col-lg-10">
<div class="card shadow"> <div class="card shadow">
<div class="card-header bg-primary text-white"> <div class="card-header bg-primary ">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<h2 class="h4 mb-0"> <h2 class="h4 mb-0">
<i class="fas fa-file-invoice me-2"></i> New Sale Order <i class="fas fa-file-invoice me-2"></i> New Sale Order