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(
name="call",
price=0,
duration=datetime.timedelta(minutes=10),
duration=datetime.timedelta(minutes=60),
currency="SAR",
description="15 min call",
)
Service.objects.create(
name="meeting",
price=0,
duration=datetime.timedelta(minutes=30),
duration=datetime.timedelta(minutes=90),
currency="SAR",
description="30 min meeting",
)
Service.objects.create(
name="email",
price=0,
duration=datetime.timedelta(minutes=30),
duration=datetime.timedelta(minutes=90),
currency="SAR",
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.core.management.base import BaseCommand
from django.contrib.contenttypes.models import ContentType
from django_ledger.models import EstimateModel,BillModel,AccountModel,LedgerModel
class Command(BaseCommand):
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 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 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))

View File

@ -123,13 +123,18 @@ class DealerSlugMiddleware:
response = self.get_response(request)
return response
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('/ar/logout/') or \
request.path_info.startswith('/en/logout/') or \
request.path_info.startswith('/en/ledger/') or \
request.path_info.startswith('/ar/ledger/') 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
if not request.user.is_authenticated:

View File

@ -1836,7 +1836,10 @@ class Schedule(models.Model):
("completed", _("Completed")),
("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(
CustomerModel,
on_delete=models.CASCADE,
@ -1850,6 +1853,7 @@ class Schedule(models.Model):
scheduled_type = models.CharField(
max_length=200, choices=ScheduledType, default="Call"
)
completed = models.BooleanField(default=False, verbose_name=_("Completed"))
duration = models.DurationField(default=timedelta(minutes=5))
notes = models.TextField(blank=True, null=True)
status = models.CharField(
@ -1859,7 +1863,7 @@ class Schedule(models.Model):
updated_at = models.DateTimeField(auto_now=True)
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
def schedule_past_date(self):
@ -2634,6 +2638,7 @@ class CustomGroup(models.Model):
"notes",
"tasks",
"activity",
"poitemsuploaded"
],
)
self.set_permissions(
@ -2641,7 +2646,7 @@ class CustomGroup(models.Model):
allowed_models=[],
other_perms=[
"view_purchaseordermodel",
"can_view_financials",
]
)
######################################
@ -2652,7 +2657,7 @@ class CustomGroup(models.Model):
elif self.name == "Sales":
self.set_permissions(
app="django_ledger",
allowed_models=["estimatemodel", "invoicemodel", "customermodel"],
allowed_models=["invoicemodel", "customermodel"],
)
self.set_permissions(
app="inventory",
@ -2662,11 +2667,13 @@ class CustomGroup(models.Model):
"staff",
"schedule",
"activity",
"lead",
"opportunity",
"customer",
"organization",
"notes",
"taska",
"tasks",
"lead"
"activity",
],
other_perms=[
@ -2678,6 +2685,10 @@ class CustomGroup(models.Model):
"can_view_inventory",
"can_view_sales",
"can_view_crm",
"view_estimatemodel",
"add_estimatemodel",
"change_estimatemodel",
"delete_estimatemodel",
],
)
######################################
@ -2709,19 +2720,18 @@ class CustomGroup(models.Model):
"bankaccountmodel",
"accountmodel",
"chartofaccountmodel",
"billmodel",
"itemmodel",
"invoicemodel",
"vendormodel",
"journalentrymodel",
"purchaseordermodel",
"estimatemodel",
"customermodel",
"vendormodel",
"ledgermodel",
"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)
updated_at = models.DateTimeField(auto_now=True)
def get_name(self):
return self.item.item.name.split('||')
class ExtraInfo(models.Model):
"""
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,
AccountModel,
PurchaseOrderModel,
EstimateModel
EstimateModel,
BillModel
)
from . import models
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):
if instance.po_status == "fulfilled":
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:
models.Notification.objects.create(
user=accountant,
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>
""",
)
@ -962,7 +963,11 @@ def create_po_fulfilled_notification(sender,instance,created,**kwargs):
@receiver(post_save, sender=models.Car)
def car_created_notification(sender, instance, created, **kwargs):
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:
models.Notification.objects.create(
user=accountant,
@ -971,11 +976,17 @@ def car_created_notification(sender, instance, created, **kwargs):
<a href="{instance.get_absolute_url()}" target="_blank">View</a>
""",
)
@receiver(post_save, sender=PurchaseOrderModel)
def po_fullfilled_notification(sender, instance, created, **kwargs):
if instance.is_fulfilled():
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:
models.Notification.objects.create(
user=recipient,
@ -987,21 +998,23 @@ def po_fullfilled_notification(sender, instance, created, **kwargs):
@receiver(post_save, sender=models.Vendor)
def vendor_created_notification(sender, instance, created, **kwargs):
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:
models.Notification.objects.create(
user=recipient,
message=f"""
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)
def sale_order_created_notification(sender, instance, created, **kwargs):
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:
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):
if instance.is_review():
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:
models.Notification.objects.create(
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)
# def lead_created_notification(sender, instance, created, **kwargs):
# if created:
# models.Notification.objects.create(
# user=instance.staff.user,
# message=f"""
# New Lead has been added.
# <a href="{reverse('lead_detail',kwargs={'dealer_slug':instance.dealer.slug,'slug':instance.slug})}" target="_blank">View</a>
# """,
# )
for recipient in recipients:
models.Notification.objects.create(
user=recipient,
message=f"""
Bill {instance.bill_number} is in review,please review and approve it
<a href="{reverse('bill-detail', kwargs={'dealer_slug': dealer.slug, 'entity_slug':dealer.entity.slug, 'bill_pk': instance.pk})}" 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}
# after po review send notification to {manager} to approve po
# after estimate review send notification to {manager} to approve estimate
for recipient in recipients:
models.Notification.objects.create(
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(
"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))
for form in itemtxs_formset.forms:
form.fields["item_model"].queryset = form.fields["item_model"].queryset.filter(
item_role="inventory"
)
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"],
"entity_slug": context["view"].kwargs["entity_slug"],
"po_model": po_model,

View File

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

View File

@ -1288,7 +1288,7 @@ def handle_account_process(invoice, amount, finance_data):
except Exception as e:
logger.error(
f"Error updating item_model.for_inventory for car {car.vin} (Invoice {invoice.invoice_number}): {e}",
exc_info=True
exc_info=True
)
print(e)
@ -1419,12 +1419,23 @@ def handle_payment(request, order):
headers = {"Content-Type": "application/json", "Accept": "application/json"}
auth = (settings.MOYASAR_SECRET_KEY, "")
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.save()
#
data = response.json()
amount = Decimal("{0:.2f}".format(Decimal(total) / Decimal(100)))
print(data)
models.PaymentHistory.objects.create(
user=request.user,
user_data=user_data,

File diff suppressed because it is too large Load Diff

View File

@ -184,10 +184,12 @@
</td>
<td class="align-items-start white-space-nowrap pe-2">
{% if bill_item.po_model_id %}
{% if perms.django_ledger.view_purchaseordermodel%}
<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 %}">
{% trans 'View PO' %}
</a>
{% endif %}
{% endif %}
</td>
</tr>
@ -231,9 +233,11 @@
<h5 class="mb-0">{% trans 'Bill Notes' %}</h5>
</div>
</div>
{% if perms.django_ledger.change_billmodel%}
<div class="card-body">
{% include 'bill/includes/card_markdown.html' with style='card_1' title='' notes_html=bill.notes_html %}
</div>
{% endif %}
</div>
</div>
</div>

View File

@ -68,7 +68,7 @@
class="btn btn-sm btn-phoenix-warning me-md-2">
{% trans 'Update' %}
</a>
{% if bill.can_pay %}
<button onclick="djLedger.toggleModal('{{ bill.get_html_id }}')"
@ -207,7 +207,7 @@
{% 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">
<i class="fas fa-edit me-2"></i>{% trans 'Update' %}
</a>
<!-- Mark as Draft -->
{% if bill.can_draft %}
<button class="btn btn-phoenix-success"
@ -223,7 +223,7 @@
</button>
{% endif %}
<!-- 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"
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' %}

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 %}
<!-- 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-dialog modal-md">
<div class="modal-content">

View File

@ -7,10 +7,6 @@
.main-tab li:last-child {
margin-left: auto;
}
.completed-task {
text-decoration: line-through;
opacity: 0.7;
}
.kanban-header {
position: relative;
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">
<h3 class="mb-0">{{ _("Lead Details")}}</h3>
<button class="btn p-0" ><span class="uil uil-times fs-7"></span></button>
</div>
<div class="card mb-2">
</div>
<div class="card mb-2">
<div class="card-body">
<div class="row align-items-center g-3 text-center text-xxl-start">
<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>
<h6 class="fw-bolder mb-2 text-body-highlight">{{ _("Opportunity") }}</h6>
{% 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 %}
<p>{{ _("No Opportunity") }}</p>
{% 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>
<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" 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="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="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 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 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="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="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%}
<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" }}')">
<i class="fa-solid fa-user-plus me-2"></i>
{% trans "Update Actions" %}
@ -207,7 +205,6 @@
<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>
{% 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 %}
</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">
@ -221,6 +218,10 @@
<div class="icon-item icon-item-md rounded-7 border border-translucent">
{% if activity.activity_type == "call" %}
<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" %}
<span class="fa-solid fa-envelope text-info-light fs-8"></span>
{% elif activity.activity_type == "visit" %}
@ -253,7 +254,7 @@
</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">
<h3 class="mb-4" id="scrollspyTask">{{ _("Opportunities") }} <span class="fw-light fs-7">({{ lead.get_opportunities.count}})</span></h3>
{% 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="mb-1 d-flex align-items-center justify-content-between">
<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>
{% endif %}
</div>
@ -301,8 +302,8 @@
<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" scope="col" style="width:20%;">{{ _("Created By")}}</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%;">{{ _("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>
@ -310,12 +311,8 @@
{% for note in notes %}
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
<td class="align-middle text-start fw-bold text-body-tertiary ps-1">{{note.note}}</td>
{% 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.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"
@ -446,7 +443,7 @@
</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="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>
</tr>
{% endfor %}
@ -469,11 +466,12 @@
</div>
</div>
{% 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">
<h3 class="mb-0" id="scrollspyEmails">{{ _("Tasks") }}</h3>
{% 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 %}
</div>
<div>
@ -495,7 +493,7 @@
</tr>
</thead>
<tbody class="list" id="all-tasks-table-body">
{% for task in tasks %}
{% for task in schedules %}
{% include "partials/task.html" %}
{% endfor %}
</tbody>
@ -533,60 +531,16 @@
</div>
</div> {% endcomment %}
<!-- activity Modal -->
{% include "components/activity_modal.html" with content_type="lead" slug=lead.slug %}
<!-- task Modal -->
<div class="modal fade" id="taskModal" 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 '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>
{% include "components/task_modal.html" with content_type="lead" slug=lead.slug %}
<!-- note Modal -->
<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 '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>
{% include "components/note_modal.html" with content_type="lead" slug=lead.slug %}
<!-- schedule Modal -->
{% include "components/schedule_modal.html" with content_type="lead" slug=lead.slug %}
{% endblock content %}
{% block customJS %}
<script>
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() {
document.querySelector('#id_note').value = ""
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.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">
{% 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.staff|upper }}</td>
{% 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" }}')">
{% trans "Update Actions" %}
</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 %}
{% if not lead.opportunity %}
{% if perms.inventory.add_opportunity%}
@ -257,8 +212,6 @@
</div>
</div>
</div>
{% endblock %}
{% block customJS %}

View File

@ -93,7 +93,7 @@
<div class="kanban-column bg-body">
<div class="kanban-header opacity-75"><span class="text-body">{{ _("Follow Ups")}} ({{follow_up|length}})</span></div>
{% 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">
<strong>{{lead.full_name|capfirst}}</strong><br>
<small>{{lead.email}}</small><br>
@ -109,7 +109,7 @@
<div class="kanban-column bg-body">
<div class="kanban-header opacity-75"><span class="text-body">{{ _("Negotiation Ups")}} ({{follow_up|length}})</span></div>
{% 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">
<strong>{{lead.full_name|capfirst}}</strong><br>
<small>{{lead.email}}</small><br>
@ -125,7 +125,7 @@
<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>
{% 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">
<strong>{{lead.full_name|capfirst}}</strong><br>
<small>{{lead.email}}</small><br>
@ -141,7 +141,7 @@
<div class="kanban-column bg-body">
<div class="kanban-header bg-danger-light opacity-75">{{ _("Lost") }} ({{lose|length}})</div>
{% 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">
<strong>{{lead.full_name|capfirst}}</strong><br>
<small>{{lead.email}}</small><br>
@ -151,7 +151,6 @@
{% endfor %}
</div>
</div>
</div>
</div>
</div>

View File

@ -333,74 +333,118 @@
</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;">
<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" 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>
<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>
{% 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 %}
<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>
<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>
{% 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="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 %}
</ul>
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade active show" 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 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">
<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>
{% 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 == "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="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="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 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>
{% endfor %}
</div>
</div>
<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>
{%if perms.inventory.change_opportunity%}
<form action="{% url 'add_note_to_opportunity' request.dealer.slug opportunity.slug %}" method="post">
@ -422,8 +466,8 @@
{% endfor %}
</div>
</div>
</div>
<div class="tab-pane fade" id="tab-meeting" role="tabpanel" aria-labelledby="meeting-tab">
</div> {% endcomment %}
{% comment %} <div class="tab-pane fade" id="tab-meeting" role="tabpanel" aria-labelledby="meeting-tab">
<h2 class="mb-4">Meeting</h2>
<div class="row align-items-center g-2 flex-wrap justify-content-start mb-3">
<div class="col-12 col-sm-auto">
@ -461,9 +505,9 @@
</div>
{% endfor %}
</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="col-auto d-flex flex-1">
<h2 class="mb-0">Call</h2>
@ -516,7 +560,7 @@
</div>
</div>
</div>
</div>
</div> {% endcomment %}
<div class="tab-pane fade" id="tab-emails" role="tabpanel" aria-labelledby="emails-tab">
<h2 class="mb-4">Emails</h2>
{% if perms.inventory.change_opportunity%}
@ -595,52 +639,7 @@
</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">
<h2 class="mb-3">Attachments</h2>
<div class="border-top border-dashed pt-3 pb-4">
@ -684,9 +683,73 @@
</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>
{% 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 %}

View File

@ -56,6 +56,16 @@
</a>
</li>
{% 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>
</div>
</div>
@ -183,16 +193,6 @@
</a>
</li>
{% 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>
</div>
</div>
@ -275,15 +275,14 @@
</a>
</li>
{% 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>
{% if perms.inventory.view_journalentrymodel %}
<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>
</li>
{% endif %}
</ul>
</div>
@ -467,8 +466,13 @@
{% endif %}
</li>
<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>
{% 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>-->
</ul>
</div>

View File

@ -52,16 +52,20 @@
</div>
</div>
<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 %}">
<!--<i class="bi bi-pencil-square"></i> -->
{{ _("Edit") }}
</a>
{% endif %}
{% if perms.django_ledger.delete_bankaccountmodel%}
<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>-->
{{ _("Delete") }}
</a>
{% endif %}
<a class="btn btn-sm btn-phoenix-secondary"
href="{% url 'bank_account_list' request.dealer.slug %}">
<!--<i class="bi bi-arrow-left-square-fill"></i>-->

View File

@ -96,12 +96,14 @@
{% endif %}
</td>
<td class="align-middle white-space-nowrap text-start">
{% if perms.django_ledger.view_transactionmodel%}
<div class="btn-reveal-trigger position-static">
<button class="btn btn-sm dropdown-toggle dropdown-caret-none transition-none btn-reveal fs-10" type="button" data-bs-toggle="dropdown" data-boundary="window" aria-haspopup="true" aria-expanded="false" data-bs-reference="parent"><span class="fas fa-ellipsis-h fs-10"></span></button>
<div class="dropdown-menu dropdown-menu-end py-2">
<a class="dropdown-item text-success-dark" href="{% url 'payment_details' request.dealer.slug tx.journal_entry.pk %}">{% trans 'view Transactions'|capfirst %}</a>
</div>
</div>
{% endif %}
</td>
</tr>
{% endfor %}
@ -135,17 +137,21 @@
</div>
<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 %}">
<!-- <i class="bi bi-pencil-square"></i> -->
<i class="fa-solid fa-pen-to-square"></i> {{ _('Edit') }}
</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">
<!-- <i class="bi bi-trash-fill"></i> -->
<i class="fa-solid fa-trash"></i> {{ _('Delete') }}
</a>
{% endif%}
<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="fa-regular fa-circle-left"></i> {% trans 'Back to List' %}
<i class="fa-regular fa-circle-left"></i> {% trans 'Back to COA List' %}
</a>
</div>

View File

@ -11,7 +11,9 @@
<div class="row mt-4">
<div class="d-flex justify-content-between mb-2">
<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>
{% endif %}
</div>
<!-- Account Type Tabs -->
@ -105,10 +107,12 @@
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
{% if perms.django_ledger.delete_chartofaccountmodel %}
<h5 class="modal-title" id="deleteModalLabel">
{% trans "Delete Account" %}
<span data-feather="alert-circle"></span>
</h5>
{% endif %}
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<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 %}">
<td class="fs-9 align-middle px-0 py-3">
<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>
</td>
<td class="subject order align-middle white-space-nowrap py-2 ps-0"><a class="fw-semibold text-primary" href="">{{task.title}}</a>
<div class="fs-10 d-block">{{task.description}}</div>
<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.scheduled_type|capfirst}}</div>
</td>
<td class="sent align-middle white-space-nowrap text-start fw-bold text-body-tertiary py-2">{{task.assigned_to}}</td>
<td class="date align-middle white-space-nowrap text-body py-2">{{task.created|naturalday|capfirst}}</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_at|naturalday|capfirst}}</td>
<td class="date align-middle white-space-nowrap text-body py-2">
{% if task.completed %}
<span class="badge badge-phoenix fs-10 badge-phoenix-success"><i class="fa-solid fa-check"></i></span>

View File

@ -21,7 +21,7 @@
<th style="min-width: 600px;" class="d-flex justify-content-between align-items-center">
{% trans 'Item' %}
{% if po_model.is_draft %}
<button type="button"
class="btn btn-sm btn-phoenix-success"
data-bs-toggle="modal"
@ -66,24 +66,21 @@
<span class="currency">{{CURRENCY}}</span>{{ f.instance.po_total_amount | currency_format }}</td>
<td>{{ f.po_item_status|add_class:"form-control" }}</td>
{% if itemtxs_formset.can_delete %}
<td class="text-center">
{{ f.DELETE|add_class:"form-check-input" }}
</td>
{% endif %}
<td class="text-center">
{% if f.instance.can_create_bill %}
{% if perms.djagno_ledger.add_billmodel%}
{% if f.instance.can_create_bill and can_add_bill %}
{{ f.create_bill|add_class:"form-check-input" }}
{% endif %}
{% elif f.instance.bill_model %}
{% if perms.djagno_ledger.view_billmodel%}
{% elif f.instance.bill_model and can_view_bill %}
<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 %}">
{% trans 'View Bill' %}
</a>
{% endif %}
{% endif %}
</td>
<td class="text-center">
{% if f.instance.bill_model %}

View File

@ -8,45 +8,61 @@
<div class="container-fluid mt-4">
<div class="row g-1">
<div class="col-lg-12">
<div class="d-flex flex-column gap-3">
<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 class="col-lg-12 mb-3 ">
<div class="row">
<div class="col-9">
<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>
<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 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="table-responsive">

View File

@ -83,7 +83,7 @@
<div class="col-12">
<div class="card shadow-sm">
<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>

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{
opacity: 0.5;
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>
{% endblock customCSS %}
{% block content %}
<div class="row mt-4">
{% if not items %}
@ -33,58 +142,53 @@
{% 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 class="custom-select">
<!-- Hidden native select for form submission -->
<select class="native-select" name="item" required tabindex="-1">
<option value="">Select a car</option>
{% for item in items %}
<option value="{{ item.hash }}"></option>
{% endfor %}
</select>
<!-- 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>
<!-- 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>
<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> -->
<button class="btn btn-sm btn-phoenix-success me-2" type="submit">
<i class="fa-solid fa-floppy-disk me-1"></i>
{{ _("Save") }}
</button>
<a href="{{request.META.HTTP_REFERER}}" class="btn btn-sm btn-phoenix-danger"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
<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({
document.addEventListener('DOMContentLoaded', function() {
const Toast = Swal.mixin({
toast: true,
position: "top-end",
showConfirmButton: false,
@ -95,44 +199,86 @@
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);
function notify(tag,msg){Toast.fire({icon: tag,titleText: msg});}
// Add remove button functionality
newForm.querySelector('.removeBtn').addEventListener('click', function() {
newForm.remove();
const customSelects = document.querySelectorAll('.custom-select');
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
document.querySelectorAll('.form-row').forEach(row => {
row.querySelector('.removeBtn').addEventListener('click', function() {
row.remove();
});
});
// Form submission
/*const form = document.getElementById('demo-form');
form.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(form);
alert(`Selected value: ${formData.get('car')}`);
});*/
// Handle form submission
document.getElementById('mainForm').addEventListener('submit', async function(e) {
document.getElementById('mainForm').addEventListener('submit', async function(e) {
e.preventDefault();
const titleInput = document.querySelector('[name="title"]');
@ -147,18 +293,16 @@
title: document.querySelector('[name=title]').value,
customer: document.querySelector('[name=customer]').value,
item: [],
quantity: [],
quantity: [1],
opportunity_id: "{{opportunity_id}}"
};
// 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);
});
document.querySelectorAll('[name="quantity[]"]').forEach(input => {
formData.quantity.push(input.value);
});
console.log(formData);
try {
@ -191,5 +335,7 @@
notify("error", error);
}
});
});
</script>
{% endblock customJS %}

View File

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