fix customer view and edit

This commit is contained in:
javad committed 2026-09-29 10:30:54 +03:30
1 parent 81335b1439
commit d5c22a39dc
13 files changed
+329 -81

No files matched your search

+60 -46
View File
@@ -21,7 +21,7 @@ import re
from difflib import SequenceMatcher
from apps.accounts.models import TicketMagicLink, User, normalize_mobile
from apps.accounts.magic_access import get_ticket_magic_access
from apps.customers.models import ContactGroup, CustomerProfile
from apps.customers.models import ContactEditLog, ContactGroup, CustomerProfile
from apps.messaging.models import Message,MessageReadReceipt
from apps.notifications.models import AnnouncementJob, NotificationJob, SMSTemplate
from apps.notifications.services import schedule_immediate_announcement
@@ -376,32 +376,26 @@ def staff_create(request):
if created:
customer=User.objects.create(mobile=mobile,username=username,account_type=User.AccountType.CUSTOMER)
if created: customer.set_unusable_password(); customer.save(update_fields=["password"])
profile,_=CustomerProfile.objects.get_or_create(user=customer)
profile,profile_created=CustomerProfile.objects.get_or_create(user=customer)
profile = CustomerProfile.objects.select_related("user").select_for_update().get(pk=profile.pk)
before = _contact_snapshot(profile) if not profile_created else None
for field in ("first_name","last_name","organization_name","stakeholder_type","province","city"):
value=form.cleaned_data.get(field)
if value: setattr(profile,field,value)
profile.save()
if form.cleaned_data.get("groups"): profile.groups.add(*form.cleaned_data["groups"])
if before is not None: _log_contact_edit(profile, request.user, before)
ticket=create_ticket(customer=customer,created_by=request.user,unit=form.cleaned_data.get("unit"),category=form.cleaned_data["category"],subject=form.cleaned_data["subject"],province=form.cleaned_data["province"],city=form.cleaned_data["city"],body=form.cleaned_data["body"],files=request.FILES.getlist("files"))
messages.success(request,"تیکت برای مشتری ثبت و پیامک لینک امن در صف ارسال قرار گرفت.")
return redirect("staff_tickets:detail",public_id=ticket.public_id)
return render(request,"tickets/staff_create.html",{"form":form,"staff_unit":TicketPolicy.primary_unit(request.user),"active_tab_title":"ثبت تیکت برای مشتری"})
def _customer_rows(search="", user=None):
# A CUSTOMER account can temporarily exist without its one-to-one profile
# (legacy/imported or interrupted registrations). Such an account is not
# an address-book contact and would make the contacts template dereference
# a missing profile, especially for superusers whose scope includes all
# customers.
# Legacy/imported CUSTOMER accounts may lack a profile. Keep them searchable
# so staff can open the editor and complete their contact information.
qs=User.objects.filter(
account_type=User.AccountType.CUSTOMER,
customer_profile__isnull=False,
).select_related("customer_profile")
has_unit_membership = user is not None and user.unit_memberships.filter(is_active=True, unit__is_active=True).exists()
if user is not None and has_unit_membership and not (user.is_superuser or user.has_perm("tickets.view_all_tickets")):
visible_customer_ids = staff_ticket_rows(user).values_list("customer_id", flat=True)
contact_unit_ids = _unit_ids_with_permission(user, "customers.view_customerprofile")
qs = qs.filter(Q(pk__in=visible_customer_ids) | Q(customer_profile__groups__unit_id__in=contact_unit_ids)).distinct()
if search:
digits=search.translate(str.maketrans("۰۱۲۳۴۵۶۷۸۹٠١٢٣٤٥٦٧٨٩","01234567890123456789"))
mobile_terms={digits}
@@ -409,12 +403,13 @@ def _customer_rows(search="", user=None):
except ValueError: pass
mobile_query=Q()
for term in mobile_terms: mobile_query|=Q(mobile__icontains=term)
qs=qs.filter(mobile_query|Q(customer_profile__first_name__icontains=search)|Q(customer_profile__last_name__icontains=search)|Q(username__icontains=search))
qs=qs.filter(mobile_query|Q(customer_profile__first_name__icontains=search)|Q(customer_profile__last_name__icontains=search)|Q(first_name__icontains=search)|Q(last_name__icontains=search)|Q(username__icontains=search))
visible_tickets = Q(customer_tickets__pk__in=staff_ticket_rows(user).values("pk")) if user is not None else Q()
return qs.annotate(
ticket_count=Count("customer_tickets",distinct=True),
active_count=Count("customer_tickets",filter=~Q(customer_tickets__status__in=[Ticket.Status.RESOLVED,Ticket.Status.CLOSED]),distinct=True),
waiting_staff_count=Count("customer_tickets",filter=Q(customer_tickets__status__in=[Ticket.Status.NEW,Ticket.Status.WAITING_STAFF,Ticket.Status.REOPENED]),distinct=True),
resolved_count=Count("customer_tickets",filter=Q(customer_tickets__status__in=[Ticket.Status.RESOLVED,Ticket.Status.CLOSED]),distinct=True),
ticket_count=Count("customer_tickets",filter=visible_tickets,distinct=True),
active_count=Count("customer_tickets",filter=visible_tickets & ~Q(customer_tickets__status__in=[Ticket.Status.RESOLVED,Ticket.Status.CLOSED]),distinct=True),
waiting_staff_count=Count("customer_tickets",filter=visible_tickets & Q(customer_tickets__status__in=[Ticket.Status.NEW,Ticket.Status.WAITING_STAFF,Ticket.Status.REOPENED]),distinct=True),
resolved_count=Count("customer_tickets",filter=visible_tickets & Q(customer_tickets__status__in=[Ticket.Status.RESOLVED,Ticket.Status.CLOSED]),distinct=True),
).order_by("-active_count","-ticket_count","-date_joined")
@staff_required
@@ -769,20 +764,44 @@ def _visible_dispatches(user):
return GroupDispatch.objects.filter(visibility)
def _contact_queryset(user):
"""Contacts visible through a permitted group or a viewable ticket.
CONTACT_AUDIT_FIELDS = {
"mobile": "شماره همراه",
"first_name": "نام",
"last_name": "نام خانوادگی",
"organization_name": "نام شرکت / سازمان",
"stakeholder_type": "نوع مخاطب",
"province": "استان",
"city": "شهر",
}
A customer created by login or ticket intake may not have a contact group
yet. Keeping ticket-visible customers in this queryset makes the contact
list and its edit endpoint use the same visibility rule.
"""
visible_groups = ContactGroup.objects.filter(unit_id__in=_unit_ids_with_permission(user, "customers.view_customerprofile")).select_related("unit")
visible_customer_ids = staff_ticket_rows(user).values_list("customer_id", flat=True)
return CustomerProfile.objects.filter(
Q(groups__in=visible_groups) | Q(user_id__in=visible_customer_ids)
).select_related("user").prefetch_related(
Prefetch("groups", queryset=visible_groups, to_attr="visible_groups")
).distinct()
def _contact_snapshot(profile):
values = {key: profile.user.mobile if key == "mobile" else getattr(profile, key) for key in CONTACT_AUDIT_FIELDS}
values["groups"] = set(profile.groups.values_list("pk", flat=True))
return values
def _log_contact_edit(profile, actor, before):
after = _contact_snapshot(profile)
changes = [
{"field": label, "before": str(before[key]), "after": str(after[key])}
for key, label in CONTACT_AUDIT_FIELDS.items() if before[key] != after[key]
]
if before["groups"] != after["groups"]:
names = dict(ContactGroup.objects.filter(pk__in=before["groups"] | after["groups"]).values_list("pk", "name"))
changes.append({
"field": "گروه‌ها",
"before": "، ".join(sorted(names.get(pk, str(pk)) for pk in before["groups"])),
"after": "، ".join(sorted(names.get(pk, str(pk)) for pk in after["groups"])),
})
if changes:
ContactEditLog.objects.create(
contact=profile,
contact_public_id=profile.user.public_id,
actor=actor,
actor_name=(actor.get_full_name() or actor.username)[:150],
changes=changes,
)
def _voip_call_methods(user):
@@ -799,7 +818,7 @@ def _voip_call_methods(user):
@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)
contact = get_object_or_404(User, public_id=public_id, account_type=User.AccountType.CUSTOMER)
if not request.user.voip_call:
raise PermissionDenied
try:
@@ -809,9 +828,9 @@ def staff_contact_call(request, public_id):
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:
if not contact.mobile:
return JsonResponse({"success": False, "message": "شماره مخاطب ثبت نشده است."}, status=400)
result = place_voip_call(agent=request.user, customer=profile.user, method=method)
result = place_voip_call(agent=request.user, customer=contact, method=method)
if not result.success:
return JsonResponse({"success": False, "message": "برقراری تماس ناموفق بود."}, status=502)
return JsonResponse({"success": True, "message": "درخواست تماس ارسال شد."})
@@ -831,7 +850,6 @@ def staff_contacts(request):
group = request.GET.get("group", "")
province = request.GET.get("province", "").strip()
visible_unit_ids = _unit_ids_with_permission(request.user, "customers.view_customerprofile")
if not visible_unit_ids: raise PermissionDenied
visible_groups = ContactGroup.objects.filter(unit_id__in=visible_unit_ids).select_related("unit")
qs = _customer_rows(search, request.user).prefetch_related(Prefetch("customer_profile__groups", queryset=visible_groups, to_attr="visible_groups"))
if stakeholder in CustomerProfile.StakeholderType.values:
@@ -844,7 +862,7 @@ def staff_contacts(request):
base_profiles = CustomerProfile.objects.filter(user_id__in=[item.pk for item in contacts])
metrics = {
"total": len(contacts),
"customers": base_profiles.filter(stakeholder_type=CustomerProfile.StakeholderType.CUSTOMER).count(),
"customers": base_profiles.filter(stakeholder_type=CustomerProfile.StakeholderType.CUSTOMER).count() + sum(not hasattr(item, "customer_profile") for item in contacts),
"suppliers": base_profiles.filter(stakeholder_type=CustomerProfile.StakeholderType.SUPPLIER).count(),
"carriers": base_profiles.filter(stakeholder_type=CustomerProfile.StakeholderType.CARRIER).count(),
"needs_action": sum(item.waiting_staff_count for item in contacts),
@@ -875,19 +893,14 @@ def staff_contact_create(request):
@staff_required
@require_http_methods(["GET", "POST"])
def staff_contact_edit(request, public_id):
profile = get_object_or_404(_contact_queryset(request.user), user__public_id=public_id)
contact = get_object_or_404(User, public_id=public_id, account_type=User.AccountType.CUSTOMER)
profile, _ = CustomerProfile.objects.get_or_create(user=contact, defaults={"first_name": contact.first_name[:100], "last_name": contact.last_name[:100]})
manageable_unit_ids = _unit_ids_with_permission(request.user, "customers.change_customerprofile")
editable_through_group = profile.groups.filter(unit_id__in=manageable_unit_ids).exists()
editable_through_ticket = staff_ticket_rows(request.user).filter(
Q(assigned_unit_id__in=manageable_unit_ids)
| Q(assigned_unit__isnull=True, owning_unit_id__in=manageable_unit_ids),
customer_id=profile.user_id,
).exists()
if not (editable_through_group or editable_through_ticket):
raise PermissionDenied
form = StaffContactForm(request.POST or None, user=request.user, instance=profile, unit_ids=manageable_unit_ids)
if request.method == "POST" and form.is_valid():
with transaction.atomic():
profile = CustomerProfile.objects.select_related("user").select_for_update().get(pk=profile.pk)
before = _contact_snapshot(profile)
profile.user.mobile = form.cleaned_data["mobile"]; profile.user.save(update_fields=["mobile"])
for key in ("first_name", "last_name", "organization_name", "stakeholder_type", "province", "city"):
setattr(profile, key, form.cleaned_data[key])
@@ -895,9 +908,10 @@ def staff_contact_edit(request, public_id):
accessible = ContactGroup.objects.filter(unit_id__in=manageable_unit_ids)
profile.groups.remove(*accessible)
profile.groups.add(*form.cleaned_data["groups"])
_log_contact_edit(profile, request.user, before)
messages.success(request, "اطلاعات مخاطب بروزرسانی شد.")
return redirect("staff_tickets:contacts")
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": "عضویت همزمان مخاطب در چند گروه قابل مدیریت است."})
return render(request, "tickets/staff_entity_form.html", {"form": form, "contact_edit_logs": profile.edit_logs.select_related("actor")[:30], "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