from django.db import models from django.contrib.auth.models import User class VisitQueue(models.Model): customer = models.ForeignKey('Customer', on_delete=models.CASCADE) dateTime = models.DateTimeField() statusList = [ ('waiting', 'waiting'), ('finish', 'finish'), ('cancell', 'cancell'), ] status = models.CharField(max_length=10, choices=statusList, default='waiting') user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) GradeList = [ ('3', '3'), ('2', '2'), ('1', '1'), ] grade = models.CharField(max_length=1, choices=GradeList) grade_desc = models.CharField(max_length=200) class Action(models.Model): """اقدام/وظیفهٔ CRM مرتبط با مشتری. ``actionType`` فقط این شش مقدار را می‌پذیرد؛ همین شش مقدار در فرم‌های پنل (``customerActionInc.html``) و در تمام مقایسه‌های کد (``== 'bazdid'``، ``exclude(actionType='vosol')`` و …) استفاده می‌شوند. ``status`` نیز همان سه مقدارِ به‌کاررفته در کد و قالب‌ها را می‌پذیرد. املای ``'waite'`` عمداً حفظ شده است: ۲٫۷۱۹ رکورد موجودِ دیتابیس و تمام مقایسه‌های کد/قالب (``== 'waite'``، ``if filter_status == 'waite'``) با همین املا کار می‌کنند و تغییرش داده را از موتور جست‌وجو جدا می‌کند. """ class ActionType(models.TextChoices): BAZDID = 'bazdid', 'بازدید' NASB = 'nasb', 'نصب' VOSOL = 'vosol', 'وصول مطالبات' HAMAHANGI = 'hamahangi', 'هماهنگی' IN_CALL = 'inCall', 'تماس ورودی' OUT_CALL = 'outCall', 'تماس خروجی' class Status(models.TextChoices): # املا عیناً مطابق داده و کد فعلی است (نه 'wait'). WAITE = 'waite', 'در انتظار (باز)' FINISH = 'finish', 'انجام شده' CANCELL = 'cancell', 'لغو شده' actionType = models.CharField( max_length=50, null=True, blank=True, choices=ActionType.choices, ) forUser = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) customer = models.ForeignKey('Customer', related_name='actionrlt', on_delete=models.CASCADE, null=True, blank=True) createdDT = models.DateTimeField(auto_now_add=True) changeDT = models.DateTimeField(auto_now=True) actionDateTime = models.DateTimeField(null=True, blank=True) finishDateTime = models.DateTimeField(null=True, blank=True) desc = models.CharField(max_length=500, null=True, blank=True) result = models.CharField(max_length=500, null=True, blank=True) status = models.CharField( max_length=50, null=True, blank=True, choices=Status.choices, ) nazar = models.CharField(max_length=50, null=True, blank=True, default=None) nextNazarDT = models.DateTimeField(null=True, blank=True, default=None) nazarDT = models.DateTimeField(null=True, blank=True, default=None) grade = models.IntegerField(null=True, blank=True, default=None) # --- حسابرسی «حذف» (soft-delete از طریق status='cancell') --- # چرا ستون جدا؟ چون ``cancell`` از قبل معنای «لغو برنامه» را دارد و در گزارش‌ها # هم استفاده می‌شود. با این دو ستون می‌توانیم «حذف توسط کاربر» را از «لغو # عادی» تفکیک کنیم، بدون آنکه منطق محاسبات (که فقط waite/finish را می‌بیند) # دست بخورد. deleted_at = models.DateTimeField( null=True, blank=True, default=None, verbose_name="زمان حذف", help_text="اگر پر باشد، این اقدام توسط کاربر حذف شده است (status=cancell).", ) deleted_by = models.ForeignKey( User, on_delete=models.SET_NULL, null=True, blank=True, related_name="deleted_actions", verbose_name="حذف‌کننده", ) @property def is_deleted(self): """آیا این اقدام با «حذف» کنار گذاشته شده است؟ (نه لغو عادی)""" return self.deleted_at is not None class Meta: # ایندکس **پوشا (covering)** برای محاسبهٔ «آخرین بازدید»: هر سه ستونِ لازم # برای فیلتر و تجمیع داخل خودِ ایندکس است، پس دیتابیس به جدول اصلی مراجعه # نمی‌کند. اندازه‌گیری روی دادهٔ واقعی: ۱۶۴ms → ۲۹ms. # ستون‌های actionType/status/customer_id برای فیلتر و group، و دو ستون # تاریخ برای محاسبهٔ «تاریخ مؤثر» در همان ایندکس. indexes = [ models.Index( fields=[ 'actionType', 'status', 'customer_id', 'actionDateTime', 'finishDateTime', ], name='act_visit_idx', ), ]