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

View File

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

644
inventory/override.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -78,9 +78,6 @@ from django.views.generic import (
from django.db.models import Case, Value, IntegerField, When
#logger
logger=logging.getLogger(__name__)
# Django Ledger
from django_ledger.io import roles
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 (
# BillModelCreateView,
BillModelDetailView,
BillModelUpdateView,
BaseBillActionView as BaseBillActionViewBase,
# BillModelDetailView,
# BillModelUpdateView,
# BaseBillActionView as BaseBillActionViewBase,
BillModelModelBaseView,
)
from django_ledger.forms.bill import (
@ -136,10 +133,18 @@ from django_ledger.forms.purchase_order import (
)
from django_ledger.views.purchase_order import (
PurchaseOrderModelDetailView as PurchaseOrderModelDetailViewBase,
PurchaseOrderModelUpdateView as PurchaseOrderModelUpdateViewBase,
BasePurchaseOrderActionActionView as BasePurchaseOrderActionActionViewBase,
# PurchaseOrderModelUpdateView as PurchaseOrderModelUpdateViewBase,
# BasePurchaseOrderActionActionView as BasePurchaseOrderActionActionViewBase,
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 (
ItemTransactionModel,
EntityModel,
@ -205,7 +210,6 @@ from django_q.tasks import async_task
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
class Hash(Func):
"""
Represents a function used to compute a hash value.
@ -2250,7 +2254,7 @@ class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView
@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):
"""
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", "tasks"),
("inventory", "activity"),
("inventory", "vendor"),
("inventory", "poitemsuploaded"),
("django_ledger", "purchaseordermodel"),
@ -5619,7 +5625,7 @@ class LeadDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
context["transfer_form"].fields[
"transfer_to"
].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["staff_task_form"] = forms.StaffTaskForm()
@ -5818,7 +5824,7 @@ def update_lead_actions(request,dealer_slug):
# Log before updating lead fields
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}'."
)
@ -5830,21 +5836,20 @@ def update_lead_actions(request,dealer_slug):
next_action_date, "%Y-%m-%dT%H:%M"
)
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:
# Log for invalid date format
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(
{"success": False, "message": "Invalid date format"}, status=400
)
# Save the lead
lead.save()
# --- Logging for successful update (main try block success) ---
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}'."
)
return JsonResponse(
@ -5932,7 +5937,7 @@ def LeadDeleteView(request,dealer_slug, slug):
# Log intent before attempting deletion
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}'."
)
@ -5943,19 +5948,19 @@ def LeadDeleteView(request,dealer_slug, slug):
# --- Single-line log for successful associated user/customer deletion ---
logger.info(
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:
# --- Single-line log for error during associated user/customer deletion ---
logger.error(
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}",
exc_info=True
)
print(e)
lead_id_final = lead.id # Capture before deletion
lead_name_final = lead.name
lead_id_final = lead.pk # Capture before deletion
lead_name_final = lead.slug
lead.delete()
# Log the final lead deletion, which happens unconditionally after the try-except
logger.info(
@ -5967,7 +5972,7 @@ def LeadDeleteView(request,dealer_slug, slug):
@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):
"""
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
@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):
"""
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
@permission_required("inventory.delete_note", raise_exception=True)
@permission_required("inventory.delete_notes", raise_exception=True)
def delete_note(request,dealer_slug, pk):
"""
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
@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.
@ -6113,29 +6118,46 @@ def schedule_lead(request, dealer_slug,slug):
method and validity of the form submission.
: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:
messages.error(request, _("You do not have permission to schedule lead"))
return redirect("lead_list", dealer_slug=dealer_slug)
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'
messages.error(request, _("You do not have permission to schedule."))
return redirect(request.META.get("HTTP_REFERER"))
if request.method == "POST":
form = forms.ScheduleForm(request.POST)
if form.is_valid():
instance = form.save(commit=False)
instance.lead = lead
instance.content_object = obj
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)
# Log attempt to create AppointmentRequest
logger.debug(
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}'."
)
@ -6149,41 +6171,34 @@ def schedule_lead(request, dealer_slug,slug):
)
except ValidationError as 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
Appointment.objects.create(
client=client,
appointment_request=appointment_request,
phone=lead.phone_number,
address=lead.address,
phone=instance.phone,
address=instance.address_1,
)
instance.save()
# --- Logging for successful AppointmentRequest and Appointment creation ---
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}."
)
messages.success(request, _("Appointment Created Successfully"))
try:
if lead.opportunity:
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)
return redirect(request.META.get("HTTP_REFERER"))
else:
# Log for invalid form data
logger.warning(
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)}")
return redirect("schedule_lead", dealer_slug=dealer_slug,slug=lead.slug)
return redirect(request.META.get("HTTP_REFERER"))
form = forms.ScheduleForm()
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)
lead = get_object_or_404(models.Lead, slug=slug)
status = request.GET.get("status")
# Get user info for logging
user_username = request.user.username if request.user.is_authenticated else 'anonymous'
if status == "draft":
@ -6264,11 +6278,12 @@ def send_lead_email(request,dealer_slug, slug, email_pk=None):
activity_type=models.ActionChoices.EMAIL,
)
messages.success(request, _("Email Draft successfully"))
try:
if lead.opportunity:
if getattr(lead, "opportunity",None):
# Log success when opportunity exists and redirecting
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."
)
response = HttpResponse(
@ -6280,16 +6295,16 @@ def send_lead_email(request,dealer_slug, slug, email_pk=None):
else:
# Log success when no opportunity and redirecting to lead detail
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."
)
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)
return response
except models.Lead.opportunity.RelatedObjectDoesNotExist:
# --- Log when Lead.opportunity does not exist (Draft status) ---
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."
)
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:
# Log success when opportunity exists and redirecting after sending email
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."
)
return redirect("opportunity_detail", dealer_slug=dealer_slug,slug=lead.opportunity.slug)
except models.Lead.opportunity.RelatedObjectDoesNotExist:
# --- Log when Lead.opportunity does not exist (POST request for sending) ---
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."
)
return redirect("lead_list",dealer_slug=dealer_slug)
@ -6393,7 +6408,6 @@ def add_activity_to_lead(request, pk):
form = forms.ActivityForm(request.POST)
if form.is_valid():
activity = form.save(commit=False)
print(activity)
activity.content_object = lead
activity.dealer = dealer
activity.activity_type = form.cleaned_data["activity_type"]
@ -6451,6 +6465,14 @@ class OpportunityCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateVie
instance.lead.save()
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):
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):
form = super().get_form(form_class)
dealer = get_object_or_404(models.Dealer,slug=self.kwargs.get("dealer_slug"))
form.fields['car'].queryset = models.Car.objects.filter(dealer=dealer)
form.fields['lead'].queryset = models.Lead.objects.filter(dealer=dealer)
staff = getattr(self.request.user.staffmember, "staff", None)
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):
@ -6530,19 +6553,19 @@ class OpportunityDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailV
content_type__model="lead", object_id=self.object.id
).order_by("-created")
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")
context["notes"] = models.Notes.objects.filter(
content_type__model="opportunity", object_id=self.object.id
).order_by("-created")
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(
content_type__model="opportunity", object_id=self.object.id
)
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(
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"),
}
context["staff_task_form"] = forms.StaffTaskForm()
context["note_form"] = forms.NoteForm()
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(
content_type__model="opportunity", object_id=self.object.id
@ -6580,7 +6604,8 @@ class OpportunityListView(LoginRequiredMixin,PermissionRequiredMixin, ListView):
def get_queryset(self):
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 = self.request.GET.get("search")
@ -7040,7 +7065,7 @@ class BillListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView):
template_name = "bill/bill_create.html"
PAGE_TITLE = _("Create Bill")
permission_required = ["django_ledger.add_billmodel"]
permission_required = "django_ledger.add_billmodel"
extra_context = {
"page_title": PAGE_TITLE,
"header_title": PAGE_TITLE,
@ -7048,7 +7073,6 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView)
}
for_purchase_order = False
for_estimate = False
permission_required = "django_ledger.add_billmodel"
# Get user info for logging
@ -7061,7 +7085,7 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView)
if self.for_estimate and "ce_pk" in self.kwargs:
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_qs, uuid__exact=self.kwargs["ce_pk"]
@ -7080,7 +7104,7 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView)
def get_context_data(self, **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'
if self.for_purchase_order:
@ -7110,7 +7134,7 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView)
return HttpResponseBadRequest()
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")
po_model: PurchaseOrderModel = get_object_or_404(po_qs, uuid__exact=po_pk)
po_itemtxs_qs = po_model.itemtransactionmodel_set.filter(
@ -7131,7 +7155,7 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView)
)
elif self.for_estimate:
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_model: EstimateModel = get_object_or_404(
@ -7175,7 +7199,7 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView)
if self.for_estimate:
ce_pk = self.kwargs["ce_pk"]
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)
@ -7188,7 +7212,7 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView)
return HttpResponseBadRequest()
item_uuids = item_uuids.split(",")
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)
@ -7259,101 +7283,33 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView)
)
class BillModelDetailViewView(BillModelDetailView):
class BillModelDetailView(BillModelDetailViewBase):
template_name = "bill/bill_detail.html"
permission_required = ["django_ledger.view_billmodel"]
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
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):
template_name = "bill/bill_update.html"
class BillModelUpdateView(BillModelUpdateViewBase):
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
# @permission_required("django_ledger.add_billmodel", raise_exception=True)
# def bill_create(request):
@ -9298,7 +9254,7 @@ def add_activity(request,dealer_slug, content_type, slug):
@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):
# Get user information for logging
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.due_date = form.cleaned_data["due_date"]
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 ---
logger.info(
f"User {user_username} successfully added task "
f"(Assigned to: {task.assigned_to.username}) 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"(Assigned to: {task.assigned_to.email}) for {content_type} ID: {obj.slug} "
f"on dealer '{dealer_slug}'. Due: {task.due_date}, Notes: '{task.description}'."
)
messages.success(request, _("Task added successfully"))
else:
@ -9347,7 +9309,7 @@ def add_task(request,dealer_slug, content_type, slug):
@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):
task = get_object_or_404(models.Tasks, pk=pk)
@ -9361,7 +9323,7 @@ def update_task(request,dealer_slug, pk):
@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):
# Get user information for logging
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.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 ---
logger.info(
f"User {user_username} successfully added a note "
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"))
else:
# --- Single-line log for invalid form data ---
logger.warning(
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"))
return redirect(f"{content_type}_detail",dealer_slug=dealer_slug, slug=slug)
@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):
note = get_object_or_404(models.Notes, pk=pk)
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.save()
messages.success(request, _("Note updated successfully"))
return redirect("lead_detail",dealer_slug=dealer_slug, slug=lead.slug)
else:
messages.error(request, _("Note form is not valid"))
notes = models.Notes.objects.filter(
content_type__model="lead", object_id=lead.id, dealer=dealer
)
return render(request, "crm/leads/lead_detail.html", {"lead": lead, "notes": notes})
# else:
# messages.error(request, _("Note form is not valid"))
# notes = models.Notes.objects.filter(
# content_type__model="lead", object_id=lead.pk, dealer=dealer
# )
return redirect(request.META.get("HTTP_REFERER", "/"))
# Admin Management
@ -9841,22 +9809,49 @@ def inventory_items_filter(request, dealer_slug):
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"
context_object_name = "po_model"
permission_required = ["django_ledger.change_purchaseordermodel"]
permission_required = ["django_ledger.view_purchaseordermodel"]
def get_queryset(self):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
self.queryset = dealer.entity.get_purchase_orders().select_related("entity", "ce_model")
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"])
context = super().get_context_data(**kwargs)
# context = super().get_context_data(object_list=object_list, **kwargs)
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
# 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):
model = PurchaseOrderModel
@ -9943,200 +9938,11 @@ class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListVie
context["entity_slug"] = dealer.entity.slug
return context
class PurchaseOrderUpdateView(PurchaseOrderModelUpdateViewBase):
template_name = "purchase_orders/po_update.html"
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(),
)
pass
class BasePurchaseOrderActionActionView(BasePurchaseOrderActionActionViewBase):
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"] = 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
permission_required = "django_ledger.change_purchaseordermodel"
class PurchaseOrderModelDeleteView(PurchaseOrderModelDeleteViewBase):
template_name = "purchase_orders/po_delete.html"
@ -10178,16 +9984,7 @@ class PurchaseOrderMarkAsVoidView(BasePurchaseOrderActionActionView):
##############################bil
class BaseBillActionView(BaseBillActionViewBase):
def get_redirect_url(self, dealer_slug, entity_slug, bill_pk, *args, **kwargs):
return reverse(
"bill-update",
kwargs={
"dealer_slug": self.kwargs["dealer_slug"],
"entity_slug": entity_slug,
"bill_pk": bill_pk,
},
)
pass
class BillModelActionMarkAsDraftView(BaseBillActionView):
action_name = "mark_as_draft"
@ -10388,6 +10185,9 @@ def upload_cars(request, dealer_slug, pk=None):
logger.info(
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 ---
logger.info(
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">
{% trans 'Update' %}
</a>
{% if bill.can_pay %}
<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 %}
<!-- task Modal -->
<style>
.completed-task {
text-decoration: line-through;
opacity: 0.7;
}
</style>
<div class="modal fade" id="taskModal" tabindex="-1" aria-labelledby="taskModalLabel" aria-hidden="true">
<div class="modal-dialog modal-md">
<div class="modal-content">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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