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 StaffMessageShareForm(TransferForm): note = forms.CharField(label="توضیح برای گیرنده", required=False, max_length=500, widget=forms.Textarea(attrs={"rows": 2})) def __init__(self, *args, actor, **kwargs): super().__init__(*args, **kwargs) from .sharing import share_recipients self.fields["expert"].label = "کارشناس گیرنده" self.fields["expert"].queryset = share_recipients(actor) class StaffTicketShareForm(StaffMessageShareForm): permission_level = forms.ChoiceField(label="سطح دسترسی", choices=TicketAccessGrant.Level.choices, initial=TicketAccessGrant.Level.VIEW_ONLY) 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=False, help_text="عضویت در گروه اختیاری است. فقط گروه‌های واحدهای قابل مدیریت شما تغییر می‌کنند.", widget=forms.CheckboxSelectMultiple(attrs={"data-contact-group-option": ""}), ) 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