Compare commits
6 Commits
23a67d22a0
...
669e6b445d
| Author | SHA1 | Date | |
|---|---|---|---|
| 669e6b445d | |||
| 62356a997e | |||
| b23e84331f | |||
| 4bfb533448 | |||
| b33d928bcc | |||
| 25d17efa11 |
@ -12,21 +12,21 @@ class Command(BaseCommand):
|
||||
Service.objects.create(
|
||||
name="call",
|
||||
price=0,
|
||||
duration=datetime.timedelta(minutes=10),
|
||||
duration=datetime.timedelta(minutes=60),
|
||||
currency="SAR",
|
||||
description="15 min call",
|
||||
)
|
||||
Service.objects.create(
|
||||
name="meeting",
|
||||
price=0,
|
||||
duration=datetime.timedelta(minutes=30),
|
||||
duration=datetime.timedelta(minutes=90),
|
||||
currency="SAR",
|
||||
description="30 min meeting",
|
||||
)
|
||||
Service.objects.create(
|
||||
name="email",
|
||||
price=0,
|
||||
duration=datetime.timedelta(minutes=30),
|
||||
duration=datetime.timedelta(minutes=90),
|
||||
currency="SAR",
|
||||
description="30 min visit",
|
||||
)
|
||||
|
||||
@ -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))
|
||||
@ -123,13 +123,18 @@ class DealerSlugMiddleware:
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
def process_view(self, request, view_func, view_args, view_kwargs):
|
||||
if request.path_info.startswith('/en/signup/') or \
|
||||
if request.path_info.startswith('/ar/signup/') or \
|
||||
request.path_info.startswith('/en/signup/') or \
|
||||
request.path_info.startswith('/ar/login/') or \
|
||||
request.path_info.startswith('/en/login/') or \
|
||||
request.path_info.startswith('/ar/logout/') or \
|
||||
request.path_info.startswith('/en/logout/') or \
|
||||
request.path_info.startswith('/en/ledger/') or \
|
||||
request.path_info.startswith('/ar/ledger/') or \
|
||||
request.path_info.startswith('/en/notifications/') or \
|
||||
request.path_info.startswith('/ar/notifications/'):
|
||||
request.path_info.startswith('/ar/notifications/') or \
|
||||
request.path_info.startswith('/en/appointment/') or \
|
||||
request.path_info.startswith('/ar/appointment/'):
|
||||
return None
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
|
||||
@ -1836,7 +1836,10 @@ class Schedule(models.Model):
|
||||
("completed", _("Completed")),
|
||||
("canceled", _("Canceled")),
|
||||
]
|
||||
lead = models.ForeignKey(Lead, on_delete=models.CASCADE, related_name="schedules")
|
||||
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE)
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||
object_id = models.PositiveIntegerField()
|
||||
content_object = GenericForeignKey('content_type', 'object_id')
|
||||
customer = models.ForeignKey(
|
||||
CustomerModel,
|
||||
on_delete=models.CASCADE,
|
||||
@ -1850,6 +1853,7 @@ class Schedule(models.Model):
|
||||
scheduled_type = models.CharField(
|
||||
max_length=200, choices=ScheduledType, default="Call"
|
||||
)
|
||||
completed = models.BooleanField(default=False, verbose_name=_("Completed"))
|
||||
duration = models.DurationField(default=timedelta(minutes=5))
|
||||
notes = models.TextField(blank=True, null=True)
|
||||
status = models.CharField(
|
||||
@ -1859,7 +1863,7 @@ class Schedule(models.Model):
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"Scheduled {self.purpose} with {self.lead.full_name} on {self.scheduled_at}"
|
||||
return f"Scheduled {self.purpose} on {self.scheduled_at}"
|
||||
|
||||
@property
|
||||
def schedule_past_date(self):
|
||||
@ -2634,6 +2638,7 @@ class CustomGroup(models.Model):
|
||||
"notes",
|
||||
"tasks",
|
||||
"activity",
|
||||
"poitemsuploaded"
|
||||
],
|
||||
)
|
||||
self.set_permissions(
|
||||
@ -2641,7 +2646,7 @@ class CustomGroup(models.Model):
|
||||
allowed_models=[],
|
||||
other_perms=[
|
||||
"view_purchaseordermodel",
|
||||
"can_view_financials",
|
||||
|
||||
]
|
||||
)
|
||||
######################################
|
||||
@ -2652,7 +2657,7 @@ class CustomGroup(models.Model):
|
||||
elif self.name == "Sales":
|
||||
self.set_permissions(
|
||||
app="django_ledger",
|
||||
allowed_models=["estimatemodel", "invoicemodel", "customermodel"],
|
||||
allowed_models=["invoicemodel", "customermodel"],
|
||||
)
|
||||
self.set_permissions(
|
||||
app="inventory",
|
||||
@ -2662,11 +2667,13 @@ class CustomGroup(models.Model):
|
||||
"staff",
|
||||
"schedule",
|
||||
"activity",
|
||||
"lead",
|
||||
"opportunity",
|
||||
"customer",
|
||||
"organization",
|
||||
"notes",
|
||||
"taska",
|
||||
"tasks",
|
||||
"lead"
|
||||
"activity",
|
||||
],
|
||||
other_perms=[
|
||||
@ -2678,6 +2685,10 @@ class CustomGroup(models.Model):
|
||||
"can_view_inventory",
|
||||
"can_view_sales",
|
||||
"can_view_crm",
|
||||
"view_estimatemodel",
|
||||
"add_estimatemodel",
|
||||
"change_estimatemodel",
|
||||
"delete_estimatemodel",
|
||||
],
|
||||
)
|
||||
######################################
|
||||
@ -2709,19 +2720,18 @@ class CustomGroup(models.Model):
|
||||
"bankaccountmodel",
|
||||
"accountmodel",
|
||||
"chartofaccountmodel",
|
||||
"billmodel",
|
||||
"itemmodel",
|
||||
"invoicemodel",
|
||||
"vendormodel",
|
||||
|
||||
"journalentrymodel",
|
||||
"purchaseordermodel",
|
||||
"estimatemodel",
|
||||
"customermodel",
|
||||
"vendormodel",
|
||||
"ledgermodel",
|
||||
"transactionmodel"
|
||||
],
|
||||
other_perms=["view_customermodel", "view_estimatemodel","can_view_inventory","can_view_sales","can_view_crm","can_view_financials","can_view_reports"],
|
||||
other_perms=["view_billmodel","add_billmodel","change_billmodel","delete_billmodel","view_customermodel", "view_estimatemodel","can_view_inventory","can_view_sales","can_view_crm","can_view_financials","can_view_reports"],
|
||||
)
|
||||
|
||||
|
||||
@ -2917,6 +2927,8 @@ class PoItemsUploaded(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def get_name(self):
|
||||
return self.item.item.name.split('||')
|
||||
class ExtraInfo(models.Model):
|
||||
"""
|
||||
Stores additional information for any model with:
|
||||
|
||||
644
inventory/override.py
Normal file
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
|
||||
@ -949,12 +950,12 @@ def create_po_item_upload(sender,instance,created,**kwargs):
|
||||
def create_po_fulfilled_notification(sender,instance,created,**kwargs):
|
||||
if instance.po_status == "fulfilled":
|
||||
dealer = models.Dealer.objects.get(entity=instance.entity)
|
||||
accountants = models.CustomGroup.objects.filter(dealer=dealer,name="Inventory").first().group.user_set.exclude(email=dealer.user.email)
|
||||
accountants = models.CustomGroup.objects.filter(dealer=dealer,name="Inventory").first().group.user_set.exclude(email=dealer.user.email).distinct()
|
||||
for accountant in accountants:
|
||||
models.Notification.objects.create(
|
||||
user=accountant,
|
||||
message=f"""
|
||||
New Purchase Order {instance.po_number} has been added to dealer {instance.dealer.name}.
|
||||
New Purchase Order {instance.po_number} has been added to dealer {dealer.name}.
|
||||
<a href="{instance.get_absolute_url()}" target="_blank">View</a>
|
||||
""",
|
||||
)
|
||||
@ -962,7 +963,11 @@ def create_po_fulfilled_notification(sender,instance,created,**kwargs):
|
||||
@receiver(post_save, sender=models.Car)
|
||||
def car_created_notification(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
accountants = models.CustomGroup.objects.filter(dealer=instance.dealer,name__in=["Manager","Accountant"]).first().group.user_set.all()
|
||||
<<<<<<< HEAD
|
||||
accountants = models.CustomGroup.objects.filter(dealer=instance.dealer,name__in=["Manager","Accountant","Inventory"]).first().group.user_set.all().distinct()
|
||||
=======
|
||||
accountants = models.CustomGroup.objects.filter(dealer=instance.dealer,name__in=["Manager","Accountant"]).first().group.user_set.all().distinct()
|
||||
>>>>>>> b23e84331fbb3a8ce0814a04b68cd3380b93d3a2
|
||||
for accountant in accountants:
|
||||
models.Notification.objects.create(
|
||||
user=accountant,
|
||||
@ -971,11 +976,17 @@ def car_created_notification(sender, instance, created, **kwargs):
|
||||
<a href="{instance.get_absolute_url()}" target="_blank">View</a>
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@receiver(post_save, sender=PurchaseOrderModel)
|
||||
def po_fullfilled_notification(sender, instance, created, **kwargs):
|
||||
if instance.is_fulfilled():
|
||||
dealer = models.Dealer.objects.get(entity=instance.entity)
|
||||
recipients = models.CustomGroup.objects.filter(dealer=instance.dealer,name="Accountant").first().group.user_set.all()
|
||||
recipients = User.objects.filter(
|
||||
groups__customgroup__dealer=instance.dealer,
|
||||
groups__customgroup__name__in=["Manager", "Inventory"]
|
||||
).distinct()
|
||||
for recipient in recipients:
|
||||
models.Notification.objects.create(
|
||||
user=recipient,
|
||||
@ -987,21 +998,23 @@ def po_fullfilled_notification(sender, instance, created, **kwargs):
|
||||
@receiver(post_save, sender=models.Vendor)
|
||||
def vendor_created_notification(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
recipients = models.CustomGroup.objects.filter(dealer=instance.dealer,name="Inventory").first().group.user_set.all()
|
||||
recipients = User.objects.filter(
|
||||
groups__customgroup__dealer=instance.dealer,
|
||||
groups__customgroup__name__in=["Manager", "Inventory"]
|
||||
).distinct()
|
||||
|
||||
for recipient in recipients:
|
||||
models.Notification.objects.create(
|
||||
user=recipient,
|
||||
message=f"""
|
||||
New Vendor {instance.name} has been added to dealer {instance.dealer.name}.
|
||||
<a href="{instance.get_absolute_url()}" target="_blank">View</a>
|
||||
""",
|
||||
)
|
||||
|
||||
@receiver(post_save, sender=models.SaleOrder)
|
||||
def sale_order_created_notification(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
recipients = models.CustomGroup.objects.filter(dealer=instance.dealer,name="Accountant").first().group.user_set.exclude(email=instance.dealer.user.email)
|
||||
recipients = models.CustomGroup.objects.filter(dealer=instance.dealer,name="Accountant").first().group.user_set.exclude(email=instance.dealer.user.email).distinct()
|
||||
|
||||
for recipient in recipients:
|
||||
models.Notification.objects.create(
|
||||
@ -1026,7 +1039,7 @@ def lead_created_notification(sender, instance, created, **kwargs):
|
||||
def estimate_in_review_notification(sender, instance, created, **kwargs):
|
||||
if instance.is_review():
|
||||
dealer = models.Dealer.objects.get(entity=instance.entity)
|
||||
recipients = models.CustomGroup.objects.filter(dealer=dealer,name="Manager").first().group.user_set.exclude(email=dealer.user.email)
|
||||
recipients = models.CustomGroup.objects.filter(dealer=dealer,name="Manager").first().group.user_set.exclude(email=dealer.user.email).distinct()
|
||||
for recipient in recipients:
|
||||
models.Notification.objects.create(
|
||||
user=recipient,
|
||||
@ -1055,19 +1068,33 @@ def estimate_in_approve_notification(sender, instance, created, **kwargs):
|
||||
"""
|
||||
)
|
||||
|
||||
@receiver(post_save, sender=BillModel)
|
||||
def bill_model_in_approve_notification(sender, instance, created, **kwargs):
|
||||
if instance.is_review():
|
||||
dealer = models.Dealer.objects.get(entity=instance.ledger.entity)
|
||||
recipients = models.CustomGroup.objects.filter(dealer=dealer,name="Manager").first().group.user_set.exclude(email=dealer.user.email).distinct()
|
||||
|
||||
# @receiver(post_save, sender=models.Lead)
|
||||
# def lead_created_notification(sender, instance, created, **kwargs):
|
||||
# if created:
|
||||
# models.Notification.objects.create(
|
||||
# user=instance.staff.user,
|
||||
# message=f"""
|
||||
# New Lead has been added.
|
||||
# <a href="{reverse('lead_detail',kwargs={'dealer_slug':instance.dealer.slug,'slug':instance.slug})}" target="_blank">View</a>
|
||||
# """,
|
||||
# )
|
||||
for recipient in recipients:
|
||||
models.Notification.objects.create(
|
||||
user=recipient,
|
||||
message=f"""
|
||||
Bill {instance.bill_number} is in review,please review and approve it
|
||||
<a href="{reverse('bill-detail', kwargs={'dealer_slug': dealer.slug, 'entity_slug':dealer.entity.slug, 'bill_pk': instance.pk})}" target="_blank">View</a>.
|
||||
"""
|
||||
)
|
||||
|
||||
@receiver(post_save, sender=BillModel)
|
||||
def bill_model_after_approve_notification(sender, instance, created, **kwargs):
|
||||
if instance.is_approved():
|
||||
dealer = models.Dealer.objects.get(entity=instance.ledger.entity)
|
||||
recipients = models.CustomGroup.objects.filter(dealer=dealer,name="Accountant").first().group.user_set.exclude(email=dealer.user.email).distinct()
|
||||
|
||||
# send notification after car is sold {manager,dealer}
|
||||
# after po review send notification to {manager} to approve po
|
||||
# after estimate review send notification to {manager} to approve estimate
|
||||
for recipient in recipients:
|
||||
models.Notification.objects.create(
|
||||
user=recipient,
|
||||
message=f"""
|
||||
Bill {instance.bill_number} has been approved.
|
||||
<a href="{reverse('bill-detail', kwargs={'dealer_slug': dealer.slug, 'entity_slug':dealer.entity.slug, 'bill_pk': instance.pk})}" target="_blank">View</a>.
|
||||
please comlete the bill payment.
|
||||
"""
|
||||
)
|
||||
@ -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,
|
||||
|
||||
@ -120,6 +120,11 @@ urlpatterns = [
|
||||
views.update_task,
|
||||
name="update_task",
|
||||
),
|
||||
path(
|
||||
"<slug:dealer_slug>/<int:pk>/update-schedule/",
|
||||
views.update_schedule,
|
||||
name="update_schedule",
|
||||
),
|
||||
path(
|
||||
"<slug:dealer_slug>/crm/<str:content_type>/<slug:slug>/add-task/",
|
||||
views.add_task,
|
||||
@ -141,9 +146,9 @@ urlpatterns = [
|
||||
name="send_lead_email_with_template",
|
||||
),
|
||||
path(
|
||||
"<slug:dealer_slug>/crm/leads/<slug:slug>/schedule/",
|
||||
views.schedule_lead,
|
||||
name="schedule_lead",
|
||||
"<slug:dealer_slug>/crm/<str:content_type>/<slug:slug>/schedule/",
|
||||
views.schedule_event,
|
||||
name="schedule_event",
|
||||
),
|
||||
path(
|
||||
"<slug:dealer_slug>/crm/leads/schedule/<int:pk>/cancel/",
|
||||
@ -802,17 +807,17 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"<slug:dealer_slug>/items/bills/<slug:entity_slug>/detail/<uuid:bill_pk>/",
|
||||
views.BillModelDetailViewView.as_view(),
|
||||
views.BillModelDetailView.as_view(),
|
||||
name="bill-detail",
|
||||
),
|
||||
path(
|
||||
"<slug:dealer_slug>/items/bills/<slug:entity_slug>/update/<uuid:bill_pk>/",
|
||||
views.BillModelUpdateViewView.as_view(),
|
||||
views.BillModelUpdateView.as_view(),
|
||||
name="bill-update",
|
||||
),
|
||||
path(
|
||||
"<slug:dealer_slug>/items/bills/<slug:entity_slug>/update/<uuid:bill_pk>/items/",
|
||||
views.BillModelUpdateViewView.as_view(action_update_items=True),
|
||||
views.BillModelUpdateView.as_view(action_update_items=True),
|
||||
name="bill-update-items",
|
||||
),
|
||||
############################################################
|
||||
|
||||
@ -1288,7 +1288,7 @@ def handle_account_process(invoice, amount, finance_data):
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error updating item_model.for_inventory for car {car.vin} (Invoice {invoice.invoice_number}): {e}",
|
||||
exc_info=True
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
print(e)
|
||||
@ -1419,12 +1419,23 @@ def handle_payment(request, order):
|
||||
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
||||
auth = (settings.MOYASAR_SECRET_KEY, "")
|
||||
response = requests.request("POST", url, auth=auth, headers=headers, data=payload)
|
||||
if response.status_code == 400:
|
||||
data = response.json()
|
||||
if data["type"] == "validation_error":
|
||||
errors = data.get("errors", {})
|
||||
if "source.year" in errors:
|
||||
raise Exception("Invalid expiry year")
|
||||
else:
|
||||
raise Exception("Validation Error: ", errors)
|
||||
else:
|
||||
print("Failed to process payment:", data)
|
||||
#
|
||||
order.status = AbstractOrder.STATUS.NEW
|
||||
order.save()
|
||||
#
|
||||
data = response.json()
|
||||
amount = Decimal("{0:.2f}".format(Decimal(total) / Decimal(100)))
|
||||
print(data)
|
||||
models.PaymentHistory.objects.create(
|
||||
user=request.user,
|
||||
user_data=user_data,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -184,10 +184,12 @@
|
||||
</td>
|
||||
<td class="align-items-start white-space-nowrap pe-2">
|
||||
{% if bill_item.po_model_id %}
|
||||
{% if perms.django_ledger.view_purchaseordermodel%}
|
||||
<a class="btn btn-sm btn-phoenix-primary"
|
||||
href="{% url 'purchase_order_detail' request.dealer.slug request.dealer.entity.slug bill_item.po_model_id %}">
|
||||
{% trans 'View PO' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
@ -231,9 +233,11 @@
|
||||
<h5 class="mb-0">{% trans 'Bill Notes' %}</h5>
|
||||
</div>
|
||||
</div>
|
||||
{% if perms.django_ledger.change_billmodel%}
|
||||
<div class="card-body">
|
||||
{% include 'bill/includes/card_markdown.html' with style='card_1' title='' notes_html=bill.notes_html %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -68,7 +68,7 @@
|
||||
class="btn btn-sm btn-phoenix-warning me-md-2">
|
||||
{% trans 'Update' %}
|
||||
</a>
|
||||
|
||||
|
||||
{% if bill.can_pay %}
|
||||
|
||||
<button onclick="djLedger.toggleModal('{{ bill.get_html_id }}')"
|
||||
@ -207,7 +207,7 @@
|
||||
{% if perms.django_ledger.change_billmodel%}
|
||||
<a href="{% url 'bill-update' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill.uuid %}" class="btn btn-phoenix-primary">
|
||||
<i class="fas fa-edit me-2"></i>{% trans 'Update' %}
|
||||
|
||||
</a>
|
||||
<!-- Mark as Draft -->
|
||||
{% if bill.can_draft %}
|
||||
<button class="btn btn-phoenix-success"
|
||||
@ -223,7 +223,7 @@
|
||||
</button>
|
||||
{% endif %}
|
||||
<!-- Mark as Approved -->
|
||||
{% if bill.can_approve and perms.django_ledger.can_approve_bill %}
|
||||
{% if bill.can_approve and perms.django_ledger.can_approve_billmodel %}
|
||||
<button class="btn btn-phoenix-success"
|
||||
onclick="showPOModal('Mark as Approved', '{% url 'bill-action-mark-as-approved' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Approved')">
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Approved' %}
|
||||
|
||||
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_event' request.dealer.slug content_type slug %}" method="post" class="add_schedule_form">
|
||||
{% csrf_token %}
|
||||
{{ schedule_form|crispy }}
|
||||
<button type="submit" class="btn btn-phoenix-success w-100">{% trans 'Save' %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -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);
|
||||
@ -60,8 +56,8 @@
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 d-md-none">
|
||||
<h3 class="mb-0">{{ _("Lead Details")}}</h3>
|
||||
<button class="btn p-0" ><span class="uil uil-times fs-7"></span></button>
|
||||
</div>
|
||||
<div class="card mb-2">
|
||||
</div>
|
||||
<div class="card mb-2">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center g-3 text-center text-xxl-start">
|
||||
<div class="col-6 col-sm-auto flex-1">
|
||||
@ -109,7 +105,7 @@
|
||||
<h5 class="fw-bolder mb-2 text-body-highlight">{{ _("Related Records") }}</h5>
|
||||
<h6 class="fw-bolder mb-2 text-body-highlight">{{ _("Opportunity") }}</h6>
|
||||
{% if lead.opportunity %}
|
||||
<a href="{% url 'opportunity_detail' lead.opportunity.slug %}" class="">{{ lead.opportunity }}</a>
|
||||
<a href="{% url 'opportunity_detail' request.dealer.slug lead.opportunity.slug %}" class="">{{ lead.opportunity }}</a>
|
||||
{% else %}
|
||||
<p>{{ _("No Opportunity") }}</p>
|
||||
{% endif %}
|
||||
@ -167,14 +163,16 @@
|
||||
<div class="kanban-header bg-secondary w-50 text-white fw-bold"><i class="fa-solid fa-circle-info me-2"></i>{{lead.next_action|capfirst}} <br> <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" %}
|
||||
@ -207,7 +205,6 @@
|
||||
<div class="mb-1 d-flex justify-content-between align-items-center">
|
||||
<h3 class="mb-4" id="s crollspyTask">{{ _("Activities") }} <span class="fw-light fs-7">({{ activities.count}})</span></h3>
|
||||
{% if perms.inventory.change_lead%}
|
||||
<button class="btn btn-phoenix-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#activityModal"><span class="fas fa-plus me-1"></span>{{ _("Add Activity") }}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="row justify-content-between align-items-md-center hover-actions-trigger btn-reveal-trigger border-translucent py-3 gx-0 border-top">
|
||||
@ -221,6 +218,10 @@
|
||||
<div class="icon-item icon-item-md rounded-7 border border-translucent">
|
||||
{% if activity.activity_type == "call" %}
|
||||
<span class="fa-solid fa-phone text-warning fs-8"></span>
|
||||
{% elif activity.activity_type == "note" %}
|
||||
<span class="fa-regular fa-sticky-note text-info-light fs-8"></span>
|
||||
{% elif activity.activity_type == "task" %}
|
||||
<span class="fa-solid fa-list-check text-success fs-8"></span>
|
||||
{% elif activity.activity_type == "email" %}
|
||||
<span class="fa-solid fa-envelope text-info-light fs-8"></span>
|
||||
{% elif activity.activity_type == "visit" %}
|
||||
@ -253,7 +254,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade active show" id="tab-opportunity" role="tabpanel" aria-labelledby="opportunity-tab">
|
||||
<div class="tab-pane fade" id="tab-opportunity" role="tabpanel" aria-labelledby="opportunity-tab">
|
||||
<div class="mb-1 d-flex justify-content-between align-items-center">
|
||||
<h3 class="mb-4" id="scrollspyTask">{{ _("Opportunities") }} <span class="fw-light fs-7">({{ lead.get_opportunities.count}})</span></h3>
|
||||
{% if perms.inventory.add_opportunity%}
|
||||
@ -290,7 +291,7 @@
|
||||
<div class="tab-pane fade" id="tab-notes" role="tabpanel" aria-labelledby="notes-tab">
|
||||
<div class="mb-1 d-flex align-items-center justify-content-between">
|
||||
<h3 class="mb-4" id="scrollspyNotes">{{ _("Notes") }}</h3>
|
||||
{% if perms.inventory.change_lead%}
|
||||
{% if perms.inventory.change_lead %}
|
||||
<button class="btn btn-phoenix-primary btn-sm" type="button" onclick="reset_form()" data-bs-toggle="modal" data-bs-target="#noteModal"><span class="fas fa-plus me-1"></span>{{ _("Add Note") }}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -301,8 +302,8 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="align-middle pe-6 text-uppercase text-start" scope="col" style="width:40%;">{{ _("Note") }}</th>
|
||||
<th class="align-middle text-start text-uppercase" scope="col" style="width:20%;">{{ _("Created By")}}</th>
|
||||
<th class="align-middle text-start text-uppercase white-space-nowrap" scope="col" style="width:20%;">{{ _("Created On")}}</th>
|
||||
<th class="align-middle text-start text-uppercase white-space-nowrap" scope="col" style="width:40%;">{{ _("Created On")}}</th>
|
||||
<th class="align-middle text-start text-uppercase white-space-nowrap" scope="col" style="width:40%;">{{ _("Last Updated")}}</th>
|
||||
<th class="align-middle pe-0 text-end" scope="col" style="width:10%;"> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -310,12 +311,8 @@
|
||||
{% for note in notes %}
|
||||
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
|
||||
<td class="align-middle text-start fw-bold text-body-tertiary ps-1">{{note.note}}</td>
|
||||
{% if note.created_by.staff %}
|
||||
<td class="align-middle white-space-nowrap text-start white-space-nowrap">{{ note.created_by.staff.name }}</td>
|
||||
{% else %}
|
||||
<td class="align-middle white-space-nowrap text-start white-space-nowrap">{{ note.created_by.dealer.get_local_name|default:note.created_by.dealer.name }}</td>
|
||||
{% endif %}
|
||||
<td class="align-middle text-body-tertiary text-start white-space-nowrap">{{ note.created|naturalday|capfirst }}</td>
|
||||
<td class="align-middle text-body-tertiary text-start white-space-nowrap">{{ note.updated|naturalday|capfirst }}</td>
|
||||
<td class="align-middle text-end white-space-nowrap pe-0 action py-2">
|
||||
{% if note.created_by == request.user %}
|
||||
<a id="updateBtn"
|
||||
@ -446,7 +443,7 @@
|
||||
</td>
|
||||
<td class="sent align-middle white-space-nowrap text-start fw-bold text-body-tertiary py-2">{{email.from_email}}</td>
|
||||
<td class="date align-middle white-space-nowrap text-body py-2">{{email.created}}</td>
|
||||
<td class="align-middle white-space-nowrap ps-3"><a class="text-body" href="{% url 'send_lead_email_with_template' lead.slug email.pk %}"><span class="fa-solid fa-email text-primary me-2"></span>Send</a></td>
|
||||
<td class="align-middle white-space-nowrap ps-3"><a class="text-body" href="{% url 'send_lead_email_with_template' request.dealer.slug lead.slug email.pk %}"><span class="fa-solid fa-email text-primary me-2"></span>Send</a></td>
|
||||
<td class="status align-middle fw-semibold text-end py-2"><span class="badge badge-phoenix fs-10 badge-phoenix-warning">draft</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@ -469,11 +466,12 @@
|
||||
</div>
|
||||
</div>
|
||||
{% comment %} {% endcomment %}
|
||||
<div class="tab-pane fade" id="tab-tasks" role="tabpanel" aria-labelledby="tasks-tab">
|
||||
<div class="tab-pane fade active show" id="tab-tasks" role="tabpanel" aria-labelledby="tasks-tab">
|
||||
<div class="mb-1 d-flex justify-content-between align-items-center">
|
||||
<h3 class="mb-0" id="scrollspyEmails">{{ _("Tasks") }}</h3>
|
||||
{% if perms.inventory.change_lead%}
|
||||
<button class="btn btn-phoenix-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#taskModal"><span class="fas fa-plus me-1"></span>{{ _("Add Task") }}</button>
|
||||
{% comment %} <button class="btn btn-phoenix-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#taskModal"><span class="fas fa-plus me-1"></span>{{ _("Add Task") }}</button> {% endcomment %}
|
||||
<button class="btn btn-phoenix-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#scheduleModal"><span class="fas fa-plus me-1"></span>{{ _("Add Task") }}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
@ -495,7 +493,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="list" id="all-tasks-table-body">
|
||||
{% for task in tasks %}
|
||||
{% for task in schedules %}
|
||||
{% include "partials/task.html" %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@ -533,60 +531,16 @@
|
||||
</div>
|
||||
</div> {% endcomment %}
|
||||
|
||||
<!-- activity Modal -->
|
||||
{% include "components/activity_modal.html" with content_type="lead" slug=lead.slug %}
|
||||
<!-- task Modal -->
|
||||
<div class="modal fade" id="taskModal" tabindex="-1" aria-labelledby="taskModalLabel" aria-hidden="true">
|
||||
|
||||
<div class="modal-dialog modal-md">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header justify-content-between align-items-start gap-5 px-4 pt-4 pb-3 border-0">
|
||||
<h4 class="modal-title" id="taskModalLabel">{% trans 'Task' %}</h4>
|
||||
<button class="btn p-0 text-body-quaternary fs-6" data-bs-dismiss="modal" aria-label="Close">
|
||||
<span class="fas fa-times"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form action="{% url 'add_task' request.dealer.slug 'lead' lead.slug %}" method="post" class="add_task_form">
|
||||
{% csrf_token %}
|
||||
{{ staff_task_form|crispy }}
|
||||
<button type="submit" class="btn btn-phoenix-success w-100">{% trans 'Save' %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include "components/task_modal.html" with content_type="lead" slug=lead.slug %}
|
||||
<!-- note Modal -->
|
||||
<div class="modal fade" id="noteModal" tabindex="-1" aria-labelledby="noteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-md">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header justify-content-between align-items-start gap-5 px-4 pt-4 pb-3 border-0">
|
||||
<h4 class="modal-title" id="noteModalLabel">{% trans 'Note' %}</h4>
|
||||
<button class="btn p-0 text-body-quaternary fs-6" data-bs-dismiss="modal" aria-label="Close">
|
||||
<span class="fas fa-times"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form action="{% url 'add_note' request.dealer.slug 'lead' lead.slug %}" method="post" class="add_note_form">
|
||||
{% csrf_token %}
|
||||
{{ note_form|crispy }}
|
||||
<button type="submit" class="btn btn-phoenix-success w-100">{% trans 'Save' %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include "components/note_modal.html" with content_type="lead" slug=lead.slug %}
|
||||
<!-- schedule Modal -->
|
||||
{% include "components/schedule_modal.html" with content_type="lead" slug=lead.slug %}
|
||||
{% endblock content %}
|
||||
|
||||
{% block customJS %}
|
||||
<script>
|
||||
function updateNote(e) {
|
||||
let url = e.getAttribute('data-url')
|
||||
let note = e.getAttribute('data-note')
|
||||
document.querySelector('#id_note').value = note
|
||||
let form = document.querySelector('.add_note_form')
|
||||
form.action = url
|
||||
}
|
||||
function reset_form() {
|
||||
document.querySelector('#id_note').value = ""
|
||||
let form = document.querySelector('.add_note_form')
|
||||
|
||||
@ -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>
|
||||
@ -467,8 +466,13 @@
|
||||
{% endif %}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link px-3 d-block" href=""> <span class="me-2 text-body align-bottom" data-feather="help-circle"></span>Help Center</a>
|
||||
<a class="nav-link px-3 d-block" href=""> <span class="me-2 text-body align-bottom" data-feather="help-circle"></span>{{ _("Help Center") }}</a>
|
||||
</li>
|
||||
{% if request.is_staff %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link px-3 d-block" href="{% url 'appointment:get_user_appointments' %}"> <span class="me-2 text-body align-bottom" data-feather="calendar"></span>{{ _("Calendar") }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<!--<li class="nav-item"><a class="nav-link px-3 d-block" href=""> Language</a></li>-->
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@ -52,16 +52,20 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex ">
|
||||
{% if perms.django_ledger.change_bankaccountmodel%}
|
||||
<a class="btn btn-sm btn-phoenix-primary me-1" href="{% url 'bank_account_update' request.dealer.slug bank_account.pk %}">
|
||||
<!--<i class="bi bi-pencil-square"></i> -->
|
||||
{{ _("Edit") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.django_ledger.delete_bankaccountmodel%}
|
||||
<a class="btn btn-sm btn-phoenix-danger me-1"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#deleteModal">
|
||||
<!--<i class="bi bi-trash-fill"></i>-->
|
||||
{{ _("Delete") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a class="btn btn-sm btn-phoenix-secondary"
|
||||
href="{% url 'bank_account_list' request.dealer.slug %}">
|
||||
<!--<i class="bi bi-arrow-left-square-fill"></i>-->
|
||||
|
||||
@ -96,12 +96,14 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap text-start">
|
||||
{% if perms.django_ledger.view_transactionmodel%}
|
||||
<div class="btn-reveal-trigger position-static">
|
||||
<button class="btn btn-sm dropdown-toggle dropdown-caret-none transition-none btn-reveal fs-10" type="button" data-bs-toggle="dropdown" data-boundary="window" aria-haspopup="true" aria-expanded="false" data-bs-reference="parent"><span class="fas fa-ellipsis-h fs-10"></span></button>
|
||||
<div class="dropdown-menu dropdown-menu-end py-2">
|
||||
<a class="dropdown-item text-success-dark" href="{% url 'payment_details' request.dealer.slug tx.journal_entry.pk %}">{% trans 'view Transactions'|capfirst %}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@ -135,17 +137,21 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-3 d-flex">
|
||||
{% if perms.django_ledger.change_chartofaccountmodel%}
|
||||
<a class="btn btn-sm btn-phoenix-primary me-1" href="{% url 'account_update' request.dealer.slug account.pk %}">
|
||||
<!-- <i class="bi bi-pencil-square"></i> -->
|
||||
<i class="fa-solid fa-pen-to-square"></i> {{ _('Edit') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.django_ledger.delete_chartofaccountmodel%}
|
||||
<a class="btn btn-sm btn-phoenix-danger me-1" data-bs-toggle="modal" data-bs-target="#deleteModal">
|
||||
<!-- <i class="bi bi-trash-fill"></i> -->
|
||||
<i class="fa-solid fa-trash"></i> {{ _('Delete') }}
|
||||
</a>
|
||||
{% endif%}
|
||||
<a class="btn btn-sm btn-phoenix-secondary" href="{% url 'account_list' request.dealer.slug %}">
|
||||
<!-- <i class="bi bi-arrow-left-square-fill"></i> -->
|
||||
<i class="fa-regular fa-circle-left"></i> {% trans 'Back to List' %}
|
||||
<i class="fa-regular fa-circle-left"></i> {% trans 'Back to COA List' %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
@ -11,7 +11,9 @@
|
||||
<div class="row mt-4">
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<h3 class=""><i class="fa-solid fa-book"></i> {% trans "Accounts" %}</h3>
|
||||
{% if perms.django_ledger.create_chartofaccountmodel %}
|
||||
<a href="{% url 'account_create' request.dealer.slug %}" class="btn btn-md btn-phoenix-primary"><i class="fa fa-plus me-2"></i>{% trans 'New Account' %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Account Type Tabs -->
|
||||
@ -105,10 +107,12 @@
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
{% if perms.django_ledger.delete_chartofaccountmodel %}
|
||||
<h5 class="modal-title" id="deleteModalLabel">
|
||||
{% trans "Delete Account" %}
|
||||
<span data-feather="alert-circle"></span>
|
||||
</h5>
|
||||
{% endif %}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
|
||||
@ -2,14 +2,14 @@
|
||||
<tr id="task-{{task.pk}}" class="hover-actions-trigger btn-reveal-trigger position-static {% if task.completed %}completed-task{% endif %}">
|
||||
<td class="fs-9 align-middle px-0 py-3">
|
||||
<div class="form-check mb-0 fs-8">
|
||||
<input class="form-check-input" {% if task.completed %}checked{% endif %} type="checkbox" hx-post="{% url 'update_task' request.dealer.slug task.pk %}" hx-trigger="change" hx-swap="outerHTML" hx-target="#task-{{task.pk}}" />
|
||||
<input class="form-check-input" {% if task.completed %}checked{% endif %} type="checkbox" hx-post="{% url 'update_schedule' request.dealer.slug task.pk %}" hx-trigger="change" hx-swap="outerHTML" hx-target="#task-{{task.pk}}" />
|
||||
</div>
|
||||
</td>
|
||||
<td class="subject order align-middle white-space-nowrap py-2 ps-0"><a class="fw-semibold text-primary" href="">{{task.title}}</a>
|
||||
<div class="fs-10 d-block">{{task.description}}</div>
|
||||
<td class="subject order align-middle white-space-nowrap py-2 ps-0"><a class="fw-semibold text-primary" href="">{{task.purpose|capfirst}}</a>
|
||||
<div class="fs-10 d-block">{{task.scheduled_type|capfirst}}</div>
|
||||
</td>
|
||||
<td class="sent align-middle white-space-nowrap text-start fw-bold text-body-tertiary py-2">{{task.assigned_to}}</td>
|
||||
<td class="date align-middle white-space-nowrap text-body py-2">{{task.created|naturalday|capfirst}}</td>
|
||||
<td class="sent align-middle white-space-nowrap text-start fw-bold text-body-tertiary py-2">{{task.scheduled_by}}</td>
|
||||
<td class="date align-middle white-space-nowrap text-body py-2">{{task.created_at|naturalday|capfirst}}</td>
|
||||
<td class="date align-middle white-space-nowrap text-body py-2">
|
||||
{% if task.completed %}
|
||||
<span class="badge badge-phoenix fs-10 badge-phoenix-success"><i class="fa-solid fa-check"></i></span>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -8,45 +8,61 @@
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="row g-1">
|
||||
|
||||
<div class="col-lg-12">
|
||||
<div class="d-flex flex-column gap-3">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{% include 'purchase_orders/includes/card_po.html' with dealer_slug=request.dealer.slug po_model=po_model entity_slug=entity_slug style='po-detail' %}
|
||||
</div>
|
||||
|
||||
<div class="col-lg-12 mb-3 ">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-9">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{% include 'purchase_orders/includes/card_po.html' with dealer_slug=request.dealer.slug po_model=po_model entity_slug=entity_slug style='po-detail' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='col-3'>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row text-center">
|
||||
<div class="col-md-6 border-end">
|
||||
<div class="p-2">
|
||||
<h6 class="text-muted mb-2">{% trans 'PO Amount' %}</h6>
|
||||
<h3 class="fw-light mb-0">
|
||||
<span class="currency">{{CURRENCY}}</span>{{ po_model.po_amount | absolute | currency_format }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="p-2">
|
||||
<h6 class="text-muted mb-2">{% trans 'Amount Received' %}</h6>
|
||||
<h3 class="fw-light mb-0 text-success">
|
||||
<span class="currency">{{CURRENCY}}</span>{{ po_model.po_amount_received | currency_format }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-12">
|
||||
<a class="btn btn-phoenix-primary w-100 py-2"
|
||||
href="{% url 'purchase_order_list' request.dealer.slug request.dealer.entity.slug %}">
|
||||
<i class="fas fa-list me-2"></i>{% trans 'PO List' %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a class="btn btn-phoenix-primary w-100 py-2"
|
||||
href="{% url 'purchase_order_list' request.dealer.slug request.dealer.entity.slug %}">
|
||||
<i class="fas fa-list me-2"></i>{% trans 'PO List' %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-12">
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row text-center">
|
||||
<div class="col-md-6 border-end">
|
||||
<div class="p-2">
|
||||
<h6 class="text-muted mb-2">{% trans 'PO Amount' %}</h6>
|
||||
<h3 class="fw-light mb-0">
|
||||
<span class="currency">{{CURRENCY}}</span>{{ po_model.po_amount | absolute | currency_format }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="p-2">
|
||||
<h6 class="text-muted mb-2">{% trans 'Amount Received' %}</h6>
|
||||
<h3 class="fw-light mb-0 text-success">
|
||||
<span class="currency">{{CURRENCY}}</span>{{ po_model.po_amount_received | currency_format }}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-12">
|
||||
<div class="table-responsive">
|
||||
|
||||
@ -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>
|
||||
|
||||
195
templates/sales/estimates/estimate_form-copy.html
Normal file
195
templates/sales/estimates/estimate_form-copy.html
Normal file
@ -0,0 +1,195 @@
|
||||
{% extends "base.html" %}
|
||||
{% load crispy_forms_filters %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block title %}{{ _("Create Quotation") }}{% endblock title %}
|
||||
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
.disabled{
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
{% endblock customCSS %}
|
||||
{% block content %}
|
||||
<div class="row mt-4">
|
||||
{% if not items %}
|
||||
<div class="alert alert-outline-warning d-flex align-items-center" role="alert">
|
||||
<i class="fa-solid fa-circle-info fs-6"></i>
|
||||
<p class="mb-0 flex-1">{{ _("Please add at least one car before creating a quotation.") }}<a class="ms-3 text-body-primary fs-9" href="{% url 'car_add' request.dealer.slug %}"> {{ _("Add Car") }} </a></p>
|
||||
<button class="btn-close" type="button" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not customer_count %}
|
||||
<div class="alert alert-outline-warning d-flex align-items-center" role="alert">
|
||||
<i class="fa-solid fa-circle-info fs-6"></i>
|
||||
<p class="mb-0 flex-1"> {{ _("Please add at least one customer before creating a quotation.") }}<a class="ms-3 text-body-primary fs-9" href="{% url 'customer_create' request.dealer.slug %}"> {{ _("Add Customer") }} </a></p>
|
||||
<button class="btn-close" type="button" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<form id="mainForm" method="post" class="needs-validation {% if not items or not customer_count %}disabled{% endif %}">
|
||||
<h3 class="text-center"><i class="fa-regular fa-file-lines"></i> {% trans "Create Quotation" %}</h3>
|
||||
{% csrf_token %}
|
||||
<div class="row g-3 col-10">
|
||||
{{ form|crispy }}
|
||||
<div class="row mt-5">
|
||||
<div id="formrow">
|
||||
<h3 class="text-start"><i class="fa-solid fa-car-side"></i> {{ _("Cars") }}</h3>
|
||||
<div class="form-row row g-3 mb-3 mt-5">
|
||||
<div class="mb-2 col-sm-4 col-md-6 col-lg-4">
|
||||
<select class="form-control item" name="item[]" required>
|
||||
{% for item in items %}
|
||||
<option style="background-color: rgb({{ item.color }});" value="{{ item.hash }}">{{ item.make }} {{item.model}} {{item.serie}} {{item.trim}} {{item.color_name}} ({{item.hash_count}})</option>
|
||||
{% empty %}
|
||||
<option disabled>{% trans "No Cars Found" %}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-2 col-sm-4 col-md-3">
|
||||
<input class="form-control quantity" type="number" placeholder="Quantity" name="quantity[]" required>
|
||||
</div>
|
||||
<div class="mb-2 col-sm-3 col-md-3">
|
||||
<button class="btn btn-sm btn-phoenix-danger removeBtn"><i class="fa-solid fa-trash me-1"></i> {{ _("Remove") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button id="addMoreBtn" class="btn btn-sm btn-phoenix-primary"><i class="fa-solid fa-plus me-2"></i> {{ _("Add More")}}</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
{% comment %} <div class="mt-5 text-center">
|
||||
<button type="submit" class="btn btn-success me-2" {% if not items %}disabled{% endif %}><i class="fa-solid fa-floppy-disk me-1"></i> {% trans "Save" %}</button>
|
||||
<a href="{% url 'estimate_list' %}" class="btn btn-danger"><i class="fa-solid fa-ban me-1"></i> {% trans "Cancel" %}</a>
|
||||
</div> {% endcomment %}
|
||||
<div class="d-flex justify-content-center">
|
||||
|
||||
<button class="btn btn-sm btn-phoenix-success me-2" type="submit"><i class="fa-solid fa-floppy-disk me-1"></i>
|
||||
<!--<i class="bi bi-save"></i> -->
|
||||
{{ _("Save") }}
|
||||
</button>
|
||||
|
||||
<a href="{{request.META.HTTP_REFERER}}" class="btn btn-sm btn-phoenix-danger"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</form>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
{% block customJS %}
|
||||
<script>
|
||||
const Toast = Swal.mixin({
|
||||
toast: true,
|
||||
position: "top-end",
|
||||
showConfirmButton: false,
|
||||
timer: 2000,
|
||||
timerProgressBar: false,
|
||||
didOpen: (toast) => {
|
||||
toast.onmouseenter = Swal.stopTimer;
|
||||
toast.onmouseleave = Swal.resumeTimer;
|
||||
}
|
||||
});
|
||||
// Add new form fields
|
||||
document.getElementById('addMoreBtn').addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const formrow = document.getElementById('formrow');
|
||||
const newForm = document.createElement('div');
|
||||
newForm.className = 'form-row row g-3 mb-3 mt-5';
|
||||
newForm.innerHTML = `
|
||||
<div class="mb-2 col-sm-4">
|
||||
<select class="form-control item" name="item[]" required>
|
||||
{% for item in items %}
|
||||
<option value="{{ item.hash }}">{{ item.make }} {{item.model}} {{item.serie}} {{item.trim}} {{item.color}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-2 col-sm-2">
|
||||
<input class="form-control quantity" type="number" placeholder="Quantity" name="quantity[]" required>
|
||||
</div>
|
||||
<div class="mb-2 col-sm-1">
|
||||
<button class="btn btn-phoenix-danger removeBtn"><i class="fa-solid fa-trash"></i> {{ _("Remove") }}</button>
|
||||
</div>
|
||||
`;
|
||||
formrow.appendChild(newForm);
|
||||
|
||||
// Add remove button functionality
|
||||
newForm.querySelector('.removeBtn').addEventListener('click', function() {
|
||||
newForm.remove();
|
||||
});
|
||||
});
|
||||
|
||||
// Add remove button functionality to the initial form
|
||||
document.querySelectorAll('.form-row').forEach(row => {
|
||||
row.querySelector('.removeBtn').addEventListener('click', function() {
|
||||
row.remove();
|
||||
});
|
||||
});
|
||||
|
||||
// Handle form submission
|
||||
document.getElementById('mainForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const titleInput = document.querySelector('[name="title"]');
|
||||
if (titleInput.value.length < 5) {
|
||||
notify("error", "Customer Estimate Title must be at least 5 characters long.");
|
||||
return; // Stop form submission
|
||||
}
|
||||
|
||||
// Collect all form data
|
||||
const formData = {
|
||||
csrfmiddlewaretoken: document.querySelector('[name=csrfmiddlewaretoken]').value,
|
||||
title: document.querySelector('[name=title]').value,
|
||||
customer: document.querySelector('[name=customer]').value,
|
||||
item: [],
|
||||
quantity: [],
|
||||
opportunity_id: "{{opportunity_id}}"
|
||||
};
|
||||
|
||||
|
||||
// Collect multi-value fields (e.g., item[], quantity[])
|
||||
document.querySelectorAll('[name="item[]"]').forEach(input => {
|
||||
formData.item.push(input.value);
|
||||
});
|
||||
document.querySelectorAll('[name="quantity[]"]').forEach(input => {
|
||||
formData.quantity.push(input.value);
|
||||
});
|
||||
console.log(formData);
|
||||
|
||||
try {
|
||||
// Send data to the server using fetch
|
||||
const response = await fetch("{% url 'estimate_create' request.dealer.slug %}", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': formData.csrfmiddlewaretoken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
// Parse the JSON response
|
||||
const data = await response.json();
|
||||
|
||||
// Handle the response
|
||||
if (data.status === "error") {
|
||||
notify("error", data.message); // Display an error message
|
||||
} else if (data.status === "success") {
|
||||
notify("success","Estimate created successfully");
|
||||
setTimeout(() => {
|
||||
window.location.assign(data.url); // Redirect to the provided URL
|
||||
}, 1000);
|
||||
} else {
|
||||
notify("error","Unexpected response from the server");
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
notify("error", error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock customJS %}
|
||||
@ -9,9 +9,118 @@
|
||||
.disabled{
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.color-box {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ccc;
|
||||
} /* Custom select styles */
|
||||
.custom-select {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.select-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.4rem 1rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.select-trigger:hover {
|
||||
border-color: #aaa;
|
||||
}
|
||||
|
||||
.select-trigger.active {
|
||||
border-color: #4a90e2;
|
||||
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2);
|
||||
}
|
||||
|
||||
.selected-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.selected-value img {
|
||||
width: 30px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.custom-select.open .dropdown-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.options-container {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
background-color: white;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
z-index: 100;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.custom-select.open .options-container {
|
||||
display: block;
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.1s ease;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background-color: #f0f7ff;
|
||||
}
|
||||
|
||||
.option.selected {
|
||||
background-color: #e6f0ff;
|
||||
}
|
||||
|
||||
.option img {
|
||||
width: 30px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* Hidden native select for form submission */
|
||||
.native-select {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
</style>
|
||||
{% endblock customCSS %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mt-4">
|
||||
{% if not items %}
|
||||
@ -33,58 +142,53 @@
|
||||
{% csrf_token %}
|
||||
<div class="row g-3 col-10">
|
||||
{{ form|crispy }}
|
||||
<div class="row mt-5">
|
||||
<div id="formrow">
|
||||
<h3 class="text-start"><i class="fa-solid fa-car-side"></i> {{ _("Cars") }}</h3>
|
||||
<div class="form-row row g-3 mb-3 mt-5">
|
||||
<div class="mb-2 col-sm-4 col-md-6 col-lg-4">
|
||||
<select class="form-control item" name="item[]" required>
|
||||
{% for item in items %}
|
||||
<option style="background-color: rgb({{ item.color }});" value="{{ item.hash }}">{{ item.make }} {{item.model}} {{item.serie}} {{item.trim}} {{item.color_name}} ({{item.hash_count}})</option>
|
||||
{% empty %}
|
||||
<option disabled>{% trans "No Cars Found" %}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-2 col-sm-4 col-md-3">
|
||||
<input class="form-control quantity" type="number" placeholder="Quantity" name="quantity[]" required>
|
||||
</div>
|
||||
<div class="mb-2 col-sm-3 col-md-3">
|
||||
<button class="btn btn-sm btn-phoenix-danger removeBtn"><i class="fa-solid fa-trash me-1"></i> {{ _("Remove") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button id="addMoreBtn" class="btn btn-sm btn-phoenix-primary"><i class="fa-solid fa-plus me-2"></i> {{ _("Add More")}}</button>
|
||||
</div>
|
||||
<div class="custom-select">
|
||||
<!-- Hidden native select for form submission -->
|
||||
<select class="native-select" name="item" required tabindex="-1">
|
||||
<option value="">Select a car</option>
|
||||
{% for item in items %}
|
||||
<option value="{{ item.hash }}"></option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<!-- Custom select UI -->
|
||||
<div class="select-trigger">
|
||||
<div class="selected-value">
|
||||
<span>Select a car</span>
|
||||
</div>
|
||||
<i class="fas fa-chevron-down dropdown-icon"></i>
|
||||
</div>
|
||||
|
||||
<div class="options-container">
|
||||
{% for item in items %}
|
||||
<div class="option" data-value="{{ item.hash }}" data-image="{{item.logo}}">
|
||||
<img src="{{item.logo}}" alt="{{item.model}}">
|
||||
<span>{{item.make}} {{item.model}} {{item.serie}} {{item.trim}} {{item.color_name}}</span>
|
||||
<div class="color-box" style="background-color: rgb({{ item.exterior_color }});"></div>
|
||||
<div class="color-box" style="background-color: rgb({{ item.interior_color }});"></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
{% comment %} <div class="mt-5 text-center">
|
||||
<button type="submit" class="btn btn-success me-2" {% if not items %}disabled{% endif %}><i class="fa-solid fa-floppy-disk me-1"></i> {% trans "Save" %}</button>
|
||||
<a href="{% url 'estimate_list' %}" class="btn btn-danger"><i class="fa-solid fa-ban me-1"></i> {% trans "Cancel" %}</a>
|
||||
</div> {% endcomment %}
|
||||
</div>
|
||||
<div class="d-flex justify-content-center">
|
||||
|
||||
<button class="btn btn-sm btn-phoenix-success me-2" type="submit"><i class="fa-solid fa-floppy-disk me-1"></i>
|
||||
<!--<i class="bi bi-save"></i> -->
|
||||
<button class="btn btn-sm btn-phoenix-success me-2" type="submit">
|
||||
<i class="fa-solid fa-floppy-disk me-1"></i>
|
||||
{{ _("Save") }}
|
||||
</button>
|
||||
|
||||
<a href="{{request.META.HTTP_REFERER}}" class="btn btn-sm btn-phoenix-danger"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
|
||||
<a href="{{request.META.HTTP_REFERER}}" class="btn btn-sm btn-phoenix-danger">
|
||||
<i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</form>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
{% block customJS %}
|
||||
<script>
|
||||
const Toast = Swal.mixin({
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const Toast = Swal.mixin({
|
||||
toast: true,
|
||||
position: "top-end",
|
||||
showConfirmButton: false,
|
||||
@ -95,44 +199,86 @@
|
||||
toast.onmouseleave = Swal.resumeTimer;
|
||||
}
|
||||
});
|
||||
// Add new form fields
|
||||
document.getElementById('addMoreBtn').addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const formrow = document.getElementById('formrow');
|
||||
const newForm = document.createElement('div');
|
||||
newForm.className = 'form-row row g-3 mb-3 mt-5';
|
||||
newForm.innerHTML = `
|
||||
<div class="mb-2 col-sm-4">
|
||||
<select class="form-control item" name="item[]" required>
|
||||
{% for item in items %}
|
||||
<option value="{{ item.hash }}">{{ item.make }} {{item.model}} {{item.serie}} {{item.trim}} {{item.color}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-2 col-sm-2">
|
||||
<input class="form-control quantity" type="number" placeholder="Quantity" name="quantity[]" required>
|
||||
</div>
|
||||
<div class="mb-2 col-sm-1">
|
||||
<button class="btn btn-phoenix-danger removeBtn"><i class="fa-solid fa-trash"></i> {{ _("Remove") }}</button>
|
||||
</div>
|
||||
`;
|
||||
formrow.appendChild(newForm);
|
||||
function notify(tag,msg){Toast.fire({icon: tag,titleText: msg});}
|
||||
|
||||
// Add remove button functionality
|
||||
newForm.querySelector('.removeBtn').addEventListener('click', function() {
|
||||
newForm.remove();
|
||||
const customSelects = document.querySelectorAll('.custom-select');
|
||||
|
||||
customSelects.forEach(select => {
|
||||
const trigger = select.querySelector('.select-trigger');
|
||||
const optionsContainer = select.querySelector('.options-container');
|
||||
const options = select.querySelectorAll('.option');
|
||||
const nativeSelect = select.querySelector('.native-select');
|
||||
const selectedValue = select.querySelector('.selected-value');
|
||||
|
||||
// Toggle dropdown
|
||||
trigger.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
select.classList.toggle('open');
|
||||
trigger.classList.toggle('active');
|
||||
|
||||
// Close other open selects
|
||||
document.querySelectorAll('.custom-select').forEach(otherSelect => {
|
||||
if (otherSelect !== select) {
|
||||
otherSelect.classList.remove('open');
|
||||
otherSelect.querySelector('.select-trigger').classList.remove('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Handle option selection
|
||||
options.forEach(option => {
|
||||
option.addEventListener('click', () => {
|
||||
const value = option.getAttribute('data-value');
|
||||
const image = option.getAttribute('data-image');
|
||||
const text = option.querySelector('span').textContent;
|
||||
|
||||
// Update selected display
|
||||
selectedValue.innerHTML = `
|
||||
<img src="${image}" alt="${text}">
|
||||
<span>${text}</span>
|
||||
`;
|
||||
|
||||
// Update native select value
|
||||
nativeSelect.value = value;
|
||||
|
||||
// Mark as selected
|
||||
options.forEach(opt => opt.classList.remove('selected'));
|
||||
option.classList.add('selected');
|
||||
|
||||
// Close dropdown
|
||||
select.classList.remove('open');
|
||||
trigger.classList.remove('active');
|
||||
|
||||
// Trigger change event
|
||||
const event = new Event('change');
|
||||
nativeSelect.dispatchEvent(event);
|
||||
});
|
||||
});
|
||||
|
||||
// Close when clicking outside
|
||||
document.addEventListener('click', () => {
|
||||
select.classList.remove('open');
|
||||
trigger.classList.remove('active');
|
||||
});
|
||||
|
||||
// Initialize with native select value
|
||||
if (nativeSelect.value) {
|
||||
const selectedOption = select.querySelector(`.option[data-value="${nativeSelect.value}"]`);
|
||||
if (selectedOption) {
|
||||
selectedOption.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add remove button functionality to the initial form
|
||||
document.querySelectorAll('.form-row').forEach(row => {
|
||||
row.querySelector('.removeBtn').addEventListener('click', function() {
|
||||
row.remove();
|
||||
});
|
||||
});
|
||||
// Form submission
|
||||
/*const form = document.getElementById('demo-form');
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(form);
|
||||
alert(`Selected value: ${formData.get('car')}`);
|
||||
});*/
|
||||
|
||||
// Handle form submission
|
||||
document.getElementById('mainForm').addEventListener('submit', async function(e) {
|
||||
document.getElementById('mainForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const titleInput = document.querySelector('[name="title"]');
|
||||
@ -147,18 +293,16 @@
|
||||
title: document.querySelector('[name=title]').value,
|
||||
customer: document.querySelector('[name=customer]').value,
|
||||
item: [],
|
||||
quantity: [],
|
||||
quantity: [1],
|
||||
opportunity_id: "{{opportunity_id}}"
|
||||
};
|
||||
|
||||
|
||||
// Collect multi-value fields (e.g., item[], quantity[])
|
||||
document.querySelectorAll('[name="item[]"]').forEach(input => {
|
||||
document.querySelectorAll('[name="item"]').forEach(input => {
|
||||
formData.item.push(input.value);
|
||||
});
|
||||
document.querySelectorAll('[name="quantity[]"]').forEach(input => {
|
||||
formData.quantity.push(input.value);
|
||||
});
|
||||
|
||||
console.log(formData);
|
||||
|
||||
try {
|
||||
@ -191,5 +335,7 @@
|
||||
notify("error", error);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
{% endblock customJS %}
|
||||
@ -9,22 +9,22 @@
|
||||
<style>
|
||||
/* Custom styling */
|
||||
.form-section {
|
||||
background-color: #f8f9fa;
|
||||
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-section-header {
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
|
||||
padding-bottom: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: #0d6efd;
|
||||
|
||||
}
|
||||
|
||||
.required-field::after {
|
||||
content: " *";
|
||||
color: #dc3545;
|
||||
|
||||
}
|
||||
|
||||
.search-select {
|
||||
@ -40,7 +40,7 @@
|
||||
right: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #6c757d;
|
||||
|
||||
}
|
||||
|
||||
.currency-input {
|
||||
@ -52,7 +52,7 @@
|
||||
left: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #6c757d;
|
||||
|
||||
}
|
||||
|
||||
.currency-input input {
|
||||
@ -60,12 +60,12 @@
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
background-color: #f8f9fa;
|
||||
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
position: sticky;
|
||||
position:static;
|
||||
bottom: 1rem;
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1);
|
||||
|
||||
}
|
||||
</style>
|
||||
{% endblock customCSS %}
|
||||
@ -75,7 +75,7 @@
|
||||
<div class="row justify-content-center mb-4">
|
||||
<div class="col-lg-10">
|
||||
<div class="card shadow">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<div class="card-header bg-primary ">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h2 class="h4 mb-0">
|
||||
<i class="fas fa-file-invoice me-2"></i> New Sale Order
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user