This commit is contained in:
Faheedkhan 2025-07-07 14:00:52 +03:00
commit 4bfb533448
19 changed files with 1188 additions and 672 deletions

View File

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

View File

@ -1836,7 +1836,9 @@ class Schedule(models.Model):
("completed", _("Completed")), ("completed", _("Completed")),
("canceled", _("Canceled")), ("canceled", _("Canceled")),
] ]
lead = models.ForeignKey(Lead, on_delete=models.CASCADE, related_name="schedules") content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
customer = models.ForeignKey( customer = models.ForeignKey(
CustomerModel, CustomerModel,
on_delete=models.CASCADE, on_delete=models.CASCADE,
@ -2634,6 +2636,7 @@ class CustomGroup(models.Model):
"notes", "notes",
"tasks", "tasks",
"activity", "activity",
"poitemsuploaded"
], ],
) )
self.set_permissions( self.set_permissions(
@ -2652,7 +2655,7 @@ class CustomGroup(models.Model):
elif self.name == "Sales": elif self.name == "Sales":
self.set_permissions( self.set_permissions(
app="django_ledger", app="django_ledger",
allowed_models=["estimatemodel", "invoicemodel", "customermodel"], allowed_models=["invoicemodel", "customermodel"],
) )
self.set_permissions( self.set_permissions(
app="inventory", app="inventory",
@ -2668,6 +2671,10 @@ class CustomGroup(models.Model):
"organization", "organization",
"notes", "notes",
"tasks", "tasks",
<<<<<<< HEAD
=======
"lead"
>>>>>>> 25d17efa11e8f03c6819b27572ca6abe91860d11
"activity", "activity",
], ],
other_perms=[ other_perms=[
@ -2679,6 +2686,10 @@ class CustomGroup(models.Model):
"can_view_inventory", "can_view_inventory",
"can_view_sales", "can_view_sales",
"can_view_crm", "can_view_crm",
"view_estimatemodel",
"add_estimatemodel",
"change_estimatemodel",
"delete_estimatemodel",
], ],
) )
###################################### ######################################
@ -2694,7 +2705,13 @@ class CustomGroup(models.Model):
"notes", "notes",
"tasks", "tasks",
"activity", "activity",
<<<<<<< HEAD
"vendor"], "vendor"],
=======
"vendor",
"poitemsuploaded"
],
>>>>>>> 25d17efa11e8f03c6819b27572ca6abe91860d11
other_perms=[ other_perms=[
"view_car", "view_car",
"view_carlocation", "view_carlocation",
@ -2711,7 +2728,6 @@ class CustomGroup(models.Model):
"bankaccountmodel", "bankaccountmodel",
"accountmodel", "accountmodel",
"chartofaccountmodel", "chartofaccountmodel",
"billmodel",
"itemmodel", "itemmodel",
"invoicemodel", "invoicemodel",
"vendormodel", "vendormodel",
@ -2723,7 +2739,7 @@ class CustomGroup(models.Model):
"ledgermodel", "ledgermodel",
"transactionmodel" "transactionmodel"
], ],
other_perms=["view_customermodel", "view_estimatemodel","can_view_inventory","can_view_sales","can_view_crm","can_view_financials","can_view_reports"], other_perms=["view_billmodel","add_billmodel","change_billmodel","delete_billmodel","view_customermodel", "view_estimatemodel","can_view_inventory","can_view_sales","can_view_crm","can_view_financials","can_view_reports"],
) )
@ -2919,6 +2935,8 @@ class PoItemsUploaded(models.Model):
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
def get_name(self):
return self.item.item.name.split('||')
class ExtraInfo(models.Model): class ExtraInfo(models.Model):
""" """
Stores additional information for any model with: Stores additional information for any model with:

644
inventory/override.py Normal file
View File

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

View File

@ -17,7 +17,8 @@ from django_ledger.models import (
LedgerModel, LedgerModel,
AccountModel, AccountModel,
PurchaseOrderModel, PurchaseOrderModel,
EstimateModel EstimateModel,
BillModel
) )
from . import models from . import models
from django.utils.timezone import now from django.utils.timezone import now
@ -954,7 +955,7 @@ def create_po_fulfilled_notification(sender,instance,created,**kwargs):
models.Notification.objects.create( models.Notification.objects.create(
user=accountant, user=accountant,
message=f""" message=f"""
New Purchase Order {instance.po_number} has been added to dealer {instance.dealer.name}. New Purchase Order {instance.po_number} has been added to dealer {dealer.name}.
<a href="{instance.get_absolute_url()}" target="_blank">View</a> <a href="{instance.get_absolute_url()}" target="_blank">View</a>
""", """,
) )
@ -975,7 +976,10 @@ def car_created_notification(sender, instance, created, **kwargs):
def po_fullfilled_notification(sender, instance, created, **kwargs): def po_fullfilled_notification(sender, instance, created, **kwargs):
if instance.is_fulfilled(): if instance.is_fulfilled():
dealer = models.Dealer.objects.get(entity=instance.entity) dealer = models.Dealer.objects.get(entity=instance.entity)
recipients = models.CustomGroup.objects.filter(dealer=instance.dealer,name="Accountant").first().group.user_set.all() recipients = User.objects.filter(
groups__customgroup__dealer=instance.dealer,
groups__customgroup__name__in=["Manager", "Inventory"]
)
for recipient in recipients: for recipient in recipients:
models.Notification.objects.create( models.Notification.objects.create(
user=recipient, user=recipient,
@ -987,14 +991,16 @@ def po_fullfilled_notification(sender, instance, created, **kwargs):
@receiver(post_save, sender=models.Vendor) @receiver(post_save, sender=models.Vendor)
def vendor_created_notification(sender, instance, created, **kwargs): def vendor_created_notification(sender, instance, created, **kwargs):
if created: if created:
recipients = models.CustomGroup.objects.filter(dealer=instance.dealer,name="Inventory").first().group.user_set.all() recipients = User.objects.filter(
groups__customgroup__dealer=instance.dealer,
groups__customgroup__name__in=["Manager", "Inventory"]
)
for recipient in recipients: for recipient in recipients:
models.Notification.objects.create( models.Notification.objects.create(
user=recipient, user=recipient,
message=f""" message=f"""
New Vendor {instance.name} has been added to dealer {instance.dealer.name}. New Vendor {instance.name} has been added to dealer {instance.dealer.name}.
<a href="{instance.get_absolute_url()}" target="_blank">View</a>
""", """,
) )
@ -1055,19 +1061,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)
# @receiver(post_save, sender=models.Lead) for recipient in recipients:
# def lead_created_notification(sender, instance, created, **kwargs): models.Notification.objects.create(
# if created: user=recipient,
# models.Notification.objects.create( message=f"""
# user=instance.staff.user, Bill {instance.bill_number} is in review,please review and approve it
# message=f""" <a href="{reverse('bill-detail', kwargs={'dealer_slug': dealer.slug, 'entity_slug':dealer.entity.slug, 'bill_pk': instance.pk})}" target="_blank">View</a>.
# New Lead has been added. """
# <a href="{reverse('lead_detail',kwargs={'dealer_slug':instance.dealer.slug,'slug':instance.slug})}" target="_blank">View</a> )
# """,
# )
@receiver(post_save, sender=BillModel)
def bill_model_after_approve_notification(sender, instance, created, **kwargs):
if instance.is_approved():
dealer = models.Dealer.objects.get(entity=instance.ledger.entity)
recipients = models.CustomGroup.objects.filter(dealer=dealer,name="Accountant").first().group.user_set.exclude(email=dealer.user.email)
# send notification after car is sold {manager,dealer} for recipient in recipients:
# after po review send notification to {manager} to approve po models.Notification.objects.create(
# after estimate review send notification to {manager} to approve estimate user=recipient,
message=f"""
Bill {instance.bill_number} has been approved.
<a href="{reverse('bill-detail', kwargs={'dealer_slug': dealer.slug, 'entity_slug':dealer.entity.slug, 'bill_pk': instance.pk})}" target="_blank">View</a>.
please comlete the bill payment.
"""
)

