voip click2calll
This commit is contained in:
1 parent
cb92abbb41
commit
38b1cf06df
18 files changed
+543
-26
No files matched your search
@@ -58,10 +58,10 @@ class StaffProfileInline(admin.StackedInline):
|
||||
|
||||
@admin.register(User)
|
||||
class CustomUserAdmin(UserAdmin):
|
||||
fieldsets = UserAdmin.fieldsets + (("سامانه تیکت", {"fields": ("mobile", "account_type")}),)
|
||||
add_fieldsets = UserAdmin.add_fieldsets + (("سامانه تیکت", {"fields": ("mobile", "account_type")}),)
|
||||
list_display = ("username", "mobile", "account_type", "is_active", "is_staff")
|
||||
list_filter = UserAdmin.list_filter + ("account_type",)
|
||||
fieldsets = UserAdmin.fieldsets + (("سامانه تیکت", {"fields": ("mobile", "account_type", "voip_extension", "voip_mobile", "voip_call")}),)
|
||||
add_fieldsets = UserAdmin.add_fieldsets + (("سامانه تیکت", {"fields": ("mobile", "account_type", "voip_extension", "voip_mobile", "voip_call")}),)
|
||||
list_display = ("username", "mobile", "voip_extension", "voip_mobile", "voip_call", "account_type", "is_active", "is_staff")
|
||||
list_filter = UserAdmin.list_filter + ("account_type", "voip_call")
|
||||
inlines = (StaffProfileInline,)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [("accounts", "0005_magiclinkaccesslog_magic_opened_idx_and_more")]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="voip_call",
|
||||
field=models.BooleanField(default=True, verbose_name="امکان تماس VOIP"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="voip_extension",
|
||||
field=models.CharField(blank=True, max_length=20, verbose_name="شماره داخلی کارشناس"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="mobile",
|
||||
field=models.CharField(blank=True, max_length=16, null=True, unique=True, verbose_name="شماره موبایل"),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,42 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def copy_staff_mobile_to_voip(apps, schema_editor):
|
||||
User = apps.get_model("accounts", "User")
|
||||
User.objects.filter(account_type="STAFF", voip_mobile="", mobile__isnull=False).exclude(mobile="").update(
|
||||
voip_mobile=models.F("mobile")
|
||||
)
|
||||
|
||||
|
||||
def clear_copied_voip_mobile(apps, schema_editor):
|
||||
User = apps.get_model("accounts", "User")
|
||||
User.objects.filter(account_type="STAFF", voip_mobile=models.F("mobile")).update(voip_mobile="")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [("accounts", "0006_user_voip_call_user_voip_extension_and_more")]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="voip_mobile",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="مستقل از شماره حساب مشتری است و میتواند تکراری باشد.",
|
||||
max_length=16,
|
||||
verbose_name="شماره موبایل کارشناس برای VOIP",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="user",
|
||||
name="mobile",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
max_length=16,
|
||||
null=True,
|
||||
unique=True,
|
||||
verbose_name="شماره موبایل حساب",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(copy_staff_mobile_to_voip, clear_copied_voip_mobile),
|
||||
]
|
||||
+13
-1
@@ -19,14 +19,26 @@ class User(PublicIdModel, AbstractUser):
|
||||
class AccountType(models.TextChoices):
|
||||
CUSTOMER = "CUSTOMER", "مشتری"
|
||||
STAFF = "STAFF", "کارشناس"
|
||||
mobile = models.CharField(max_length=16, unique=True, null=True, blank=True)
|
||||
mobile = models.CharField("شماره موبایل حساب", max_length=16, unique=True, null=True, blank=True)
|
||||
account_type = models.CharField(max_length=10, choices=AccountType.choices, default=AccountType.CUSTOMER)
|
||||
voip_extension = models.CharField("شماره داخلی کارشناس", max_length=20, blank=True)
|
||||
voip_mobile = models.CharField(
|
||||
"شماره موبایل کارشناس برای VOIP",
|
||||
max_length=16,
|
||||
blank=True,
|
||||
help_text="مستقل از شماره حساب مشتری است و میتواند تکراری باشد.",
|
||||
)
|
||||
voip_call = models.BooleanField("امکان تماس VOIP", default=True)
|
||||
def save(self, *args, **kwargs):
|
||||
if self.mobile: self.mobile = normalize_mobile(self.mobile)
|
||||
if self.voip_mobile: self.voip_mobile = normalize_mobile(self.voip_mobile)
|
||||
super().save(*args, **kwargs)
|
||||
@property
|
||||
def local_mobile(self):
|
||||
return f"0{self.mobile[3:]}" if self.mobile and self.mobile.startswith("+98") else (self.mobile or "")
|
||||
@property
|
||||
def voip_local_mobile(self):
|
||||
return f"0{self.voip_mobile[3:]}" if self.voip_mobile.startswith("+98") else self.voip_mobile
|
||||
|
||||
class OTPChallenge(models.Model):
|
||||
class Purpose(models.TextChoices): LOGIN = "LOGIN", "ورود"
|
||||
|
||||
+13
-1
@@ -1,6 +1,6 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import PageTitle, ProductionSeedState, SiteBranding
|
||||
from .models import PageTitle, ProductionSeedState, SiteBranding, VoipConfiguration
|
||||
|
||||
|
||||
@admin.register(SiteBranding)
|
||||
@@ -29,6 +29,18 @@ class SiteBrandingAdmin(admin.ModelAdmin):
|
||||
return False
|
||||
|
||||
|
||||
@admin.register(VoipConfiguration)
|
||||
class VoipConfigurationAdmin(admin.ModelAdmin):
|
||||
fields = ("voip_call_url", "updated_at")
|
||||
readonly_fields = ("updated_at",)
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return not VoipConfiguration.objects.exists()
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
|
||||
@admin.register(PageTitle)
|
||||
class PageTitleAdmin(admin.ModelAdmin):
|
||||
list_display = ("route_name", "title", "is_active", "updated_at")
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [("core", "0006_productionseedstate")]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="VoipConfiguration",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("voip_call_url", models.URLField(blank=True, help_text="نشانی کامل endpoint تماس، مانند http://127.0.0.1:8000/v1/click2call", verbose_name="VOIP_CALL_URL")),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={"verbose_name": "تنظیمات تماس VOIP", "verbose_name_plural": "تنظیمات تماس VOIP"},
|
||||
),
|
||||
]
|
||||
+32
-9
@@ -31,7 +31,20 @@ class PublicIdModel(models.Model):
|
||||
abstract = True
|
||||
|
||||
|
||||
class SiteBranding(models.Model):
|
||||
class SingletonConfiguration(models.Model):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.pk = 1
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
return cls.objects.filter(pk=1).first() or cls()
|
||||
|
||||
|
||||
class SiteBranding(SingletonConfiguration):
|
||||
"""Database-backed copy shared by customer, authentication and staff pages."""
|
||||
site_title = models.CharField(max_length=160, default="گفتوگو با واحد فروش")
|
||||
header_title = models.CharField(max_length=160, default="گفتوگو با واحد فروش")
|
||||
@@ -99,18 +112,28 @@ class SiteBranding(models.Model):
|
||||
verbose_name = "تنظیمات عنوان و برند"
|
||||
verbose_name_plural = "تنظیمات عنوان و برند"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.pk = 1
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
return cls.objects.filter(pk=1).first() or cls()
|
||||
|
||||
def __str__(self):
|
||||
return self.site_title
|
||||
|
||||
|
||||
class VoipConfiguration(SingletonConfiguration):
|
||||
"""Singleton configuration for the backend click-to-call service."""
|
||||
|
||||
voip_call_url = models.URLField(
|
||||
"VOIP_CALL_URL",
|
||||
blank=True,
|
||||
help_text="نشانی کامل endpoint تماس، مانند http://127.0.0.1:8000/v1/click2call",
|
||||
)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "تنظیمات تماس VOIP"
|
||||
verbose_name_plural = "تنظیمات تماس VOIP"
|
||||
|
||||
def __str__(self):
|
||||
return "تنظیمات تماس VOIP"
|
||||
|
||||
|
||||
class PageTitle(models.Model):
|
||||
"""Editable browser title for a named Django route."""
|
||||
route_name = models.CharField(max_length=160, unique=True, help_text="مانند accounts:login")
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from uuid import uuid4
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
|
||||
from apps.core.models import VoipConfiguration
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class VoipCallResult:
|
||||
success: bool
|
||||
error: str = ""
|
||||
|
||||
|
||||
def place_voip_call(*, agent, customer, method):
|
||||
configuration = VoipConfiguration.load()
|
||||
if not configuration.voip_call_url:
|
||||
return VoipCallResult(False, "VOIP_CALL_URL تنظیم نشده است.")
|
||||
|
||||
payload = {
|
||||
"method": method,
|
||||
"customer_number": customer.local_mobile,
|
||||
"metadata": {
|
||||
"agent_id": str(agent.public_id),
|
||||
"customer_id": str(customer.public_id),
|
||||
},
|
||||
}
|
||||
if method == "ip_phone" and agent.voip_extension:
|
||||
payload["agent_extension"] = agent.voip_extension
|
||||
key_prefix = "internal"
|
||||
elif method == "mobile" and agent.voip_local_mobile:
|
||||
payload["agent_mobile"] = agent.voip_local_mobile
|
||||
key_prefix = "mobile"
|
||||
else:
|
||||
return VoipCallResult(False, "روش تماس انتخابشده در دسترس نیست.")
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
configuration.voip_call_url,
|
||||
json=payload,
|
||||
headers={"Idempotency-Key": f"{key_prefix}-{uuid4().hex}"},
|
||||
timeout=settings.INTEGRATION_HTTP_TIMEOUT_SECONDS,
|
||||
)
|
||||
response.raise_for_status()
|
||||
if response.content:
|
||||
body = response.json()
|
||||
if isinstance(body, dict) and body.get("success") is False:
|
||||
return VoipCallResult(False, "سرویس تماس درخواست را نپذیرفت.")
|
||||
return VoipCallResult(True)
|
||||
except (requests.RequestException, ValueError):
|
||||
logger.exception("VOIP click-to-call failed")
|
||||
return VoipCallResult(False, "خطا در ارتباط با سرویس تماس.")
|
||||
@@ -1,4 +1,4 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
app_name="staff_tickets"
|
||||
urlpatterns=[path("",views.staff_list,name="list"),path("customers/",views.staff_customers,name="customers"),path("customers/search/",views.staff_customer_search,name="customer_search"),path("contacts/",views.staff_contacts,name="contacts"),path("contacts/new/",views.staff_contact_create,name="contact_create"),path("contacts/<uuid:public_id>/edit/",views.staff_contact_edit,name="contact_edit"),path("contact-groups/",views.staff_groups,name="groups"),path("contact-groups/<int:group_id>/",views.staff_groups,name="group_edit"),path("magic-links/",views.staff_magic_audit,name="magic_audit"),path("magic-links/<int:link_id>/revoke/",views.staff_magic_revoke,name="magic_revoke"),path("sms-templates/",views.staff_sms_templates,name="sms_templates"),path("sms-templates/<int:template_id>/",views.staff_sms_templates,name="sms_template_edit"),path("dispatches/",views.staff_dispatch_create,name="dispatch_create"),path("dispatches/<uuid:public_id>/",views.staff_dispatch_detail,name="dispatch_detail"),path("dispatches/<uuid:public_id>/retry/",views.staff_dispatch_retry,name="dispatch_retry"),path("dispatches/<uuid:public_id>/resolve/",views.staff_dispatch_resolve,name="dispatch_resolve"),path("experts/<uuid:public_id>/avatar/",views.expert_avatar,name="expert_avatar"),path("tickets/",views.staff_list,name="tickets"),path("tickets/new/",views.staff_create,name="create"),path("tickets/<uuid:public_id>/messages/older/",views.staff_messages,name="messages"),path("tickets/<uuid:public_id>/messages/poll/",views.staff_poll,name="poll"),path("tickets/<uuid:public_id>/sms/<int:job_id>/retry/",views.staff_sms_retry,name="sms_retry"),path("tickets/<uuid:public_id>/",views.staff_detail,name="detail"),path("tickets/<uuid:public_id>/claim/",views.staff_claim,name="claim"),path("tickets/<uuid:public_id>/reply/",views.staff_reply,name="reply"),path("tickets/<uuid:public_id>/note/",lambda r,public_id:views.staff_reply(r,public_id,note=True),name="note"),path("tickets/<uuid:public_id>/transfer/",views.staff_transfer,name="transfer"),path("tickets/<uuid:public_id>/share/",views.staff_share,name="share"),path("tickets/<uuid:public_id>/revoke/<uuid:user_id>/",views.staff_revoke,name="revoke"),path("tickets/<uuid:public_id>/status/",views.staff_status,name="status"),path("tickets/<uuid:public_id>/priority/",views.staff_priority,name="priority"),path("tickets/<uuid:public_id>/delete/",views.staff_delete,name="delete")]
|
||||
urlpatterns=[path("",views.staff_list,name="list"),path("customers/",views.staff_customers,name="customers"),path("customers/search/",views.staff_customer_search,name="customer_search"),path("contacts/",views.staff_contacts,name="contacts"),path("contacts/new/",views.staff_contact_create,name="contact_create"),path("contacts/<uuid:public_id>/edit/",views.staff_contact_edit,name="contact_edit"),path("contacts/<uuid:public_id>/call/",views.staff_contact_call,name="contact_call"),path("contact-groups/",views.staff_groups,name="groups"),path("contact-groups/<int:group_id>/",views.staff_groups,name="group_edit"),path("magic-links/",views.staff_magic_audit,name="magic_audit"),path("magic-links/<int:link_id>/revoke/",views.staff_magic_revoke,name="magic_revoke"),path("sms-templates/",views.staff_sms_templates,name="sms_templates"),path("sms-templates/<int:template_id>/",views.staff_sms_templates,name="sms_template_edit"),path("dispatches/",views.staff_dispatch_create,name="dispatch_create"),path("dispatches/<uuid:public_id>/",views.staff_dispatch_detail,name="dispatch_detail"),path("dispatches/<uuid:public_id>/retry/",views.staff_dispatch_retry,name="dispatch_retry"),path("dispatches/<uuid:public_id>/resolve/",views.staff_dispatch_resolve,name="dispatch_resolve"),path("experts/<uuid:public_id>/avatar/",views.expert_avatar,name="expert_avatar"),path("tickets/",views.staff_list,name="tickets"),path("tickets/new/",views.staff_create,name="create"),path("tickets/<uuid:public_id>/messages/older/",views.staff_messages,name="messages"),path("tickets/<uuid:public_id>/messages/poll/",views.staff_poll,name="poll"),path("tickets/<uuid:public_id>/sms/<int:job_id>/retry/",views.staff_sms_retry,name="sms_retry"),path("tickets/<uuid:public_id>/",views.staff_detail,name="detail"),path("tickets/<uuid:public_id>/claim/",views.staff_claim,name="claim"),path("tickets/<uuid:public_id>/reply/",views.staff_reply,name="reply"),path("tickets/<uuid:public_id>/note/",lambda r,public_id:views.staff_reply(r,public_id,note=True),name="note"),path("tickets/<uuid:public_id>/transfer/",views.staff_transfer,name="transfer"),path("tickets/<uuid:public_id>/share/",views.staff_share,name="share"),path("tickets/<uuid:public_id>/revoke/<uuid:user_id>/",views.staff_revoke,name="revoke"),path("tickets/<uuid:public_id>/status/",views.staff_status,name="status"),path("tickets/<uuid:public_id>/priority/",views.staff_priority,name="priority"),path("tickets/<uuid:public_id>/delete/",views.staff_delete,name="delete")]
|
||||
+36
-3
@@ -26,6 +26,7 @@ from apps.messaging.models import Message,MessageReadReceipt
|
||||
from apps.notifications.models import NotificationJob, SMSTemplate
|
||||
from apps.core.iran_locations import location_data
|
||||
from apps.core.persian_text import normalize_persian_text
|
||||
from apps.integrations.voip import place_voip_call
|
||||
from apps.staff.models import OrganizationalUnit, StaffProfile
|
||||
from .forms import ContactGroupForm,GroupDispatchForm,ReplyForm,ShareForm,StaffContactForm,StaffCreateTicketForm,StaffReplyForm,TicketForm,TransferForm,UnitSMSTemplateForm
|
||||
from .models import GroupDispatch,Ticket,TicketCategory,TicketReadState,TicketTag
|
||||
@@ -450,7 +451,7 @@ def staff_detail(request,public_id):
|
||||
group_dispatch = ticket.group_dispatch
|
||||
can_resolve_dispatch = bool(group_dispatch and DispatchPolicy.can_view(request.user, group_dispatch) and (request.user.has_perm("tickets.change_ticket") or TicketPolicy.has_unit_permission(request.user, group_dispatch.unit, "tickets.change_ticket")))
|
||||
group_open_count = group_dispatch.tickets.exclude(status__in=(Ticket.Status.RESOLVED, Ticket.Status.CLOSED)).count() if group_dispatch else 0
|
||||
return render(request,"tickets/staff_detail.html",{"ticket":ticket,"conversation":conversation,"has_older":has_older,"older_cursor":older_cursor,"form":StaffReplyForm(),"transfer_form":TransferForm(),"share_form":ShareForm(),"can_claim":TicketPolicy.can_claim(request.user,ticket),"can_transfer":TicketPolicy.can_transfer(request.user,ticket),"can_share":TicketPolicy.can_share(request.user,ticket),"can_delete":TicketPolicy.can_delete(request.user,ticket),"can_resolve_dispatch":can_resolve_dispatch,"group_open_count":group_open_count,"statuses":Ticket.Status.choices,"priorities":Ticket.Priority.choices,"grants":ticket.access_grants.filter(revoked_at__isnull=True).select_related("user__staff_profile"),"customer_history":history,"sms_jobs":sms_jobs,"viewer_user":request.user,"poll_version":conversation_version(ticket,latest),"poll_cursor":latest.public_id if latest else ""})
|
||||
return render(request,"tickets/staff_detail.html",{"ticket":ticket,"conversation":conversation,"has_older":has_older,"older_cursor":older_cursor,"form":StaffReplyForm(),"transfer_form":TransferForm(),"share_form":ShareForm(),"can_claim":TicketPolicy.can_claim(request.user,ticket),"can_transfer":TicketPolicy.can_transfer(request.user,ticket),"can_share":TicketPolicy.can_share(request.user,ticket),"can_delete":TicketPolicy.can_delete(request.user,ticket),"can_resolve_dispatch":can_resolve_dispatch,"group_open_count":group_open_count,"statuses":Ticket.Status.choices,"priorities":Ticket.Priority.choices,"grants":ticket.access_grants.filter(revoked_at__isnull=True).select_related("user__staff_profile"),"customer_history":history,"sms_jobs":sms_jobs,"viewer_user":request.user,"voip_call_methods":_voip_call_methods(request.user),"poll_version":conversation_version(ticket,latest),"poll_cursor":latest.public_id if latest else ""})
|
||||
@staff_required
|
||||
@require_POST
|
||||
def staff_sms_retry(request,public_id,job_id):
|
||||
@@ -750,6 +751,38 @@ def _contact_queryset(user):
|
||||
).distinct()
|
||||
|
||||
|
||||
def _voip_call_methods(user):
|
||||
if not user.voip_call:
|
||||
return ()
|
||||
methods = []
|
||||
if user.voip_extension:
|
||||
methods.append("ip_phone")
|
||||
if user.voip_mobile:
|
||||
methods.append("mobile")
|
||||
return tuple(methods)
|
||||
|
||||
|
||||
@staff_required
|
||||
@require_POST
|
||||
def staff_contact_call(request, public_id):
|
||||
profile = get_object_or_404(_contact_queryset(request.user), user__public_id=public_id)
|
||||
if not request.user.voip_call:
|
||||
raise PermissionDenied
|
||||
try:
|
||||
data = json.loads(request.body or "{}")
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({"success": False, "message": "درخواست تماس معتبر نیست."}, status=400)
|
||||
method = data.get("method")
|
||||
if method not in _voip_call_methods(request.user):
|
||||
return JsonResponse({"success": False, "message": "روش تماس در دسترس نیست."}, status=400)
|
||||
if not profile.user.mobile:
|
||||
return JsonResponse({"success": False, "message": "شماره مخاطب ثبت نشده است."}, status=400)
|
||||
result = place_voip_call(agent=request.user, customer=profile.user, method=method)
|
||||
if not result.success:
|
||||
return JsonResponse({"success": False, "message": "برقراری تماس ناموفق بود."}, status=502)
|
||||
return JsonResponse({"success": True, "message": "درخواست تماس ارسال شد."})
|
||||
|
||||
|
||||
def _has_any_unit_permission(user, permission):
|
||||
return user.is_superuser or any(
|
||||
TicketPolicy.has_unit_permission(user, membership.unit, permission)
|
||||
@@ -782,7 +815,7 @@ def staff_contacts(request):
|
||||
"carriers": base_profiles.filter(stakeholder_type=CustomerProfile.StakeholderType.CARRIER).count(),
|
||||
"needs_action": sum(item.waiting_staff_count for item in contacts),
|
||||
}
|
||||
context = {"contacts": contacts, "metrics": metrics, "groups": visible_groups.annotate(contact_count=Count("contacts", distinct=True)), "stakeholder_types": CustomerProfile.StakeholderType.choices, "provinces": [row["province"] for row in location_data()], "can_add_contact": _has_any_unit_permission(request.user, "customers.add_customerprofile"), "active_staff_section": "contacts", "active_tab_title": "دفترچه مخاطبان"}
|
||||
context = {"contacts": contacts, "metrics": metrics, "groups": visible_groups.annotate(contact_count=Count("contacts", distinct=True)), "stakeholder_types": CustomerProfile.StakeholderType.choices, "provinces": [row["province"] for row in location_data()], "can_add_contact": _has_any_unit_permission(request.user, "customers.add_customerprofile"), "voip_call_methods": _voip_call_methods(request.user), "active_staff_section": "contacts", "active_tab_title": "دفترچه مخاطبان"}
|
||||
return render(request, "tickets/staff_contacts.html", context)
|
||||
|
||||
|
||||
@@ -830,7 +863,7 @@ def staff_contact_edit(request, public_id):
|
||||
profile.groups.add(*form.cleaned_data["groups"])
|
||||
messages.success(request, "اطلاعات مخاطب بروزرسانی شد.")
|
||||
return redirect("staff_tickets:contacts")
|
||||
return render(request, "tickets/staff_entity_form.html", {"form": form, "active_staff_section": "contacts", "active_tab_title": "ویرایش مخاطب", "form_title": str(profile), "form_help": "عضویت همزمان مخاطب در چند گروه قابل مدیریت است."})
|
||||
return render(request, "tickets/staff_entity_form.html", {"form": form, "call_customer": profile.user, "voip_call_methods": _voip_call_methods(request.user), "active_staff_section": "contacts", "active_tab_title": "ویرایش مخاطب", "form_title": str(profile), "form_help": "عضویت همزمان مخاطب در چند گروه قابل مدیریت است."})
|
||||
|
||||
|
||||
@staff_required
|
||||
|
||||
Reference in new issue
Block a user