first
This commit is contained in:
commit
fa9d154db9
260 files changed
+17643
No files matched your search
@@ -0,0 +1,246 @@
|
||||
from django import forms
|
||||
from django.db.models import Q
|
||||
from apps.staff.models import StaffProfile
|
||||
from .models import GroupDispatch,Ticket,TicketAccessGrant,TicketCategory,TicketTag
|
||||
from apps.customers.models import ContactGroup
|
||||
from apps.staff.models import OrganizationalUnit
|
||||
from apps.core.iran_locations import location_data, validate_location
|
||||
from apps.accounts.models import normalize_mobile
|
||||
from apps.accounts.models import User
|
||||
from apps.customers.models import CustomerProfile
|
||||
from apps.notifications.models import SMSTemplate
|
||||
from .policies import DispatchPolicy, TicketPolicy
|
||||
class MultipleFileInput(forms.ClearableFileInput): allow_multiple_selected=True
|
||||
class MultipleFileField(forms.FileField):
|
||||
def clean(self, data, initial=None):
|
||||
clean_one = super().clean
|
||||
return [clean_one(item, initial) for item in data] if isinstance(data, (list, tuple)) else ([clean_one(data, initial)] if data else [])
|
||||
class TicketForm(forms.Form):
|
||||
unit=forms.ModelChoiceField(label="واحد سازمانی",queryset=OrganizationalUnit.objects.none(),widget=forms.Select(attrs={"data-ticket-unit":""}))
|
||||
category=forms.ModelChoiceField(label="دستهبندی",queryset=TicketCategory.objects.none(),widget=forms.Select(attrs={"data-ticket-category":""})); subject=forms.CharField(label="موضوع",min_length=5,max_length=200,widget=forms.TextInput(attrs={"autocomplete":"off","data-ticket-subject":""})); province=forms.ChoiceField(label="استان",required=False,widget=forms.Select(attrs={"data-province-input":""})); city=forms.ChoiceField(label="شهر",required=False,widget=forms.Select(attrs={"data-city-input":""})); selected_expert=forms.ModelChoiceField(label="کارشناس دلخواه",required=False,queryset=StaffProfile.objects.none()); body=forms.CharField(label="شرح درخواست",required=False,widget=forms.Textarea); files=MultipleFileField(label="پیوستها",required=False,widget=MultipleFileInput)
|
||||
def __init__(self,*a,**kw):
|
||||
super().__init__(*a,**kw)
|
||||
units=OrganizationalUnit.objects.filter(is_active=True,accepts_external_tickets=True,show_in_customer_form=True).order_by("sort_order","name","pk")
|
||||
self.fields["unit"].queryset=units
|
||||
self.fields["unit"].required=units.exists()
|
||||
selected_unit=(self.data.get("unit") if self.is_bound else self.initial.get("unit"))
|
||||
if not selected_unit:
|
||||
selected_unit=units.values_list("pk",flat=True).first()
|
||||
self.initial["unit"]=selected_unit
|
||||
try: selected_unit_id=int(selected_unit)
|
||||
except (TypeError,ValueError): selected_unit_id=None
|
||||
self.fields["category"].queryset=TicketCategory.objects.filter(is_active=True,show_in_customer_form=True,unit_id=selected_unit_id).order_by("sort_order","title","pk")
|
||||
self.fields["selected_expert"].queryset=StaffProfile.objects.filter(is_active=True,is_customer_selectable=True,user__is_active=True).filter(Q(primary_unit_id=selected_unit_id)|Q(user__unit_memberships__unit_id=selected_unit_id,user__unit_memberships__is_active=True)).select_related("user").distinct()
|
||||
rows=location_data(); selected_province=(self.data.get("province") if self.is_bound else self.initial.get("province")) or ""
|
||||
cities=next((row["cities"] for row in rows if row["province"]==selected_province),[])
|
||||
self.fields["province"].choices=[("","انتخاب استان یا جستوجو...")]+[(row["province"],row["province"]) for row in rows]
|
||||
self.fields["city"].choices=[("","ابتدا استان را انتخاب کنید")]+[(value,value) for value in cities]
|
||||
submitted_city=self.data.get("city") if self.is_bound else ""
|
||||
if submitted_city and submitted_city not in cities: self.fields["city"].choices.append((submitted_city,submitted_city))
|
||||
def clean_selected_expert(self): return self.cleaned_data["selected_expert"].user if self.cleaned_data.get("selected_expert") else None
|
||||
def clean(self):
|
||||
data=super().clean()
|
||||
unit,category=data.get("unit"),data.get("category")
|
||||
if unit and category and category.unit_id != unit.pk: self.add_error("category","این دستهبندی متعلق به واحد انتخابشده نیست.")
|
||||
selected_expert=data.get("selected_expert")
|
||||
profile=getattr(selected_expert,"staff_profile",None) if selected_expert else None
|
||||
if unit and selected_expert and not ((profile and profile.primary_unit_id==unit.pk) or selected_expert.unit_memberships.filter(unit=unit,is_active=True).exists()): self.add_error("selected_expert","کارشناس انتخابشده عضو این واحد نیست.")
|
||||
province,city,error=validate_location(data.get("province"),data.get("city")); data["province"],data["city"]=province,city
|
||||
if error: self.add_error("city" if city else "province",error)
|
||||
if not data.get("body"," ").strip() and not self.files.getlist("files"): raise forms.ValidationError("متن یا پیوست الزامی است.")
|
||||
return data
|
||||
class ReplyForm(forms.Form):
|
||||
body=forms.CharField(label="متن",required=False,widget=forms.Textarea); files=MultipleFileField(label="پیوستها",required=False,widget=MultipleFileInput)
|
||||
def clean(self):
|
||||
data=super().clean()
|
||||
if not data.get("body","").strip() and not self.files.getlist("files"): raise forms.ValidationError("متن یا فایل الزامی است.")
|
||||
return data
|
||||
class StaffReplyForm(ReplyForm):
|
||||
mentioned_experts=forms.ModelMultipleChoiceField(label="اطلاع به کارشناسان",required=False,queryset=StaffProfile.objects.none(),widget=forms.SelectMultiple)
|
||||
def __init__(self,*a,**kw):
|
||||
super().__init__(*a,**kw); self.fields["mentioned_experts"].queryset=StaffProfile.objects.filter(is_active=True,user__is_active=True).select_related("user")
|
||||
def clean_mentioned_experts(self): return [profile.user for profile in self.cleaned_data["mentioned_experts"]]
|
||||
class TransferForm(forms.Form):
|
||||
expert=forms.ModelChoiceField(label="کارشناس جدید",queryset=StaffProfile.objects.none())
|
||||
def __init__(self,*a,**kw): super().__init__(*a,**kw); self.fields["expert"].queryset=StaffProfile.objects.filter(is_active=True,user__is_active=True).select_related("user")
|
||||
def clean_expert(self): return self.cleaned_data["expert"].user
|
||||
class ShareForm(TransferForm):
|
||||
permission_level=forms.ChoiceField(label="سطح دسترسی",choices=TicketAccessGrant.Level.choices)
|
||||
|
||||
class StaffCreateTicketForm(forms.Form):
|
||||
unit=forms.ModelChoiceField(label="واحد ایجادکننده",queryset=OrganizationalUnit.objects.none(),required=False,widget=forms.HiddenInput())
|
||||
customer_id=forms.UUIDField(required=False,widget=forms.HiddenInput(attrs={"data-customer-id":""}))
|
||||
customer_mobile=forms.CharField(label="شماره موبایل مشتری",max_length=16,widget=forms.TextInput(attrs={"dir":"ltr","inputmode":"tel"}))
|
||||
first_name=forms.CharField(label="نام مشتری",required=False,max_length=100); last_name=forms.CharField(label="نام خانوادگی مشتری",required=False,max_length=100)
|
||||
organization_name=forms.CharField(label="نام شرکت / سازمان",required=False,max_length=180)
|
||||
stakeholder_type=forms.ChoiceField(label="نوع مخاطب",choices=CustomerProfile.StakeholderType.choices,initial=CustomerProfile.StakeholderType.CUSTOMER,required=False)
|
||||
groups=forms.ModelMultipleChoiceField(label="گروههای مخاطب",queryset=ContactGroup.objects.none(),required=False,help_text="برای حفظ مخاطب در دفترچه واحد، حداقل یک گروه مناسب انتخاب کنید.")
|
||||
category=forms.ModelChoiceField(label="دستهبندی",queryset=TicketCategory.objects.none()); subject=forms.CharField(label="موضوع",min_length=5,max_length=200)
|
||||
province=forms.CharField(label="استان",required=False,max_length=100,widget=forms.TextInput(attrs={"list":"ticket-province-options","autocomplete":"off","data-province-input":""})); city=forms.CharField(label="شهر",required=False,max_length=100,widget=forms.TextInput(attrs={"list":"ticket-city-options","autocomplete":"off","data-city-input":""}))
|
||||
body=forms.CharField(label="شرح پیام کارشناس",widget=forms.Textarea); files=MultipleFileField(label="پیوستها",required=False,widget=MultipleFileInput)
|
||||
def __init__(self,*args,user=None,**kwargs):
|
||||
super().__init__(*args,**kwargs)
|
||||
self.user=user
|
||||
if user is None: return
|
||||
primary_unit=TicketPolicy.primary_unit(user)
|
||||
allowed = bool(primary_unit and primary_unit.is_active and (user.is_superuser or user.has_perm("tickets.add_ticket") or TicketPolicy.has_unit_permission(user,primary_unit,"tickets.add_ticket")))
|
||||
allowed_ids=[primary_unit.pk] if allowed else []
|
||||
self.fields["unit"].queryset=OrganizationalUnit.objects.filter(pk__in=allowed_ids)
|
||||
self.fields["unit"].initial=primary_unit.pk if allowed else None
|
||||
self.initial["unit"]=primary_unit.pk if allowed else None
|
||||
categories=TicketCategory.objects.filter(unit_id__in=allowed_ids,is_active=True)
|
||||
if not allowed_ids or not categories.exists():
|
||||
categories=TicketCategory.objects.filter(unit__isnull=True,is_active=True)
|
||||
self.fields["category"].queryset=categories.select_related("unit")
|
||||
self.fields["groups"].queryset=ContactGroup.objects.filter(unit_id__in=allowed_ids,is_active=True).select_related("unit")
|
||||
def clean_customer_mobile(self):
|
||||
try: mobile=normalize_mobile(self.cleaned_data["customer_mobile"])
|
||||
except ValueError as exc: raise forms.ValidationError(str(exc)) from exc
|
||||
existing=User.objects.filter(mobile=mobile).only("account_type").first()
|
||||
if existing and existing.account_type != User.AccountType.CUSTOMER:
|
||||
raise forms.ValidationError("این شماره متعلق به حساب کارشناسی است و نمیتواند بهعنوان مشتری انتخاب شود.")
|
||||
return mobile
|
||||
def clean(self):
|
||||
data=super().clean(); province,city,error=validate_location(data.get("province"),data.get("city")); data["province"],data["city"]=province,city
|
||||
if error: self.add_error("city" if city else "province",error)
|
||||
mobile=data.get("customer_mobile")
|
||||
selected=None
|
||||
if data.get("customer_id"):
|
||||
selected=User.objects.filter(public_id=data["customer_id"],account_type=User.AccountType.CUSTOMER).first()
|
||||
if not selected: self.add_error("customer_mobile","مشتری انتخابشده معتبر نیست؛ دوباره جستوجو کنید.")
|
||||
elif mobile and selected.mobile != mobile: self.add_error("customer_mobile","شماره موبایل با مشتری انتخابشده تطابق ندارد.")
|
||||
existing=User.objects.filter(mobile=mobile,account_type=User.AccountType.CUSTOMER).first() if mobile else None
|
||||
if selected and existing and selected.pk != existing.pk: self.add_error("customer_mobile","این شماره متعلق به مشتری دیگری است.")
|
||||
data["customer_user"]=selected or existing
|
||||
if not data["customer_user"]:
|
||||
if not (data.get("first_name") or "").strip(): self.add_error("first_name","برای مشتری جدید، نام را وارد کنید.")
|
||||
if not (data.get("last_name") or "").strip(): self.add_error("last_name","برای مشتری جدید، نام خانوادگی را وارد کنید.")
|
||||
unit=data.get("unit")
|
||||
category=data.get("category")
|
||||
primary_unit=TicketPolicy.primary_unit(self.user) if self.user else None
|
||||
if unit and (not primary_unit or unit.pk != primary_unit.pk): self.add_error("unit","واحد ایجادکننده باید واحد اصلی کاربر باشد.")
|
||||
if unit and category and category.unit_id and category.unit_id != unit.pk: self.add_error("category","این دستهبندی متعلق به واحد ایجادکننده نیست.")
|
||||
if self.user and self.fields["unit"].queryset.exists() and not unit: self.add_error("unit","واحد ایجادکننده مشخص نیست.")
|
||||
if unit and any(group.unit_id != unit.pk for group in data.get("groups",())): self.add_error("groups","همه گروهها باید متعلق به واحد ایجادکننده باشند.")
|
||||
return data
|
||||
|
||||
|
||||
class GroupDispatchForm(forms.Form):
|
||||
operation_type = forms.ChoiceField(label="نوع عملیات", choices=(), required=False)
|
||||
unit = forms.ModelChoiceField(label="واحد ارسالکننده", queryset=OrganizationalUnit.objects.none())
|
||||
contact_group = forms.ModelChoiceField(label="گروه مخاطبان", queryset=ContactGroup.objects.none())
|
||||
category = forms.ModelChoiceField(label="دستهبندی", queryset=TicketCategory.objects.none())
|
||||
subject = forms.CharField(label="موضوع استعلام یا پیام", min_length=5, max_length=200)
|
||||
body = forms.CharField(label="متن پیام", widget=forms.Textarea)
|
||||
files = MultipleFileField(label="پیوستهای مشترک", required=False, widget=MultipleFileInput, help_text="اسناد انتخابشده بهصورت مستقل داخل تیکت خصوصی هر مخاطب قرار میگیرند.")
|
||||
tags = forms.ModelMultipleChoiceField(label="برچسبها", queryset=TicketTag.objects.none(), required=False)
|
||||
|
||||
def __init__(self, *args, user, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.user = user
|
||||
candidate_units = OrganizationalUnit.objects.filter(pk__in=TicketPolicy.unit_ids(user), is_active=True)
|
||||
operation_types = [value for value, label in GroupDispatch.OperationType.choices if any(DispatchPolicy.can_create(user, unit, value) for unit in candidate_units) or user.is_superuser]
|
||||
self.fields["operation_type"].choices = [item for item in GroupDispatch.OperationType.choices if item[0] in operation_types]
|
||||
selected_type = self.data.get("operation_type") if self.is_bound else self.initial.get("operation_type")
|
||||
if selected_type not in operation_types:
|
||||
selected_type = operation_types[0] if operation_types else None
|
||||
if selected_type:
|
||||
self.initial["operation_type"] = selected_type
|
||||
unit_ids = [unit.pk for unit in candidate_units if selected_type and DispatchPolicy.can_create(user, unit, selected_type)]
|
||||
self.fields["unit"].queryset = OrganizationalUnit.objects.filter(pk__in=unit_ids)
|
||||
self.fields["contact_group"].queryset = ContactGroup.objects.filter(unit_id__in=unit_ids, is_active=True).select_related("unit")
|
||||
selected_unit=self.data.get("unit") if self.is_bound else self.initial.get("unit")
|
||||
categories=TicketCategory.objects.filter(is_active=True, unit_id__in=unit_ids)
|
||||
if selected_unit: categories=categories.filter(unit_id=selected_unit)
|
||||
if selected_unit and not categories.exists():
|
||||
categories=TicketCategory.objects.filter(is_active=True, unit__isnull=True)
|
||||
self.fields["category"].queryset=categories.select_related("unit")
|
||||
self.fields["tags"].queryset = TicketTag.objects.filter(is_active=True)
|
||||
|
||||
def clean(self):
|
||||
data = super().clean()
|
||||
if not data.get("operation_type") and GroupDispatch.OperationType.BULK in dict(self.fields["operation_type"].choices):
|
||||
data["operation_type"] = GroupDispatch.OperationType.BULK
|
||||
unit = data.get("unit")
|
||||
group = data.get("contact_group")
|
||||
if unit and group and group.unit_id != unit.pk:
|
||||
self.add_error("contact_group", "این گروه متعلق به واحد انتخابشده نیست.")
|
||||
category=data.get("category")
|
||||
if unit and category and category.unit_id and category.unit_id != unit.pk:
|
||||
self.add_error("category", "این دستهبندی متعلق به واحد انتخابشده نیست.")
|
||||
operation_type=data.get("operation_type")
|
||||
if unit and operation_type and not DispatchPolicy.can_create(self.user, unit, operation_type):
|
||||
self.add_error("operation_type", "اجازه ایجاد این نوع عملیات را ندارید.")
|
||||
return data
|
||||
|
||||
|
||||
class StaffContactForm(forms.Form):
|
||||
"""Create or update an external contact without giving it login credentials."""
|
||||
mobile = forms.CharField(label="شماره همراه", max_length=16, widget=forms.TextInput(attrs={"dir": "ltr", "inputmode": "tel"}))
|
||||
first_name = forms.CharField(label="نام", max_length=100)
|
||||
last_name = forms.CharField(label="نام خانوادگی", max_length=100, required=False)
|
||||
organization_name = forms.CharField(label="نام شرکت / سازمان", max_length=180, required=False)
|
||||
stakeholder_type = forms.ChoiceField(label="نوع مخاطب", choices=CustomerProfile.StakeholderType.choices)
|
||||
groups = forms.ModelMultipleChoiceField(label="عضویت در گروهها", queryset=ContactGroup.objects.none(), required=True, help_text="حداقل یک گروه برای تعیین مالکیت واحدی مخاطب انتخاب کنید.")
|
||||
province = forms.ChoiceField(label="استان", required=False)
|
||||
city = forms.ChoiceField(label="شهر", required=False)
|
||||
|
||||
def __init__(self, *args, user, instance=None, unit_ids=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.instance = instance
|
||||
rows = location_data()
|
||||
self.fields["province"].choices = [("", "انتخاب استان")] + [(row["province"], row["province"]) for row in rows]
|
||||
selected_province = (self.data.get("province") if self.is_bound else self.initial.get("province")) or getattr(instance, "province", "")
|
||||
selected_city = (self.data.get("city") if self.is_bound else self.initial.get("city")) or getattr(instance, "city", "")
|
||||
cities = next((row["cities"] for row in rows if row["province"] == selected_province), [])
|
||||
self.fields["city"].choices = [("", "ابتدا استان را انتخاب کنید")] + [(city, city) for city in cities]
|
||||
self.fields["province"].widget.attrs.update({"data-province-input": "", "autocomplete": "off"})
|
||||
self.fields["city"].widget.attrs.update({"data-city-input": "", "autocomplete": "off"})
|
||||
if selected_city and selected_city not in dict(self.fields["city"].choices):
|
||||
self.fields["city"].choices.append((selected_city, selected_city))
|
||||
unit_ids = set(unit_ids if unit_ids is not None else TicketPolicy.unit_ids(user))
|
||||
self.fields["groups"].queryset = ContactGroup.objects.filter(unit_id__in=unit_ids, is_active=True).select_related("unit")
|
||||
if instance and not self.is_bound:
|
||||
self.initial.update({"mobile": instance.user.mobile, "first_name": instance.first_name, "last_name": instance.last_name, "organization_name": instance.organization_name, "stakeholder_type": instance.stakeholder_type, "groups": instance.groups.filter(unit_id__in=unit_ids), "province": instance.province, "city": instance.city})
|
||||
|
||||
def clean_mobile(self):
|
||||
try:
|
||||
mobile = normalize_mobile(self.cleaned_data["mobile"])
|
||||
except ValueError as exc:
|
||||
raise forms.ValidationError(str(exc)) from exc
|
||||
current_user_id = getattr(getattr(self.instance, "user", None), "pk", None)
|
||||
if User.objects.filter(mobile=mobile).exclude(pk=current_user_id).exists():
|
||||
raise forms.ValidationError("این شماره همراه قبلاً ثبت شده است.")
|
||||
return mobile
|
||||
|
||||
def clean(self):
|
||||
data = super().clean()
|
||||
province, city, error = validate_location(data.get("province"), data.get("city"))
|
||||
data["province"], data["city"] = province, city
|
||||
if error:
|
||||
self.add_error("city" if city else "province", error)
|
||||
return data
|
||||
|
||||
|
||||
class ContactGroupForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = ContactGroup
|
||||
fields = ("unit", "name", "description", "is_active")
|
||||
labels = {"unit": "واحد مالک", "name": "نام گروه", "description": "توضیح", "is_active": "فعال"}
|
||||
|
||||
def __init__(self, *args, user, unit_ids=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["unit"].queryset = OrganizationalUnit.objects.filter(pk__in=unit_ids if unit_ids is not None else TicketPolicy.unit_ids(user), is_active=True)
|
||||
|
||||
|
||||
class UnitSMSTemplateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = SMSTemplate
|
||||
fields = ("unit", "event_type", "title", "body", "is_active")
|
||||
labels = {"unit": "واحد", "event_type": "رویداد", "title": "عنوان مدیریتی", "body": "متن پیامک", "is_active": "فعال"}
|
||||
widgets = {"body": forms.Textarea(attrs={"rows": 6})}
|
||||
|
||||
def __init__(self, *args, user, unit_ids=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["unit"].queryset = OrganizationalUnit.objects.filter(pk__in=unit_ids if unit_ids is not None else TicketPolicy.unit_ids(user), is_active=True)
|
||||
self.fields["unit"].required = True
|
||||
Reference in new issue
Block a user