first
This commit is contained in:
commit
fa9d154db9
260 files changed
+17643
No files matched your search
@@ -0,0 +1,140 @@
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.core.validators import MinLengthValidator
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from apps.core.models import PersianTextNormalizationMixin, PublicIdModel, TimeStampedModel
|
||||
|
||||
class TicketCategory(PersianTextNormalizationMixin, TimeStampedModel):
|
||||
persian_text_fields = {"title": False, "description": True, "search_keywords": True}
|
||||
unit=models.ForeignKey("staff.OrganizationalUnit",on_delete=models.PROTECT,null=True,blank=True,related_name="ticket_categories",verbose_name="واحد سازمانی",help_text="برای دستههای قدیمی عمومی میتواند خالی باشد؛ دستههای جدید باید واحد داشته باشند.")
|
||||
title=models.CharField(max_length=100); slug=models.SlugField(unique=True); description=models.TextField(blank=True); search_keywords=models.TextField(blank=True,verbose_name="کلیدواژههای جستجو",help_text="مترادفها و عبارتهای رایج مشتری را وارد کنید."); icon=models.CharField(max_length=50, blank=True); sort_order=models.PositiveIntegerField(default=0,db_index=True,verbose_name="ترتیب نمایش"); is_active=models.BooleanField(default=True); show_in_customer_form=models.BooleanField(default=True,verbose_name="نمایش در فرم مشتری",help_text="در صورت خاموش بودن، این دسته در انتخاب و جستجوی مشتری دیده نمیشود.")
|
||||
class Meta: ordering=["unit__sort_order", "unit__name", "sort_order", "title", "pk"]
|
||||
def __str__(self): return f"{self.unit.name} — {self.title}" if self.unit_id else self.title
|
||||
class SLAConfig(models.Model):
|
||||
priority=models.CharField(max_length=15, unique=True); first_response_minutes=models.PositiveIntegerField(); resolution_minutes=models.PositiveIntegerField(); is_active=models.BooleanField(default=True)
|
||||
|
||||
|
||||
class TicketTag(PersianTextNormalizationMixin, TimeStampedModel):
|
||||
persian_text_fields = {"name": False}
|
||||
name = models.CharField(max_length=80, unique=True)
|
||||
slug = models.SlugField(max_length=80, unique=True)
|
||||
color = models.CharField(max_length=7, default="#52677d")
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["name"]
|
||||
verbose_name = "برچسب تیکت"
|
||||
verbose_name_plural = "برچسبهای تیکت"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class GroupDispatch(PersianTextNormalizationMixin, PublicIdModel, TimeStampedModel):
|
||||
"""One bulk operation whose recipients each receive an isolated ticket."""
|
||||
|
||||
persian_text_fields = {"subject": False, "body": True}
|
||||
|
||||
class Status(models.TextChoices):
|
||||
DRAFT = "DRAFT", "پیشنویس"
|
||||
QUEUED = "QUEUED", "در صف ارسال"
|
||||
SENT = "SENT", "ارسالشده"
|
||||
PARTIAL = "PARTIAL", "ارسال ناقص"
|
||||
CANCELLED = "CANCELLED", "لغوشده"
|
||||
|
||||
class OperationType(models.TextChoices):
|
||||
BULK = "BULK", "ارسال گروهی تیکت"
|
||||
INQUIRY = "INQUIRY", "استعلام"
|
||||
|
||||
unit = models.ForeignKey(
|
||||
"staff.OrganizationalUnit",
|
||||
on_delete=models.PROTECT,
|
||||
related_name="group_dispatches",
|
||||
)
|
||||
operation_type = models.CharField(max_length=12, choices=OperationType.choices, default=OperationType.BULK, db_index=True)
|
||||
contact_group = models.ForeignKey(
|
||||
"customers.ContactGroup",
|
||||
on_delete=models.PROTECT,
|
||||
related_name="dispatches",
|
||||
)
|
||||
created_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.PROTECT,
|
||||
related_name="created_group_dispatches",
|
||||
)
|
||||
category = models.ForeignKey(TicketCategory, on_delete=models.PROTECT)
|
||||
subject = models.CharField(max_length=200, validators=[MinLengthValidator(5)])
|
||||
body = models.TextField()
|
||||
status = models.CharField(max_length=12, choices=Status.choices, default=Status.DRAFT, db_index=True)
|
||||
recipient_count = models.PositiveIntegerField(default=0)
|
||||
dispatched_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-created_at"]
|
||||
permissions = [
|
||||
("dispatch_group_tickets", "Can dispatch isolated tickets to a contact group"),
|
||||
("create_bulk_dispatch", "Can create bulk ticket dispatches"),
|
||||
("view_all_bulk_dispatches", "Can view all bulk ticket dispatches"),
|
||||
("create_inquiry", "Can create inquiries"),
|
||||
("view_all_inquiries", "Can view all inquiries"),
|
||||
]
|
||||
verbose_name = "ارسال گروهی"
|
||||
verbose_name_plural = "ارسالهای گروهی"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.subject} ({self.contact_group})"
|
||||
|
||||
|
||||
class Ticket(PersianTextNormalizationMixin, PublicIdModel, TimeStampedModel):
|
||||
persian_text_fields = {"subject": False, "province": False, "city": False}
|
||||
class Status(models.TextChoices): NEW="NEW","جدید"; OPEN="OPEN","باز"; WAITING_CUSTOMER="WAITING_CUSTOMER","در انتظار مشتری"; WAITING_STAFF="WAITING_STAFF","در انتظار کارشناس"; RESOLVED="RESOLVED","حلشده"; CLOSED="CLOSED","بسته"; REOPENED="REOPENED","بازگشاییشده"
|
||||
class Priority(models.TextChoices): NORMAL="NORMAL","عادی"; IMPORTANT="IMPORTANT","مهم"; URGENT="URGENT","فوری"
|
||||
class Visibility(models.TextChoices): PUBLIC="PUBLIC","عمومی"; SELECTED="SELECTED","کارشناس انتخابی"
|
||||
class DeliveryType(models.TextChoices):
|
||||
INDIVIDUAL = "INDIVIDUAL", "فردی"
|
||||
GROUP = "GROUP", "ارسال گروهی"
|
||||
ticket_number=models.CharField(max_length=30, unique=True, editable=False)
|
||||
customer=models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name="customer_tickets")
|
||||
created_by=models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.PROTECT,null=True,blank=True,related_name="created_tickets")
|
||||
owning_unit=models.ForeignKey("staff.OrganizationalUnit",on_delete=models.PROTECT,null=True,blank=True,related_name="owned_tickets")
|
||||
assigned_unit=models.ForeignKey("staff.OrganizationalUnit",on_delete=models.PROTECT,null=True,blank=True,related_name="assigned_tickets")
|
||||
delivery_type=models.CharField(max_length=12,choices=DeliveryType.choices,default=DeliveryType.INDIVIDUAL,db_index=True)
|
||||
group_dispatch=models.ForeignKey(GroupDispatch,on_delete=models.PROTECT,null=True,blank=True,related_name="tickets")
|
||||
tags=models.ManyToManyField(TicketTag,blank=True,related_name="tickets")
|
||||
category=models.ForeignKey(TicketCategory, on_delete=models.PROTECT, related_name="tickets")
|
||||
subject=models.CharField(max_length=200, validators=[MinLengthValidator(5)])
|
||||
province=models.CharField(max_length=100, blank=True)
|
||||
city=models.CharField(max_length=100, blank=True)
|
||||
status=models.CharField(max_length=25, choices=Status.choices, default=Status.NEW, db_index=True)
|
||||
priority=models.CharField(max_length=15, choices=Priority.choices, default=Priority.NORMAL, db_index=True)
|
||||
selected_expert=models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, null=True, blank=True, related_name="selected_tickets")
|
||||
owner=models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, null=True, blank=True, related_name="owned_tickets")
|
||||
origin_visibility=models.CharField(max_length=15, choices=Visibility.choices, default=Visibility.PUBLIC)
|
||||
last_activity_at=models.DateTimeField(db_index=True); first_response_at=models.DateTimeField(null=True, blank=True); last_customer_message_at=models.DateTimeField(null=True, blank=True); last_staff_message_at=models.DateTimeField(null=True, blank=True); first_response_due_at=models.DateTimeField(null=True, blank=True); resolution_due_at=models.DateTimeField(null=True, blank=True); resolved_at=models.DateTimeField(null=True, blank=True); closed_at=models.DateTimeField(null=True, blank=True); reopened_at=models.DateTimeField(null=True, blank=True)
|
||||
class Meta:
|
||||
ordering=["-last_activity_at"]
|
||||
permissions=[("view_all_tickets","Can view all tickets"),("claim_ticket","Can claim ticket"),("transfer_ticket","Can transfer ticket"),("share_ticket","Can share ticket"),("change_ticket_priority","Can change ticket priority"),("view_customer_history","Can view customer history")]
|
||||
indexes=[models.Index(fields=["status","last_activity_at"]), models.Index(fields=["owner","status"]), models.Index(fields=["selected_expert","status"]), models.Index(fields=["owning_unit","status"]), models.Index(fields=["delivery_type","last_activity_at"])]
|
||||
def save(self,*args,**kwargs):
|
||||
if not self.ticket_number:
|
||||
from django.utils import timezone
|
||||
self.ticket_number=f"TK-{timezone.localdate().year}-{uuid.uuid4().hex[:8].upper()}"
|
||||
super().save(*args,**kwargs)
|
||||
def __str__(self): return f"{self.ticket_number} - {self.subject}"
|
||||
@property
|
||||
def assigned_expert_name(self):
|
||||
expert=self.owner or self.selected_expert
|
||||
profile=getattr(expert,"staff_profile",None) if expert else None
|
||||
return getattr(profile,"display_name","") or "صف عمومی"
|
||||
class TicketAccessGrant(models.Model):
|
||||
class Level(models.TextChoices): VIEW_ONLY="VIEW_ONLY","فقط مشاهده"; REPLY="REPLY","پاسخ"; FULL="FULL","کامل"
|
||||
ticket=models.ForeignKey(Ticket,on_delete=models.CASCADE,related_name="access_grants"); user=models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.CASCADE,related_name="ticket_grants"); granted_by=models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.PROTECT,related_name="granted_ticket_access"); permission_level=models.CharField(max_length=15,choices=Level.choices,default=Level.FULL); created_at=models.DateTimeField(auto_now_add=True); revoked_at=models.DateTimeField(null=True,blank=True)
|
||||
class Meta:
|
||||
constraints=[models.UniqueConstraint(fields=["ticket","user"],condition=Q(revoked_at__isnull=True),name="unique_active_ticket_grant")]
|
||||
class TicketEvent(models.Model):
|
||||
class Type(models.TextChoices): CREATED="CREATED","ایجاد"; CLAIMED="CLAIMED","برداشت"; TRANSFERRED="TRANSFERRED","انتقال"; SHARED="SHARED","اشتراک"; ACCESS_REVOKED="ACCESS_REVOKED","لغو دسترسی"; STATUS_CHANGED="STATUS_CHANGED","تغییر وضعیت"; PRIORITY_CHANGED="PRIORITY_CHANGED","تغییر اولویت"; REOPENED="REOPENED","بازگشایی"; RESOLVED="RESOLVED","حل"; CLOSED="CLOSED","بستن"
|
||||
ticket=models.ForeignKey(Ticket,on_delete=models.CASCADE,related_name="events"); event_type=models.CharField(max_length=30,choices=Type.choices); actor=models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.PROTECT,null=True); metadata=models.JSONField(default=dict,blank=True); created_at=models.DateTimeField(auto_now_add=True)
|
||||
class TicketReadState(models.Model):
|
||||
ticket=models.ForeignKey(Ticket,on_delete=models.CASCADE,related_name="read_states"); user=models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.CASCADE); last_read_at=models.DateTimeField(); last_read_message=models.ForeignKey("messaging.Message",on_delete=models.SET_NULL,null=True,blank=True)
|
||||
class Meta: constraints=[models.UniqueConstraint(fields=["ticket","user"],name="unique_ticket_read_state")]
|
||||
Reference in new issue
Block a user