View File

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

View File

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

View File

@ -1288,7 +1288,7 @@ def handle_account_process(invoice, amount, finance_data):
except Exception as e: except Exception as e:
logger.error( logger.error(
f"Error updating item_model.for_inventory for car {car.vin} (Invoice {invoice.invoice_number}): {e}", f"Error updating item_model.for_inventory for car {car.vin} (Invoice {invoice.invoice_number}): {e}",
exc_info=True exc_info=True
) )
print(e) print(e)
@ -1419,12 +1419,23 @@ def handle_payment(request, order):
headers = {"Content-Type": "application/json", "Accept": "application/json"} headers = {"Content-Type": "application/json", "Accept": "application/json"}
auth = (settings.MOYASAR_SECRET_KEY, "") auth = (settings.MOYASAR_SECRET_KEY, "")
response = requests.request("POST", url, auth=auth, headers=headers, data=payload) response = requests.request("POST", url, auth=auth, headers=headers, data=payload)
if response.status_code == 400:
data = response.json()
if data["type"] == "validation_error":
errors = data.get("errors", {})
if "source.year" in errors:
raise Exception("Invalid expiry year")
else:
raise Exception("Validation Error: ", errors)
else:
print("Failed to process payment:", data)
# #
order.status = AbstractOrder.STATUS.NEW order.status = AbstractOrder.STATUS.NEW
order.save() order.save()
# #
data = response.json() data = response.json()
amount = Decimal("{0:.2f}".format(Decimal(total) / Decimal(100))) amount = Decimal("{0:.2f}".format(Decimal(total) / Decimal(100)))
print(data)
models.PaymentHistory.objects.create( models.PaymentHistory.objects.create(
user=request.user, user=request.user,
user_data=user_data, user_data=user_data,

View File

@ -78,9 +78,6 @@ from django.views.generic import (
from django.db.models import Case, Value, IntegerField, When from django.db.models import Case, Value, IntegerField, When
#logger
logger=logging.getLogger(__name__)
# Django Ledger # Django Ledger
from django_ledger.io import roles from django_ledger.io import roles
from django_ledger.utils import accruable_net_summary from django_ledger.utils import accruable_net_summary
@ -107,9 +104,9 @@ from django_ledger.forms.bank_account import (
) )
from django_ledger.views.bill import ( from django_ledger.views.bill import (
# BillModelCreateView, # BillModelCreateView,
BillModelDetailView, # BillModelDetailView,
BillModelUpdateView, # BillModelUpdateView,
BaseBillActionView as BaseBillActionViewBase, # BaseBillActionView as BaseBillActionViewBase,
BillModelModelBaseView, BillModelModelBaseView,
) )
from django_ledger.forms.bill import ( from django_ledger.forms.bill import (
@ -136,10 +133,18 @@ from django_ledger.forms.purchase_order import (
) )
from django_ledger.views.purchase_order import ( from django_ledger.views.purchase_order import (
PurchaseOrderModelDetailView as PurchaseOrderModelDetailViewBase, PurchaseOrderModelDetailView as PurchaseOrderModelDetailViewBase,
PurchaseOrderModelUpdateView as PurchaseOrderModelUpdateViewBase, # PurchaseOrderModelUpdateView as PurchaseOrderModelUpdateViewBase,
BasePurchaseOrderActionActionView as BasePurchaseOrderActionActionViewBase, # BasePurchaseOrderActionActionView as BasePurchaseOrderActionActionViewBase,
PurchaseOrderModelDeleteView as PurchaseOrderModelDeleteViewBase, PurchaseOrderModelDeleteView as PurchaseOrderModelDeleteViewBase,
) )
from .override import (
PurchaseOrderModelUpdateView as PurchaseOrderModelUpdateViewBase,
BasePurchaseOrderActionActionView as BasePurchaseOrderActionActionViewBase,
BillModelDetailView as BillModelDetailViewBase,
BillModelUpdateView as BillModelUpdateViewBase,
BaseBillActionView as BaseBillActionViewBase,
)
from django_ledger.models import ( from django_ledger.models import (
ItemTransactionModel, ItemTransactionModel,
EntityModel, EntityModel,
@ -205,7 +210,6 @@ from django_q.tasks import async_task
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
class Hash(Func): class Hash(Func):
""" """
Represents a function used to compute a hash value. Represents a function used to compute a hash value.
@ -2250,7 +2254,7 @@ class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView
@login_required @login_required
@permission_required("inventory.add_note", raise_exception=True) @permission_required("inventory.add_notes", raise_exception=True)
def add_note_to_customer(request,dealer_slug, slug): def add_note_to_customer(request,dealer_slug, slug):
""" """
This function allows authenticated users to add a note to a specific customer. The This function allows authenticated users to add a note to a specific customer. The
@ -2922,6 +2926,8 @@ def GroupPermissionView(request, dealer_slug, pk):
("inventory", "notes"), ("inventory", "notes"),
("inventory", "tasks"), ("inventory", "tasks"),
("inventory", "activity"), ("inventory", "activity"),
("inventory", "vendor"),
("inventory", "poitemsuploaded"),
("django_ledger", "purchaseordermodel"), ("django_ledger", "purchaseordermodel"),
@ -5619,7 +5625,7 @@ class LeadDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
context["transfer_form"].fields[ context["transfer_form"].fields[
"transfer_to" "transfer_to"
].queryset = models.Staff.objects.filter( ].queryset = models.Staff.objects.filter(
dealer=dealer,staff_member__user__groups__permissions__codename__contains="add_lead").exclude(staff_member__user=self.request.user).distinct() dealer=dealer,staff_member__user__groups__permissions__codename__contains="can_reassign_lead").exclude(staff_member__user=self.request.user).distinct()
context["activity_form"] = forms.ActivityForm() context["activity_form"] = forms.ActivityForm()
context["staff_task_form"] = forms.StaffTaskForm() context["staff_task_form"] = forms.StaffTaskForm()
@ -5818,7 +5824,7 @@ def update_lead_actions(request,dealer_slug):
# Log before updating lead fields # Log before updating lead fields
logger.debug( logger.debug(
f"User {user_username} found Lead ID: {lead.id} ('{lead.name}') " f"User {user_username} found Lead ID: {lead.pk} ('{lead.slug}') "
f"for update. Current action: '{current_action}', Next action: '{next_action}', Next action date: '{next_action_date}'." f"for update. Current action: '{current_action}', Next action: '{next_action}', Next action date: '{next_action_date}'."
) )
@ -5830,21 +5836,20 @@ def update_lead_actions(request,dealer_slug):
next_action_date, "%Y-%m-%dT%H:%M" next_action_date, "%Y-%m-%dT%H:%M"
) )
lead.next_action_date = timezone.make_aware(next_action_datetime) lead.next_action_date = timezone.make_aware(next_action_datetime)
logger.debug(f"Lead ID: {lead.id} next_action_date parsed to {lead.next_action_date}.") logger.debug(f"Lead ID: {lead.pk} next_action_date parsed to {lead.next_action_date}.")
except ValueError as ve: except ValueError as ve:
# Log for invalid date format # Log for invalid date format
logger.warning( f"submitted invalid date format ('{next_action_date}') " logger.warning( f"submitted invalid date format ('{next_action_date}') "
f"for Lead ID: {lead.id}. Error: {ve}" f"for Lead ID: {lead.pk}. Error: {ve}"
) )
return JsonResponse( return JsonResponse(
{"success": False, "message": "Invalid date format"}, status=400 {"success": False, "message": "Invalid date format"}, status=400
) )
# Save the lead # Save the lead
lead.save() lead.save()
# --- Logging for successful update (main try block success) --- # --- Logging for successful update (main try block success) ---
logger.info( logger.info(
f"User {user_username} successfully updated Lead ID: {lead.id} ('{lead.name}'). " f"User {user_username} successfully updated Lead ID: {lead.pk} ('{lead.slug}'). "
f"New Status: '{lead.status}', Next Action: '{lead.next_action}', Next Action Date: '{lead.next_action_date}'." f"New Status: '{lead.status}', Next Action: '{lead.next_action}', Next Action Date: '{lead.next_action_date}'."
) )
return JsonResponse( return JsonResponse(
@ -5932,7 +5937,7 @@ def LeadDeleteView(request,dealer_slug, slug):
# Log intent before attempting deletion # Log intent before attempting deletion
logger.debug( logger.debug(
f"User {user_username} attempting to delete Lead ID: {lead.id} ('{lead.name}') " f"User {user_username} attempting to delete Lead ID: {lead.pk} ('{lead.slug}') "
f"and its associated customer/user for dealer '{dealer_slug}'." f"and its associated customer/user for dealer '{dealer_slug}'."
) )
@ -5943,19 +5948,19 @@ def LeadDeleteView(request,dealer_slug, slug):
# --- Single-line log for successful associated user/customer deletion --- # --- Single-line log for successful associated user/customer deletion ---
logger.info( logger.info(
f"User {user_username} successfully deleted associated user and customer " f"User {user_username} successfully deleted associated user and customer "
f"for Lead ID: {lead.id} ('{lead.name}') (Email: {lead.customer.email})." f"for Lead ID: {lead.pk} ('{lead.slug}') (Email: {lead.customer.email})."
) )
except Exception as e: except Exception as e:
# --- Single-line log for error during associated user/customer deletion --- # --- Single-line log for error during associated user/customer deletion ---
logger.error( logger.error(
f"User {user_username} encountered an error deleting associated user/customer " f"User {user_username} encountered an error deleting associated user/customer "
f"for Lead ID: {lead.id} ('{lead.name}') (Email: {getattr(lead.customer, 'email', 'N/A')}). " # Safely get email f"for Lead ID: {lead.slug} ('{lead.slug}') (Email: {getattr(lead.customer, 'email', 'N/A')}). " # Safely get email
f"Error: {e}", f"Error: {e}",
exc_info=True exc_info=True
) )
print(e) print(e)
lead_id_final = lead.id # Capture before deletion lead_id_final = lead.pk # Capture before deletion
lead_name_final = lead.name lead_name_final = lead.slug
lead.delete() lead.delete()
# Log the final lead deletion, which happens unconditionally after the try-except # Log the final lead deletion, which happens unconditionally after the try-except
logger.info( logger.info(
@ -5967,7 +5972,7 @@ def LeadDeleteView(request,dealer_slug, slug):
@login_required @login_required
@permission_required("inventory.add_note", raise_exception=True) @permission_required("inventory.add_notes", raise_exception=True)
def add_note_to_lead(request,dealer_slug, slug): def add_note_to_lead(request,dealer_slug, slug):
""" """
Adds a note to a specific lead. This view is accessible only to authenticated Adds a note to a specific lead. This view is accessible only to authenticated
@ -6000,7 +6005,7 @@ def add_note_to_lead(request,dealer_slug, slug):
@login_required @login_required
@permission_required("inventory.add_note", raise_exception=True) @permission_required("inventory.add_notes", raise_exception=True)
def add_note_to_opportunity(request,dealer_slug, slug): def add_note_to_opportunity(request,dealer_slug, slug):
""" """
Add a note to a specific opportunity identified by its primary key. Add a note to a specific opportunity identified by its primary key.
@ -6033,7 +6038,7 @@ def add_note_to_opportunity(request,dealer_slug, slug):
@login_required @login_required
@permission_required("inventory.delete_note", raise_exception=True) @permission_required("inventory.delete_notes", raise_exception=True)
def delete_note(request,dealer_slug, pk): def delete_note(request,dealer_slug, pk):
""" """
Deletes a specific note created by the currently logged-in user and redirects Deletes a specific note created by the currently logged-in user and redirects
@ -6095,7 +6100,7 @@ def lead_convert(request,dealer_slug, slug):
@login_required @login_required
@permission_required("inventory.add_schedule", raise_exception=True) @permission_required("inventory.add_schedule", raise_exception=True)
def schedule_lead(request, dealer_slug,slug): def schedule_event(request, dealer_slug,content_type,slug):
""" """
Handles the scheduling of a lead for an appointment. Handles the scheduling of a lead for an appointment.
@ -6113,29 +6118,46 @@ def schedule_lead(request, dealer_slug,slug):
method and validity of the form submission. method and validity of the form submission.
:rtype: HttpResponse :rtype: HttpResponse
""" """
user_username = request.user.username if request.user.is_authenticated else 'anonymous'
# Log the attempt to retrieve the model dynamically
logger.debug(
f"User {user_username} attempting to retrieve model "
f"for content_type '{content_type}' for dealer '{dealer_slug}'."
)
try:
model = apps.get_model(f"inventory.{content_type}")
except LookupError:
# --- Single-line log for LookupError (Model not found) ---
logger.warning(
f"User {user_username} requested an invalid model content_type: '{content_type}'. "
f"Raising Http404 for dealer '{dealer_slug}'."
)
raise Http404("Model not found")
dealer = get_object_or_404(models.Dealer,slug=dealer_slug)
obj = get_object_or_404(model, slug=slug)
if not request.is_staff: if not request.is_staff:
messages.error(request, _("You do not have permission to schedule lead")) messages.error(request, _("You do not have permission to schedule."))
return redirect("lead_list", dealer_slug=dealer_slug) return redirect(request.META.get("HTTP_REFERER"))
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
lead = get_object_or_404(models.Lead, slug=slug, dealer=dealer)
if request.method == "POST":
# Get user info for logging (available throughout the POST handling)
user_username = request.user.username if request.user.is_authenticated else 'anonymous'
if request.method == "POST":
form = forms.ScheduleForm(request.POST) form = forms.ScheduleForm(request.POST)
if form.is_valid(): if form.is_valid():
instance = form.save(commit=False) instance = form.save(commit=False)
instance.lead = lead instance.content_object = obj
instance.scheduled_by = request.user instance.scheduled_by = request.user
instance.customer = lead.get_customer_model() if obj.customer:
instance.customer = obj.customer.customer_model
elif obj.organization:
instance.cutsomer = obj.organization.customer_model
service = Service.objects.get(name=instance.scheduled_type) service = Service.objects.get(name=instance.scheduled_type)
# Log attempt to create AppointmentRequest # Log attempt to create AppointmentRequest
logger.debug( logger.debug(
f"User {user_username} attempting to create AppointmentRequest " f"User {user_username} attempting to create AppointmentRequest "
f"for Lead ID: {lead.id} ('{lead.name}'). Service: '{service.name}', " f"for {content_type} ID: {obj.pk} ('{obj.slug}'). Service: '{service.name}', "
f"Scheduled At: '{instance.scheduled_at}', Duration: '{instance.duration}'." f"Scheduled At: '{instance.scheduled_at}', Duration: '{instance.duration}'."
) )
@ -6149,41 +6171,34 @@ def schedule_lead(request, dealer_slug,slug):
) )
except ValidationError as e: except ValidationError as e:
messages.error(request, str(e)) messages.error(request, str(e))
return redirect("schedule_lead", dealer_slug=lead.dealer.slug, slug=lead.slug) return redirect(request.META.get("HTTP_REFERER"))
client = get_object_or_404(User, email=lead.email) client = get_object_or_404(User, email=instance.customer.email)
# Create Appointment # Create Appointment
Appointment.objects.create( Appointment.objects.create(
client=client, client=client,
appointment_request=appointment_request, appointment_request=appointment_request,
phone=lead.phone_number, phone=instance.phone,
address=lead.address, address=instance.address_1,
) )
instance.save() instance.save()
# --- Logging for successful AppointmentRequest and Appointment creation --- # --- Logging for successful AppointmentRequest and Appointment creation ---
logger.info( logger.info(
f"User {user_username} successfully scheduled Lead ID: {lead.id} ('{lead.name}'). " f"User {user_username} successfully scheduled {content_type} ID: {obj.pk} ('{obj.slug}'). "
f"AppointmentRequest ID: {appointment_request.pk}, Appointment ID: {appointment_request.appointment.pk}." f"AppointmentRequest ID: {appointment_request.pk}, Appointment ID: {appointment_request.appointment.pk}."
) )
messages.success(request, _("Appointment Created Successfully")) messages.success(request, _("Appointment Created Successfully"))
try:
if lead.opportunity: return redirect(request.META.get("HTTP_REFERER"))
return redirect("opportunity_detail", dealer_slug=lead.dealer.slug, slug=lead.opportunity.slug)
except models.Lead.opportunity.RelatedObjectDoesNotExist:
logger.info(
f"Lead ID: {lead.id} ('{lead.name}') has no associated opportunity. "
f"Redirecting to lead list for user {user_username} "
)
return redirect("lead_list", dealer_slug=lead.dealer.slug)
else: else:
# Log for invalid form data # Log for invalid form data
logger.warning( logger.warning(
f"User {user_username} submitted invalid schedule form data " f"User {user_username} submitted invalid schedule form data "
f"for Lead ID: {lead.id} ('{lead.name}'). Errors: {form.errors.as_json()}" f"for Lead ID: {obj.pk} ('{obj.slug}'). Errors: {form.errors.as_json()}"
) )
messages.error(request, f"Invalid form data: {str(form.errors)}") messages.error(request, f"Invalid form data: {str(form.errors)}")
return redirect("schedule_lead", dealer_slug=dealer_slug,slug=lead.slug) return redirect(request.META.get("HTTP_REFERER"))
form = forms.ScheduleForm() form = forms.ScheduleForm()
return render(request, "crm/leads/schedule_lead.html", {"lead": lead, "form": form}) return render(request, "crm/leads/schedule_lead.html", {"lead": lead, "form": form})
@ -6243,7 +6258,6 @@ def send_lead_email(request,dealer_slug, slug, email_pk=None):
dealer = get_object_or_404(models.Dealer, slug=dealer_slug) dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
lead = get_object_or_404(models.Lead, slug=slug) lead = get_object_or_404(models.Lead, slug=slug)
status = request.GET.get("status") status = request.GET.get("status")
# Get user info for logging
user_username = request.user.username if request.user.is_authenticated else 'anonymous' user_username = request.user.username if request.user.is_authenticated else 'anonymous'
if status == "draft": if status == "draft":
@ -6264,11 +6278,12 @@ def send_lead_email(request,dealer_slug, slug, email_pk=None):
activity_type=models.ActionChoices.EMAIL, activity_type=models.ActionChoices.EMAIL,
) )
messages.success(request, _("Email Draft successfully")) messages.success(request, _("Email Draft successfully"))
try: try:
if lead.opportunity: if getattr(lead, "opportunity",None):
# Log success when opportunity exists and redirecting # Log success when opportunity exists and redirecting
logger.info( logger.info(
f"User {user_username} successfully drafted email for Lead ID: {lead.id} ('{lead.name}'). " f"User {user_username} successfully drafted email for Lead ID: {lead.pk} ('{lead.slug}'). "
f"Lead has an Opportunity (ID: {lead.opportunity.pk}), redirecting to opportunity detail." f"Lead has an Opportunity (ID: {lead.opportunity.pk}), redirecting to opportunity detail."
) )
response = HttpResponse( response = HttpResponse(
@ -6280,16 +6295,16 @@ def send_lead_email(request,dealer_slug, slug, email_pk=None):
else: else:
# Log success when no opportunity and redirecting to lead detail # Log success when no opportunity and redirecting to lead detail
logger.info( logger.info(
f"User {user_username} successfully drafted email for Lead ID: {lead.id} ('{lead.name}'). " f"User {user_username} successfully drafted email for Lead ID: {lead.pk} ('{lead.slug}'). "
f"Lead has no Opportunity, redirecting to lead detail." f"Lead has no Opportunity, redirecting to lead detail."
) )
response = HttpResponse(redirect("lead_detail", dealer_slug=dealer_slug,slug=lead.slug)) response = HttpResponse()
response["HX-Redirect"] = reverse("lead_detail", dealer_slug=dealer_slug,slug=lead.slug) response["HX-Redirect"] = reverse("lead_detail", dealer_slug=dealer_slug,slug=lead.slug)
return response return response
except models.Lead.opportunity.RelatedObjectDoesNotExist: except models.Lead.opportunity.RelatedObjectDoesNotExist:
# --- Log when Lead.opportunity does not exist (Draft status) --- # --- Log when Lead.opportunity does not exist (Draft status) ---
logger.info( logger.info(
f"User {user_username} drafted email for Lead ID: {lead.id} ('{lead.name}'). " f"User {user_username} drafted email for Lead ID: {lead.pk} ('{lead.slug}'). "
f"Lead's opportunity does not exist. Redirecting to lead list." f"Lead's opportunity does not exist. Redirecting to lead list."
) )
return redirect("lead_list",dealer_slug=dealer.slug) return redirect("lead_list",dealer_slug=dealer.slug)
@ -6328,14 +6343,14 @@ def send_lead_email(request,dealer_slug, slug, email_pk=None):
if lead.opportunity: if lead.opportunity:
# Log success when opportunity exists and redirecting after sending email # Log success when opportunity exists and redirecting after sending email
logger.info( logger.info(
f"User {user_username} successfully sent email for Lead ID: {lead.id} ('{lead.name}'). " f"User {user_username} successfully sent email for Lead ID: {lead.pk} ('{lead.slug}'). "
f"Lead has an Opportunity (ID: {lead.opportunity.pk}), redirecting to opportunity detail." f"Lead has an Opportunity (ID: {lead.opportunity.pk}), redirecting to opportunity detail."
) )
return redirect("opportunity_detail", dealer_slug=dealer_slug,slug=lead.opportunity.slug) return redirect("opportunity_detail", dealer_slug=dealer_slug,slug=lead.opportunity.slug)
except models.Lead.opportunity.RelatedObjectDoesNotExist: except models.Lead.opportunity.RelatedObjectDoesNotExist:
# --- Log when Lead.opportunity does not exist (POST request for sending) --- # --- Log when Lead.opportunity does not exist (POST request for sending) ---
logger.info( logger.info(
f"User {user_username} sent email for Lead ID: {lead.id} ('{lead.name}'). " f"User {user_username} sent email for Lead ID: {lead.pk} ('{lead.slug}'). "
f"Lead's opportunity does not exist. Redirecting to lead list." f"Lead's opportunity does not exist. Redirecting to lead list."
) )
return redirect("lead_list",dealer_slug=dealer_slug) return redirect("lead_list",dealer_slug=dealer_slug)
@ -6393,7 +6408,6 @@ def add_activity_to_lead(request, pk):
form = forms.ActivityForm(request.POST) form = forms.ActivityForm(request.POST)
if form.is_valid(): if form.is_valid():
activity = form.save(commit=False) activity = form.save(commit=False)
print(activity)
activity.content_object = lead activity.content_object = lead
activity.dealer = dealer activity.dealer = dealer
activity.activity_type = form.cleaned_data["activity_type"] activity.activity_type = form.cleaned_data["activity_type"]
@ -6451,6 +6465,14 @@ class OpportunityCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateVie
instance.lead.save() instance.lead.save()
return super().form_valid(form) return super().form_valid(form)
def get_form(self,form_class=None):
dealer = get_object_or_404(models.Dealer,slug=self.kwargs.get("dealer_slug"))
staff = getattr(self.request.user.staffmember, "staff", None)
form = super().get_form(form_class)
form.fields["car"].queryset = models.Car.objects.filter(dealer=dealer,status='available',finances__selling_price__gt=0)
form.fields["lead"].queryset = models.Lead.objects.filter(dealer=dealer,staff=staff)
return form
def get_success_url(self): def get_success_url(self):
return reverse_lazy("opportunity_detail", kwargs={"dealer_slug":self.kwargs.get("dealer_slug"),"slug": self.object.slug}) return reverse_lazy("opportunity_detail", kwargs={"dealer_slug":self.kwargs.get("dealer_slug"),"slug": self.object.slug})
@ -6487,8 +6509,9 @@ class OpportunityUpdateView(LoginRequiredMixin,PermissionRequiredMixin, SuccessM
def get_form(self, form_class=None): def get_form(self, form_class=None):
form = super().get_form(form_class) form = super().get_form(form_class)
dealer = get_object_or_404(models.Dealer,slug=self.kwargs.get("dealer_slug")) dealer = get_object_or_404(models.Dealer,slug=self.kwargs.get("dealer_slug"))
form.fields['car'].queryset = models.Car.objects.filter(dealer=dealer) staff = getattr(self.request.user.staffmember, "staff", None)
form.fields['lead'].queryset = models.Lead.objects.filter(dealer=dealer) form.fields["car"].queryset = models.Car.objects.filter(dealer=dealer,status='available',finances__selling_price__gt=0)
form.fields["lead"].queryset = models.Lead.objects.filter(dealer=dealer,staff=staff)
return form return form
def get_success_url(self): def get_success_url(self):
@ -6530,19 +6553,19 @@ class OpportunityDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailV
content_type__model="lead", object_id=self.object.id content_type__model="lead", object_id=self.object.id
).order_by("-created") ).order_by("-created")
context["lead_notes"] = models.Notes.objects.filter( context["lead_notes"] = models.Notes.objects.filter(
content_type__model="lead", object_id=self.object.lead.id content_type__model="lead", object_id=self.object.lead.pk
).order_by("-created") ).order_by("-created")
context["notes"] = models.Notes.objects.filter( context["notes"] = models.Notes.objects.filter(
content_type__model="opportunity", object_id=self.object.id content_type__model="opportunity", object_id=self.object.id
).order_by("-created") ).order_by("-created")
context["lead_activities"] = models.Activity.objects.filter( context["lead_activities"] = models.Activity.objects.filter(
content_type__model="lead", object_id=self.object.lead.id content_type__model="lead", object_id=self.object.lead.pk
) )
context["activities"] = models.Activity.objects.filter( context["activities"] = models.Activity.objects.filter(
content_type__model="opportunity", object_id=self.object.id content_type__model="opportunity", object_id=self.object.id
) )
lead_email_qs = models.Email.objects.filter( lead_email_qs = models.Email.objects.filter(
content_type__model="lead", object_id=self.object.lead.id content_type__model="lead", object_id=self.object.lead.pk
) )
email_qs = models.Email.objects.filter( email_qs = models.Email.objects.filter(
content_type__model="opportunity", object_id=self.object.id content_type__model="opportunity", object_id=self.object.id
@ -6556,8 +6579,9 @@ class OpportunityDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailV
"draft": lead_email_qs.filter(status="DRAFT"), "draft": lead_email_qs.filter(status="DRAFT"),
} }
context["staff_task_form"] = forms.StaffTaskForm() context["staff_task_form"] = forms.StaffTaskForm()
context["note_form"] = forms.NoteForm()
context["lead_tasks"] = models.Tasks.objects.filter( context["lead_tasks"] = models.Tasks.objects.filter(
content_type__model="lead", object_id=self.object.lead.id content_type__model="lead", object_id=self.object.lead.pk
) )
context["tasks"] = models.Tasks.objects.filter( context["tasks"] = models.Tasks.objects.filter(
content_type__model="opportunity", object_id=self.object.id content_type__model="opportunity", object_id=self.object.id
@ -6580,7 +6604,8 @@ class OpportunityListView(LoginRequiredMixin,PermissionRequiredMixin, ListView):
def get_queryset(self): def get_queryset(self):
dealer = get_user_type(self.request) dealer = get_user_type(self.request)
queryset = models.Opportunity.objects.filter(dealer=dealer) staff = getattr(self.request.user.staffmember, "staff", None)
queryset = models.Opportunity.objects.filter(dealer=dealer, lead__staff=staff)
# Search filter # Search filter
search = self.request.GET.get("search") search = self.request.GET.get("search")
@ -7040,7 +7065,7 @@ class BillListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView): class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView):
template_name = "bill/bill_create.html" template_name = "bill/bill_create.html"
PAGE_TITLE = _("Create Bill") PAGE_TITLE = _("Create Bill")
permission_required = ["django_ledger.add_billmodel"] permission_required = "django_ledger.add_billmodel"
extra_context = { extra_context = {
"page_title": PAGE_TITLE, "page_title": PAGE_TITLE,
"header_title": PAGE_TITLE, "header_title": PAGE_TITLE,
@ -7048,7 +7073,6 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView)
} }
for_purchase_order = False for_purchase_order = False
for_estimate = False for_estimate = False
permission_required = "django_ledger.add_billmodel"
# Get user info for logging # Get user info for logging
@ -7061,7 +7085,7 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView)
if self.for_estimate and "ce_pk" in self.kwargs: if self.for_estimate and "ce_pk" in self.kwargs:
estimate_qs = EstimateModel.objects.for_entity( estimate_qs = EstimateModel.objects.for_entity(
entity_slug=self.kwargs["entity_slug"], user_model=self.request.user entity_slug=self.kwargs["entity_slug"], user_model=dealer.entity.admin
) )
estimate_model: EstimateModel = get_object_or_404( estimate_model: EstimateModel = get_object_or_404(
estimate_qs, uuid__exact=self.kwargs["ce_pk"] estimate_qs, uuid__exact=self.kwargs["ce_pk"]
@ -7080,7 +7104,7 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(BillModelCreateView, self).get_context_data(**kwargs) context = super(BillModelCreateView, self).get_context_data(**kwargs)
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
user_username = self.request.user.username if self.request.user.is_authenticated else 'anonymous' user_username = self.request.user.username if self.request.user.is_authenticated else 'anonymous'
if self.for_purchase_order: if self.for_purchase_order:
@ -7110,7 +7134,7 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView)
return HttpResponseBadRequest() return HttpResponseBadRequest()
po_qs = PurchaseOrderModel.objects.for_entity( po_qs = PurchaseOrderModel.objects.for_entity(
entity_slug=self.kwargs["entity_slug"], user_model=self.request.user entity_slug=self.kwargs["entity_slug"], user_model=dealer.entity.admin
).prefetch_related("itemtransactionmodel_set") ).prefetch_related("itemtransactionmodel_set")
po_model: PurchaseOrderModel = get_object_or_404(po_qs, uuid__exact=po_pk) po_model: PurchaseOrderModel = get_object_or_404(po_qs, uuid__exact=po_pk)
po_itemtxs_qs = po_model.itemtransactionmodel_set.filter( po_itemtxs_qs = po_model.itemtransactionmodel_set.filter(
@ -7131,7 +7155,7 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView)
) )
elif self.for_estimate: elif self.for_estimate:
estimate_qs = EstimateModel.objects.for_entity( estimate_qs = EstimateModel.objects.for_entity(
entity_slug=self.kwargs["entity_slug"], user_model=self.request.user entity_slug=self.kwargs["entity_slug"], user_model=dealer.entity.admin
) )
estimate_uuid = self.kwargs["ce_pk"] estimate_uuid = self.kwargs["ce_pk"]
estimate_model: EstimateModel = get_object_or_404( estimate_model: EstimateModel = get_object_or_404(
@ -7175,7 +7199,7 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView)
if self.for_estimate: if self.for_estimate:
ce_pk = self.kwargs["ce_pk"] ce_pk = self.kwargs["ce_pk"]
estimate_model_qs = EstimateModel.objects.for_entity( estimate_model_qs = EstimateModel.objects.for_entity(
entity_slug=self.kwargs["entity_slug"], user_model=self.request.user entity_slug=self.kwargs["entity_slug"], user_model=dealer.entity.admin
) )
estimate_model = get_object_or_404(estimate_model_qs, uuid__exact=ce_pk) estimate_model = get_object_or_404(estimate_model_qs, uuid__exact=ce_pk)
@ -7188,7 +7212,7 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView)
return HttpResponseBadRequest() return HttpResponseBadRequest()
item_uuids = item_uuids.split(",") item_uuids = item_uuids.split(",")
po_qs = PurchaseOrderModel.objects.for_entity( po_qs = PurchaseOrderModel.objects.for_entity(
entity_slug=self.kwargs["entity_slug"], user_model=self.request.user entity_slug=self.kwargs["entity_slug"], user_model=dealer.entity.admin
) )
po_model: PurchaseOrderModel = get_object_or_404(po_qs, uuid__exact=po_pk) po_model: PurchaseOrderModel = get_object_or_404(po_qs, uuid__exact=po_pk)
@ -7259,101 +7283,33 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView)
) )
class BillModelDetailViewView(BillModelDetailView): class BillModelDetailView(BillModelDetailViewBase):
template_name = "bill/bill_detail.html" template_name = "bill/bill_detail.html"
permission_required = ["django_ledger.view_billmodel"] permission_required = ["django_ledger.view_billmodel"]
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(BillModelDetailViewView, self).get_context_data(**kwargs) context = super(BillModelDetailView, self).get_context_data(**kwargs)
context["dealer"] = self.request.dealer context["dealer"] = self.request.dealer
return context return context
def get_queryset(self):
qs = super().get_queryset()
return qs.select_related(
'ledger',
'ledger__entity',
'vendor',
'cash_account',
'prepaid_account',
'unearned_account',
'cash_account__coa_model',
'prepaid_account__coa_model',
'unearned_account__coa_model'
)
class BillModelUpdateViewView(BillModelUpdateView): class BillModelUpdateView(BillModelUpdateViewBase):
template_name = "bill/bill_update.html"
permission_required = ["django_ledger.change_billmodel"] permission_required = ["django_ledger.change_billmodel"]
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()
entity_model: EntityModel = self.get_authorized_entity_instance()
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(BillModelUpdateViewView, self).post(
request, dealer_slug, entity_slug, bill_pk, **kwargs
)
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"],
},
)
# @login_required # @login_required
# @permission_required("django_ledger.add_billmodel", raise_exception=True) # @permission_required("django_ledger.add_billmodel", raise_exception=True)
# def bill_create(request): # def bill_create(request):
@ -9298,7 +9254,7 @@ def add_activity(request,dealer_slug, content_type, slug):
@login_required @login_required
@permission_required("inventory.add_task", raise_exception=True) @permission_required("inventory.add_tasks", raise_exception=True)
def add_task(request,dealer_slug, content_type, slug): def add_task(request,dealer_slug, content_type, slug):
# Get user information for logging # Get user information for logging
user_username = request.user.username if request.user.is_authenticated else 'anonymous' user_username = request.user.username if request.user.is_authenticated else 'anonymous'
@ -9329,11 +9285,17 @@ def add_task(request,dealer_slug, content_type, slug):
task.created_by = request.user task.created_by = request.user
task.due_date = form.cleaned_data["due_date"] task.due_date = form.cleaned_data["due_date"]
task.save() task.save()
models.Activity.objects.create(
dealer=dealer,
content_object=obj,
activity_type="task",
created_by=request.user,
notes="Task Added")
# --- Log for successful task creation --- # --- Log for successful task creation ---
logger.info( logger.info(
f"User {user_username} successfully added task " f"User {user_username} successfully added task "
f"(Assigned to: {task.assigned_to.username}) for {content_type} ID: {obj.slug} " f"(Assigned to: {task.assigned_to.email}) for {content_type} ID: {obj.slug} "
f"on dealer '{dealer_slug}'. Due: {task.due_date}, Notes: '{task.notes if hasattr(task, 'notes') else 'N/A'}'." f"on dealer '{dealer_slug}'. Due: {task.due_date}, Notes: '{task.description}'."
) )
messages.success(request, _("Task added successfully")) messages.success(request, _("Task added successfully"))
else: else:
@ -9347,7 +9309,7 @@ def add_task(request,dealer_slug, content_type, slug):
@login_required @login_required
@permission_required("inventory.change_task", raise_exception=True) @permission_required("inventory.change_tasks", raise_exception=True)
def update_task(request,dealer_slug, pk): def update_task(request,dealer_slug, pk):
task = get_object_or_404(models.Tasks, pk=pk) task = get_object_or_404(models.Tasks, pk=pk)
@ -9361,7 +9323,7 @@ def update_task(request,dealer_slug, pk):
@login_required @login_required
@permission_required("inventory.add_note", raise_exception=True) @permission_required("inventory.add_notes", raise_exception=True)
def add_note(request,dealer_slug, content_type, slug): def add_note(request,dealer_slug, content_type, slug):
# Get user information for logging # Get user information for logging
user_username = request.user.username if request.user.is_authenticated else 'anonymous' user_username = request.user.username if request.user.is_authenticated else 'anonymous'
@ -9391,25 +9353,32 @@ def add_note(request,dealer_slug, content_type, slug):
note.created_by = request.user note.created_by = request.user
note.save() note.save()
models.Activity.objects.create(
dealer=dealer,
content_object=obj,
activity_type="note",
created_by=request.user,
notes="Note Added")
# --- Single-line log for successful note creation --- # --- Single-line log for successful note creation ---
logger.info( logger.info(
f"User {user_username} successfully added a note " f"User {user_username} successfully added a note "
f"for {content_type} ID: {obj.slug} (Dealer: {dealer_slug}). " f"for {content_type} ID: {obj.slug} (Dealer: {dealer_slug}). "
f"Note: '{note.notes[:50]}...'." f"Note: '{note.note[:50]}...'."
) )
messages.success(request, _("Note added successfully")) messages.success(request, _("Note added successfully"))
else: else:
# --- Single-line log for invalid form data --- # --- Single-line log for invalid form data ---
logger.warning( logger.warning(
f"User {user_username} submitted invalid note form data " f"User {user_username} submitted invalid note form data "
f"for {content_type} ID: {obj.slug} (Dealer: {dealer_slug}). Errors: {form.errors.as_json()}" f"for {content_type} ID: {obj.slug} (Dealer: {dealer_slug}). Errors: {form.errors.as_text()}"
) )
messages.error(request, _("Note form is not valid")) messages.error(request, _("Note form is not valid"))
return redirect(f"{content_type}_detail",dealer_slug=dealer_slug, slug=slug) return redirect(f"{content_type}_detail",dealer_slug=dealer_slug, slug=slug)
@login_required @login_required
@permission_required("inventory.change_note", raise_exception=True) @require_http_methods(["POST"])
@permission_required("inventory.change_notes", raise_exception=True)
def update_note(request,dealer_slug, pk): def update_note(request,dealer_slug, pk):
note = get_object_or_404(models.Notes, pk=pk) note = get_object_or_404(models.Notes, pk=pk)
lead = get_object_or_404(models.Lead, pk=note.content_object.id) lead = get_object_or_404(models.Lead, pk=note.content_object.id)
@ -9418,13 +9387,12 @@ def update_note(request,dealer_slug, pk):
note.note = request.POST.get("note") note.note = request.POST.get("note")
note.save() note.save()
messages.success(request, _("Note updated successfully")) messages.success(request, _("Note updated successfully"))
return redirect("lead_detail",dealer_slug=dealer_slug, slug=lead.slug) # else:
else: # messages.error(request, _("Note form is not valid"))
messages.error(request, _("Note form is not valid")) # notes = models.Notes.objects.filter(
notes = models.Notes.objects.filter( # content_type__model="lead", object_id=lead.pk, dealer=dealer
content_type__model="lead", object_id=lead.id, dealer=dealer # )
) return redirect(request.META.get("HTTP_REFERER", "/"))
return render(request, "crm/leads/lead_detail.html", {"lead": lead, "notes": notes})
# Admin Management # Admin Management
@ -9841,22 +9809,49 @@ def inventory_items_filter(request, dealer_slug):
return render(request, "purchase_orders/car_inventory_item_form.html", context) return render(request, "purchase_orders/car_inventory_item_form.html", context)
class PurchaseOrderDetailView(PurchaseOrderModelDetailViewBase): class PurchaseOrderDetailView(LoginRequiredMixin,PermissionRequiredMixin, DetailView):
slug_url_kwarg = 'po_pk'
slug_field = 'uuid'
context_object_name = 'po_model'
extra_context = {
'header_subtitle_icon': 'uil:bill',
'hide_menu': True
}
template_name = "purchase_orders/po_detail.html" template_name = "purchase_orders/po_detail.html"
context_object_name = "po_model" permission_required = ["django_ledger.view_purchaseordermodel"]
permission_required = ["django_ledger.change_purchaseordermodel"]
def get_queryset(self): def get_queryset(self):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
self.queryset = dealer.entity.get_purchase_orders().select_related("entity", "ce_model") self.queryset = dealer.entity.get_purchase_orders().select_related("entity", "ce_model")
return super().get_queryset() return super().get_queryset()
def get_context_data(self, **kwargs):
def get_context_data(self, *, object_list=None, **kwargs):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
# context = super().get_context_data(object_list=object_list, **kwargs)
context["entity_slug"] = dealer.entity.slug context["entity_slug"] = dealer.entity.slug
po_model: PurchaseOrderModel = self.object
title = f'Purchase Order {po_model.po_number}'
context['page_title'] = title
context['header_title'] = title
po_model: PurchaseOrderModel = self.object
po_items_qs, item_data = po_model.get_itemtxs_data(
queryset=po_model.itemtransactionmodel_set.all().select_related('item_model', 'bill_model')
)
context['po_items'] = po_items_qs
context['po_total_amount'] = sum(
i['po_total_amount'] for i in po_items_qs.values(
'po_total_amount', 'po_item_status') if i['po_item_status'] != 'cancelled')
return context return context
# def get_context_data(self, **kwargs):
# dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
# context = super().get_context_data(**kwargs)
# context["entity_slug"] = dealer.entity.slug
# return context
class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
model = PurchaseOrderModel model = PurchaseOrderModel
@ -9943,200 +9938,11 @@ class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListVie
context["entity_slug"] = dealer.entity.slug context["entity_slug"] = dealer.entity.slug
return context return context
class PurchaseOrderUpdateView(PurchaseOrderModelUpdateViewBase): class PurchaseOrderUpdateView(PurchaseOrderModelUpdateViewBase):
template_name = "purchase_orders/po_update.html" pass
context_object_name = "po_model"
permission_required = ["django_ledger.change_purchaseordermodel"]
def get_context_data(self, itemtxs_formset=None, **kwargs):
dealer = get_object_or_404(models.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_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(PurchaseOrderUpdateView, 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(PurchaseOrderUpdateView, self).post(
request, dealer_slug, entity_slug, *args, **kwargs
)
def get_form(self, form_class=None):
po_model: PurchaseOrderModel = self.object
if po_model.is_draft():
return DraftPurchaseOrderModelUpdateForm(
entity_slug=self.kwargs["entity_slug"],
user_model=self.request.user,
**self.get_form_kwargs(),
)
elif po_model.is_review():
return ReviewPurchaseOrderModelUpdateForm(
entity_slug=self.kwargs["entity_slug"],
user_model=self.request.user,
**self.get_form_kwargs(),
)
elif po_model.is_approved():
return ApprovedPurchaseOrderModelUpdateForm(
entity_slug=self.kwargs["entity_slug"],
user_model=self.request.user,
**self.get_form_kwargs(),
)
return BasePurchaseOrderModelUpdateForm(
entity_slug=self.kwargs["entity_slug"],
user_model=self.request.user,
**self.get_form_kwargs(),
)
class BasePurchaseOrderActionActionView(BasePurchaseOrderActionActionViewBase): class BasePurchaseOrderActionActionView(BasePurchaseOrderActionActionViewBase):
def get_redirect_url(self, dealer_slug, entity_slug, po_pk, *args, **kwargs): permission_required = "django_ledger.change_purchaseordermodel"
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"] = self.request.user
# Get user information for logging
user_username = request.user.username if request.user.is_authenticated else 'anonymous'
dealer = get_object_or_404(models.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 PurchaseOrderModelDeleteView(PurchaseOrderModelDeleteViewBase): class PurchaseOrderModelDeleteView(PurchaseOrderModelDeleteViewBase):
template_name = "purchase_orders/po_delete.html" template_name = "purchase_orders/po_delete.html"
@ -10178,16 +9984,7 @@ class PurchaseOrderMarkAsVoidView(BasePurchaseOrderActionActionView):
##############################bil ##############################bil
class BaseBillActionView(BaseBillActionViewBase): class BaseBillActionView(BaseBillActionViewBase):
def get_redirect_url(self, dealer_slug, entity_slug, bill_pk, *args, **kwargs): pass
return reverse(
"bill-update",
kwargs={
"dealer_slug": self.kwargs["dealer_slug"],
"entity_slug": entity_slug,
"bill_pk": bill_pk,
},
)
class BillModelActionMarkAsDraftView(BaseBillActionView): class BillModelActionMarkAsDraftView(BaseBillActionView):
action_name = "mark_as_draft" action_name = "mark_as_draft"
@ -10388,6 +10185,9 @@ def upload_cars(request, dealer_slug, pk=None):
logger.info( logger.info(
f"User {user_username} updated PoItemsUploaded status to 'uploaded' for Item PK: {item.pk}." f"User {user_username} updated PoItemsUploaded status to 'uploaded' for Item PK: {item.pk}."
) )
return redirect(
"inventory_stats",kwargs={"dealer_slug": dealer_slug}
)
# --- Log for successful CSV import and car creation --- # --- Log for successful CSV import and car creation ---
logger.info( logger.info(
f"User {user_username} successfully imported {cars_created} cars " f"User {user_username} successfully imported {cars_created} cars "

View File

@ -68,7 +68,7 @@
class="btn btn-sm btn-phoenix-warning me-md-2"> class="btn btn-sm btn-phoenix-warning me-md-2">
{% trans 'Update' %} {% trans 'Update' %}
</a> </a>
{% if bill.can_pay %} {% if bill.can_pay %}
<button onclick="djLedger.toggleModal('{{ bill.get_html_id }}')" <button onclick="djLedger.toggleModal('{{ bill.get_html_id }}')"

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_lead' request.dealer.slug content_type slug %}" method="post" class="add_schedule_form">
{% csrf_token %}
{{ schedule_form|crispy }}
<button type="submit" class="btn btn-phoenix-success w-100">{% trans 'Save' %}</button>
</form>
</div>
</div>
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -56,6 +56,16 @@
</a> </a>
</li> </li>
{% endif %} {% endif %}
{% if perms.django_ledger.view_purchaseordermodel %}
<li class="nav-item">
<a class="nav-link" href="{% url 'purchase_order_list' request.dealer.slug request.dealer.entity.slug %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span class="fas fa-warehouse"></span></span><span class="nav-link-text">{% trans "purchase Orders"|capfirst %}</span>
</div>
</a>
</li>
{% endif %}
</ul> </ul>
</div> </div>
</div> </div>
@ -183,16 +193,6 @@
</a> </a>
</li> </li>
{% endif %} {% endif %}
{% if perms.inventory.view_payment %}
<li class="nav-item">
<a class="nav-link" href="{% url 'payment_list' request.dealer.slug %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span class="fas fa-money-check"></span></span><span class="nav-link-text">{% trans "payments"|capfirst %}</span>
</div>
</a>
</li>
{% endif %}
</ul> </ul>
</div> </div>
</div> </div>
@ -275,15 +275,14 @@
</a> </a>
</li> </li>
{% endif %} {% endif %}
{% if perms.inventory.view_journalentrymodel %}
{% if perms.django_ledger.view_purchaseordermodel %} <li class="nav-item">
<li class="nav-item"> <a class="nav-link" href="{% url 'payment_list' request.dealer.slug %}">
<a class="nav-link" href="{% url 'purchase_order_list' request.dealer.slug request.dealer.entity.slug %}"> <div class="d-flex align-items-center">
<div class="d-flex align-items-center"> <span class="nav-link-icon"><span class="fas fa-money-check"></span></span><span class="nav-link-text">{% trans "payments"|capfirst %}</span>
<span class="nav-link-icon"><span class="fas fa-warehouse"></span></span><span class="nav-link-text">{% trans "purchase Orders"|capfirst %}</span> </div>
</div>
</a> </a>
</li> </li>
{% endif %} {% endif %}
</ul> </ul>
</div> </div>

View File

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

View File

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