update
This commit is contained in:
commit
4bfb533448
@ -1,16 +1,16 @@
|
|||||||
from inventory import models
|
from inventory.models import Lead,Car
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django_ledger.models import EstimateModel,BillModel,AccountModel,LedgerModel
|
from django_ledger.models import EstimateModel,BillModel,AccountModel,LedgerModel
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
def handle(self, *args, **kwargs):
|
def handle(self, *args, **kwargs):
|
||||||
Permission.objects.get_or_create(name="Can view crm",codename="can_view_crm",content_type=ContentType.objects.get_for_model(models.Lead))
|
Permission.objects.get_or_create(name="Can view crm",codename="can_view_crm",content_type=ContentType.objects.get_for_model(Lead))
|
||||||
|
Permission.objects.get_or_create(name="Can reassign lead",codename="can_reassign_lead",content_type=ContentType.objects.get_for_model(Lead))
|
||||||
Permission.objects.get_or_create(name="Can view sales",codename="can_view_sales",content_type=ContentType.objects.get_for_model(EstimateModel))
|
Permission.objects.get_or_create(name="Can view sales",codename="can_view_sales",content_type=ContentType.objects.get_for_model(EstimateModel))
|
||||||
Permission.objects.get_or_create(name="Can view reports",codename="can_view_reports",content_type=ContentType.objects.get_for_model(LedgerModel))
|
Permission.objects.get_or_create(name="Can view reports",codename="can_view_reports",content_type=ContentType.objects.get_for_model(LedgerModel))
|
||||||
Permission.objects.get_or_create(name="Can view inventory",codename="can_view_inventory",content_type=ContentType.objects.get_for_model(models.Car))
|
Permission.objects.get_or_create(name="Can view inventory",codename="can_view_inventory",content_type=ContentType.objects.get_for_model(Car))
|
||||||
Permission.objects.get_or_create(name="Can approve bill",codename="can_approve_billmodel",content_type=ContentType.objects.get_for_model(BillModel))
|
Permission.objects.get_or_create(name="Can approve bill",codename="can_approve_billmodel",content_type=ContentType.objects.get_for_model(BillModel))
|
||||||
Permission.objects.get_or_create(name="Can view financials",codename="can_view_financials",content_type=ContentType.objects.get_for_model(AccountModel))
|
Permission.objects.get_or_create(name="Can view financials",codename="can_view_financials",content_type=ContentType.objects.get_for_model(AccountModel))
|
||||||
Permission.objects.get_or_create(name="Can approve estimate",codename="can_approve_estimatemodel",content_type=ContentType.objects.get_for_model(EstimateModel))
|
Permission.objects.get_or_create(name="Can approve estimate",codename="can_approve_estimatemodel",content_type=ContentType.objects.get_for_model(EstimateModel))
|
||||||
@ -1836,7 +1836,9 @@ class Schedule(models.Model):
|
|||||||
("completed", _("Completed")),
|
("completed", _("Completed")),
|
||||||
("canceled", _("Canceled")),
|
("canceled", _("Canceled")),
|
||||||
]
|
]
|
||||||
lead = models.ForeignKey(Lead, on_delete=models.CASCADE, related_name="schedules")
|
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||||
|
object_id = models.PositiveIntegerField()
|
||||||
|
content_object = GenericForeignKey('content_type', 'object_id')
|
||||||
customer = models.ForeignKey(
|
customer = models.ForeignKey(
|
||||||
CustomerModel,
|
CustomerModel,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
@ -2634,6 +2636,7 @@ class CustomGroup(models.Model):
|
|||||||
"notes",
|
"notes",
|
||||||
"tasks",
|
"tasks",
|
||||||
"activity",
|
"activity",
|
||||||
|
"poitemsuploaded"
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
self.set_permissions(
|
self.set_permissions(
|
||||||
@ -2652,7 +2655,7 @@ class CustomGroup(models.Model):
|
|||||||
elif self.name == "Sales":
|
elif self.name == "Sales":
|
||||||
self.set_permissions(
|
self.set_permissions(
|
||||||
app="django_ledger",
|
app="django_ledger",
|
||||||
allowed_models=["estimatemodel", "invoicemodel", "customermodel"],
|
allowed_models=["invoicemodel", "customermodel"],
|
||||||
)
|
)
|
||||||
self.set_permissions(
|
self.set_permissions(
|
||||||
app="inventory",
|
app="inventory",
|
||||||
@ -2668,6 +2671,10 @@ class CustomGroup(models.Model):
|
|||||||
"organization",
|
"organization",
|
||||||
"notes",
|
"notes",
|
||||||
"tasks",
|
"tasks",
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
"lead"
|
||||||
|
>>>>>>> 25d17efa11e8f03c6819b27572ca6abe91860d11
|
||||||
"activity",
|
"activity",
|
||||||
],
|
],
|
||||||
other_perms=[
|
other_perms=[
|
||||||
@ -2679,6 +2686,10 @@ class CustomGroup(models.Model):
|
|||||||
"can_view_inventory",
|
"can_view_inventory",
|
||||||
"can_view_sales",
|
"can_view_sales",
|
||||||
"can_view_crm",
|
"can_view_crm",
|
||||||
|
"view_estimatemodel",
|
||||||
|
"add_estimatemodel",
|
||||||
|
"change_estimatemodel",
|
||||||
|
"delete_estimatemodel",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
######################################
|
######################################
|
||||||
@ -2694,7 +2705,13 @@ class CustomGroup(models.Model):
|
|||||||
"notes",
|
"notes",
|
||||||
"tasks",
|
"tasks",
|
||||||
"activity",
|
"activity",
|
||||||
|
<<<<<<< HEAD
|
||||||
"vendor"],
|
"vendor"],
|
||||||
|
=======
|
||||||
|
"vendor",
|
||||||
|
"poitemsuploaded"
|
||||||
|
],
|
||||||
|
>>>>>>> 25d17efa11e8f03c6819b27572ca6abe91860d11
|
||||||
other_perms=[
|
other_perms=[
|
||||||
"view_car",
|
"view_car",
|
||||||
"view_carlocation",
|
"view_carlocation",
|
||||||
@ -2711,7 +2728,6 @@ class CustomGroup(models.Model):
|
|||||||
"bankaccountmodel",
|
"bankaccountmodel",
|
||||||
"accountmodel",
|
"accountmodel",
|
||||||
"chartofaccountmodel",
|
"chartofaccountmodel",
|
||||||
"billmodel",
|
|
||||||
"itemmodel",
|
"itemmodel",
|
||||||
"invoicemodel",
|
"invoicemodel",
|
||||||
"vendormodel",
|
"vendormodel",
|
||||||
@ -2723,7 +2739,7 @@ class CustomGroup(models.Model):
|
|||||||
"ledgermodel",
|
"ledgermodel",
|
||||||
"transactionmodel"
|
"transactionmodel"
|
||||||
],
|
],
|
||||||
other_perms=["view_customermodel", "view_estimatemodel","can_view_inventory","can_view_sales","can_view_crm","can_view_financials","can_view_reports"],
|
other_perms=["view_billmodel","add_billmodel","change_billmodel","delete_billmodel","view_customermodel", "view_estimatemodel","can_view_inventory","can_view_sales","can_view_crm","can_view_financials","can_view_reports"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -2919,6 +2935,8 @@ class PoItemsUploaded(models.Model):
|
|||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
return self.item.item.name.split('||')
|
||||||
class ExtraInfo(models.Model):
|
class ExtraInfo(models.Model):
|
||||||
"""
|
"""
|
||||||
Stores additional information for any model with:
|
Stores additional information for any model with:
|
||||||
|
|||||||
644
inventory/override.py
Normal file
644
inventory/override.py
Normal 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
|
||||||
@ -17,7 +17,8 @@ from django_ledger.models import (
|
|||||||
LedgerModel,
|
LedgerModel,
|
||||||
AccountModel,
|
AccountModel,
|
||||||
PurchaseOrderModel,
|
PurchaseOrderModel,
|
||||||
EstimateModel
|
EstimateModel,
|
||||||
|
BillModel
|
||||||
)
|
)
|
||||||
from . import models
|
from . import models
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
@ -954,7 +955,7 @@ def create_po_fulfilled_notification(sender,instance,created,**kwargs):
|
|||||||
models.Notification.objects.create(
|
models.Notification.objects.create(
|
||||||
user=accountant,
|
user=accountant,
|
||||||
message=f"""
|
message=f"""
|
||||||
New Purchase Order {instance.po_number} has been added to dealer {instance.dealer.name}.
|
New Purchase Order {instance.po_number} has been added to dealer {dealer.name}.
|
||||||
<a href="{instance.get_absolute_url()}" target="_blank">View</a>
|
<a href="{instance.get_absolute_url()}" target="_blank">View</a>
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
@ -975,7 +976,10 @@ def car_created_notification(sender, instance, created, **kwargs):
|
|||||||
def po_fullfilled_notification(sender, instance, created, **kwargs):
|
def po_fullfilled_notification(sender, instance, created, **kwargs):
|
||||||
if instance.is_fulfilled():
|
if instance.is_fulfilled():
|
||||||
dealer = models.Dealer.objects.get(entity=instance.entity)
|
dealer = models.Dealer.objects.get(entity=instance.entity)
|
||||||
recipients = models.CustomGroup.objects.filter(dealer=instance.dealer,name="Accountant").first().group.user_set.all()
|
recipients = User.objects.filter(
|
||||||
|
groups__customgroup__dealer=instance.dealer,
|
||||||
|
groups__customgroup__name__in=["Manager", "Inventory"]
|
||||||
|
)
|
||||||
for recipient in recipients:
|
for recipient in recipients:
|
||||||
models.Notification.objects.create(
|
models.Notification.objects.create(
|
||||||
user=recipient,
|
user=recipient,
|
||||||
@ -987,14 +991,16 @@ def po_fullfilled_notification(sender, instance, created, **kwargs):
|
|||||||
@receiver(post_save, sender=models.Vendor)
|
@receiver(post_save, sender=models.Vendor)
|
||||||
def vendor_created_notification(sender, instance, created, **kwargs):
|
def vendor_created_notification(sender, instance, created, **kwargs):
|
||||||
if created:
|
if created:
|
||||||
recipients = models.CustomGroup.objects.filter(dealer=instance.dealer,name="Inventory").first().group.user_set.all()
|
recipients = User.objects.filter(
|
||||||
|
groups__customgroup__dealer=instance.dealer,
|
||||||
|
groups__customgroup__name__in=["Manager", "Inventory"]
|
||||||
|
)
|
||||||
|
|
||||||
for recipient in recipients:
|
for recipient in recipients:
|
||||||
models.Notification.objects.create(
|
models.Notification.objects.create(
|
||||||
user=recipient,
|
user=recipient,
|
||||||
message=f"""
|
message=f"""
|
||||||
New Vendor {instance.name} has been added to dealer {instance.dealer.name}.
|
New Vendor {instance.name} has been added to dealer {instance.dealer.name}.
|
||||||
<a href="{instance.get_absolute_url()}" target="_blank">View</a>
|
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1055,19 +1061,33 @@ def estimate_in_approve_notification(sender, instance, created, **kwargs):
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@receiver(post_save, sender=BillModel)
|
||||||
|
def bill_model_in_approve_notification(sender, instance, created, **kwargs):
|
||||||
|
if instance.is_review():
|
||||||
|
dealer = models.Dealer.objects.get(entity=instance.ledger.entity)
|
||||||
|
recipients = models.CustomGroup.objects.filter(dealer=dealer,name="Manager").first().group.user_set.exclude(email=dealer.user.email)
|
||||||
|
|
||||||
# @receiver(post_save, sender=models.Lead)
|
for recipient in recipients:
|
||||||
# def lead_created_notification(sender, instance, created, **kwargs):
|
models.Notification.objects.create(
|
||||||
# if created:
|
user=recipient,
|
||||||
# models.Notification.objects.create(
|
message=f"""
|
||||||
# user=instance.staff.user,
|
Bill {instance.bill_number} is in review,please review and approve it
|
||||||
# message=f"""
|
<a href="{reverse('bill-detail', kwargs={'dealer_slug': dealer.slug, 'entity_slug':dealer.entity.slug, 'bill_pk': instance.pk})}" target="_blank">View</a>.
|
||||||
# New Lead has been added.
|
"""
|
||||||
# <a href="{reverse('lead_detail',kwargs={'dealer_slug':instance.dealer.slug,'slug':instance.slug})}" target="_blank">View</a>
|
)
|
||||||
# """,
|
|
||||||
# )
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=BillModel)
|
||||||
|
def bill_model_after_approve_notification(sender, instance, created, **kwargs):
|
||||||
|
if instance.is_approved():
|
||||||
|
dealer = models.Dealer.objects.get(entity=instance.ledger.entity)
|
||||||
|
recipients = models.CustomGroup.objects.filter(dealer=dealer,name="Accountant").first().group.user_set.exclude(email=dealer.user.email)
|
||||||
|
|
||||||
# send notification after car is sold {manager,dealer}
|
for recipient in recipients:
|
||||||
# after po review send notification to {manager} to approve po
|
models.Notification.objects.create(
|
||||||
# after estimate review send notification to {manager} to approve estimate
|
user=recipient,
|
||||||
|
message=f"""
|
||||||
|
Bill {instance.bill_number} has been approved.
|
||||||
|
<a href="{reverse('bill-detail', kwargs={'dealer_slug': dealer.slug, 'entity_slug':dealer.entity.slug, 'bill_pk': instance.pk})}" target="_blank">View</a>.
|
||||||
|
please comlete the bill payment.
|
||||||
|
"""
|
||||||
|
)
|
||||||
@ -454,14 +454,16 @@ def po_item_table1(context, queryset):
|
|||||||
@register.inclusion_tag(
|
@register.inclusion_tag(
|
||||||
"purchase_orders/includes/po_item_formset.html", takes_context=True
|
"purchase_orders/includes/po_item_formset.html", takes_context=True
|
||||||
)
|
)
|
||||||
def po_item_formset_table(context, po_model, itemtxs_formset):
|
def po_item_formset_table(context, po_model, itemtxs_formset,user):
|
||||||
# print(len(itemtxs_formset.forms))
|
# print(len(itemtxs_formset.forms))
|
||||||
|
|
||||||
for form in itemtxs_formset.forms:
|
for form in itemtxs_formset.forms:
|
||||||
form.fields["item_model"].queryset = form.fields["item_model"].queryset.filter(
|
form.fields["item_model"].queryset = form.fields["item_model"].queryset.filter(
|
||||||
item_role="inventory"
|
item_role="inventory"
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
"can_add_bill": user.has_perm("django_ledger.add_billmodel"),
|
||||||
|
"can_view_bill": user.has_perm("django_ledger.view_billmodel"),
|
||||||
"dealer_slug": context["view"].kwargs["dealer_slug"],
|
"dealer_slug": context["view"].kwargs["dealer_slug"],
|
||||||
"entity_slug": context["view"].kwargs["entity_slug"],
|
"entity_slug": context["view"].kwargs["entity_slug"],
|
||||||
"po_model": po_model,
|
"po_model": po_model,
|
||||||
|
|||||||
@ -141,9 +141,9 @@ urlpatterns = [
|
|||||||
name="send_lead_email_with_template",
|
name="send_lead_email_with_template",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"<slug:dealer_slug>/crm/leads/<slug:slug>/schedule/",
|
"<slug:dealer_slug>/crm/<str:content_type>/<slug:slug>/schedule/",
|
||||||
views.schedule_lead,
|
views.schedule_event,
|
||||||
name="schedule_lead",
|
name="schedule_event",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"<slug:dealer_slug>/crm/leads/schedule/<int:pk>/cancel/",
|
"<slug:dealer_slug>/crm/leads/schedule/<int:pk>/cancel/",
|
||||||
@ -802,17 +802,17 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"<slug:dealer_slug>/items/bills/<slug:entity_slug>/detail/<uuid:bill_pk>/",
|
"<slug:dealer_slug>/items/bills/<slug:entity_slug>/detail/<uuid:bill_pk>/",
|
||||||
views.BillModelDetailViewView.as_view(),
|
views.BillModelDetailView.as_view(),
|
||||||
name="bill-detail",
|
name="bill-detail",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"<slug:dealer_slug>/items/bills/<slug:entity_slug>/update/<uuid:bill_pk>/",
|
"<slug:dealer_slug>/items/bills/<slug:entity_slug>/update/<uuid:bill_pk>/",
|
||||||
views.BillModelUpdateViewView.as_view(),
|
views.BillModelUpdateView.as_view(),
|
||||||
name="bill-update",
|
name="bill-update",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"<slug:dealer_slug>/items/bills/<slug:entity_slug>/update/<uuid:bill_pk>/items/",
|
"<slug:dealer_slug>/items/bills/<slug:entity_slug>/update/<uuid:bill_pk>/items/",
|
||||||
views.BillModelUpdateViewView.as_view(action_update_items=True),
|
views.BillModelUpdateView.as_view(action_update_items=True),
|
||||||
name="bill-update-items",
|
name="bill-update-items",
|
||||||
),
|
),
|
||||||
############################################################
|
############################################################
|
||||||
|
|||||||
@ -1288,7 +1288,7 @@ def handle_account_process(invoice, amount, finance_data):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Error updating item_model.for_inventory for car {car.vin} (Invoice {invoice.invoice_number}): {e}",
|
f"Error updating item_model.for_inventory for car {car.vin} (Invoice {invoice.invoice_number}): {e}",
|
||||||
exc_info=True
|
exc_info=True
|
||||||
)
|
)
|
||||||
|
|
||||||
print(e)
|
print(e)
|
||||||
@ -1419,12 +1419,23 @@ def handle_payment(request, order):
|
|||||||
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
||||||
auth = (settings.MOYASAR_SECRET_KEY, "")
|
auth = (settings.MOYASAR_SECRET_KEY, "")
|
||||||
response = requests.request("POST", url, auth=auth, headers=headers, data=payload)
|
response = requests.request("POST", url, auth=auth, headers=headers, data=payload)
|
||||||
|
if response.status_code == 400:
|
||||||
|
data = response.json()
|
||||||
|
if data["type"] == "validation_error":
|
||||||
|
errors = data.get("errors", {})
|
||||||
|
if "source.year" in errors:
|
||||||
|
raise Exception("Invalid expiry year")
|
||||||
|
else:
|
||||||
|
raise Exception("Validation Error: ", errors)
|
||||||
|
else:
|
||||||
|
print("Failed to process payment:", data)
|
||||||
#
|
#
|
||||||
order.status = AbstractOrder.STATUS.NEW
|
order.status = AbstractOrder.STATUS.NEW
|
||||||
order.save()
|
order.save()
|
||||||
#
|
#
|
||||||
data = response.json()
|
data = response.json()
|
||||||
amount = Decimal("{0:.2f}".format(Decimal(total) / Decimal(100)))
|
amount = Decimal("{0:.2f}".format(Decimal(total) / Decimal(100)))
|
||||||
|
print(data)
|
||||||
models.PaymentHistory.objects.create(
|
models.PaymentHistory.objects.create(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
user_data=user_data,
|
user_data=user_data,
|
||||||
|
|||||||
@ -78,9 +78,6 @@ from django.views.generic import (
|
|||||||
|
|
||||||
from django.db.models import Case, Value, IntegerField, When
|
from django.db.models import Case, Value, IntegerField, When
|
||||||
|
|
||||||
#logger
|
|
||||||
logger=logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# Django Ledger
|
# Django Ledger
|
||||||
from django_ledger.io import roles
|
from django_ledger.io import roles
|
||||||
from django_ledger.utils import accruable_net_summary
|
from django_ledger.utils import accruable_net_summary
|
||||||
@ -107,9 +104,9 @@ from django_ledger.forms.bank_account import (
|
|||||||
)
|
)
|
||||||
from django_ledger.views.bill import (
|
from django_ledger.views.bill import (
|
||||||
# BillModelCreateView,
|
# BillModelCreateView,
|
||||||
BillModelDetailView,
|
# BillModelDetailView,
|
||||||
BillModelUpdateView,
|
# BillModelUpdateView,
|
||||||
BaseBillActionView as BaseBillActionViewBase,
|
# BaseBillActionView as BaseBillActionViewBase,
|
||||||
BillModelModelBaseView,
|
BillModelModelBaseView,
|
||||||
)
|
)
|
||||||
from django_ledger.forms.bill import (
|
from django_ledger.forms.bill import (
|
||||||
@ -136,10 +133,18 @@ from django_ledger.forms.purchase_order import (
|
|||||||
)
|
)
|
||||||
from django_ledger.views.purchase_order import (
|
from django_ledger.views.purchase_order import (
|
||||||
PurchaseOrderModelDetailView as PurchaseOrderModelDetailViewBase,
|
PurchaseOrderModelDetailView as PurchaseOrderModelDetailViewBase,
|
||||||
PurchaseOrderModelUpdateView as PurchaseOrderModelUpdateViewBase,
|
# PurchaseOrderModelUpdateView as PurchaseOrderModelUpdateViewBase,
|
||||||
BasePurchaseOrderActionActionView as BasePurchaseOrderActionActionViewBase,
|
# BasePurchaseOrderActionActionView as BasePurchaseOrderActionActionViewBase,
|
||||||
PurchaseOrderModelDeleteView as PurchaseOrderModelDeleteViewBase,
|
PurchaseOrderModelDeleteView as PurchaseOrderModelDeleteViewBase,
|
||||||
)
|
)
|
||||||
|
from .override import (
|
||||||
|
PurchaseOrderModelUpdateView as PurchaseOrderModelUpdateViewBase,
|
||||||
|
BasePurchaseOrderActionActionView as BasePurchaseOrderActionActionViewBase,
|
||||||
|
BillModelDetailView as BillModelDetailViewBase,
|
||||||
|
BillModelUpdateView as BillModelUpdateViewBase,
|
||||||
|
BaseBillActionView as BaseBillActionViewBase,
|
||||||
|
)
|
||||||
|
|
||||||
from django_ledger.models import (
|
from django_ledger.models import (
|
||||||
ItemTransactionModel,
|
ItemTransactionModel,
|
||||||
EntityModel,
|
EntityModel,
|
||||||
@ -205,7 +210,6 @@ from django_q.tasks import async_task
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
class Hash(Func):
|
class Hash(Func):
|
||||||
"""
|
"""
|
||||||
Represents a function used to compute a hash value.
|
Represents a function used to compute a hash value.
|
||||||
@ -2250,7 +2254,7 @@ class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView
|
|||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required("inventory.add_note", raise_exception=True)
|
@permission_required("inventory.add_notes", raise_exception=True)
|
||||||
def add_note_to_customer(request,dealer_slug, slug):
|
def add_note_to_customer(request,dealer_slug, slug):
|
||||||
"""
|
"""
|
||||||
This function allows authenticated users to add a note to a specific customer. The
|
This function allows authenticated users to add a note to a specific customer. The
|
||||||
@ -2922,6 +2926,8 @@ def GroupPermissionView(request, dealer_slug, pk):
|
|||||||
("inventory", "notes"),
|
("inventory", "notes"),
|
||||||
("inventory", "tasks"),
|
("inventory", "tasks"),
|
||||||
("inventory", "activity"),
|
("inventory", "activity"),
|
||||||
|
("inventory", "vendor"),
|
||||||
|
("inventory", "poitemsuploaded"),
|
||||||
|
|
||||||
|
|
||||||
("django_ledger", "purchaseordermodel"),
|
("django_ledger", "purchaseordermodel"),
|
||||||
@ -5619,7 +5625,7 @@ class LeadDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|||||||
context["transfer_form"].fields[
|
context["transfer_form"].fields[
|
||||||
"transfer_to"
|
"transfer_to"
|
||||||
].queryset = models.Staff.objects.filter(
|
].queryset = models.Staff.objects.filter(
|
||||||
dealer=dealer,staff_member__user__groups__permissions__codename__contains="add_lead").exclude(staff_member__user=self.request.user).distinct()
|
dealer=dealer,staff_member__user__groups__permissions__codename__contains="can_reassign_lead").exclude(staff_member__user=self.request.user).distinct()
|
||||||
|
|
||||||
context["activity_form"] = forms.ActivityForm()
|
context["activity_form"] = forms.ActivityForm()
|
||||||
context["staff_task_form"] = forms.StaffTaskForm()
|
context["staff_task_form"] = forms.StaffTaskForm()
|
||||||
@ -5818,7 +5824,7 @@ def update_lead_actions(request,dealer_slug):
|
|||||||
|
|
||||||
# Log before updating lead fields
|
# Log before updating lead fields
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"User {user_username} found Lead ID: {lead.id} ('{lead.name}') "
|
f"User {user_username} found Lead ID: {lead.pk} ('{lead.slug}') "
|
||||||
f"for update. Current action: '{current_action}', Next action: '{next_action}', Next action date: '{next_action_date}'."
|
f"for update. Current action: '{current_action}', Next action: '{next_action}', Next action date: '{next_action_date}'."
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -5830,21 +5836,20 @@ def update_lead_actions(request,dealer_slug):
|
|||||||
next_action_date, "%Y-%m-%dT%H:%M"
|
next_action_date, "%Y-%m-%dT%H:%M"
|
||||||
)
|
)
|
||||||
lead.next_action_date = timezone.make_aware(next_action_datetime)
|
lead.next_action_date = timezone.make_aware(next_action_datetime)
|
||||||
logger.debug(f"Lead ID: {lead.id} next_action_date parsed to {lead.next_action_date}.")
|
logger.debug(f"Lead ID: {lead.pk} next_action_date parsed to {lead.next_action_date}.")
|
||||||
except ValueError as ve:
|
except ValueError as ve:
|
||||||
# Log for invalid date format
|
# Log for invalid date format
|
||||||
logger.warning( f"submitted invalid date format ('{next_action_date}') "
|
logger.warning( f"submitted invalid date format ('{next_action_date}') "
|
||||||
f"for Lead ID: {lead.id}. Error: {ve}"
|
f"for Lead ID: {lead.pk}. Error: {ve}"
|
||||||
)
|
)
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
{"success": False, "message": "Invalid date format"}, status=400
|
{"success": False, "message": "Invalid date format"}, status=400
|
||||||
)
|
)
|
||||||
|
|
||||||
# Save the lead
|
# Save the lead
|
||||||
lead.save()
|
lead.save()
|
||||||
# --- Logging for successful update (main try block success) ---
|
# --- Logging for successful update (main try block success) ---
|
||||||
logger.info(
|
logger.info(
|
||||||
f"User {user_username} successfully updated Lead ID: {lead.id} ('{lead.name}'). "
|
f"User {user_username} successfully updated Lead ID: {lead.pk} ('{lead.slug}'). "
|
||||||
f"New Status: '{lead.status}', Next Action: '{lead.next_action}', Next Action Date: '{lead.next_action_date}'."
|
f"New Status: '{lead.status}', Next Action: '{lead.next_action}', Next Action Date: '{lead.next_action_date}'."
|
||||||
)
|
)
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
@ -5932,7 +5937,7 @@ def LeadDeleteView(request,dealer_slug, slug):
|
|||||||
|
|
||||||
# Log intent before attempting deletion
|
# Log intent before attempting deletion
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"User {user_username} attempting to delete Lead ID: {lead.id} ('{lead.name}') "
|
f"User {user_username} attempting to delete Lead ID: {lead.pk} ('{lead.slug}') "
|
||||||
f"and its associated customer/user for dealer '{dealer_slug}'."
|
f"and its associated customer/user for dealer '{dealer_slug}'."
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -5943,19 +5948,19 @@ def LeadDeleteView(request,dealer_slug, slug):
|
|||||||
# --- Single-line log for successful associated user/customer deletion ---
|
# --- Single-line log for successful associated user/customer deletion ---
|
||||||
logger.info(
|
logger.info(
|
||||||
f"User {user_username} successfully deleted associated user and customer "
|
f"User {user_username} successfully deleted associated user and customer "
|
||||||
f"for Lead ID: {lead.id} ('{lead.name}') (Email: {lead.customer.email})."
|
f"for Lead ID: {lead.pk} ('{lead.slug}') (Email: {lead.customer.email})."
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# --- Single-line log for error during associated user/customer deletion ---
|
# --- Single-line log for error during associated user/customer deletion ---
|
||||||
logger.error(
|
logger.error(
|
||||||
f"User {user_username} encountered an error deleting associated user/customer "
|
f"User {user_username} encountered an error deleting associated user/customer "
|
||||||
f"for Lead ID: {lead.id} ('{lead.name}') (Email: {getattr(lead.customer, 'email', 'N/A')}). " # Safely get email
|
f"for Lead ID: {lead.slug} ('{lead.slug}') (Email: {getattr(lead.customer, 'email', 'N/A')}). " # Safely get email
|
||||||
f"Error: {e}",
|
f"Error: {e}",
|
||||||
exc_info=True
|
exc_info=True
|
||||||
)
|
)
|
||||||
print(e)
|
print(e)
|
||||||
lead_id_final = lead.id # Capture before deletion
|
lead_id_final = lead.pk # Capture before deletion
|
||||||
lead_name_final = lead.name
|
lead_name_final = lead.slug
|
||||||
lead.delete()
|
lead.delete()
|
||||||
# Log the final lead deletion, which happens unconditionally after the try-except
|
# Log the final lead deletion, which happens unconditionally after the try-except
|
||||||
logger.info(
|
logger.info(
|
||||||
@ -5967,7 +5972,7 @@ def LeadDeleteView(request,dealer_slug, slug):
|
|||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required("inventory.add_note", raise_exception=True)
|
@permission_required("inventory.add_notes", raise_exception=True)
|
||||||
def add_note_to_lead(request,dealer_slug, slug):
|
def add_note_to_lead(request,dealer_slug, slug):
|
||||||
"""
|
"""
|
||||||
Adds a note to a specific lead. This view is accessible only to authenticated
|
Adds a note to a specific lead. This view is accessible only to authenticated
|
||||||
@ -6000,7 +6005,7 @@ def add_note_to_lead(request,dealer_slug, slug):
|
|||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required("inventory.add_note", raise_exception=True)
|
@permission_required("inventory.add_notes", raise_exception=True)
|
||||||
def add_note_to_opportunity(request,dealer_slug, slug):
|
def add_note_to_opportunity(request,dealer_slug, slug):
|
||||||
"""
|
"""
|
||||||
Add a note to a specific opportunity identified by its primary key.
|
Add a note to a specific opportunity identified by its primary key.
|
||||||
@ -6033,7 +6038,7 @@ def add_note_to_opportunity(request,dealer_slug, slug):
|
|||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required("inventory.delete_note", raise_exception=True)
|
@permission_required("inventory.delete_notes", raise_exception=True)
|
||||||
def delete_note(request,dealer_slug, pk):
|
def delete_note(request,dealer_slug, pk):
|
||||||
"""
|
"""
|
||||||
Deletes a specific note created by the currently logged-in user and redirects
|
Deletes a specific note created by the currently logged-in user and redirects
|
||||||
@ -6095,7 +6100,7 @@ def lead_convert(request,dealer_slug, slug):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required("inventory.add_schedule", raise_exception=True)
|
@permission_required("inventory.add_schedule", raise_exception=True)
|
||||||
def schedule_lead(request, dealer_slug,slug):
|
def schedule_event(request, dealer_slug,content_type,slug):
|
||||||
"""
|
"""
|
||||||
Handles the scheduling of a lead for an appointment.
|
Handles the scheduling of a lead for an appointment.
|
||||||
|
|
||||||
@ -6113,29 +6118,46 @@ def schedule_lead(request, dealer_slug,slug):
|
|||||||
method and validity of the form submission.
|
method and validity of the form submission.
|
||||||
:rtype: HttpResponse
|
:rtype: HttpResponse
|
||||||
"""
|
"""
|
||||||
|
user_username = request.user.username if request.user.is_authenticated else 'anonymous'
|
||||||
|
# Log the attempt to retrieve the model dynamically
|
||||||
|
logger.debug(
|
||||||
|
f"User {user_username} attempting to retrieve model "
|
||||||
|
f"for content_type '{content_type}' for dealer '{dealer_slug}'."
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
model = apps.get_model(f"inventory.{content_type}")
|
||||||
|
except LookupError:
|
||||||
|
# --- Single-line log for LookupError (Model not found) ---
|
||||||
|
logger.warning(
|
||||||
|
f"User {user_username} requested an invalid model content_type: '{content_type}'. "
|
||||||
|
f"Raising Http404 for dealer '{dealer_slug}'."
|
||||||
|
)
|
||||||
|
raise Http404("Model not found")
|
||||||
|
|
||||||
|
dealer = get_object_or_404(models.Dealer,slug=dealer_slug)
|
||||||
|
obj = get_object_or_404(model, slug=slug)
|
||||||
|
|
||||||
if not request.is_staff:
|
if not request.is_staff:
|
||||||
messages.error(request, _("You do not have permission to schedule lead"))
|
messages.error(request, _("You do not have permission to schedule."))
|
||||||
return redirect("lead_list", dealer_slug=dealer_slug)
|
return redirect(request.META.get("HTTP_REFERER"))
|
||||||
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
||||||
lead = get_object_or_404(models.Lead, slug=slug, dealer=dealer)
|
|
||||||
if request.method == "POST":
|
|
||||||
# Get user info for logging (available throughout the POST handling)
|
|
||||||
user_username = request.user.username if request.user.is_authenticated else 'anonymous'
|
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
form = forms.ScheduleForm(request.POST)
|
form = forms.ScheduleForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
instance = form.save(commit=False)
|
instance = form.save(commit=False)
|
||||||
instance.lead = lead
|
instance.content_object = obj
|
||||||
instance.scheduled_by = request.user
|
instance.scheduled_by = request.user
|
||||||
|
|
||||||
instance.customer = lead.get_customer_model()
|
if obj.customer:
|
||||||
|
instance.customer = obj.customer.customer_model
|
||||||
|
elif obj.organization:
|
||||||
|
instance.cutsomer = obj.organization.customer_model
|
||||||
|
|
||||||
service = Service.objects.get(name=instance.scheduled_type)
|
service = Service.objects.get(name=instance.scheduled_type)
|
||||||
# Log attempt to create AppointmentRequest
|
# Log attempt to create AppointmentRequest
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"User {user_username} attempting to create AppointmentRequest "
|
f"User {user_username} attempting to create AppointmentRequest "
|
||||||
f"for Lead ID: {lead.id} ('{lead.name}'). Service: '{service.name}', "
|
f"for {content_type} ID: {obj.pk} ('{obj.slug}'). Service: '{service.name}', "
|
||||||
f"Scheduled At: '{instance.scheduled_at}', Duration: '{instance.duration}'."
|
f"Scheduled At: '{instance.scheduled_at}', Duration: '{instance.duration}'."
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -6149,41 +6171,34 @@ def schedule_lead(request, dealer_slug,slug):
|
|||||||
)
|
)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
messages.error(request, str(e))
|
messages.error(request, str(e))
|
||||||
return redirect("schedule_lead", dealer_slug=lead.dealer.slug, slug=lead.slug)
|
return redirect(request.META.get("HTTP_REFERER"))
|
||||||
|
|
||||||
client = get_object_or_404(User, email=lead.email)
|
client = get_object_or_404(User, email=instance.customer.email)
|
||||||
# Create Appointment
|
# Create Appointment
|
||||||
Appointment.objects.create(
|
Appointment.objects.create(
|
||||||
client=client,
|
client=client,
|
||||||
appointment_request=appointment_request,
|
appointment_request=appointment_request,
|
||||||
phone=lead.phone_number,
|
phone=instance.phone,
|
||||||
address=lead.address,
|
address=instance.address_1,
|
||||||
)
|
)
|
||||||
|
|
||||||
instance.save()
|
instance.save()
|
||||||
# --- Logging for successful AppointmentRequest and Appointment creation ---
|
# --- Logging for successful AppointmentRequest and Appointment creation ---
|
||||||
logger.info(
|
logger.info(
|
||||||
f"User {user_username} successfully scheduled Lead ID: {lead.id} ('{lead.name}'). "
|
f"User {user_username} successfully scheduled {content_type} ID: {obj.pk} ('{obj.slug}'). "
|
||||||
f"AppointmentRequest ID: {appointment_request.pk}, Appointment ID: {appointment_request.appointment.pk}."
|
f"AppointmentRequest ID: {appointment_request.pk}, Appointment ID: {appointment_request.appointment.pk}."
|
||||||
)
|
)
|
||||||
messages.success(request, _("Appointment Created Successfully"))
|
messages.success(request, _("Appointment Created Successfully"))
|
||||||
try:
|
|
||||||
if lead.opportunity:
|
return redirect(request.META.get("HTTP_REFERER"))
|
||||||
return redirect("opportunity_detail", dealer_slug=lead.dealer.slug, slug=lead.opportunity.slug)
|
|
||||||
except models.Lead.opportunity.RelatedObjectDoesNotExist:
|
|
||||||
logger.info(
|
|
||||||
f"Lead ID: {lead.id} ('{lead.name}') has no associated opportunity. "
|
|
||||||
f"Redirecting to lead list for user {user_username} "
|
|
||||||
)
|
|
||||||
return redirect("lead_list", dealer_slug=lead.dealer.slug)
|
|
||||||
else:
|
else:
|
||||||
# Log for invalid form data
|
# Log for invalid form data
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"User {user_username} submitted invalid schedule form data "
|
f"User {user_username} submitted invalid schedule form data "
|
||||||
f"for Lead ID: {lead.id} ('{lead.name}'). Errors: {form.errors.as_json()}"
|
f"for Lead ID: {obj.pk} ('{obj.slug}'). Errors: {form.errors.as_json()}"
|
||||||
)
|
)
|
||||||
messages.error(request, f"Invalid form data: {str(form.errors)}")
|
messages.error(request, f"Invalid form data: {str(form.errors)}")
|
||||||
return redirect("schedule_lead", dealer_slug=dealer_slug,slug=lead.slug)
|
return redirect(request.META.get("HTTP_REFERER"))
|
||||||
form = forms.ScheduleForm()
|
form = forms.ScheduleForm()
|
||||||
return render(request, "crm/leads/schedule_lead.html", {"lead": lead, "form": form})
|
return render(request, "crm/leads/schedule_lead.html", {"lead": lead, "form": form})
|
||||||
|
|
||||||
@ -6243,7 +6258,6 @@ def send_lead_email(request,dealer_slug, slug, email_pk=None):
|
|||||||
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
||||||
lead = get_object_or_404(models.Lead, slug=slug)
|
lead = get_object_or_404(models.Lead, slug=slug)
|
||||||
status = request.GET.get("status")
|
status = request.GET.get("status")
|
||||||
# Get user info for logging
|
|
||||||
user_username = request.user.username if request.user.is_authenticated else 'anonymous'
|
user_username = request.user.username if request.user.is_authenticated else 'anonymous'
|
||||||
|
|
||||||
if status == "draft":
|
if status == "draft":
|
||||||
@ -6264,11 +6278,12 @@ def send_lead_email(request,dealer_slug, slug, email_pk=None):
|
|||||||
activity_type=models.ActionChoices.EMAIL,
|
activity_type=models.ActionChoices.EMAIL,
|
||||||
)
|
)
|
||||||
messages.success(request, _("Email Draft successfully"))
|
messages.success(request, _("Email Draft successfully"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if lead.opportunity:
|
if getattr(lead, "opportunity",None):
|
||||||
# Log success when opportunity exists and redirecting
|
# Log success when opportunity exists and redirecting
|
||||||
logger.info(
|
logger.info(
|
||||||
f"User {user_username} successfully drafted email for Lead ID: {lead.id} ('{lead.name}'). "
|
f"User {user_username} successfully drafted email for Lead ID: {lead.pk} ('{lead.slug}'). "
|
||||||
f"Lead has an Opportunity (ID: {lead.opportunity.pk}), redirecting to opportunity detail."
|
f"Lead has an Opportunity (ID: {lead.opportunity.pk}), redirecting to opportunity detail."
|
||||||
)
|
)
|
||||||
response = HttpResponse(
|
response = HttpResponse(
|
||||||
@ -6280,16 +6295,16 @@ def send_lead_email(request,dealer_slug, slug, email_pk=None):
|
|||||||
else:
|
else:
|
||||||
# Log success when no opportunity and redirecting to lead detail
|
# Log success when no opportunity and redirecting to lead detail
|
||||||
logger.info(
|
logger.info(
|
||||||
f"User {user_username} successfully drafted email for Lead ID: {lead.id} ('{lead.name}'). "
|
f"User {user_username} successfully drafted email for Lead ID: {lead.pk} ('{lead.slug}'). "
|
||||||
f"Lead has no Opportunity, redirecting to lead detail."
|
f"Lead has no Opportunity, redirecting to lead detail."
|
||||||
)
|
)
|
||||||
response = HttpResponse(redirect("lead_detail", dealer_slug=dealer_slug,slug=lead.slug))
|
response = HttpResponse()
|
||||||
response["HX-Redirect"] = reverse("lead_detail", dealer_slug=dealer_slug,slug=lead.slug)
|
response["HX-Redirect"] = reverse("lead_detail", dealer_slug=dealer_slug,slug=lead.slug)
|
||||||
return response
|
return response
|
||||||
except models.Lead.opportunity.RelatedObjectDoesNotExist:
|
except models.Lead.opportunity.RelatedObjectDoesNotExist:
|
||||||
# --- Log when Lead.opportunity does not exist (Draft status) ---
|
# --- Log when Lead.opportunity does not exist (Draft status) ---
|
||||||
logger.info(
|
logger.info(
|
||||||
f"User {user_username} drafted email for Lead ID: {lead.id} ('{lead.name}'). "
|
f"User {user_username} drafted email for Lead ID: {lead.pk} ('{lead.slug}'). "
|
||||||
f"Lead's opportunity does not exist. Redirecting to lead list."
|
f"Lead's opportunity does not exist. Redirecting to lead list."
|
||||||
)
|
)
|
||||||
return redirect("lead_list",dealer_slug=dealer.slug)
|
return redirect("lead_list",dealer_slug=dealer.slug)
|
||||||
@ -6328,14 +6343,14 @@ def send_lead_email(request,dealer_slug, slug, email_pk=None):
|
|||||||
if lead.opportunity:
|
if lead.opportunity:
|
||||||
# Log success when opportunity exists and redirecting after sending email
|
# Log success when opportunity exists and redirecting after sending email
|
||||||
logger.info(
|
logger.info(
|
||||||
f"User {user_username} successfully sent email for Lead ID: {lead.id} ('{lead.name}'). "
|
f"User {user_username} successfully sent email for Lead ID: {lead.pk} ('{lead.slug}'). "
|
||||||
f"Lead has an Opportunity (ID: {lead.opportunity.pk}), redirecting to opportunity detail."
|
f"Lead has an Opportunity (ID: {lead.opportunity.pk}), redirecting to opportunity detail."
|
||||||
)
|
)
|
||||||
return redirect("opportunity_detail", dealer_slug=dealer_slug,slug=lead.opportunity.slug)
|
return redirect("opportunity_detail", dealer_slug=dealer_slug,slug=lead.opportunity.slug)
|
||||||
except models.Lead.opportunity.RelatedObjectDoesNotExist:
|
except models.Lead.opportunity.RelatedObjectDoesNotExist:
|
||||||
# --- Log when Lead.opportunity does not exist (POST request for sending) ---
|
# --- Log when Lead.opportunity does not exist (POST request for sending) ---
|
||||||
logger.info(
|
logger.info(
|
||||||
f"User {user_username} sent email for Lead ID: {lead.id} ('{lead.name}'). "
|
f"User {user_username} sent email for Lead ID: {lead.pk} ('{lead.slug}'). "
|
||||||
f"Lead's opportunity does not exist. Redirecting to lead list."
|
f"Lead's opportunity does not exist. Redirecting to lead list."
|
||||||
)
|
)
|
||||||
return redirect("lead_list",dealer_slug=dealer_slug)
|
return redirect("lead_list",dealer_slug=dealer_slug)
|
||||||
@ -6393,7 +6408,6 @@ def add_activity_to_lead(request, pk):
|
|||||||
form = forms.ActivityForm(request.POST)
|
form = forms.ActivityForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
activity = form.save(commit=False)
|
activity = form.save(commit=False)
|
||||||
print(activity)
|
|
||||||
activity.content_object = lead
|
activity.content_object = lead
|
||||||
activity.dealer = dealer
|
activity.dealer = dealer
|
||||||
activity.activity_type = form.cleaned_data["activity_type"]
|
activity.activity_type = form.cleaned_data["activity_type"]
|
||||||
@ -6451,6 +6465,14 @@ class OpportunityCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateVie
|
|||||||
instance.lead.save()
|
instance.lead.save()
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
def get_form(self,form_class=None):
|
||||||
|
dealer = get_object_or_404(models.Dealer,slug=self.kwargs.get("dealer_slug"))
|
||||||
|
staff = getattr(self.request.user.staffmember, "staff", None)
|
||||||
|
form = super().get_form(form_class)
|
||||||
|
form.fields["car"].queryset = models.Car.objects.filter(dealer=dealer,status='available',finances__selling_price__gt=0)
|
||||||
|
form.fields["lead"].queryset = models.Lead.objects.filter(dealer=dealer,staff=staff)
|
||||||
|
return form
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse_lazy("opportunity_detail", kwargs={"dealer_slug":self.kwargs.get("dealer_slug"),"slug": self.object.slug})
|
return reverse_lazy("opportunity_detail", kwargs={"dealer_slug":self.kwargs.get("dealer_slug"),"slug": self.object.slug})
|
||||||
|
|
||||||
@ -6487,8 +6509,9 @@ class OpportunityUpdateView(LoginRequiredMixin,PermissionRequiredMixin, SuccessM
|
|||||||
def get_form(self, form_class=None):
|
def get_form(self, form_class=None):
|
||||||
form = super().get_form(form_class)
|
form = super().get_form(form_class)
|
||||||
dealer = get_object_or_404(models.Dealer,slug=self.kwargs.get("dealer_slug"))
|
dealer = get_object_or_404(models.Dealer,slug=self.kwargs.get("dealer_slug"))
|
||||||
form.fields['car'].queryset = models.Car.objects.filter(dealer=dealer)
|
staff = getattr(self.request.user.staffmember, "staff", None)
|
||||||
form.fields['lead'].queryset = models.Lead.objects.filter(dealer=dealer)
|
form.fields["car"].queryset = models.Car.objects.filter(dealer=dealer,status='available',finances__selling_price__gt=0)
|
||||||
|
form.fields["lead"].queryset = models.Lead.objects.filter(dealer=dealer,staff=staff)
|
||||||
return form
|
return form
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
@ -6530,19 +6553,19 @@ class OpportunityDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailV
|
|||||||
content_type__model="lead", object_id=self.object.id
|
content_type__model="lead", object_id=self.object.id
|
||||||
).order_by("-created")
|
).order_by("-created")
|
||||||
context["lead_notes"] = models.Notes.objects.filter(
|
context["lead_notes"] = models.Notes.objects.filter(
|
||||||
content_type__model="lead", object_id=self.object.lead.id
|
content_type__model="lead", object_id=self.object.lead.pk
|
||||||
).order_by("-created")
|
).order_by("-created")
|
||||||
context["notes"] = models.Notes.objects.filter(
|
context["notes"] = models.Notes.objects.filter(
|
||||||
content_type__model="opportunity", object_id=self.object.id
|
content_type__model="opportunity", object_id=self.object.id
|
||||||
).order_by("-created")
|
).order_by("-created")
|
||||||
context["lead_activities"] = models.Activity.objects.filter(
|
context["lead_activities"] = models.Activity.objects.filter(
|
||||||
content_type__model="lead", object_id=self.object.lead.id
|
content_type__model="lead", object_id=self.object.lead.pk
|
||||||
)
|
)
|
||||||
context["activities"] = models.Activity.objects.filter(
|
context["activities"] = models.Activity.objects.filter(
|
||||||
content_type__model="opportunity", object_id=self.object.id
|
content_type__model="opportunity", object_id=self.object.id
|
||||||
)
|
)
|
||||||
lead_email_qs = models.Email.objects.filter(
|
lead_email_qs = models.Email.objects.filter(
|
||||||
content_type__model="lead", object_id=self.object.lead.id
|
content_type__model="lead", object_id=self.object.lead.pk
|
||||||
)
|
)
|
||||||
email_qs = models.Email.objects.filter(
|
email_qs = models.Email.objects.filter(
|
||||||
content_type__model="opportunity", object_id=self.object.id
|
content_type__model="opportunity", object_id=self.object.id
|
||||||
@ -6556,8 +6579,9 @@ class OpportunityDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailV
|
|||||||
"draft": lead_email_qs.filter(status="DRAFT"),
|
"draft": lead_email_qs.filter(status="DRAFT"),
|
||||||
}
|
}
|
||||||
context["staff_task_form"] = forms.StaffTaskForm()
|
context["staff_task_form"] = forms.StaffTaskForm()
|
||||||
|
context["note_form"] = forms.NoteForm()
|
||||||
context["lead_tasks"] = models.Tasks.objects.filter(
|
context["lead_tasks"] = models.Tasks.objects.filter(
|
||||||
content_type__model="lead", object_id=self.object.lead.id
|
content_type__model="lead", object_id=self.object.lead.pk
|
||||||
)
|
)
|
||||||
context["tasks"] = models.Tasks.objects.filter(
|
context["tasks"] = models.Tasks.objects.filter(
|
||||||
content_type__model="opportunity", object_id=self.object.id
|
content_type__model="opportunity", object_id=self.object.id
|
||||||
@ -6580,7 +6604,8 @@ class OpportunityListView(LoginRequiredMixin,PermissionRequiredMixin, ListView):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
dealer = get_user_type(self.request)
|
dealer = get_user_type(self.request)
|
||||||
queryset = models.Opportunity.objects.filter(dealer=dealer)
|
staff = getattr(self.request.user.staffmember, "staff", None)
|
||||||
|
queryset = models.Opportunity.objects.filter(dealer=dealer, lead__staff=staff)
|
||||||
|
|
||||||
# Search filter
|
# Search filter
|
||||||
search = self.request.GET.get("search")
|
search = self.request.GET.get("search")
|
||||||
@ -7040,7 +7065,7 @@ class BillListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|||||||
class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView):
|
class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView):
|
||||||
template_name = "bill/bill_create.html"
|
template_name = "bill/bill_create.html"
|
||||||
PAGE_TITLE = _("Create Bill")
|
PAGE_TITLE = _("Create Bill")
|
||||||
permission_required = ["django_ledger.add_billmodel"]
|
permission_required = "django_ledger.add_billmodel"
|
||||||
extra_context = {
|
extra_context = {
|
||||||
"page_title": PAGE_TITLE,
|
"page_title": PAGE_TITLE,
|
||||||
"header_title": PAGE_TITLE,
|
"header_title": PAGE_TITLE,
|
||||||
@ -7048,7 +7073,6 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView)
|
|||||||
}
|
}
|
||||||
for_purchase_order = False
|
for_purchase_order = False
|
||||||
for_estimate = False
|
for_estimate = False
|
||||||
permission_required = "django_ledger.add_billmodel"
|
|
||||||
|
|
||||||
# Get user info for logging
|
# Get user info for logging
|
||||||
|
|
||||||
@ -7061,7 +7085,7 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView)
|
|||||||
|
|
||||||
if self.for_estimate and "ce_pk" in self.kwargs:
|
if self.for_estimate and "ce_pk" in self.kwargs:
|
||||||
estimate_qs = EstimateModel.objects.for_entity(
|
estimate_qs = EstimateModel.objects.for_entity(
|
||||||
entity_slug=self.kwargs["entity_slug"], user_model=self.request.user
|
entity_slug=self.kwargs["entity_slug"], user_model=dealer.entity.admin
|
||||||
)
|
)
|
||||||
estimate_model: EstimateModel = get_object_or_404(
|
estimate_model: EstimateModel = get_object_or_404(
|
||||||
estimate_qs, uuid__exact=self.kwargs["ce_pk"]
|
estimate_qs, uuid__exact=self.kwargs["ce_pk"]
|
||||||
@ -7080,7 +7104,7 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView)
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(BillModelCreateView, self).get_context_data(**kwargs)
|
context = super(BillModelCreateView, self).get_context_data(**kwargs)
|
||||||
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
||||||
user_username = self.request.user.username if self.request.user.is_authenticated else 'anonymous'
|
user_username = self.request.user.username if self.request.user.is_authenticated else 'anonymous'
|
||||||
|
|
||||||
if self.for_purchase_order:
|
if self.for_purchase_order:
|
||||||
@ -7110,7 +7134,7 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView)
|
|||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
po_qs = PurchaseOrderModel.objects.for_entity(
|
po_qs = PurchaseOrderModel.objects.for_entity(
|
||||||
entity_slug=self.kwargs["entity_slug"], user_model=self.request.user
|
entity_slug=self.kwargs["entity_slug"], user_model=dealer.entity.admin
|
||||||
).prefetch_related("itemtransactionmodel_set")
|
).prefetch_related("itemtransactionmodel_set")
|
||||||
po_model: PurchaseOrderModel = get_object_or_404(po_qs, uuid__exact=po_pk)
|
po_model: PurchaseOrderModel = get_object_or_404(po_qs, uuid__exact=po_pk)
|
||||||
po_itemtxs_qs = po_model.itemtransactionmodel_set.filter(
|
po_itemtxs_qs = po_model.itemtransactionmodel_set.filter(
|
||||||
@ -7131,7 +7155,7 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView)
|
|||||||
)
|
)
|
||||||
elif self.for_estimate:
|
elif self.for_estimate:
|
||||||
estimate_qs = EstimateModel.objects.for_entity(
|
estimate_qs = EstimateModel.objects.for_entity(
|
||||||
entity_slug=self.kwargs["entity_slug"], user_model=self.request.user
|
entity_slug=self.kwargs["entity_slug"], user_model=dealer.entity.admin
|
||||||
)
|
)
|
||||||
estimate_uuid = self.kwargs["ce_pk"]
|
estimate_uuid = self.kwargs["ce_pk"]
|
||||||
estimate_model: EstimateModel = get_object_or_404(
|
estimate_model: EstimateModel = get_object_or_404(
|
||||||
@ -7175,7 +7199,7 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView)
|
|||||||
if self.for_estimate:
|
if self.for_estimate:
|
||||||
ce_pk = self.kwargs["ce_pk"]
|
ce_pk = self.kwargs["ce_pk"]
|
||||||
estimate_model_qs = EstimateModel.objects.for_entity(
|
estimate_model_qs = EstimateModel.objects.for_entity(
|
||||||
entity_slug=self.kwargs["entity_slug"], user_model=self.request.user
|
entity_slug=self.kwargs["entity_slug"], user_model=dealer.entity.admin
|
||||||
)
|
)
|
||||||
|
|
||||||
estimate_model = get_object_or_404(estimate_model_qs, uuid__exact=ce_pk)
|
estimate_model = get_object_or_404(estimate_model_qs, uuid__exact=ce_pk)
|
||||||
@ -7188,7 +7212,7 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView)
|
|||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
item_uuids = item_uuids.split(",")
|
item_uuids = item_uuids.split(",")
|
||||||
po_qs = PurchaseOrderModel.objects.for_entity(
|
po_qs = PurchaseOrderModel.objects.for_entity(
|
||||||
entity_slug=self.kwargs["entity_slug"], user_model=self.request.user
|
entity_slug=self.kwargs["entity_slug"], user_model=dealer.entity.admin
|
||||||
)
|
)
|
||||||
po_model: PurchaseOrderModel = get_object_or_404(po_qs, uuid__exact=po_pk)
|
po_model: PurchaseOrderModel = get_object_or_404(po_qs, uuid__exact=po_pk)
|
||||||
|
|
||||||
@ -7259,101 +7283,33 @@ class BillModelCreateView(LoginRequiredMixin,PermissionRequiredMixin,CreateView)
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BillModelDetailViewView(BillModelDetailView):
|
class BillModelDetailView(BillModelDetailViewBase):
|
||||||
template_name = "bill/bill_detail.html"
|
template_name = "bill/bill_detail.html"
|
||||||
permission_required = ["django_ledger.view_billmodel"]
|
permission_required = ["django_ledger.view_billmodel"]
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(BillModelDetailViewView, self).get_context_data(**kwargs)
|
context = super(BillModelDetailView, self).get_context_data(**kwargs)
|
||||||
context["dealer"] = self.request.dealer
|
context["dealer"] = self.request.dealer
|
||||||
return context
|
return context
|
||||||
|
def get_queryset(self):
|
||||||
|
qs = super().get_queryset()
|
||||||
|
return qs.select_related(
|
||||||
|
'ledger',
|
||||||
|
'ledger__entity',
|
||||||
|
'vendor',
|
||||||
|
'cash_account',
|
||||||
|
'prepaid_account',
|
||||||
|
'unearned_account',
|
||||||
|
'cash_account__coa_model',
|
||||||
|
'prepaid_account__coa_model',
|
||||||
|
'unearned_account__coa_model'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BillModelUpdateViewView(BillModelUpdateView):
|
class BillModelUpdateView(BillModelUpdateViewBase):
|
||||||
template_name = "bill/bill_update.html"
|
|
||||||
permission_required = ["django_ledger.change_billmodel"]
|
permission_required = ["django_ledger.change_billmodel"]
|
||||||
|
|
||||||
|
|
||||||
def post(self, request, dealer_slug, entity_slug, bill_pk, *args, **kwargs):
|
|
||||||
if self.action_update_items:
|
|
||||||
if not request.user.is_authenticated:
|
|
||||||
return HttpResponseForbidden()
|
|
||||||
|
|
||||||
queryset = self.get_queryset()
|
|
||||||
entity_model: EntityModel = self.get_authorized_entity_instance()
|
|
||||||
bill_model: BillModel = self.get_object(queryset=queryset)
|
|
||||||
bill_pk = bill_model.uuid
|
|
||||||
|
|
||||||
self.object = bill_model
|
|
||||||
|
|
||||||
bill_itemtxs_formset_class = get_bill_itemtxs_formset_class(bill_model)
|
|
||||||
itemtxs_formset = bill_itemtxs_formset_class(
|
|
||||||
request.POST, bill_model=bill_model, entity_model=entity_model
|
|
||||||
)
|
|
||||||
|
|
||||||
if itemtxs_formset.has_changed():
|
|
||||||
if itemtxs_formset.is_valid():
|
|
||||||
itemtxs_list = itemtxs_formset.save(commit=False)
|
|
||||||
|
|
||||||
for itemtxs in itemtxs_list:
|
|
||||||
itemtxs.bill_model_id = bill_model.uuid
|
|
||||||
itemtxs.clean()
|
|
||||||
|
|
||||||
itemtxs_formset.save()
|
|
||||||
itemtxs_qs = bill_model.update_amount_due()
|
|
||||||
bill_model.get_state(commit=True)
|
|
||||||
bill_model.clean()
|
|
||||||
bill_model.save(
|
|
||||||
update_fields=[
|
|
||||||
"amount_due",
|
|
||||||
"amount_receivable",
|
|
||||||
"amount_unearned",
|
|
||||||
"amount_earned",
|
|
||||||
"updated",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
bill_model.migrate_state(
|
|
||||||
entity_slug=self.kwargs["entity_slug"],
|
|
||||||
user_model=self.request.user,
|
|
||||||
itemtxs_qs=itemtxs_qs,
|
|
||||||
raise_exception=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
messages.add_message(
|
|
||||||
request,
|
|
||||||
message=f"Items for Invoice {bill_model.bill_number} saved.",
|
|
||||||
level=messages.SUCCESS,
|
|
||||||
)
|
|
||||||
|
|
||||||
# if valid get saved formset from DB
|
|
||||||
return HttpResponseRedirect(
|
|
||||||
redirect_to=reverse(
|
|
||||||
"bill-update",
|
|
||||||
kwargs={
|
|
||||||
"dealer_slug": dealer_slug,
|
|
||||||
"entity_slug": entity_model.slug,
|
|
||||||
"bill_pk": bill_pk,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
context = self.get_context_data(itemtxs_formset=itemtxs_formset)
|
|
||||||
return self.render_to_response(context=context)
|
|
||||||
return super(BillModelUpdateViewView, self).post(
|
|
||||||
request, dealer_slug, entity_slug, bill_pk, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
return reverse(
|
|
||||||
"bill-update",
|
|
||||||
kwargs={
|
|
||||||
"dealer_slug": self.kwargs["dealer_slug"],
|
|
||||||
"entity_slug": self.kwargs["entity_slug"],
|
|
||||||
"bill_pk": self.kwargs["bill_pk"],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# @login_required
|
# @login_required
|
||||||
# @permission_required("django_ledger.add_billmodel", raise_exception=True)
|
# @permission_required("django_ledger.add_billmodel", raise_exception=True)
|
||||||
# def bill_create(request):
|
# def bill_create(request):
|
||||||
@ -9298,7 +9254,7 @@ def add_activity(request,dealer_slug, content_type, slug):
|
|||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required("inventory.add_task", raise_exception=True)
|
@permission_required("inventory.add_tasks", raise_exception=True)
|
||||||
def add_task(request,dealer_slug, content_type, slug):
|
def add_task(request,dealer_slug, content_type, slug):
|
||||||
# Get user information for logging
|
# Get user information for logging
|
||||||
user_username = request.user.username if request.user.is_authenticated else 'anonymous'
|
user_username = request.user.username if request.user.is_authenticated else 'anonymous'
|
||||||
@ -9329,11 +9285,17 @@ def add_task(request,dealer_slug, content_type, slug):
|
|||||||
task.created_by = request.user
|
task.created_by = request.user
|
||||||
task.due_date = form.cleaned_data["due_date"]
|
task.due_date = form.cleaned_data["due_date"]
|
||||||
task.save()
|
task.save()
|
||||||
|
models.Activity.objects.create(
|
||||||
|
dealer=dealer,
|
||||||
|
content_object=obj,
|
||||||
|
activity_type="task",
|
||||||
|
created_by=request.user,
|
||||||
|
notes="Task Added")
|
||||||
# --- Log for successful task creation ---
|
# --- Log for successful task creation ---
|
||||||
logger.info(
|
logger.info(
|
||||||
f"User {user_username} successfully added task "
|
f"User {user_username} successfully added task "
|
||||||
f"(Assigned to: {task.assigned_to.username}) for {content_type} ID: {obj.slug} "
|
f"(Assigned to: {task.assigned_to.email}) for {content_type} ID: {obj.slug} "
|
||||||
f"on dealer '{dealer_slug}'. Due: {task.due_date}, Notes: '{task.notes if hasattr(task, 'notes') else 'N/A'}'."
|
f"on dealer '{dealer_slug}'. Due: {task.due_date}, Notes: '{task.description}'."
|
||||||
)
|
)
|
||||||
messages.success(request, _("Task added successfully"))
|
messages.success(request, _("Task added successfully"))
|
||||||
else:
|
else:
|
||||||
@ -9347,7 +9309,7 @@ def add_task(request,dealer_slug, content_type, slug):
|
|||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required("inventory.change_task", raise_exception=True)
|
@permission_required("inventory.change_tasks", raise_exception=True)
|
||||||
def update_task(request,dealer_slug, pk):
|
def update_task(request,dealer_slug, pk):
|
||||||
task = get_object_or_404(models.Tasks, pk=pk)
|
task = get_object_or_404(models.Tasks, pk=pk)
|
||||||
|
|
||||||
@ -9361,7 +9323,7 @@ def update_task(request,dealer_slug, pk):
|
|||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required("inventory.add_note", raise_exception=True)
|
@permission_required("inventory.add_notes", raise_exception=True)
|
||||||
def add_note(request,dealer_slug, content_type, slug):
|
def add_note(request,dealer_slug, content_type, slug):
|
||||||
# Get user information for logging
|
# Get user information for logging
|
||||||
user_username = request.user.username if request.user.is_authenticated else 'anonymous'
|
user_username = request.user.username if request.user.is_authenticated else 'anonymous'
|
||||||
@ -9391,25 +9353,32 @@ def add_note(request,dealer_slug, content_type, slug):
|
|||||||
note.created_by = request.user
|
note.created_by = request.user
|
||||||
|
|
||||||
note.save()
|
note.save()
|
||||||
|
models.Activity.objects.create(
|
||||||
|
dealer=dealer,
|
||||||
|
content_object=obj,
|
||||||
|
activity_type="note",
|
||||||
|
created_by=request.user,
|
||||||
|
notes="Note Added")
|
||||||
# --- Single-line log for successful note creation ---
|
# --- Single-line log for successful note creation ---
|
||||||
logger.info(
|
logger.info(
|
||||||
f"User {user_username} successfully added a note "
|
f"User {user_username} successfully added a note "
|
||||||
f"for {content_type} ID: {obj.slug} (Dealer: {dealer_slug}). "
|
f"for {content_type} ID: {obj.slug} (Dealer: {dealer_slug}). "
|
||||||
f"Note: '{note.notes[:50]}...'."
|
f"Note: '{note.note[:50]}...'."
|
||||||
)
|
)
|
||||||
messages.success(request, _("Note added successfully"))
|
messages.success(request, _("Note added successfully"))
|
||||||
else:
|
else:
|
||||||
# --- Single-line log for invalid form data ---
|
# --- Single-line log for invalid form data ---
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"User {user_username} submitted invalid note form data "
|
f"User {user_username} submitted invalid note form data "
|
||||||
f"for {content_type} ID: {obj.slug} (Dealer: {dealer_slug}). Errors: {form.errors.as_json()}"
|
f"for {content_type} ID: {obj.slug} (Dealer: {dealer_slug}). Errors: {form.errors.as_text()}"
|
||||||
)
|
)
|
||||||
messages.error(request, _("Note form is not valid"))
|
messages.error(request, _("Note form is not valid"))
|
||||||
return redirect(f"{content_type}_detail",dealer_slug=dealer_slug, slug=slug)
|
return redirect(f"{content_type}_detail",dealer_slug=dealer_slug, slug=slug)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required("inventory.change_note", raise_exception=True)
|
@require_http_methods(["POST"])
|
||||||
|
@permission_required("inventory.change_notes", raise_exception=True)
|
||||||
def update_note(request,dealer_slug, pk):
|
def update_note(request,dealer_slug, pk):
|
||||||
note = get_object_or_404(models.Notes, pk=pk)
|
note = get_object_or_404(models.Notes, pk=pk)
|
||||||
lead = get_object_or_404(models.Lead, pk=note.content_object.id)
|
lead = get_object_or_404(models.Lead, pk=note.content_object.id)
|
||||||
@ -9418,13 +9387,12 @@ def update_note(request,dealer_slug, pk):
|
|||||||
note.note = request.POST.get("note")
|
note.note = request.POST.get("note")
|
||||||
note.save()
|
note.save()
|
||||||
messages.success(request, _("Note updated successfully"))
|
messages.success(request, _("Note updated successfully"))
|
||||||
return redirect("lead_detail",dealer_slug=dealer_slug, slug=lead.slug)
|
# else:
|
||||||
else:
|
# messages.error(request, _("Note form is not valid"))
|
||||||
messages.error(request, _("Note form is not valid"))
|
# notes = models.Notes.objects.filter(
|
||||||
notes = models.Notes.objects.filter(
|
# content_type__model="lead", object_id=lead.pk, dealer=dealer
|
||||||
content_type__model="lead", object_id=lead.id, dealer=dealer
|
# )
|
||||||
)
|
return redirect(request.META.get("HTTP_REFERER", "/"))
|
||||||
return render(request, "crm/leads/lead_detail.html", {"lead": lead, "notes": notes})
|
|
||||||
|
|
||||||
|
|
||||||
# Admin Management
|
# Admin Management
|
||||||
@ -9841,22 +9809,49 @@ def inventory_items_filter(request, dealer_slug):
|
|||||||
return render(request, "purchase_orders/car_inventory_item_form.html", context)
|
return render(request, "purchase_orders/car_inventory_item_form.html", context)
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderDetailView(PurchaseOrderModelDetailViewBase):
|
class PurchaseOrderDetailView(LoginRequiredMixin,PermissionRequiredMixin, DetailView):
|
||||||
|
slug_url_kwarg = 'po_pk'
|
||||||
|
slug_field = 'uuid'
|
||||||
|
context_object_name = 'po_model'
|
||||||
|
extra_context = {
|
||||||
|
'header_subtitle_icon': 'uil:bill',
|
||||||
|
'hide_menu': True
|
||||||
|
}
|
||||||
template_name = "purchase_orders/po_detail.html"
|
template_name = "purchase_orders/po_detail.html"
|
||||||
context_object_name = "po_model"
|
permission_required = ["django_ledger.view_purchaseordermodel"]
|
||||||
permission_required = ["django_ledger.change_purchaseordermodel"]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
||||||
self.queryset = dealer.entity.get_purchase_orders().select_related("entity", "ce_model")
|
self.queryset = dealer.entity.get_purchase_orders().select_related("entity", "ce_model")
|
||||||
return super().get_queryset()
|
return super().get_queryset()
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
|
def get_context_data(self, *, object_list=None, **kwargs):
|
||||||
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
# context = super().get_context_data(object_list=object_list, **kwargs)
|
||||||
context["entity_slug"] = dealer.entity.slug
|
context["entity_slug"] = dealer.entity.slug
|
||||||
|
po_model: PurchaseOrderModel = self.object
|
||||||
|
title = f'Purchase Order {po_model.po_number}'
|
||||||
|
context['page_title'] = title
|
||||||
|
context['header_title'] = title
|
||||||
|
|
||||||
|
po_model: PurchaseOrderModel = self.object
|
||||||
|
po_items_qs, item_data = po_model.get_itemtxs_data(
|
||||||
|
queryset=po_model.itemtransactionmodel_set.all().select_related('item_model', 'bill_model')
|
||||||
|
)
|
||||||
|
context['po_items'] = po_items_qs
|
||||||
|
context['po_total_amount'] = sum(
|
||||||
|
i['po_total_amount'] for i in po_items_qs.values(
|
||||||
|
'po_total_amount', 'po_item_status') if i['po_item_status'] != 'cancelled')
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
# def get_context_data(self, **kwargs):
|
||||||
|
# dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
||||||
|
# context = super().get_context_data(**kwargs)
|
||||||
|
# context["entity_slug"] = dealer.entity.slug
|
||||||
|
# return context
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||||
model = PurchaseOrderModel
|
model = PurchaseOrderModel
|
||||||
@ -9943,200 +9938,11 @@ class PurchaseOrderListView(LoginRequiredMixin, PermissionRequiredMixin, ListVie
|
|||||||
context["entity_slug"] = dealer.entity.slug
|
context["entity_slug"] = dealer.entity.slug
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderUpdateView(PurchaseOrderModelUpdateViewBase):
|
class PurchaseOrderUpdateView(PurchaseOrderModelUpdateViewBase):
|
||||||
template_name = "purchase_orders/po_update.html"
|
pass
|
||||||
context_object_name = "po_model"
|
|
||||||
permission_required = ["django_ledger.change_purchaseordermodel"]
|
|
||||||
|
|
||||||
def get_context_data(self, itemtxs_formset=None, **kwargs):
|
|
||||||
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
context["entity_slug"] = dealer.entity.slug
|
|
||||||
po_model: PurchaseOrderModel = self.object
|
|
||||||
if not itemtxs_formset:
|
|
||||||
itemtxs_qs = self.get_po_itemtxs_qs(po_model)
|
|
||||||
itemtxs_qs, itemtxs_agg = po_model.get_itemtxs_data(queryset=itemtxs_qs)
|
|
||||||
po_itemtxs_formset_class = get_po_itemtxs_formset_class(po_model)
|
|
||||||
itemtxs_formset = po_itemtxs_formset_class(
|
|
||||||
entity_slug=dealer.entity.slug,
|
|
||||||
user_model=dealer.entity.admin,
|
|
||||||
po_model=po_model,
|
|
||||||
queryset=itemtxs_qs,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
itemtxs_qs, itemtxs_agg = po_model.get_itemtxs_data()
|
|
||||||
|
|
||||||
context["itemtxs_qs"] = itemtxs_qs
|
|
||||||
context["itemtxs_formset"] = itemtxs_formset
|
|
||||||
return context
|
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
return reverse(
|
|
||||||
"purchase_order_update",
|
|
||||||
kwargs={
|
|
||||||
"dealer_slug": self.kwargs["dealer_slug"],
|
|
||||||
"entity_slug": self.kwargs["entity_slug"],
|
|
||||||
"po_pk": self.kwargs["po_pk"],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def get(self, request, dealer_slug, entity_slug, po_pk, *args, **kwargs):
|
|
||||||
if self.action_update_items:
|
|
||||||
return HttpResponseRedirect(
|
|
||||||
redirect_to=reverse(
|
|
||||||
"purchase_order_update",
|
|
||||||
kwargs={
|
|
||||||
"dealer_slug": dealer_slug,
|
|
||||||
"entity_slug": entity_slug,
|
|
||||||
"po_pk": po_pk,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return super(PurchaseOrderUpdateView, self).get(
|
|
||||||
request, dealer_slug, entity_slug, po_pk, *args, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
def post(self, request, dealer_slug, entity_slug, *args, **kwargs):
|
|
||||||
if self.action_update_items:
|
|
||||||
if not request.user.is_authenticated:
|
|
||||||
return HttpResponseForbidden()
|
|
||||||
queryset = self.get_queryset()
|
|
||||||
po_model: PurchaseOrderModel = self.get_object(queryset=queryset)
|
|
||||||
self.object = po_model
|
|
||||||
po_itemtxs_formset_class = get_po_itemtxs_formset_class(po_model)
|
|
||||||
itemtxs_formset = po_itemtxs_formset_class(
|
|
||||||
request.POST,
|
|
||||||
user_model=request.dealer.entity.admin,
|
|
||||||
po_model=po_model,
|
|
||||||
entity_slug=entity_slug,
|
|
||||||
)
|
|
||||||
|
|
||||||
if itemtxs_formset.has_changed():
|
|
||||||
if itemtxs_formset.is_valid():
|
|
||||||
itemtxs_list = itemtxs_formset.save(commit=False)
|
|
||||||
create_bill_uuids = [
|
|
||||||
str(i["uuid"].uuid)
|
|
||||||
for i in itemtxs_formset.cleaned_data
|
|
||||||
if i and i["create_bill"] is True
|
|
||||||
]
|
|
||||||
|
|
||||||
if create_bill_uuids:
|
|
||||||
item_uuids = ",".join(create_bill_uuids)
|
|
||||||
redirect_url = reverse(
|
|
||||||
"bill-create-po",
|
|
||||||
kwargs={
|
|
||||||
"dealer_slug": self.kwargs["dealer_slug"],
|
|
||||||
"entity_slug": self.kwargs["entity_slug"],
|
|
||||||
"po_pk": po_model.uuid,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
redirect_url += f"?item_uuids={item_uuids}"
|
|
||||||
return HttpResponseRedirect(redirect_url)
|
|
||||||
|
|
||||||
for itemtxs in itemtxs_list:
|
|
||||||
if not itemtxs.po_model_id:
|
|
||||||
itemtxs.po_model_id = po_model.uuid
|
|
||||||
itemtxs.clean()
|
|
||||||
|
|
||||||
itemtxs_list = itemtxs_formset.save()
|
|
||||||
po_model.update_state()
|
|
||||||
po_model.clean()
|
|
||||||
po_model.save(
|
|
||||||
update_fields=["po_amount", "po_amount_received", "updated"]
|
|
||||||
)
|
|
||||||
# if valid get saved formset from DB
|
|
||||||
messages.add_message(
|
|
||||||
request, messages.SUCCESS, "PO items updated successfully."
|
|
||||||
)
|
|
||||||
return self.render_to_response(context=self.get_context_data())
|
|
||||||
# if not valid, return formset with errors...
|
|
||||||
return self.render_to_response(
|
|
||||||
context=self.get_context_data(itemtxs_formset=itemtxs_formset)
|
|
||||||
)
|
|
||||||
return super(PurchaseOrderUpdateView, self).post(
|
|
||||||
request, dealer_slug, entity_slug, *args, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_form(self, form_class=None):
|
|
||||||
po_model: PurchaseOrderModel = self.object
|
|
||||||
if po_model.is_draft():
|
|
||||||
return DraftPurchaseOrderModelUpdateForm(
|
|
||||||
entity_slug=self.kwargs["entity_slug"],
|
|
||||||
user_model=self.request.user,
|
|
||||||
**self.get_form_kwargs(),
|
|
||||||
)
|
|
||||||
elif po_model.is_review():
|
|
||||||
return ReviewPurchaseOrderModelUpdateForm(
|
|
||||||
entity_slug=self.kwargs["entity_slug"],
|
|
||||||
user_model=self.request.user,
|
|
||||||
**self.get_form_kwargs(),
|
|
||||||
)
|
|
||||||
elif po_model.is_approved():
|
|
||||||
return ApprovedPurchaseOrderModelUpdateForm(
|
|
||||||
entity_slug=self.kwargs["entity_slug"],
|
|
||||||
user_model=self.request.user,
|
|
||||||
**self.get_form_kwargs(),
|
|
||||||
)
|
|
||||||
return BasePurchaseOrderModelUpdateForm(
|
|
||||||
entity_slug=self.kwargs["entity_slug"],
|
|
||||||
user_model=self.request.user,
|
|
||||||
**self.get_form_kwargs(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BasePurchaseOrderActionActionView(BasePurchaseOrderActionActionViewBase):
|
class BasePurchaseOrderActionActionView(BasePurchaseOrderActionActionViewBase):
|
||||||
def get_redirect_url(self, dealer_slug, entity_slug, po_pk, *args, **kwargs):
|
permission_required = "django_ledger.change_purchaseordermodel"
|
||||||
return reverse(
|
|
||||||
"purchase_order_update",
|
|
||||||
kwargs={
|
|
||||||
"dealer_slug": dealer_slug,
|
|
||||||
"entity_slug": entity_slug,
|
|
||||||
"po_pk": po_pk,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def get(self, request, dealer_slug, entity_slug, po_pk, *args, **kwargs):
|
|
||||||
# kwargs["user_model"] = self.request.user
|
|
||||||
# Get user information for logging
|
|
||||||
user_username = request.user.username if request.user.is_authenticated else 'anonymous'
|
|
||||||
|
|
||||||
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
|
||||||
kwargs["user_model"] = dealer.entity.admin
|
|
||||||
if not self.action_name:
|
|
||||||
raise ImproperlyConfigured("View attribute action_name is required.")
|
|
||||||
response = super(BasePurchaseOrderActionActionView, self).get(
|
|
||||||
request, dealer_slug, entity_slug, po_pk, *args, **kwargs
|
|
||||||
)
|
|
||||||
po_model: PurchaseOrderModel = self.get_object()
|
|
||||||
|
|
||||||
# Log the attempt to perform the action
|
|
||||||
logger.debug(
|
|
||||||
f"User {user_username} attempting to call action '{self.action_name}' "
|
|
||||||
f"on Purchase Order ID: {po_model.pk} (Entity: {entity_slug})."
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
getattr(po_model, self.action_name)(commit=self.commit, **kwargs)
|
|
||||||
# --- Single-line log for successful action ---
|
|
||||||
logger.info(
|
|
||||||
f"User {user_username} successfully executed action '{self.action_name}' "
|
|
||||||
f"on Purchase Order ID: {po_model.pk}."
|
|
||||||
)
|
|
||||||
messages.add_message(
|
|
||||||
request,
|
|
||||||
message="PO updated successfully.",
|
|
||||||
level=messages.SUCCESS,
|
|
||||||
)
|
|
||||||
except ValidationError as e:
|
|
||||||
# --- Single-line log for ValidationError ---
|
|
||||||
logger.warning(
|
|
||||||
f"User {user_username} encountered a validation error "
|
|
||||||
f"while performing action '{self.action_name}' on Purchase Order ID: {po_model.pk}. "
|
|
||||||
f"Error: {e}"
|
|
||||||
)
|
|
||||||
print(e)
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
class PurchaseOrderModelDeleteView(PurchaseOrderModelDeleteViewBase):
|
class PurchaseOrderModelDeleteView(PurchaseOrderModelDeleteViewBase):
|
||||||
template_name = "purchase_orders/po_delete.html"
|
template_name = "purchase_orders/po_delete.html"
|
||||||
@ -10178,16 +9984,7 @@ class PurchaseOrderMarkAsVoidView(BasePurchaseOrderActionActionView):
|
|||||||
|
|
||||||
##############################bil
|
##############################bil
|
||||||
class BaseBillActionView(BaseBillActionViewBase):
|
class BaseBillActionView(BaseBillActionViewBase):
|
||||||
def get_redirect_url(self, dealer_slug, entity_slug, bill_pk, *args, **kwargs):
|
pass
|
||||||
return reverse(
|
|
||||||
"bill-update",
|
|
||||||
kwargs={
|
|
||||||
"dealer_slug": self.kwargs["dealer_slug"],
|
|
||||||
"entity_slug": entity_slug,
|
|
||||||
"bill_pk": bill_pk,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BillModelActionMarkAsDraftView(BaseBillActionView):
|
class BillModelActionMarkAsDraftView(BaseBillActionView):
|
||||||
action_name = "mark_as_draft"
|
action_name = "mark_as_draft"
|
||||||
@ -10388,6 +10185,9 @@ def upload_cars(request, dealer_slug, pk=None):
|
|||||||
logger.info(
|
logger.info(
|
||||||
f"User {user_username} updated PoItemsUploaded status to 'uploaded' for Item PK: {item.pk}."
|
f"User {user_username} updated PoItemsUploaded status to 'uploaded' for Item PK: {item.pk}."
|
||||||
)
|
)
|
||||||
|
return redirect(
|
||||||
|
"inventory_stats",kwargs={"dealer_slug": dealer_slug}
|
||||||
|
)
|
||||||
# --- Log for successful CSV import and car creation ---
|
# --- Log for successful CSV import and car creation ---
|
||||||
logger.info(
|
logger.info(
|
||||||
f"User {user_username} successfully imported {cars_created} cars "
|
f"User {user_username} successfully imported {cars_created} cars "
|
||||||
|
|||||||
@ -68,7 +68,7 @@
|
|||||||
class="btn btn-sm btn-phoenix-warning me-md-2">
|
class="btn btn-sm btn-phoenix-warning me-md-2">
|
||||||
{% trans 'Update' %}
|
{% trans 'Update' %}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{% if bill.can_pay %}
|
{% if bill.can_pay %}
|
||||||
|
|
||||||
<button onclick="djLedger.toggleModal('{{ bill.get_html_id }}')"
|
<button onclick="djLedger.toggleModal('{{ bill.get_html_id }}')"
|
||||||
|
|||||||
30
templates/components/note_modal.html
Normal file
30
templates/components/note_modal.html
Normal 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>
|
||||||
20
templates/components/schedule_modal.html
Normal file
20
templates/components/schedule_modal.html
Normal 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>
|
||||||
@ -1,5 +1,11 @@
|
|||||||
{% load static i18n crispy_forms_tags %}
|
{% load static i18n crispy_forms_tags %}
|
||||||
<!-- task Modal -->
|
<!-- task Modal -->
|
||||||
|
<style>
|
||||||
|
.completed-task {
|
||||||
|
text-decoration: line-through;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<div class="modal fade" id="taskModal" tabindex="-1" aria-labelledby="taskModalLabel" aria-hidden="true">
|
<div class="modal fade" id="taskModal" tabindex="-1" aria-labelledby="taskModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-md">
|
<div class="modal-dialog modal-md">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
|||||||
@ -7,10 +7,6 @@
|
|||||||
.main-tab li:last-child {
|
.main-tab li:last-child {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
.completed-task {
|
|
||||||
text-decoration: line-through;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
.kanban-header {
|
.kanban-header {
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color:rgb(237, 241, 245);
|
background-color:rgb(237, 241, 245);
|
||||||
@ -109,7 +105,7 @@
|
|||||||
<h5 class="fw-bolder mb-2 text-body-highlight">{{ _("Related Records") }}</h5>
|
<h5 class="fw-bolder mb-2 text-body-highlight">{{ _("Related Records") }}</h5>
|
||||||
<h6 class="fw-bolder mb-2 text-body-highlight">{{ _("Opportunity") }}</h6>
|
<h6 class="fw-bolder mb-2 text-body-highlight">{{ _("Opportunity") }}</h6>
|
||||||
{% if lead.opportunity %}
|
{% if lead.opportunity %}
|
||||||
<a href="{% url 'opportunity_detail' lead.opportunity.slug %}" class="">{{ lead.opportunity }}</a>
|
<a href="{% url 'opportunity_detail' request.dealer.slug lead.opportunity.slug %}" class="">{{ lead.opportunity }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>{{ _("No Opportunity") }}</p>
|
<p>{{ _("No Opportunity") }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -167,14 +163,16 @@
|
|||||||
<div class="kanban-header bg-secondary w-50 text-white fw-bold"><i class="fa-solid fa-circle-info me-2"></i>{{lead.next_action|capfirst}} <br> <small>{% trans "Next Action" %} :</small> <small>{{lead.next_action_date|naturalday|capfirst}}</small></div>
|
<div class="kanban-header bg-secondary w-50 text-white fw-bold"><i class="fa-solid fa-circle-info me-2"></i>{{lead.next_action|capfirst}} <br> <small>{% trans "Next Action" %} :</small> <small>{{lead.next_action_date|naturalday|capfirst}}</small></div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="nav main-tab nav-underline fs-9 deal-details scrollbar flex-nowrap w-100 pb-1 mb-6 justify-content-end mt-5" id="myTab" role="tablist" style="overflow-y: hidden;">
|
<ul class="nav main-tab nav-underline fs-9 deal-details scrollbar flex-nowrap w-100 pb-1 mb-6 justify-content-end mt-5" id="myTab" role="tablist" style="overflow-y: hidden;">
|
||||||
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link active" id="opportunity-tab" data-bs-toggle="tab" href="#tab-opportunity" role="tab" aria-controls="tab-opportunity" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-chart-line me-2 tab-icon-color fs-8"></span>{{ _("Opportunities") }}</a></li>
|
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link active" id="tasks-tab" data-bs-toggle="tab" href="#tab-tasks" role="tab" aria-controls="tab-tasks" aria-selected="true"> <span class="fa-solid fa-envelope me-2 tab-icon-color fs-8"></span>{{ _("Tasks") }}</a></li>
|
||||||
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="activity-tab" data-bs-toggle="tab" href="#tab-activity" role="tab" aria-controls="tab-activity" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-chart-line me-2 tab-icon-color fs-8"></span>{{ _("Activity") }}</a></li>
|
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="notes-tab" data-bs-toggle="tab" href="#tab-notes" role="tab" aria-controls="tab-notes" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-clipboard me-2 tab-icon-color fs-8"></span>{{ _("Notes") }}</a></li>
|
||||||
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="notes-tab" data-bs-toggle="tab" href="#tab-notes" role="tab" aria-controls="tab-notes" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-clipboard me-2 tab-icon-color fs-8"></span>{{ _("Notes") }}</a></li>
|
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="emails-tab" data-bs-toggle="tab" href="#tab-emails" role="tab" aria-controls="tab-emails" aria-selected="true"> <span class="fa-solid fa-envelope me-2 tab-icon-color fs-8"></span>{{ _("Emails") }}</a></li>
|
||||||
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="emails-tab" data-bs-toggle="tab" href="#tab-emails" role="tab" aria-controls="tab-emails" aria-selected="true"> <span class="fa-solid fa-envelope me-2 tab-icon-color fs-8"></span>{{ _("Emails") }}</a></li>
|
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="activity-tab" data-bs-toggle="tab" href="#tab-activity" role="tab" aria-controls="tab-activity" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-chart-line me-2 tab-icon-color fs-8"></span>{{ _("Activity") }}</a></li>
|
||||||
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="tasks-tab" data-bs-toggle="tab" href="#tab-tasks" role="tab" aria-controls="tab-tasks" aria-selected="true"> <span class="fa-solid fa-envelope me-2 tab-icon-color fs-8"></span>{{ _("Tasks") }}</a></li>
|
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="opportunity-tab" data-bs-toggle="tab" href="#tab-opportunity" role="tab" aria-controls="tab-opportunity" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-chart-line me-2 tab-icon-color fs-8"></span>{{ _("Opportunities") }}</a></li>
|
||||||
{% if perms.inventory.change_lead%}
|
{% if perms.inventory.change_lead%}
|
||||||
<li class="nav-item text-nowrap ml-auto" role="presentation">
|
<li class="nav-item text-nowrap ml-auto" role="presentation">
|
||||||
<button class="btn btn-phoenix-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#exampleModal"> <i class="fa-solid fa-user-plus me-2"></i> Reassign Lead</button>
|
{% if perms.inventory.can_reassign_lead %}
|
||||||
|
<button class="btn btn-phoenix-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#exampleModal"> <i class="fa-solid fa-user-plus me-2"></i> Reassign Lead</button>
|
||||||
|
{% endif %}
|
||||||
<button class="btn btn-phoenix-primary btn-sm" onclick="openActionModal('{{ lead.id }}', '{{ lead.action }}', '{{ lead.next_action }}', '{{ lead.next_action_date|date:"Y-m-d\TH:i" }}')">
|
<button class="btn btn-phoenix-primary btn-sm" onclick="openActionModal('{{ lead.id }}', '{{ lead.action }}', '{{ lead.next_action }}', '{{ lead.next_action_date|date:"Y-m-d\TH:i" }}')">
|
||||||
<i class="fa-solid fa-user-plus me-2"></i>
|
<i class="fa-solid fa-user-plus me-2"></i>
|
||||||
{% trans "Update Actions" %}
|
{% trans "Update Actions" %}
|
||||||
@ -221,6 +219,10 @@
|
|||||||
<div class="icon-item icon-item-md rounded-7 border border-translucent">
|
<div class="icon-item icon-item-md rounded-7 border border-translucent">
|
||||||
{% if activity.activity_type == "call" %}
|
{% if activity.activity_type == "call" %}
|
||||||
<span class="fa-solid fa-phone text-warning fs-8"></span>
|
<span class="fa-solid fa-phone text-warning fs-8"></span>
|
||||||
|
{% elif activity.activity_type == "note" %}
|
||||||
|
<span class="fa-regular fa-sticky-note text-info-light fs-8"></span>
|
||||||
|
{% elif activity.activity_type == "task" %}
|
||||||
|
<span class="fa-solid fa-list-check text-success fs-8"></span>
|
||||||
{% elif activity.activity_type == "email" %}
|
{% elif activity.activity_type == "email" %}
|
||||||
<span class="fa-solid fa-envelope text-info-light fs-8"></span>
|
<span class="fa-solid fa-envelope text-info-light fs-8"></span>
|
||||||
{% elif activity.activity_type == "visit" %}
|
{% elif activity.activity_type == "visit" %}
|
||||||
@ -253,7 +255,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane fade active show" id="tab-opportunity" role="tabpanel" aria-labelledby="opportunity-tab">
|
<div class="tab-pane fade" id="tab-opportunity" role="tabpanel" aria-labelledby="opportunity-tab">
|
||||||
<div class="mb-1 d-flex justify-content-between align-items-center">
|
<div class="mb-1 d-flex justify-content-between align-items-center">
|
||||||
<h3 class="mb-4" id="scrollspyTask">{{ _("Opportunities") }} <span class="fw-light fs-7">({{ lead.get_opportunities.count}})</span></h3>
|
<h3 class="mb-4" id="scrollspyTask">{{ _("Opportunities") }} <span class="fw-light fs-7">({{ lead.get_opportunities.count}})</span></h3>
|
||||||
{% if perms.inventory.add_opportunity%}
|
{% if perms.inventory.add_opportunity%}
|
||||||
@ -301,8 +303,8 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="align-middle pe-6 text-uppercase text-start" scope="col" style="width:40%;">{{ _("Note") }}</th>
|
<th class="align-middle pe-6 text-uppercase text-start" scope="col" style="width:40%;">{{ _("Note") }}</th>
|
||||||
<th class="align-middle text-start text-uppercase" scope="col" style="width:20%;">{{ _("Created By")}}</th>
|
<th class="align-middle text-start text-uppercase white-space-nowrap" scope="col" style="width:40%;">{{ _("Created On")}}</th>
|
||||||
<th class="align-middle text-start text-uppercase white-space-nowrap" scope="col" style="width:20%;">{{ _("Created On")}}</th>
|
<th class="align-middle text-start text-uppercase white-space-nowrap" scope="col" style="width:40%;">{{ _("Last Updated")}}</th>
|
||||||
<th class="align-middle pe-0 text-end" scope="col" style="width:10%;"> </th>
|
<th class="align-middle pe-0 text-end" scope="col" style="width:10%;"> </th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -310,12 +312,8 @@
|
|||||||
{% for note in notes %}
|
{% for note in notes %}
|
||||||
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
|
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
|
||||||
<td class="align-middle text-start fw-bold text-body-tertiary ps-1">{{note.note}}</td>
|
<td class="align-middle text-start fw-bold text-body-tertiary ps-1">{{note.note}}</td>
|
||||||
{% if note.created_by.staff %}
|
|
||||||
<td class="align-middle white-space-nowrap text-start white-space-nowrap">{{ note.created_by.staff.name }}</td>
|
|
||||||
{% else %}
|
|
||||||
<td class="align-middle white-space-nowrap text-start white-space-nowrap">{{ note.created_by.dealer.get_local_name|default:note.created_by.dealer.name }}</td>
|
|
||||||
{% endif %}
|
|
||||||
<td class="align-middle text-body-tertiary text-start white-space-nowrap">{{ note.created|naturalday|capfirst }}</td>
|
<td class="align-middle text-body-tertiary text-start white-space-nowrap">{{ note.created|naturalday|capfirst }}</td>
|
||||||
|
<td class="align-middle text-body-tertiary text-start white-space-nowrap">{{ note.updated|naturalday|capfirst }}</td>
|
||||||
<td class="align-middle text-end white-space-nowrap pe-0 action py-2">
|
<td class="align-middle text-end white-space-nowrap pe-0 action py-2">
|
||||||
{% if note.created_by == request.user %}
|
{% if note.created_by == request.user %}
|
||||||
<a id="updateBtn"
|
<a id="updateBtn"
|
||||||
@ -446,7 +444,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="sent align-middle white-space-nowrap text-start fw-bold text-body-tertiary py-2">{{email.from_email}}</td>
|
<td class="sent align-middle white-space-nowrap text-start fw-bold text-body-tertiary py-2">{{email.from_email}}</td>
|
||||||
<td class="date align-middle white-space-nowrap text-body py-2">{{email.created}}</td>
|
<td class="date align-middle white-space-nowrap text-body py-2">{{email.created}}</td>
|
||||||
<td class="align-middle white-space-nowrap ps-3"><a class="text-body" href="{% url 'send_lead_email_with_template' lead.slug email.pk %}"><span class="fa-solid fa-email text-primary me-2"></span>Send</a></td>
|
<td class="align-middle white-space-nowrap ps-3"><a class="text-body" href="{% url 'send_lead_email_with_template' request.dealer.slug lead.slug email.pk %}"><span class="fa-solid fa-email text-primary me-2"></span>Send</a></td>
|
||||||
<td class="status align-middle fw-semibold text-end py-2"><span class="badge badge-phoenix fs-10 badge-phoenix-warning">draft</span></td>
|
<td class="status align-middle fw-semibold text-end py-2"><span class="badge badge-phoenix fs-10 badge-phoenix-warning">draft</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -469,7 +467,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% comment %} {% endcomment %}
|
{% comment %} {% endcomment %}
|
||||||
<div class="tab-pane fade" id="tab-tasks" role="tabpanel" aria-labelledby="tasks-tab">
|
<div class="tab-pane fade active show" id="tab-tasks" role="tabpanel" aria-labelledby="tasks-tab">
|
||||||
<div class="mb-1 d-flex justify-content-between align-items-center">
|
<div class="mb-1 d-flex justify-content-between align-items-center">
|
||||||
<h3 class="mb-0" id="scrollspyEmails">{{ _("Tasks") }}</h3>
|
<h3 class="mb-0" id="scrollspyEmails">{{ _("Tasks") }}</h3>
|
||||||
{% if perms.inventory.change_lead%}
|
{% if perms.inventory.change_lead%}
|
||||||
@ -533,60 +531,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div> {% endcomment %}
|
</div> {% endcomment %}
|
||||||
|
|
||||||
<!-- activity Modal -->
|
|
||||||
{% include "components/activity_modal.html" with content_type="lead" slug=lead.slug %}
|
|
||||||
<!-- task Modal -->
|
<!-- task Modal -->
|
||||||
<div class="modal fade" id="taskModal" tabindex="-1" aria-labelledby="taskModalLabel" aria-hidden="true">
|
{% include "components/task_modal.html" with content_type="lead" slug=lead.slug %}
|
||||||
|
|
||||||
<div class="modal-dialog modal-md">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header justify-content-between align-items-start gap-5 px-4 pt-4 pb-3 border-0">
|
|
||||||
<h4 class="modal-title" id="taskModalLabel">{% trans 'Task' %}</h4>
|
|
||||||
<button class="btn p-0 text-body-quaternary fs-6" data-bs-dismiss="modal" aria-label="Close">
|
|
||||||
<span class="fas fa-times"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<form action="{% url 'add_task' request.dealer.slug 'lead' lead.slug %}" method="post" class="add_task_form">
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ staff_task_form|crispy }}
|
|
||||||
<button type="submit" class="btn btn-phoenix-success w-100">{% trans 'Save' %}</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- note Modal -->
|
<!-- note Modal -->
|
||||||
<div class="modal fade" id="noteModal" tabindex="-1" aria-labelledby="noteModalLabel" aria-hidden="true">
|
{% include "components/note_modal.html" with content_type="lead" slug=lead.slug %}
|
||||||
<div class="modal-dialog modal-md">
|
<!-- schedule Modal -->
|
||||||
<div class="modal-content">
|
{% include "components/schedule_modal.html" with content_type="lead" slug=lead.slug %}
|
||||||
<div class="modal-header justify-content-between align-items-start gap-5 px-4 pt-4 pb-3 border-0">
|
|
||||||
<h4 class="modal-title" id="noteModalLabel">{% trans 'Note' %}</h4>
|
|
||||||
<button class="btn p-0 text-body-quaternary fs-6" data-bs-dismiss="modal" aria-label="Close">
|
|
||||||
<span class="fas fa-times"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<form action="{% url 'add_note' request.dealer.slug 'lead' lead.slug %}" method="post" class="add_note_form">
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ note_form|crispy }}
|
|
||||||
<button type="submit" class="btn btn-phoenix-success w-100">{% trans 'Save' %}</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
{% block customJS %}
|
{% block customJS %}
|
||||||
<script>
|
<script>
|
||||||
function updateNote(e) {
|
|
||||||
let url = e.getAttribute('data-url')
|
|
||||||
let note = e.getAttribute('data-note')
|
|
||||||
document.querySelector('#id_note').value = note
|
|
||||||
let form = document.querySelector('.add_note_form')
|
|
||||||
form.action = url
|
|
||||||
}
|
|
||||||
function reset_form() {
|
function reset_form() {
|
||||||
document.querySelector('#id_note').value = ""
|
document.querySelector('#id_note').value = ""
|
||||||
let form = document.querySelector('.add_note_form')
|
let form = document.querySelector('.add_note_form')
|
||||||
|
|||||||
@ -129,50 +129,7 @@
|
|||||||
<td class="align-middle white-space-nowrap fw-semibold"><a class="text-body-highlight" href="">{{ lead.id_car_make.get_local_name }} - {{ lead.id_car_model.get_local_name }} {{ lead.year }}</a></td>
|
<td class="align-middle white-space-nowrap fw-semibold"><a class="text-body-highlight" href="">{{ lead.id_car_make.get_local_name }} - {{ lead.id_car_model.get_local_name }} {{ lead.year }}</a></td>
|
||||||
<td class="align-middle white-space-nowrap fw-semibold"><a class="text-body-highlight" href="">{{ lead.email }}</a></td>
|
<td class="align-middle white-space-nowrap fw-semibold"><a class="text-body-highlight" href="">{{ lead.email }}</a></td>
|
||||||
<td class="align-middle white-space-nowrap fw-semibold"><a class="text-body-highlight" href="tel:{{ lead.phone_number }}">{{ lead.phone_number }}</a></td>
|
<td class="align-middle white-space-nowrap fw-semibold"><a class="text-body-highlight" href="tel:{{ lead.phone_number }}">{{ lead.phone_number }}</a></td>
|
||||||
<td class="align-middle white-space-nowrap fw-semibold">
|
|
||||||
{% if request.user.staffmember.staff %}
|
|
||||||
<div class="accordion" id="accordionExample">
|
|
||||||
<div class="accordion-item">
|
|
||||||
<h2 class="accordion-header" id="headingTwo">
|
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse{{lead.slug}}" aria-expanded="false" aria-controls="collapseTwo">
|
|
||||||
{{ _("View Schedules")}} ({{lead.get_latest_schedules.count}})
|
|
||||||
</button>
|
|
||||||
</h2>
|
|
||||||
<div class="accordion-collapse collapse" id="collapse{{lead.slug}}" aria-labelledby="headingTwo" data-bs-parent="#accordionExample">
|
|
||||||
<div class="accordion-body pt-0">
|
|
||||||
<div class="d-flex flex-column gap-2">
|
|
||||||
<table><tbody>
|
|
||||||
{% for schedule in lead.get_latest_schedules %}
|
|
||||||
<tr class="schedule-{{ schedule.pk }}">
|
|
||||||
<td class="align-middle white-space-nowrap">
|
|
||||||
{% if schedule.scheduled_type == "call" %}
|
|
||||||
<a href="{% url 'appointment:get_user_appointments' %}">
|
|
||||||
<span class="badge badge-phoenix badge-phoenix-primary text-primary {% if schedule.schedule_past_date %}badge-phoenix-danger text-danger{% endif %} fw-semibold"><span class="text-primary {% if schedule.schedule_past_date %}text-danger{% endif %}" data-feather="phone"></span>
|
|
||||||
{{ schedule.scheduled_at|naturaltime|capfirst }}</span></a>
|
|
||||||
{% elif schedule.scheduled_type == "meeting" %}
|
|
||||||
<a href="{% url 'appointment:get_user_appointments' %}">
|
|
||||||
<span class="badge badge-phoenix badge-phoenix-success text-success fw-semibold"><span class="text-success" data-feather="calendar"></span>
|
|
||||||
{{ schedule.scheduled_at|naturaltime|capfirst }}</span></a>
|
|
||||||
{% elif schedule.scheduled_type == "email" %}
|
|
||||||
<a href="{% url 'appointment:get_user_appointments' %}">
|
|
||||||
<span class="badge badge-phoenix badge-phoenix-warning text-warning fw-semibold"><span class="text-warning" data-feather="email"></span>
|
|
||||||
{{ schedule.scheduled_at|naturaltime|capfirst }}</span></a>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a style="cursor: pointer;" hx-delete="{% url 'schedule_cancel' schedule.pk %}" hx-target=".schedule-{{ schedule.pk }}" hx-confirm="Are you sure you want to cancel this schedule?"><i class="fa-solid fa-ban text-danger"></i></a>
|
|
||||||
</td>
|
|
||||||
{% endfor %}
|
|
||||||
</tr>
|
|
||||||
<tr><td><small><a href="{% url 'appointment:get_user_appointments' %}">View All ...</a></small></td></tr>
|
|
||||||
</tbody></table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">{{ lead.get_status|upper }}</td>
|
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">{{ lead.get_status|upper }}</td>
|
||||||
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">{{ lead.staff|upper }}</td>
|
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">{{ lead.staff|upper }}</td>
|
||||||
{% comment %} <td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">
|
{% comment %} <td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">
|
||||||
@ -216,8 +173,6 @@
|
|||||||
<button class="dropdown-item text-primary" onclick="openActionModal('{{ lead.pk }}', '{{ lead.action }}', '{{ lead.next_action }}', '{{ lead.next_action_date|date:"Y-m-d\TH:i" }}')">
|
<button class="dropdown-item text-primary" onclick="openActionModal('{{ lead.pk }}', '{{ lead.action }}', '{{ lead.next_action }}', '{{ lead.next_action_date|date:"Y-m-d\TH:i" }}')">
|
||||||
{% trans "Update Actions" %}
|
{% trans "Update Actions" %}
|
||||||
</button>
|
</button>
|
||||||
<a href="{% url 'send_lead_email' request.dealer.slug lead.slug %}" class="dropdown-item text-success-dark">{% trans "Send Email" %}</a>
|
|
||||||
<a href="{% url 'schedule_lead' request.dealer.slug lead.slug %}" class="dropdown-item text-success-dark">{% trans "Schedule Event" %}</a>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not lead.opportunity %}
|
{% if not lead.opportunity %}
|
||||||
{% if perms.inventory.add_opportunity%}
|
{% if perms.inventory.add_opportunity%}
|
||||||
@ -257,8 +212,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block customJS %}
|
{% block customJS %}
|
||||||
|
|||||||
@ -93,7 +93,7 @@
|
|||||||
<div class="kanban-column bg-body">
|
<div class="kanban-column bg-body">
|
||||||
<div class="kanban-header opacity-75"><span class="text-body">{{ _("Follow Ups")}} ({{follow_up|length}})</span></div>
|
<div class="kanban-header opacity-75"><span class="text-body">{{ _("Follow Ups")}} ({{follow_up|length}})</span></div>
|
||||||
{% for lead in follow_up %}
|
{% for lead in follow_up %}
|
||||||
<a href="{% url 'lead_detail' lead.slug %}">
|
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
|
||||||
<div class="lead-card">
|
<div class="lead-card">
|
||||||
<strong>{{lead.full_name|capfirst}}</strong><br>
|
<strong>{{lead.full_name|capfirst}}</strong><br>
|
||||||
<small>{{lead.email}}</small><br>
|
<small>{{lead.email}}</small><br>
|
||||||
@ -109,7 +109,7 @@
|
|||||||
<div class="kanban-column bg-body">
|
<div class="kanban-column bg-body">
|
||||||
<div class="kanban-header opacity-75"><span class="text-body">{{ _("Negotiation Ups")}} ({{follow_up|length}})</span></div>
|
<div class="kanban-header opacity-75"><span class="text-body">{{ _("Negotiation Ups")}} ({{follow_up|length}})</span></div>
|
||||||
{% for lead in negotiation %}
|
{% for lead in negotiation %}
|
||||||
<a href="{% url 'lead_detail' lead.slug %}">
|
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
|
||||||
<div class="lead-card">
|
<div class="lead-card">
|
||||||
<strong>{{lead.full_name|capfirst}}</strong><br>
|
<strong>{{lead.full_name|capfirst}}</strong><br>
|
||||||
<small>{{lead.email}}</small><br>
|
<small>{{lead.email}}</small><br>
|
||||||
@ -125,7 +125,7 @@
|
|||||||
<div class="kanban-column bg-body">
|
<div class="kanban-column bg-body">
|
||||||
<div class="kanban-header bg-success-light opacity-75"><span class="text-body">{{ _("Won") }} ({{won|length}}) ({{follow_up|length}})</span></div>
|
<div class="kanban-header bg-success-light opacity-75"><span class="text-body">{{ _("Won") }} ({{won|length}}) ({{follow_up|length}})</span></div>
|
||||||
{% for lead in won %}
|
{% for lead in won %}
|
||||||
<a href="{% url 'lead_detail' lead.slug %}">
|
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
|
||||||
<div class="lead-card">
|
<div class="lead-card">
|
||||||
<strong>{{lead.full_name|capfirst}}</strong><br>
|
<strong>{{lead.full_name|capfirst}}</strong><br>
|
||||||
<small>{{lead.email}}</small><br>
|
<small>{{lead.email}}</small><br>
|
||||||
@ -141,7 +141,7 @@
|
|||||||
<div class="kanban-column bg-body">
|
<div class="kanban-column bg-body">
|
||||||
<div class="kanban-header bg-danger-light opacity-75">{{ _("Lost") }} ({{lose|length}})</div>
|
<div class="kanban-header bg-danger-light opacity-75">{{ _("Lost") }} ({{lose|length}})</div>
|
||||||
{% for lead in lose %}
|
{% for lead in lose %}
|
||||||
<a href="{% url 'lead_detail' lead.slug %}">
|
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
|
||||||
<div class="lead-card">
|
<div class="lead-card">
|
||||||
<strong>{{lead.full_name|capfirst}}</strong><br>
|
<strong>{{lead.full_name|capfirst}}</strong><br>
|
||||||
<small>{{lead.email}}</small><br>
|
<small>{{lead.email}}</small><br>
|
||||||
@ -151,7 +151,6 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -333,74 +333,118 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul class="nav nav-underline fs-9 deal-details scrollbar flex-nowrap w-100 pb-1 mb-6" id="myTab" role="tablist" style="overflow-y: hidden;">
|
<ul class="nav nav-underline fs-9 deal-details scrollbar flex-nowrap w-100 pb-1 mb-6" id="myTab" role="tablist" style="overflow-y: hidden;">
|
||||||
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link active" id="activity-tab" data-bs-toggle="tab" href="#tab-activity" role="tab" aria-controls="tab-activity" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-chart-line me-2 tab-icon-color"></span>{{ _("Activity") }}</a></li>
|
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link active" id="tasks-tab" data-bs-toggle="tab" href="#tab-tasks" role="tab" aria-controls="tab-tasks" aria-selected="true"> <span class="fa-solid fa-envelope me-2 tab-icon-color fs-8"></span>{{ _("Tasks") }}</a></li>
|
||||||
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="notes-tab" data-bs-toggle="tab" href="#tab-notes" role="tab" aria-controls="tab-notes" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-clipboard me-2 tab-icon-color"></span>{{ _("Notes") }}</a></li>
|
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="notes-tab" data-bs-toggle="tab" href="#tab-notes" role="tab" aria-controls="tab-notes" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-clipboard me-2 tab-icon-color"></span>{{ _("Notes") }}</a></li>
|
||||||
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="meeting-tab" data-bs-toggle="tab" href="#tab-meeting" role="tab" aria-controls="tab-meeting" aria-selected="true"> <span class="fa-solid fa-video me-2 tab-icon-color"></span>{{ _("Meetings") }}</a></li>
|
{% comment %} <li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="meeting-tab" data-bs-toggle="tab" href="#tab-meeting" role="tab" aria-controls="tab-meeting" aria-selected="true"> <span class="fa-solid fa-video me-2 tab-icon-color"></span>{{ _("Meetings") }}</a></li> {% endcomment %}
|
||||||
{% comment %} <li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="task-tab" data-bs-toggle="tab" href="#tab-task" role="tab" aria-controls="tab-task" aria-selected="true"> <span class="fa-solid fa-square-check me-2 tab-icon-color"></span>Task</a></li> {% endcomment %}
|
{% comment %} <li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="task-tab" data-bs-toggle="tab" href="#tab-task" role="tab" aria-controls="tab-task" aria-selected="true"> <span class="fa-solid fa-square-check me-2 tab-icon-color"></span>Task</a></li> {% endcomment %}
|
||||||
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="call-tab" data-bs-toggle="tab" href="#tab-call" role="tab" aria-controls="tab-call" aria-selected="true"> <span class="fa-solid fa-phone me-2 tab-icon-color"></span>{{ _("Calls") }}</a></li>
|
{% comment %} <li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="call-tab" data-bs-toggle="tab" href="#tab-call" role="tab" aria-controls="tab-call" aria-selected="true"> <span class="fa-solid fa-phone me-2 tab-icon-color"></span>{{ _("Calls") }}</a></li> {% endcomment %}
|
||||||
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="emails-tab" data-bs-toggle="tab" href="#tab-emails" role="tab" aria-controls="tab-emails" aria-selected="true"> <span class="fa-solid fa-envelope me-2 tab-icon-color"></span>{{ _("Emails")}} </a></li>
|
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="emails-tab" data-bs-toggle="tab" href="#tab-emails" role="tab" aria-controls="tab-emails" aria-selected="true"> <span class="fa-solid fa-envelope me-2 tab-icon-color"></span>{{ _("Emails")}} </a></li>
|
||||||
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="tasks-tab" data-bs-toggle="tab" href="#tab-tasks" role="tab" aria-controls="tab-tasks" aria-selected="true"> <span class="fa-solid fa-envelope me-2 tab-icon-color fs-8"></span>{{ _("Tasks") }}</a></li>
|
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="activity-tab" data-bs-toggle="tab" href="#tab-activity" role="tab" aria-controls="tab-activity" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-chart-line me-2 tab-icon-color"></span>{{ _("Activity") }}</a></li>
|
||||||
|
|
||||||
{% comment %} <li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="attachments-tab" data-bs-toggle="tab" href="#tab-attachments" role="tab" aria-controls="tab-attachments" aria-selected="true"> <span class="fa-solid fa-paperclip me-2 tab-icon-color"></span>Attachments</a></li> {% endcomment %}
|
{% comment %} <li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="attachments-tab" data-bs-toggle="tab" href="#tab-attachments" role="tab" aria-controls="tab-attachments" aria-selected="true"> <span class="fa-solid fa-paperclip me-2 tab-icon-color"></span>Attachments</a></li> {% endcomment %}
|
||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content" id="myTabContent">
|
<div class="tab-content" id="myTabContent">
|
||||||
<div class="tab-pane fade active show" id="tab-activity" role="tabpanel" aria-labelledby="activity-tab">
|
<div class="tab-pane fade active show" id="tab-tasks" role="tabpanel" aria-labelledby="tasks-tab">
|
||||||
<h2 class="mb-4">Activity</h2>
|
<div class="mb-1 d-flex justify-content-between align-items-center">
|
||||||
<div class="row align-items-center g-3 justify-content-between justify-content-start">
|
<h3 class="mb-0" id="scrollspyEmails">{{ _("Tasks") }}</h3>
|
||||||
<div class="col-12 col-sm-auto">
|
{% if perms.inventory.change_opportunity%}
|
||||||
<div class="search-box mb-2 mb-sm-0">
|
<button class="btn btn-phoenix-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#taskModal"><span class="fas fa-plus me-1"></span>{{ _("Add Task") }}</button>
|
||||||
<form class="position-relative">
|
{% endif %}
|
||||||
<input class="form-control search-input search" type="search" placeholder="Search Activity" aria-label="Search" />
|
|
||||||
<span class="fas fa-search search-box-icon"></span>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{%if perms.inventory.change_opportunity%}
|
|
||||||
<div class="col-auto">
|
|
||||||
<button class="btn btn-phoenix-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#activityModal"><span class="fas fa-plus me-1"></span>{{ _("Add Activity") }}</button>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% for activity in opportunity.get_activities %}
|
<div>
|
||||||
<div class="border-bottom border-translucent py-4">
|
|
||||||
<div class="d-flex">
|
<div class="border-top border-bottom border-translucent" id="allEmailsTable" data-list='{"valueNames":["subject","sent","date","source","status"],"page":7,"pagination":true}'>
|
||||||
<div class="d-flex bg-primary-subtle rounded-circle flex-center me-3 bg-primary-subtle" style="width:25px; height:25px">
|
<div class="table-responsive scrollbar mx-n1 px-1">
|
||||||
{% if activity.activity_type == "call" %}
|
<table class="table fs-9 mb-0">
|
||||||
<span class="fa-solid fa-phone text-warning fs-8"></span>
|
<thead>
|
||||||
{% elif activity.activity_type == "email" %}
|
<tr>
|
||||||
<span class="fa-solid fa-envelope text-info-light fs-8"></span>
|
<th class="white-space-nowrap fs-9 align-middle ps-0" style="width:26px;">
|
||||||
{% elif activity.activity_type == "meeting" %}
|
<div class="form-check mb-0 fs-8">
|
||||||
<span class="fa-solid fa-users text-danger fs-8"></span>
|
<input class="form-check-input" type="checkbox" data-bulk-select='{"body":"all-email-table-body"}' />
|
||||||
{% elif activity.activity_type == "whatsapp" %}
|
</div>
|
||||||
<span class="fab fa-whatsapp text-success-dark fs-7"></span>
|
</th>
|
||||||
{% endif %}
|
<th class="sort white-space-nowrap align-middle pe-3 ps-0 text-uppercase" scope="col" data-sort="subject" style="width:31%; min-width:350px">Title</th>
|
||||||
|
<th class="sort align-middle pe-3 text-uppercase" scope="col" data-sort="sent" style="width:15%; min-width:130px">Assigned to</th>
|
||||||
|
<th class="sort align-middle text-start text-uppercase" scope="col" data-sort="date" style="min-width:165px">Due Date</th>
|
||||||
|
<th class="sort align-middle text-start text-uppercase" scope="col" data-sort="date" style="min-width:165px">Completed</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="list" id="all-tasks-table-body">
|
||||||
|
{% for task in opportunity.get_tasks %}
|
||||||
|
{% include "partials/task.html" %}
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="row align-items-center justify-content-between py-2 pe-0 fs-9">
|
||||||
|
<div class="col-auto d-flex">
|
||||||
|
<p class="mb-0 d-none d-sm-block me-3 fw-semibold text-body" data-list-info="data-list-info"></p><a class="fw-semibold" href="" data-list-view="*">View all<span class="fas fa-angle-right ms-1" data-fa-transform="down-1"></span></a><a class="fw-semibold d-none" href="" data-list-view="less">View Less<span class="fas fa-angle-right ms-1" data-fa-transform="down-1"></span></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="col-auto d-flex">
|
||||||
<div class="d-flex justify-content-between flex-column flex-xl-row mb-2 mb-sm-0">
|
<button class="page-link" data-list-pagination="prev"><span class="fas fa-chevron-left"></span></button>
|
||||||
<div class="flex-1 me-2">
|
<ul class="mb-0 pagination"></ul>
|
||||||
<h5 class="text-body-highlight lh-sm"></h5>
|
<button class="page-link pe-0" data-list-pagination="next"><span class="fas fa-chevron-right"></span></button>
|
||||||
<p class="fs-9 mb-0">{{activity.notes}}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex justify-content-between flex-column flex-xl-row mb-2 mb-sm-0">
|
|
||||||
<div class="flex-1 me-2">
|
|
||||||
<h5 class="text-body-highlight lh-sm"></h5>
|
|
||||||
{% if request.user.email == activity.created_by %}
|
|
||||||
<p class="fs-9 mb-0">by <a class="ms-1" href="#!">You</a></p>
|
|
||||||
{% else %}
|
|
||||||
<p class="fs-9 mb-0">by<a class="ms-1" href="#!">{{activity.created_by}}</a></p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="fs-9"><span class="fa-regular fa-calendar-days text-primary me-2"></span><span class="fw-semibold">{{activity.created|naturalday|capfirst}}</span></div>
|
|
||||||
</div>
|
|
||||||
<p class="fs-9 mb-0"></p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane fade" id="tab-notes" role="tabpanel" aria-labelledby="notes-tab">
|
<div class="tab-pane fade" id="tab-notes" role="tabpanel" aria-labelledby="notes-tab">
|
||||||
|
<div class="mb-1 d-flex align-items-center justify-content-between">
|
||||||
|
<h3 class="mb-4" id="scrollspyNotes">{{ _("Notes") }}</h3>
|
||||||
|
{% if perms.inventory.change_lead%}
|
||||||
|
<button class="btn btn-phoenix-primary btn-sm" type="button" onclick="reset_form()" data-bs-toggle="modal" data-bs-target="#noteModal"><span class="fas fa-plus me-1"></span>{{ _("Add Note") }}</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-top border-bottom border-translucent" id="leadDetailsTable">
|
||||||
|
<div class="table-responsive scrollbar mx-n1 px-1">
|
||||||
|
<table class="table fs-9 mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="align-middle pe-6 text-uppercase text-start" scope="col" style="width:40%;">{{ _("Note") }}</th>
|
||||||
|
<th class="align-middle text-start text-uppercase white-space-nowrap" scope="col" style="width:40%;">{{ _("Created On")}}</th>
|
||||||
|
<th class="align-middle text-start text-uppercase white-space-nowrap" scope="col" style="width:40%;">{{ _("Last Updated")}}</th>
|
||||||
|
<th class="align-middle pe-0 text-end" scope="col" style="width:10%;"> </th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody >
|
||||||
|
{% for note in opportunity.get_notes %}
|
||||||
|
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
|
||||||
|
<td class="align-middle text-start fw-bold text-body-tertiary ps-1">{{note.note}}</td>
|
||||||
|
<td class="align-middle text-body-tertiary text-start white-space-nowrap">{{ note.created|naturalday|capfirst }}</td>
|
||||||
|
<td class="align-middle text-body-tertiary text-start white-space-nowrap">{{ note.updated|naturalday|capfirst }}</td>
|
||||||
|
<td class="align-middle text-end white-space-nowrap pe-0 action py-2">
|
||||||
|
{% if note.created_by == request.user %}
|
||||||
|
<a id="updateBtn"
|
||||||
|
href="#"
|
||||||
|
onclick="updateNote(this)"
|
||||||
|
class="btn btn-sm btn-phoenix-primary me-2"
|
||||||
|
data-pk="{{ note.pk }}"
|
||||||
|
data-note="{{ note.note|escapejs }}"
|
||||||
|
data-url="{% url 'update_note' request.dealer.slug note.pk %}"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#noteModal"
|
||||||
|
data-note-title="{{ _('Update') }}<i class='fas fa-pen-square text-primary ms-2'></i>">
|
||||||
|
{{ _("Update") }}
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-phoenix-danger btn-sm delete-btn"
|
||||||
|
data-url="{% url 'delete_note_to_lead' request.dealer.slug note.pk %}"
|
||||||
|
data-message="Are you sure you want to delete this note?"
|
||||||
|
data-bs-toggle="modal" data-bs-target="#deleteModal">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% comment %} <div class="tab-pane fade" id="tab-notes" role="tabpanel" aria-labelledby="notes-tab">
|
||||||
<h2 class="mb-4">Notes</h2>
|
<h2 class="mb-4">Notes</h2>
|
||||||
{%if perms.inventory.change_opportunity%}
|
{%if perms.inventory.change_opportunity%}
|
||||||
<form action="{% url 'add_note_to_opportunity' request.dealer.slug opportunity.slug %}" method="post">
|
<form action="{% url 'add_note_to_opportunity' request.dealer.slug opportunity.slug %}" method="post">
|
||||||
@ -422,8 +466,8 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> {% endcomment %}
|
||||||
<div class="tab-pane fade" id="tab-meeting" role="tabpanel" aria-labelledby="meeting-tab">
|
{% comment %} <div class="tab-pane fade" id="tab-meeting" role="tabpanel" aria-labelledby="meeting-tab">
|
||||||
<h2 class="mb-4">Meeting</h2>
|
<h2 class="mb-4">Meeting</h2>
|
||||||
<div class="row align-items-center g-2 flex-wrap justify-content-start mb-3">
|
<div class="row align-items-center g-2 flex-wrap justify-content-start mb-3">
|
||||||
<div class="col-12 col-sm-auto">
|
<div class="col-12 col-sm-auto">
|
||||||
@ -461,9 +505,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> {% endcomment %}
|
||||||
|
|
||||||
<div class="tab-pane fade" id="tab-call" role="tabpanel" aria-labelledby="call-tab">
|
{% comment %} <div class="tab-pane fade" id="tab-call" role="tabpanel" aria-labelledby="call-tab">
|
||||||
<div class="row align-items-center gx-4 gy-3 flex-wrap mb-3">
|
<div class="row align-items-center gx-4 gy-3 flex-wrap mb-3">
|
||||||
<div class="col-auto d-flex flex-1">
|
<div class="col-auto d-flex flex-1">
|
||||||
<h2 class="mb-0">Call</h2>
|
<h2 class="mb-0">Call</h2>
|
||||||
@ -516,7 +560,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> {% endcomment %}
|
||||||
<div class="tab-pane fade" id="tab-emails" role="tabpanel" aria-labelledby="emails-tab">
|
<div class="tab-pane fade" id="tab-emails" role="tabpanel" aria-labelledby="emails-tab">
|
||||||
<h2 class="mb-4">Emails</h2>
|
<h2 class="mb-4">Emails</h2>
|
||||||
{% if perms.inventory.change_opportunity%}
|
{% if perms.inventory.change_opportunity%}
|
||||||
@ -595,52 +639,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane fade" id="tab-tasks" role="tabpanel" aria-labelledby="tasks-tab">
|
|
||||||
<div class="mb-1 d-flex justify-content-between align-items-center">
|
|
||||||
<h3 class="mb-0" id="scrollspyEmails">{{ _("Tasks") }}</h3>
|
|
||||||
{% if perms.inventory.change_opportunity%}
|
|
||||||
<button class="btn btn-phoenix-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#taskModal"><span class="fas fa-plus me-1"></span>{{ _("Add Task") }}</button>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
|
|
||||||
<div class="border-top border-bottom border-translucent" id="allEmailsTable" data-list='{"valueNames":["subject","sent","date","source","status"],"page":7,"pagination":true}'>
|
|
||||||
<div class="table-responsive scrollbar mx-n1 px-1">
|
|
||||||
<table class="table fs-9 mb-0">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="white-space-nowrap fs-9 align-middle ps-0" style="width:26px;">
|
|
||||||
<div class="form-check mb-0 fs-8">
|
|
||||||
<input class="form-check-input" type="checkbox" data-bulk-select='{"body":"all-email-table-body"}' />
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
<th class="sort white-space-nowrap align-middle pe-3 ps-0 text-uppercase" scope="col" data-sort="subject" style="width:31%; min-width:350px">Title</th>
|
|
||||||
<th class="sort align-middle pe-3 text-uppercase" scope="col" data-sort="sent" style="width:15%; min-width:130px">Assigned to</th>
|
|
||||||
<th class="sort align-middle text-start text-uppercase" scope="col" data-sort="date" style="min-width:165px">Due Date</th>
|
|
||||||
<th class="sort align-middle text-start text-uppercase" scope="col" data-sort="date" style="min-width:165px">Completed</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="list" id="all-tasks-table-body">
|
|
||||||
{% for task in opportunity.get_tasks %}
|
|
||||||
{% include "partials/task.html" %}
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="row align-items-center justify-content-between py-2 pe-0 fs-9">
|
|
||||||
<div class="col-auto d-flex">
|
|
||||||
<p class="mb-0 d-none d-sm-block me-3 fw-semibold text-body" data-list-info="data-list-info"></p><a class="fw-semibold" href="" data-list-view="*">View all<span class="fas fa-angle-right ms-1" data-fa-transform="down-1"></span></a><a class="fw-semibold d-none" href="" data-list-view="less">View Less<span class="fas fa-angle-right ms-1" data-fa-transform="down-1"></span></a>
|
|
||||||
</div>
|
|
||||||
<div class="col-auto d-flex">
|
|
||||||
<button class="page-link" data-list-pagination="prev"><span class="fas fa-chevron-left"></span></button>
|
|
||||||
<ul class="mb-0 pagination"></ul>
|
|
||||||
<button class="page-link pe-0" data-list-pagination="next"><span class="fas fa-chevron-right"></span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="tab-pane fade" id="tab-attachments" role="tabpanel" aria-labelledby="attachments-tab">
|
<div class="tab-pane fade" id="tab-attachments" role="tabpanel" aria-labelledby="attachments-tab">
|
||||||
<h2 class="mb-3">Attachments</h2>
|
<h2 class="mb-3">Attachments</h2>
|
||||||
<div class="border-top border-dashed pt-3 pb-4">
|
<div class="border-top border-dashed pt-3 pb-4">
|
||||||
@ -684,9 +683,73 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tab-pane fade" id="tab-activity" role="tabpanel" aria-labelledby="activity-tab">
|
||||||
|
<h2 class="mb-4">Activity</h2>
|
||||||
|
<div class="row align-items-center g-3 justify-content-between justify-content-start">
|
||||||
|
<div class="col-12 col-sm-auto">
|
||||||
|
<div class="search-box mb-2 mb-sm-0">
|
||||||
|
<form class="position-relative">
|
||||||
|
<input class="form-control search-input search" type="search" placeholder="Search Activity" aria-label="Search" />
|
||||||
|
<span class="fas fa-search search-box-icon"></span>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{%if perms.inventory.change_opportunity%}
|
||||||
|
<div class="col-auto">
|
||||||
|
<button class="btn btn-phoenix-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#activityModal"><span class="fas fa-plus me-1"></span>{{ _("Add Activity") }}</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% for activity in opportunity.get_activities %}
|
||||||
|
<div class="border-bottom border-translucent py-4">
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="d-flex bg-primary-subtle rounded-circle flex-center me-3 bg-primary-subtle" style="width:25px; height:25px">
|
||||||
|
{% if activity.activity_type == "call" %}
|
||||||
|
<span class="fa-solid fa-phone text-warning fs-8"></span>
|
||||||
|
{% elif activity.activity_type == "email" %}
|
||||||
|
<span class="fa-solid fa-envelope text-info-light fs-8"></span>
|
||||||
|
{% elif activity.activity_type == "note" %}
|
||||||
|
<span class="fa-regular fa-sticky-note text-info-light fs-8"></span>
|
||||||
|
{% elif activity.activity_type == "task" %}
|
||||||
|
<span class="fa-solid fa-list-check text-success fs-8"></span>
|
||||||
|
{% elif activity.activity_type == "meeting" %}
|
||||||
|
<span class="fa-solid fa-users text-danger fs-8"></span>
|
||||||
|
{% elif activity.activity_type == "whatsapp" %}
|
||||||
|
<span class="fab fa-whatsapp text-success-dark fs-7"></span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="d-flex justify-content-between flex-column flex-xl-row mb-2 mb-sm-0">
|
||||||
|
<div class="flex-1 me-2">
|
||||||
|
<h5 class="text-body-highlight lh-sm"></h5>
|
||||||
|
<p class="fs-9 mb-0">{{activity.notes}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between flex-column flex-xl-row mb-2 mb-sm-0">
|
||||||
|
<div class="flex-1 me-2">
|
||||||
|
<h5 class="text-body-highlight lh-sm"></h5>
|
||||||
|
{% if request.user.email == activity.created_by %}
|
||||||
|
<p class="fs-9 mb-0">by <a class="ms-1" href="#!">You</a></p>
|
||||||
|
{% else %}
|
||||||
|
<p class="fs-9 mb-0">by<a class="ms-1" href="#!">{{activity.created_by}}</a></p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="fs-9"><span class="fa-regular fa-calendar-days text-primary me-2"></span><span class="fw-semibold">{{activity.created|naturalday|capfirst}}</span></div>
|
||||||
|
</div>
|
||||||
|
<p class="fs-9 mb-0"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include "components/activity_modal.html" with content_type="opportunity" slug=opportunity.slug %}
|
|
||||||
{% include "components/task_modal.html" with content_type="opportunity" slug=opportunity.slug %}
|
|
||||||
|
<!-- task Modal -->
|
||||||
|
{% include "components/task_modal.html" with content_type="opportunity" slug=opportunity.slug %}
|
||||||
|
<!-- note Modal -->
|
||||||
|
{% include "components/note_modal.html" with content_type="opportunity" slug=opportunity.slug %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -56,6 +56,16 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if perms.django_ledger.view_purchaseordermodel %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{% url 'purchase_order_list' request.dealer.slug request.dealer.entity.slug %}">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span class="nav-link-icon"><span class="fas fa-warehouse"></span></span><span class="nav-link-text">{% trans "purchase Orders"|capfirst %}</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -183,16 +193,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.inventory.view_payment %}
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="{% url 'payment_list' request.dealer.slug %}">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<span class="nav-link-icon"><span class="fas fa-money-check"></span></span><span class="nav-link-text">{% trans "payments"|capfirst %}</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -275,15 +275,14 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if perms.inventory.view_journalentrymodel %}
|
||||||
{% if perms.django_ledger.view_purchaseordermodel %}
|
<li class="nav-item">
|
||||||
<li class="nav-item">
|
<a class="nav-link" href="{% url 'payment_list' request.dealer.slug %}">
|
||||||
<a class="nav-link" href="{% url 'purchase_order_list' request.dealer.slug request.dealer.entity.slug %}">
|
<div class="d-flex align-items-center">
|
||||||
<div class="d-flex align-items-center">
|
<span class="nav-link-icon"><span class="fas fa-money-check"></span></span><span class="nav-link-text">{% trans "payments"|capfirst %}</span>
|
||||||
<span class="nav-link-icon"><span class="fas fa-warehouse"></span></span><span class="nav-link-text">{% trans "purchase Orders"|capfirst %}</span>
|
</div>
|
||||||
</div>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
<th style="min-width: 600px;" class="d-flex justify-content-between align-items-center">
|
<th style="min-width: 600px;" class="d-flex justify-content-between align-items-center">
|
||||||
{% trans 'Item' %}
|
{% trans 'Item' %}
|
||||||
{% if po_model.is_draft %}
|
{% if po_model.is_draft %}
|
||||||
|
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-sm btn-phoenix-success"
|
class="btn btn-sm btn-phoenix-success"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
@ -66,24 +66,21 @@
|
|||||||
<span class="currency">{{CURRENCY}}</span>{{ f.instance.po_total_amount | currency_format }}</td>
|
<span class="currency">{{CURRENCY}}</span>{{ f.instance.po_total_amount | currency_format }}</td>
|
||||||
<td>{{ f.po_item_status|add_class:"form-control" }}</td>
|
<td>{{ f.po_item_status|add_class:"form-control" }}</td>
|
||||||
{% if itemtxs_formset.can_delete %}
|
{% if itemtxs_formset.can_delete %}
|
||||||
|
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
{{ f.DELETE|add_class:"form-check-input" }}
|
{{ f.DELETE|add_class:"form-check-input" }}
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
{% if f.instance.can_create_bill %}
|
{% if f.instance.can_create_bill and can_add_bill %}
|
||||||
{% if perms.djagno_ledger.add_billmodel%}
|
|
||||||
{{ f.create_bill|add_class:"form-check-input" }}
|
{{ f.create_bill|add_class:"form-check-input" }}
|
||||||
{% endif %}
|
{% elif f.instance.bill_model and can_view_bill %}
|
||||||
{% elif f.instance.bill_model %}
|
|
||||||
{% if perms.djagno_ledger.view_billmodel%}
|
|
||||||
<a class="btn btn-sm btn-phoenix-secondary"
|
<a class="btn btn-sm btn-phoenix-secondary"
|
||||||
href="{% url 'bill-detail' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=f.instance.bill_model_id %}">
|
href="{% url 'bill-detail' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=f.instance.bill_model_id %}">
|
||||||
{% trans 'View Bill' %}
|
{% trans 'View Bill' %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
{% if f.instance.bill_model %}
|
{% if f.instance.bill_model %}
|
||||||
|
|||||||
@ -83,7 +83,7 @@
|
|||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% po_item_formset_table po_model itemtxs_formset %}
|
{% po_item_formset_table po_model itemtxs_formset request.user %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user