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