+
{{ message }}
diff --git a/admin_panel/views/actions.py b/admin_panel/views/actions.py
index e5e5446..d734b42 100644
--- a/admin_panel/views/actions.py
+++ b/admin_panel/views/actions.py
@@ -8,6 +8,7 @@ from django.contrib import messages
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
from django.utils import timezone
+from persiantools.jdatetime import JalaliDate
from ..models import Action, Customer
from ..sms.triggers import notify_visit_scheduled
@@ -91,6 +92,8 @@ def _action_payload_from_request(request):
'finishDateTime': _parse_form_datetime(request.POST.get('FinishDateTime')),
# فقط برای «هماهنگی» معنا دارد؛ تاریخِ «تا کِی پیگیری نشود».
'holdRaw': _parse_form_datetime(request.POST.get('visitHoldUntil')),
+ # تأیید صریح کاربر برای «عقب بردن» تاریخ قرار دستی.
+ 'force_hold': bool(request.POST.get('force_hold')),
'desc': request.POST.get('desc', None),
'result': request.POST.get('result', None),
'userID': request.POST.get('userID', None),
@@ -98,13 +101,13 @@ def _action_payload_from_request(request):
}
-def _apply_visit_hold(customer_obj, payload, *, status, is_new):
+def _apply_visit_hold(customer_obj, payload, *, status, is_new, force_hold=False):
"""همراستاکردن «قرار بعدی» با اقدام ثبت/ویرایششده.
قواعد:
* برای اقدام نوع «هماهنگی»:
- - تاریخ قرار داده شده ⇒ ``set_visit_hold`` (فقط رو به جلو)
+ - تاریخ قرار داده شده ⇒ ``set_visit_hold`` (فقط رو به جلو، مگر تأیید کاربر)
- «انجام شد» تیک خورده و تاریخ خالی ⇒ پیشفرض ۱۴ روز
- لغو شده ⇒ (در ایجاد) پاک میشود
* برای **هر اقدام دیگری** (از جمله «بازدید»):
@@ -114,18 +117,77 @@ def _apply_visit_hold(customer_obj, payload, *, status, is_new):
هماهنگیِ *جدیدتر* آمده باشد؛ پس دست نمیزنیم. اعتبارسنجی
:func:`manual_holds_map` (شرط «آخرین فعالیت = هماهنگی») خودش تصمیم
میگیرد. بدون این تفکیک، ویرایش یک اقدام قدیمی قرار معتبر را پاک میکرد.
+
+ خروجی (دیکشنری، برای بازخورد شفاف به کاربر)::
+
+ {'ok': bool, 'reason': 'written'|'default'|'forward_only'|'cleared'|'',
+ 'current': datetime|None, 'target': datetime|None}
+
+ ``reason='forward_only'`` یعنی تاریخ درخواستی از قرار فعلی عقبتر است و
+ نوشته **نشد**؛ لایهٔ ویو باید همین را با تاریخها به کاربر نشان بدهد و اگر
+ خودش تأیید کرد، با ``force_hold=True`` دوباره صدا بزند.
"""
+ result = {'ok': True, 'reason': '', 'current': None, 'target': None,
+ 'force_allowed': not is_new}
+ # «عقببردن قرار» فقط در ویرایش مجاز است. در ایجادِ وظیفهٔ تازه، کاربر قصد
+ # اصلاح قرارِ موجود را ندارد و اجازه دادن به آن میتواند قولِ قبلیِ ثبتشده
+ # (شاید توسط کارشناس دیگر) را بیآنکه کسی بخواهد کوتاه کند.
+ force_hold = bool(force_hold) and not is_new
is_coordination = payload['actionType'] == Action.ActionType.HAMAHANGI
if not is_coordination or status == Action.Status.CANCELL:
- return clear_visit_hold(customer_obj) if is_new else True
+ if is_new:
+ result['ok'] = bool(clear_visit_hold(customer_obj))
+ result['reason'] = 'cleared' if result['ok'] else ''
+ return result
raw = payload.get('holdRaw')
if raw is not None:
- set_visit_hold(customer_obj, normalize_hold_until(raw))
- return True
- if payload['status'] == Action.Status.FINISH:
- set_visit_hold(customer_obj, default_hold_until())
- return True
+ target = normalize_hold_until(raw)
+ reason = 'written'
+ elif payload['status'] == Action.Status.FINISH:
+ target = default_hold_until()
+ reason = 'default'
+ else:
+ return result
+
+ current = as_aware(customer_obj.visit_hold_until)
+ result['current'] = current
+ result['target'] = target
+ written = set_visit_hold(customer_obj, target, force=force_hold)
+ result['ok'] = written
+ result['reason'] = reason if written else 'forward_only'
+ return result
+
+
+def _jalali_day(value):
+ """تاریخ شمسی روز (به وقت محلی) برای پیامهای کاربر."""
+ if value is None:
+ return '—'
+ return JalaliDate(timezone.localtime(value)).strftime('%Y/%m/%d')
+
+
+def _report_hold_result(request, result):
+ """بازخورد شفافِ نتیجهٔ نوشتن «تا تاریخ پیگیری نشود» به کاربر.
+
+ قاعدهٔ «فقط رو به جلو» بیصدا نیست: اگر تاریخ درخواستی از قرار فعلی عقبتر
+ باشد، اینجا گفته میشود که ذخیره نشد و کاربر چطور میتواند خودش انجامش دهد.
+ """
+ if result.get('reason') != 'forward_only':
+ return
+ current, target = result.get('current'), result.get('target')
+ tz = timezone.get_current_timezone()
+ if current is not None and target is not None \
+ and target.astimezone(tz).date() >= current.astimezone(tz).date():
+ # همان روز ⇒ تغییر واقعیای لازم نیست؛ پیام دادن گمراهکننده است.
+ return
+ tail = ('برای ثبت، گزینهٔ «تاریخ قرار را عقب ببر» را تیک بزنید.'
+ if result.get('force_allowed')
+ else 'برای تغییر قرار، همان وظیفهٔ هماهنگی را ویرایش کنید.')
+ messages.warning(
+ request,
+ 'تاریخ قرار ذخیره نشد؛ از قرار فعلی این مشتری (%s) عقبتر است. %s'
+ % (_jalali_day(current), tail)
+ )
def _reject(request, error, redirect_to):
@@ -164,7 +226,11 @@ def customerNewAction(request ,c_id):
# last_bazdid_dt فقط از تابع واحد بازمحاسبه میشود؛ نه از finishDateTime.
refresh_last_bazdid(customer_obj, reason=REASON_CREATE, user=request.user)
# «قرار بعدی» فقط برای هماهنگی.
- _apply_visit_hold(customer_obj, payload, status=newAction.status, is_new=True)
+ hold_result = _apply_visit_hold(
+ customer_obj, payload, status=newAction.status, is_new=True,
+ force_hold=payload.get('force_hold', False),
+ )
+ _report_hold_result(request, hold_result)
# پیامک «بازدید شما برای تاریخ … ثبت شد» فقط برای اقدام از نوع بازدید.
if newAction.actionType == Action.ActionType.BAZDID:
@@ -225,7 +291,11 @@ def customerEditAction(request ,c_id, action_id):
# «قرار بعدی»: با ویرایش همراستا میشود. برای اقدام غیرهماهنگی عمداً
# پاک نمیکنیم؛ رکورد ویرایششده ممکن است قدیمیتر از یک هماهنگیِ جدیدتر
# باشد و پاککردن، قرار معتبر را از بین ببرد.
- _apply_visit_hold(customer_obj, payload, status=newAction.status, is_new=False)
+ hold_result = _apply_visit_hold(
+ customer_obj, payload, status=newAction.status, is_new=False,
+ force_hold=payload.get('force_hold', False),
+ )
+ _report_hold_result(request, hold_result)
#return redirect('/panel/customer/'+str(c_id)+'/action')
return redirect(request.META.get('HTTP_REFERER', 'redirect_if_referer_not_found'))
@@ -272,14 +342,35 @@ def ActionListAll(request):
data = {'actions': actions, 'allUsers': allUsers}
return render(request, 'admin_panel/ActionList.html', data)
+def _local_day_range(day=None):
+ """بازهٔ ``[آغاز روز، پایان روز]`` به وقت محلی و aware.
+
+ قبلاً این دو ویو رشتههای naive میساختند (``'%Y-%m-%d 00:00:00'``) و Django
+ آنها را با منطقهٔ زمانی پیشفرض تفسیر میکرد؛ هر تغییر ساعت/تایمزون باعث
+ جابهجایی مرز «وظایف امروز» میشد. حالا مرز صریح و aware است.
+
+ در روزهای تغییر ساعت (مثلاً ۲۲ مارس در تهران) نیمهشب ممکن است وجود نداشته
+ باشد؛ آن حالت با ``is_dst=False`` پوشش داده میشود تا یک تاریخ مرزی، صفحهٔ
+ وظایف را نشکند (همان کاری که ``visit_policy.as_aware`` میکند).
+ """
+ day = day or timezone.localdate()
+ bounds = []
+ for moment in (datetime.datetime.combine(day, datetime.time.min),
+ datetime.datetime.combine(day, datetime.time.max)):
+ try:
+ bounds.append(timezone.make_aware(moment))
+ except Exception:
+ bounds.append(timezone.make_aware(moment, is_dst=False))
+ return bounds[0], bounds[1]
+
+
@login_required(login_url='/panel/p_login/')
def ActionListToday(request):
- startTodayDT= datetime.datetime.today().strftime('%Y-%m-%d 00:00:00')
- endTodayDT= datetime.datetime.today().strftime('%Y-%m-%d 23:59:59')
- #startTodayDT = datetime.datetime.strptime(startTodayDT, '%Y-%m-%d-00:00:00')
- #endTodayDT = datetime.datetime.strptime(endTodayDT, '%Y-%m-%d-23:59:59')
- #return HttpResponse(todayDT)
- actions = Action.objects.filter(actionDateTime__gte=startTodayDT,actionDateTime__lte=endTodayDT, forUser__id=request.user.id, status='waite').order_by("-actionDateTime")[:50]
+ startTodayDT, endTodayDT = _local_day_range()
+ actions = Action.objects.filter(
+ actionDateTime__range=(startTodayDT, endTodayDT),
+ forUser__id=request.user.id, status='waite',
+ ).order_by("-actionDateTime")[:50]
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
data = {'actions': actions, 'allUsers': allUsers}
return render(request, 'admin_panel/ActionList.html', data)
@@ -290,22 +381,19 @@ def ActionListUserToday(request, u_id):
status = request.GET.get('status', None)
unixADT = dateTimeAction
if dateTimeAction:
- dateTimeAction = datetime.datetime.fromtimestamp(int(dateTimeAction)/1e3)
- startTodayDT = dateTimeAction.strftime('%Y-%m-%d 00:00:00')
- endTodayDT= dateTimeAction.strftime('%Y-%m-%d 23:59:59')
+ # timestamp میلیثانیهای GET → تاریخ محلی (نه naive UTC).
+ moment = as_aware(datetime.datetime.fromtimestamp(int(dateTimeAction) / 1e3))
+ day = moment.astimezone(timezone.get_current_timezone()).date()
else:
- startTodayDT= datetime.datetime.today().strftime('%Y-%m-%d 00:00:00')
- endTodayDT= datetime.datetime.today().strftime('%Y-%m-%d 23:59:59')
- #startTodayDT = datetime.datetime.strptime(startTodayDT, '%Y-%m-%d-00:00:00')
- #endTodayDT = datetime.datetime.strptime(endTodayDT, '%Y-%m-%d-23:59:59')
- #return HttpResponse(todayDT)
+ day = timezone.localdate()
+ startTodayDT, endTodayDT = _local_day_range(day)
if status == 'waite' or status == None or status == '' or status == ' ':
status = 'waite'
- actions = Action.objects.filter(actionDateTime__gte=startTodayDT,actionDateTime__lte=endTodayDT, forUser__id=u_id, status=status).order_by("-actionDateTime")[:50]
+ actions = Action.objects.filter(actionDateTime__range=(startTodayDT, endTodayDT), forUser__id=u_id, status=status).order_by("-actionDateTime")[:50]
actionPr = Action.objects.filter(actionDateTime__lt=startTodayDT, forUser__id=u_id, status=status).order_by("-actionDateTime")[:200]
elif status == 'finish':
actionPr = None
- actions = Action.objects.filter(actionDateTime__gte=startTodayDT,actionDateTime__lte=endTodayDT, forUser__id=u_id, status=status).order_by("-actionDateTime")[:50]
+ actions = Action.objects.filter(actionDateTime__range=(startTodayDT, endTodayDT), forUser__id=u_id, status=status).order_by("-actionDateTime")[:50]
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
data = {'actions': actions, 'allUsers': allUsers, 'u_id': u_id, "unixADT": unixADT, "status": status, 'actionPr': actionPr}
diff --git a/admin_panel/views/customers.py b/admin_panel/views/customers.py
index dbe6d2f..981bb1c 100644
--- a/admin_panel/views/customers.py
+++ b/admin_panel/views/customers.py
@@ -628,7 +628,8 @@ def entezar(request):
entry = sources.get(cid) or {}
if not entry:
return 0
- if len(entry) > 1:
+ days = {value.astimezone(current_tz).date() for value in entry.values()}
+ if len(days) > 1:
return 4
if SOURCE_MANUAL in entry:
return 1
@@ -699,8 +700,10 @@ def entezar(request):
try:
entezar_bazdid = CacheTable.objects.get(key='9mahbazdid')
# کارت داشبورد = «کارِ فوری» = تعیین تکلیف نشده.
- entezar_bazdid.value = counts['uncoordinated']
- entezar_bazdid.save()
+ # فقط در صورت تغییر مینویسیم تا یک GET ساده بیدلیل دیتابیس را تغییر ندهد.
+ if str(entezar_bazdid.value) != str(counts['uncoordinated']):
+ entezar_bazdid.value = str(counts['uncoordinated'])
+ entezar_bazdid.save(update_fields=['value'])
except CacheTable.DoesNotExist:
pass
@@ -812,14 +815,20 @@ def entezar(request):
entry.update(followup_entry)
flags = [key for key in (SOURCE_MANUAL, SOURCE_SCHEDULED,
SOURCE_COORDINATION) if key in entry]
+ # «N قرار» = تعداد **روزهای یکتای** قرار، نه تعداد منابع. یک هماهنگی
+ # که تاریخ تعویقش همان روز است، همزمان «manual» و «coordination»
+ # تولید میکند ولی دو قرارِ متفاوت نیست (برچسب «۲ قرار» گمراهکننده بود).
+ appointment_days = {
+ entry[key].astimezone(current_tz).date() for key in flags
+ }
appointment_rows[customer.id] = {
'manual': entry.get(SOURCE_MANUAL),
'scheduled': entry.get(SOURCE_SCHEDULED),
'coordination': entry.get(SOURCE_COORDINATION),
'overdue_days': overdue_days.get(customer.id, 0),
'last_visit': visits.get(customer.id),
- # تعداد منابع (۰ تا ۳) — قالب بر اساس آن برچسب وضعیت را میسازد.
- 'source_count': len(flags),
+ # تعداد قرارهای یکتا (۱ تا ۳ روز متمایز) — قالب بر اساس آن برچسب میسازد.
+ 'source_count': len(appointment_days),
# تاریخ پیگیری این نمای خاص (برای ستون اختصاصی نمای «امروز»).
'followup_at': (min(followup_entry.values())
if followup_entry else None),
diff --git a/admin_panel/visit_policy.py b/admin_panel/visit_policy.py
index fa921f3..e825861 100644
--- a/admin_panel/visit_policy.py
+++ b/admin_panel/visit_policy.py
@@ -337,6 +337,14 @@ SOURCE_MANUAL = "manual"
SOURCE_SCHEDULED = "scheduled"
SOURCE_COORDINATION = "coordination"
+# اقدامهایی که **واقعاً** یک «قرار بعدی» را جایگزین میکنند.
+# چرا فقط اینها؟ قرار دستی یعنی «تا این تاریخ تماس نگیر». تنها چیزی که این
+# قرار را منتفی میکند، خودِ بازدیدی است که بهجای تماس انجام شده (یا نصب که
+# چرخه را از نو شروع میکند). یک سند وصول، یک تماس ورودی یا خروجی، یا هر
+# اقدام دیگر، قرار را باطل نمیکند؛ در غیر اینصورت قرارِ ثبتشده بیصدا میمرد
+# و مشتری دوباره در فهرست تماس ظاهر میشد.
+_SUPERSEDING_TYPES = (Action.ActionType.BAZDID, Action.ActionType.NASB)
+
def _future_actions(action_type, statuses, *, now_utc, whole_day=False):
"""اقدامهای آیندهٔ یک نوع با تاریخ مؤثر (تاریخ اقدام، وگرنه تاریخ اتمام).
@@ -409,10 +417,13 @@ def _valid_manual_holds(holds):
قرارهایی که معتبرند:
۱. یک اقدام «هماهنگی» فعال (``waite``/``finish``) برای مشتری وجود دارد،
- ۲. و **آخرین فعالیتِ مشتری همان هماهنگی است** — اگر پس از هماهنگی، اقدام
- دیگری (بهویژه «بازدید») ثبت شده باشد، قرار باطل است.
+ ۲. و **آخرین فعالیتِ جایگزینکنندهٔ قرار** همان هماهنگی است — یعنی پس از
+ هماهنگی، بازدید (``bazdid``) یا نصب (``nasb``) ثبت نشده باشد.
بند ۲ همان چیزی است که «ثبت بازدید پس از هماهنگی» را خودکار بیاثر میکند.
+ ولی فقط اقدامهای جایگزینکننده (``_SUPERSEDING_TYPES``) قرار را باطل
+ میکنند: سند وصول، تماس ورودی/خروجی و بقیهٔ اقدامها قرار را نمیکشند —
+ وگرنه یک پرداخت ساده، قرارِ توافقشده را بیصدا از بین میبرد.
این تابع جدا شده تا هم :func:`manual_holds_map` (قرارهای آینده) و هم
:func:`followups_on_date` (قرارهای یک روز مشخص، حتی گذشته) از **یک قاعدهٔ
@@ -436,10 +447,12 @@ def _valid_manual_holds(holds):
.annotate(last=Max("createdDT"))
.values_list("customer_id", "last")
)
- # آخرین فعالیتِ غیرهماهنگی (بازدید، نصب، تماس، …) برای هر مشتری.
+ # آخرین فعالیتِ **جایگزینکنندهٔ قرار** (بازدید/نصب) برای هر مشتری.
+ # عمداً فقط این دو نوع: یک سند وصول یا تماس ورودی نباید قرار را باطل کند.
other_latest = dict(
- Action.objects.filter(customer_id__in=customer_ids)
- .exclude(actionType=Action.ActionType.HAMAHANGI)
+ Action.objects.filter(
+ customer_id__in=customer_ids, actionType__in=_SUPERSEDING_TYPES,
+ )
.values("customer_id")
.annotate(last=Max("createdDT"))
.values_list("customer_id", "last")
@@ -534,7 +547,8 @@ def followups_on_date(target_date, *, now=None):
امروز یا آینده — را میبیند تا کارشناس بتواند با تقویم عقب/جلو برود.
خروجی: ``{customer_id: {منبع: تاریخ}}`` مرتب بر اساس زودترین ساعت موعد.
- مشتریانی که قرار **بازدید** دارند در خروجی نیستند.
+ مشتریانی که قرار **بازدید** دارند، یا بازدیدشان از همان روز موعد به بعد
+ **انجام شده**، در خروجی نیستند (فهرست، «کارِ باقیمانده» است نه آرشیو).
"""
day_start, day_end = _local_day_bounds(target_date)
@@ -584,6 +598,25 @@ def followups_on_date(target_date, *, now=None):
for customer_id in _pending_visit_customer_ids(since=day_start):
result.pop(customer_id, None)
+ # ۴) حذف مشتریانی که بازدیدشان از همان روزِ موعد به بعد **انجام شده** است.
+ # قاعدهٔ کسبوکار: اگر بازدید انجام شده، تماسِ آن روز لازم نیست. قبلاً فقط
+ # بازدیدِ «در انتظار» دیده میشد، پس مشتریِ بازدیدشده باز هم در فهرست
+ # «نیازمند پیگیری امروز» میماند (۹۲٪ ردیفهای دادهٔ واقعی).
+ # فقط برای امروز/آینده اعمال میشود تا مرور روزهای گذشته (تقویم) سندِ کاریِ
+ # همان روز را با دادههای بعدی بازنویسی نکند.
+ if result:
+ now = _now(now)
+ current_tz = timezone.get_current_timezone()
+ if target_date >= now.astimezone(current_tz).date():
+ visits = last_visit_map(now=now, customer_ids=list(result))
+ for customer_id, visited_at in visits.items():
+ bucket = result.get(customer_id)
+ if not bucket or visited_at is None:
+ continue
+ due_day = min(bucket.values()).astimezone(current_tz).date()
+ if visited_at.astimezone(current_tz).date() >= due_day:
+ result.pop(customer_id, None)
+
return result
@@ -600,6 +633,10 @@ def overdue_followups(*, now=None, target_date=None):
(موعدش تازه است).
* اگر برایش **قرار بازدید** ثبت شده باشد، از فهرست خارج میشود — همان قاعدهٔ
:func:`followups_on_date` («بازدید گذاشته شده ⇒ تماس لازم نیست»).
+ * اگر بازدیدش **از همان موعد به بعد انجام شده** باشد، از فهرست خارج میشود
+ (پیگیری با بازدید بسته شده است).
+ * اگر **قرار دستیِ معتبر** داشته باشد («تا تاریخ پیگیری نشود»)، از فهرست
+ خارج میشود؛ چون خودِ کاربر صریحاً خواسته تا آن تاریخ تماس نگیریم.
* هماهنگیِ لغوشده (``cancell``) اصلاً دیده نمیشود.
خروجی: ``{customer_id: تاریخِ موعدِ ازدسترفته}`` — تا هم شمارش کنیم و هم
@@ -641,6 +678,24 @@ def overdue_followups(*, now=None, target_date=None):
for customer_id in pending_visits:
overdue.pop(customer_id, None)
+ current_tz = timezone.get_current_timezone()
+
+ # بازدیدِ **انجامشده** از همان موعد به بعد ⇒ این پیگیری انجام شده است.
+ # قبلاً فقط بازدیدِ «در انتظار» دیده میشد و همین، فهرست «از روزهای قبل»
+ # را پر از مشتریانی میکرد که بازدیدشان ثبت شده بود (۸۸٪ دادهٔ واقعی).
+ visits = last_visit_map(now=now, customer_ids=list(overdue))
+ for customer_id, visited_at in visits.items():
+ if visited_at is None:
+ continue
+ if visited_at.astimezone(current_tz).date() >= overdue[customer_id]:
+ overdue.pop(customer_id, None)
+
+ # «قرار دستیِ معتبر» یعنی کاربر صریحاً گفته «تا این تاریخ پیگیری نشود»؛
+ # چنین مشتریای نمیتواند همزمان «عقبافتاده» شمرده شود. قبلاً این ستون
+ # در این محاسبه دیده نمیشد (۳۸ ردیف متناقض در دادهٔ واقعی).
+ for customer_id in manual_holds_map(now=now):
+ overdue.pop(customer_id, None)
+
return overdue
@@ -726,12 +781,20 @@ def default_hold_until(*, now=None, days=HOLD_DEFAULT_DAYS):
return end_of_local_day(base)
-def set_visit_hold(customer, until, *, now=None):
- """ثبت قرار دستی؛ فقط اگر **جلوتر** از مقدار فعلی باشد مینویسد.
+def set_visit_hold(customer, until, *, now=None, force=False):
+ """ثبت قرار دستی؛ بهطور پیشفرض فقط اگر **جلوتر** از مقدار فعلی باشد مینویسد.
- قاعدهٔ «فقط رو به جلو» (مثل ``refresh_last_bazdid``) جلوی این را میگیرد که
- ثبت هماهنگی توسط کارشناس دوم، قرارِ دورترِ قبلی را عقب بکشد.
- خروجی: ``True`` اگر نوشته شد.
+ قاعدهٔ «فقط رو به جلو» جلوی این را میگیرد که ثبت هماهنگی توسط کارشناس دوم،
+ قرارِ دورترِ قبلی را عقب بکشد.
+
+ ``force=True`` برای وقتی است که کاربر **خودش** صریحاً تأیید کرده تاریخ عقب
+ برود (تیک «تاریخ قرار را عقب ببر» در مودالِ **ویرایش** ⇒ فیلد ``force_hold``).
+ در «ایجاد وظیفهٔ تازه» ویو این پارامتر را نمیفرستد. این پارامتر فقط از مسیر
+ ویو و پس از تأیید کاربر استفاده میشود؛ منطق نمایش/فهرستها تغییری نمیکند و
+ ستون همچنان همان جای ذخیرهٔ تاریخ است.
+
+ خروجی: ``True`` اگر نوشته شد، وگرنه ``False`` (یا مقدار فعلی جلوتر است، یا
+ تاریخ نامعتبر).
"""
if customer is None or customer.pk is None:
return False
@@ -739,7 +802,7 @@ def set_visit_hold(customer, until, *, now=None):
if target is None:
return False
current = as_aware(customer.visit_hold_until)
- if current is not None and current >= target:
+ if current is not None and current >= target and not force:
return False
Customer.objects.filter(pk=customer.pk).update(
visit_hold_until=target, visit_hold_set_at=_now(now),
diff --git a/docs/entezar_action_fields_report.html b/docs/entezar_action_fields_report.html
new file mode 100644
index 0000000..04ea197
--- /dev/null
+++ b/docs/entezar_action_fields_report.html
@@ -0,0 +1,1233 @@
+
+
+
+
+
+
گزارش رفتاری: اقدام، هماهنگی، قرار و صفحهٔ انتظار
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Django 3.2 · SQLite
+ ۳۲ شبیهسازی سبز
+ ممیزی روی کپی دیتابیس واقعی
+ کدِ تولیدی بدون تغییر
+
+
+ دو فیلد تاریخِ «اقدام»، صفحهٔ انتظار و پیامکهای بازدید
+
+
+ تحلیل کامل رفتار Action نوع «هماهنگی» و «بازدید»،
+ اثرشان بر /panel/entezar/?view=today و
+ ?view=today&missed=1،
+ بههمراه اعداد واقعی، خط زمانی نمونهها و ریشهٔ باگ.
+
+
+
+
دیتابیس ممیزی
+
khatibi92669266_…_20261021_223009.sqlite3
+
+
+
مصنوعات
+
audit script · simulation tests · md report
+
+
+
داده
+
۱۰٬۸۲۲ مشتری · ۲٬۵۴۱ هماهنگی فعال · ۵۰٬۰۹۰ بازدید انجامشده
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/entezar_action_fields_report.md b/docs/entezar_action_fields_report.md
new file mode 100644
index 0000000..3026cf3
--- /dev/null
+++ b/docs/entezar_action_fields_report.md
@@ -0,0 +1,742 @@
+# تحلیل کامل: دو فیلد تاریخِ «اقدام»، صفحهٔ انتظار و پیامکهای بازدید
+
+> دامنه: `Action` (ایجاد/ویرایش/حذف)، `Customer.visit_hold_until`، ویوی
+> `/panel/entezar/` (`?view=today` و `?view=today&missed=1`)، و اسکنر/تریگرهای پیامک.
+> روش: خواندن کدِ تولیدی + `ripwire` (blast radius) + `rg` + **ممیزی فقط-خواندنی روی
+> کپیِ دیتابیس واقعی** + شبیهسازیِ ۳۲ سناریو در تستهای Django.
+> **هیچ خطی از کدِ تولیدی تغییر نکرد.** مصنوعات این بررسی:
+>
+> | فایل | نقش |
+> |---|---|
+> | `scripts/entezar_action_audit.py` | ممیزی عددیِ فقط-خواندنی روی دیتابیس واقعی (کپی موقت در /tmp) |
+> | `tests/test_entezar_action_simulation.py` | ۳۲ سناریوی شبیهسازی (فرم، انتظار، پیامک، باگ missed) |
+> | `docs/entezar_action_fields_report.md` | همین سند |
+> | `docs/entezar_action_fields_report.html` | نسخهٔ بصریِ تکفایل (Tailwind CDN، RTL، نمودار و خط زمانی) |
+>
+> تاریخ اجرای ممیزی: ۲۰۲۶-۱۰-۲۲ (۱۴۰۵/۰۷/۳۰) — دیتابیس
+> `data9/khatibi92669266_backups_db_backup_20261021_223009.sqlite3`.
+
+---
+
+## ۰. پاسخ کوتاه (Executive Summary)
+
+| پرسش | پاسخ |
+|---|---|
+| فیلد «تاریخ و ساعتی که کار باید انجام شود» چیست؟ | `Action.actionDateTime` — «زمانِ انجام/تماس» وظیفه. برای «بازدید» = زمان قرار بازدید؛ برای «هماهنگی» = **زمان تماس**؛ برای بقیه = زمان انجام وظیفه. |
+| فیلد «تا تاریخ پیگیری نشود» چیست؟ | `Customer.visit_hold_until` (پایانِ روز) — فقط برای «هماهنگی» ذخیره میشود؛ قرارِ بعدیِ مشتری. |
+| اثر فیلد اول کجاست؟ | ۸ مصرفکننده: لیست وظایف پنل، «آخرین بازدید» و دورهٔ بازدید، لنگر چرخهٔ پیامک، قرار «بازدید برنامهریزیشده»، قرار «هماهنگی»، فهرست پیگیری یک روز، فهرست «عقبافتادهها»، و متن پیامک «ثبت بازدید». |
+| اثر فیلد دوم کجاست؟ | ۳ مصرفکننده: ستون/برچسب «قرار دستی» در صفحهٔ انتظار، فهرست پیگیری روزِ همان قرار، و **مسدودسازی یادآوری دورهای «بازدید» (SMS)** تا پایان آن روز. |
+| روی SMS چه اثری دارد؟ | `visit` (دورهای) با هر سه منبعِ قرار (دستی، بازدید برنامهریزیشده، هماهنگی آینده) مسدود میشود؛ `visit_created` (رویدادی) **فقط** با ثبت اقدام «بازدید» ارسال میشود و از `actionDateTime` مقدار `{date}` میگیرد — فیلد تعویق روی آن هیچ اثری ندارد. |
+| تیک «این کار انجام شده است» در هماهنگی؟ | تیکدار ⇒ `status=finish` + `finishDateTime` (خالی ⇒ زمان ثبت) + اگر تاریخ تعویق خالی باشد **پیشفرض ۱۴ روز** قرار. بدون تیک ⇒ `waite` و بدون پیشفرض؛ فقط اگر تاریخ تعویق پر شود قرار ثبت میشود. |
+| چرا `missed=1` پر از مشتریِ بازدیدشده است؟ | چون `overdue_followups` **فقط بازدیدِ `waite` (در انتظار)** را حذف میکند و بازدیدِ `finish` را هرگز نمیبیند؛ همچنین ستون «تا تاریخ پیگیری نشود» را هم نمیبیند. |
+| «۲ قرار» یعنی چه؟ | برچسب `source_count > 1` — مشتری همزمان بیش از یک **منبعِ** قرار دارد (دستی/بازدید برنامهریزیشده/هماهنگی)، نه لزوماً دو قرارِ متفاوت. |
+| «قرار دستی» یعنی چه؟ | همان `Customer.visit_hold_until`؛ از فیلد «تا تاریخ پیگیری نشود» نوشته میشود و فقط وقتی معتبر است که آخرین فعالیتِ مشتری یک «هماهنگیِ فعال» باشد. |
+
+---
+
+## ۱. نقشهٔ داده و مسیر ذخیرهسازی
+
+### ۱.۱ ستونهای دخیل
+
+| موجودیت.ستون | نوع | نویسنده | خوانندهها |
+|---|---|---|---|
+| `Action.actionDateTime` | `DateTimeField(null=True)` | `dateTimeAnjam` فرم (`views/actions.py:90,151,209`) | ۸ مصرفکننده — بخش ۲ |
+| `Action.finishDateTime` | `DateTimeField(null=True)` | `is_done` + `FinishDateTime` (خالی ⇒ `now`) | «تاریخ مؤثر بازدید»، فهرست پیگیری، تصمیم «انجامشده» |
+| `Action.status` | `waite/finish/cancell` | تیک «انجام شد» | تمام تصمیمها؛ `cancell` = لغو/حذف نرم |
+| `Action.createdDT` | `auto_now_add` | سیستم | قاعدهٔ «آخرین فعالیت» در `_valid_manual_holds` |
+| `Customer.visit_hold_until` | `DateTimeField(null=True)` | `visitHoldUntil` هماهنگی (`_apply_visit_hold`) | ۳ مصرفکننده — بخش ۳ |
+| `Customer.visit_hold_set_at` | `DateTimeField(null=True)` | همان لحظهٔ ثبت (حسابرسی) | فقط نمایش/دیباگ |
+| `Customer.last_bazdid_dt` | `DateTimeField(null=True)` | `refresh_last_bazdid` (مشتق) | داشبورد/گزارشهای قدیمی |
+
+### ۱.۲ مسیر فرم تا دیتابیس
+
+```
+customerActionInc.html
+ dateTimeAnjam ─────────────► _parse_form_datetime(ms) ─► payload['actionDateTime'] ─► Action.actionDateTime
+ visitHoldUntil ────────────► _parse_form_datetime(ms) ─► payload['holdRaw']
+ checkIsDone ───────────────► payload['status'] = finish | waite
+ FinishDateTime ────────────► normalize_finish_datetime (خالی ⇒ now)
+ │
+ └─ اگر نوع = هماهنگی:
+ holdRaw → normalize_hold_until (گذشته ✗، >۱۸۳ روز ✗) → set_visit_hold (فقط رو به جلو)
+ خالی + finish → default_hold_until() = +۱۴ روز
+ └─ اگر نوع ≠ هماهنگی و ایجاد تازه:
+ clear_visit_hold(customer)
+```
+
+نکتهٔ کلیدی: `visitHoldUntil` **فقط** زمانی نوشته میشود که `actionType == 'hamahangi'`
+باشد (`views/actions.py:118-128`). برای «بازدید» و بقیهٔ انواع، حتی اگر مقدار بفرستید،
+نادیده گرفته میشود (تست: `test_hold_until_ignored_for_visit_actions`).
+
+
+---
+
+## ۲. فیلد «تاریخ و ساعتی که کار باید انجام شود» — `actionDateTime`
+
+### ۲.۱ یک فیلد، سه معنا (بسته به نوع اقدام)
+
+| نوع اقدام | معنی `actionDateTime` | مرز زمانی در تصمیمها |
+|---|---|---|
+| `bazdid` + `waite` | زمانِ **قرار بازدید** | تا **پایان روز** (`whole_day=True`) |
+| `bazdid` + `finish` | زمانِ **انجام بازدید** | اگر گذشته باشد، «تاریخ مؤثر بازدید» |
+| `hamahangi` (هر دو وضعیت) | زمانِ **تماس/هماهنگی** | **لحظهای** (`whole_day=False`) — نه پایان روز |
+| `nasb` / `vosol` / `inCall` / `outCall` | زمانِ انجام وظیفه | فقط لیست وظایف |
+
+این تفکیک در `visit_policy._future_actions` (خط ۳۴۱-۳۶۱) صریح مستند شده است: در
+هماهنگی، `actionDateTime` «زمان تماس» است، پس «تماسِ امروز» نباید مشتری را تا آخر
+امروز پنهان کند؛ در بازدید برنامهریزیشده برعکس، قرارِ امروز باید تا پایان امروز معتبر بماند.
+
+### ۲.۲ جدول مصرفکنندهها
+
+| # | مصرفکننده | فایل:تابع | چه میکند |
+|---|---|---|---|
+| ۱ | لیست وظایف امروز/عقبافتادهٔ کاربر | `views/actions.py:276-308` (`ActionListToday`, `ActionListUserToday`) | `actionDateTime` بین ۰۰:۰۰ تا ۲۳:۵۹ امروز + `status=waite` ⇒ «وظایف امروز»؛ `< شروع امروز` ⇒ «وظایف عقبافتاده» |
+| ۲ | «آخرین بازدید» واقعی | `visit_policy.last_visit_map` (۱۴۳) و `effective_visit_dt` (۹۰) | فقط `bazdid+finish`؛ تاریخ مؤثر = `actionDateTime` اگر از حالا جلوتر نباشد، وگرنه `finishDateTime` |
+| ۳ | لنگر چرخهٔ بازدید و پیامک | `visit_policy.cycle_anchor_map` (۲۱۴) → `sms/scanner.scan_visit` (۲۱۸-۲۴۵) | آخرین بازدید مؤثر (یا تاریخ نصب) + دورهٔ ۹ ماه ⇒ موعد یادآوری |
+| ۴ | قرار «بازدید برنامهریزیشده» | `scheduled_visits_map` (۳۸۰) — `bazdid+waite` با `actionDateTime ≥ آغاز امروز` | مشتری از «تعیینتکلیفنشده» خارج و از یادآوری SMS معاف میشود |
+| ۵ | قرار «هماهنگی» | `coordination_holds_map` (۳۹۱) — `hamahangi` با `actionDateTime ≥ now` | همان اثر، ولی مرز **لحظهای** |
+| ۶ | فهرست پیگیری یک روز | `followups_on_date` (۵۲۲) | `hamahangi` با تاریخ همان روز (یا `finishDateTime` اگر `actionDateTime` خالی باشد) + قرار دستی همان روز |
+| ۷ | فهرست «عقبافتادهها» | `overdue_followups` (۵۹۰) | **آخرین** `hamahangi` با تاریخ قبل از امروز ⇒ «موعد ازدسترفته» (منشأ باگ — بخش ۶) |
+| ۸ | متن پیامک «ثبت بازدید» | `sms/triggers.notify_visit_scheduled` (۵۲-۶۱) | `{date}` = `action.actionDateTime` (شمسی میشود) |
+
+همچنین در قالبها: ستون تاریخ در `customerActionInc.html:117,169`، `customerAction.html:42`
+و مودال ویرایش (`ActionJson` → `dateTimePickerAnjam.setDate`).
+
+### ۲.۳ اعتبارسنجی
+
+* `validate_action_payload` (۹۰۳): نوع/وضعیت باید مجاز باشد؛ فقط برای
+ `bazdid+finish` سقف «۳۰ روز آینده» اعمال میشود (`FINISH_MAX_FUTURE_DAYS`).
+* برای هماهنگی هیچ سقفی روی `actionDateTime` نیست.
+* اگر تاریخ اتمام خالی بماند و تیک «انجام شد» زده شود، `finishDateTime = now`.
+
+---
+
+## ۳. فیلد «تا تاریخ پیگیری نشود» — `Customer.visit_hold_until`
+
+### ۳.۱ قواعد نوشتن
+
+| قاعده | پیادهسازی | اثر |
+|---|---|---|
+| فقط هماهنگی | `_apply_visit_hold` (`actions.py:101-128`) | برای بازدید/نصب/وصول بیاثر |
+| پایانِ روز | `end_of_local_day` (۷۰۳) | «تا آخر ۲۳:۵۹:۵۹ همان روز» |
+| خالی + تیک «انجام شد» | `default_hold_until()` با `HOLD_DEFAULT_DAYS=14` (۶۷۹، ۷۲۳) | ۱۴ روز تعویق خودکار |
+| خالی + بدون تیک | هیچ نوشتنی | مشتری در فهرست میماند |
+| تاریخ گذشته | `normalize_hold_until` (۷۶۵) خطا | فرم با پیام فارسی رد میشود |
+| بیش از ۱۸۳ روز | `HOLD_MAX_DAYS=183` (۶۸۰) خطا | جلوگیری از تعویق بیپایان |
+| فقط رو به جلو | `set_visit_hold` (۷۲۹): مقدار فعلی ≥ هدف ⇒ نمینویسد | هماهنگی دومِ کوتاهتر قرارِ دورتر را عقب نمیکشد |
+| ثبت اقدام غیرهماهنگی در حالت ایجاد | `clear_visit_hold` | ستون پاک میشود (تست: `test_creating_visit_clears_hold_column`) |
+| ویرایش اقدام | عمداً دست نمیزند | ویرایش گذشته نباید قرارِ فعلی را پاک کند |
+
+### ۳.۲ قاعدهٔ اعتبار («ستون، مرجع نیست»)
+
+`_valid_manual_holds` (۴۰۵-۴۶۰): یک قرار دستی فقط وقتی معتبر است که
+
+1. برای مشتری یک اقدام `hamahangi` با وضعیت `waite` یا `finish` وجود داشته باشد، و
+2. **آخرین فعالیتِ مشتری همان هماهنگی باشد** — اگر بعد از آن، اقدامِ غیرهماهنگی
+ (بازدید، وصول، تماس، …) با `createdDT` بزرگتر ثبت شده باشد، قرار باطل است.
+
+نتیجهٔ عملی: ستون ممکن است پر باشد ولی «مرده». در دادهٔ واقعی از ۵۴ مقدار پرشده،
+۵۱ معتبر و ۳ بیاعتبار است (۱ بدون هماهنگی فعال، ۲ بهخاطر اقدام بعدی).
+
+### ۳.۳ مصرفکنندهها
+
+| مصرفکننده | فایل:تابع | اثر |
+|---|---|---|
+| منبع `manual` صفحهٔ انتظار | `manual_holds_map` (۴۶۳) → `appointment_sources_map` (۶۴۷) | مشتری از «تعیینتکلیفنشده» خارج و در `?view=manual` دیده میشود |
+| فهرست پیگیری روزِ قرار | `followups_on_date` (۵۴۳-۵۵۴) | دقیقاً در همان روز (حتی گذشته) در `?view=today` ظاهر میشود |
+| مسدودسازی یادآوری دورهای SMS | `next_appointment_map` (۲۹۷) → `scan_visit` (`scanner.py:220,232`) | تا پایان آن روز پیامک `visit` صف نمیشود |
+| نمایش در جدول | `entezar.html:516-518` | ستون «قرار دستی» + برچسب `قرار دستی` |
+| مودال ویرایش | `customerActionInc.html:66,706` (`customer_hold`) | مقدار ذخیرهشده در فیلد بارگذاری میشود |
+
+
+---
+
+## ۴. اثر دو فیلد بر پیامکها
+
+### ۴.۱ دو نوع پیامکِ بازدید — دو منطق کاملاً متفاوت
+
+| نوع | زمان ارسال | تصمیمگیر | دادههای دخیل |
+|---|---|---|---|
+| `visit` (دورهای) | اسکنر (`scan_visit`) وقتی موعدِ «آخرین بازدید + ۹ ماه» رسیده باشد | `cycle_anchor_map` + `next_appointment_map` | **هر دو فیلد** (فیلد اول بهواسطهٔ لنگر، فیلد دوم بهواسطهٔ قرار) |
+| `visit_created` (رویدادی) | بلافاصله پس از ثبت اقدام **بازدید** | `notify_visit_scheduled` | فقط `actionDateTime` (برای متن `{date}`) |
+
+### ۴.۲ ماتریس اثر (نتیجهٔ شبیهسازی، همه با `scan_visit`)
+
+| سناریو | یادآوری `visit` | پیامک `visit_created` |
+|---|---|---|
+| مشتری عقبافتاده، هیچ اقدامی | ✅ ارسال | — |
+| «تا تاریخ پیگیری نشود» = +۱۰ روز (با هماهنگی فعال) | ⛔ تا پایان روزِ دهم؛ از فردای آن ✅ | — |
+| ستون `visit_hold_until` پر، ولی **بدون** اقدام هماهنگی | ✅ ارسال (ستون بیاثر) | — |
+| بازدید `waite` با تاریخ +۵ روز | ⛔ (تا پایان آن روز) | — |
+| هماهنگی با تاریخ +۵ روز (بدون تعویق) | ⛔ | — |
+| هماهنگی «انجامشده» با تاریخ گذشته و بدون تعویق | ✅ ارسال | — |
+| ثبت اقدام بازدید (هر وضعیتی) | — | ✅ با `{date}` = `actionDateTime` |
+| ثبت اقدام هماهنگی | — | ❌ |
+| قرار دستی فعال + ثبت بازدید | — | ✅ (فیلد دوم مانع نیست) |
+
+تستهای متناظر: `SmsSimulationTests` در `tests/test_entezar_action_simulation.py`
+(`test_hold_until_suppresses_visit_reminder`، `test_hold_column_without_coordination_is_inert`،
+`test_scheduled_future_visit_suppresses_reminder`، `test_future_coordination_suppresses_reminder`،
+`test_visit_action_sends_visit_created_with_action_datetime`، …).
+
+### ۴.۳ چرا «تعویق» روی `visit_created` اثر ندارد؟
+
+چون این پیامک در `views/actions.py:170-171` فقط برای `actionType == bazdid` صدا زده
+میشود، و اصلاً از `next_appointment_map` عبور نمیکند. یعنی اگر هماهنگی بگذارید و بعد
+بازدیدی ثبت کنید، پیامکِ «بازدید شما برای تاریخ … ثبت شد» میرود حتی اگر «تا تاریخ
+پیگیری نشود» فعال باشد. (اگر رفتار مطلوب چیز دیگری است، باید در `notify_visit_scheduled`
+یا در شرط فراخوانی آن تغییر کند.)
+
+### ۴.۴ جزئیات مهم `scan_visit`
+
+* `next_appointment_map` = **اجتماع** سه منبع (`manual`، `scheduled`، `coordination`)؛
+ وجود هر کدام ⇒ `continue` (خط ۲۳۲). پس قرار دستی و تاریخ بازدید و تاریخ هماهنگی هر سه
+ یادآوری را متوقف میکنند — این همان «یک جواب برای صفحه و پیامک» است.
+* `dedup_key = visit:{customer_id}:{YYYY-MM موعد}` ⇒ هر دوره حداکثر یک پیامک؛ حذف از صف
+ فقط همان دوره را کنسل میکند.
+* خط شروع (`scan_since`) و سقفها میتوانند ارسال موارد قدیمی را متوقف کنند.
+* لنگر چرخه **تماس تلفنی را ریست نمیکند**؛ فقط بازدیدِ `finish` (یا نصب) لنگر است.
+
+---
+
+## ۵. تیک «این کار انجام شده است» در اقدام هماهنگی
+
+مسیر تصمیم (`_action_payload_from_request` + `_apply_visit_hold`):
+
+| وضعیت فرم | `Action.status` | `finishDateTime` | `visit_hold_until` | اثر بر فهرست انتظار | اثر بر SMS |
+|---|---|---|---|---|---|
+| تیک + تاریخ تعویق پر | `finish` | اگر خالی ⇒ `now` | همان تاریخ (پایان روز، فقط رو به جلو) | مشتری تا آن روز پنهان؛ همان روز در `?view=today` برمیگردد | یادآوری `visit` تا آن روز قطع |
+| تیک + تاریخ تعویق خالی | `finish` | `now` | **+۱۴ روز** | همان | همان |
+| بدون تیک + تاریخ تعویق پر | `waite` | `NULL` | همان تاریخ | همان | همان |
+| بدون تیک + تاریخ خالی | `waite` | `NULL` | بدون تغییر | مشتری در فهرست میماند | بدون اثر |
+
+نکتههای ریز:
+
+* در UI، برای هماهنگیِ تیکخورده پیش از submit یک **تأیید دو مرحلهای** نمایش داده میشود
+ (`holdDoneWarning` / `holdConfirmYes` / `holdConfirmNo` در `customerActionInc.html:609-665`).
+ اگر کاربر «نه» را بزند، تیک بهصورت سمتکلاینت برداشته میشود ⇒ همان سطر سوم/چهارم جدول.
+* «انجام شد» هیچ اثر مستقیمی روی «آخرین بازدید» (`last_bazdid_dt`) ندارد؛ هماهنگی هرگز
+ وارد `last_visit_map` نمیشود (`HamahangiIsolationTests`).
+* «انجام شد» بدون تاریخ تعویق، **همیشه** ۱۴ روز تعویق میسازد؛ اگر کارشناس فقط میخواست
+ تماس را لاگ کند، این پیشفرض ناخواسته مشتری را دو هفته پنهان میکند.
+* در حالت **ویرایش**، `_apply_visit_hold` قرار را پاک نمیکند؛ حتی تغییر نوع اقدام از
+ هماهنگی به بازدید، ستون را دستنخورده میگذارد (ولی قاعدهٔ «آخرین فعالیت» میتواند
+ آن را بیاعتبار کند).
+
+
+---
+
+## ۶. صفحهٔ `/panel/entezar/` — نمای `?view=today`
+
+### ۶.۱ مسیر محاسبه (گامبهگام)
+
+`views/customers.py:entezar` (۳۷۲-۸۵۳):
+
+1. `now`, `due_cut = visit_due_at(now)` (امروز − ۹ ماه)، `period_months`.
+2. `visits = last_visit_map()` — منبع «آخرین بازدید».
+3. `sources = appointment_sources_map()` — سه منبع قرارِ **آینده**:
+ `manual` (ستون تعویق معتبر)، `scheduled` (`bazdid+waite` با تاریخ ≥ آغاز امروز)،
+ `coordination` (`hamahangi` با تاریخ ≥ همین لحظه).
+4. پایهٔ نمای «عقبافتاده» (سایر `view`ها): مشتریان `laghv_bazdid=False`، نامدار،
+ `installDate ≤ due_cut`، و سپس `stale_ids` = کسانی که «بازدید معتبر ندارند یا بازدیدشان
+ ≤ `due_cut` است».
+5. نمای `today` **جدا** است: `today_followups = followups_on_date(today)` — یعنی
+ «قرار دستیِ همان روز» + «هماهنگی با تاریخ همان روز»؛ منهای مشتریانی که بازدیدِ
+ **در انتظار** از آغاز امروز دارند.
+6. `_followup_ids`: فیلتر `is_active`، `laghv_bazdid=False`، نام غیرتهی + جستوجو؛
+ مرتبسازی بر اساس زودترین موعد.
+7. `appointment_rows` برای هر ردیف: ادغام `sources[cid]` با `followups[cid]`، سپس
+ `source_count` = تعداد کلیدهای موجود از میان `manual/scheduled/coordination`.
+8. شمارندهها: `counts['followup_today']` و `counts['followup_overdue']` از همان مسیر
+ فهرست ⇒ «عدد = تعداد ردیف».
+9. عارضهٔ جانبی: ویو در هر GET مقدار `CacheTable('9mahbazdid')` را با
+ `counts['uncoordinated']` بازنویسی میکند (خط ۶۹۹-۷۰۵) — یعنی یک صفحهٔ خواندنی،
+ نوشتن در دیتابیس دارد.
+
+### ۶.۲ ستون «وضعیت قرار» — معنی برچسبها
+
+قالب `entezar.html:502-515`:
+
+```django
+{% if appt.source_count > 1 %}
{{ appt.source_count }} قرار ← «۲ قرار»
+{% elif appt.manual %} قرار دستی
+{% elif appt.scheduled %} بازدید برنامه
+{% elif appt.coordination %} هماهنگی
+{% else %} بدون قرار
+```
+
+* **«N قرار»** = تعداد **منابعِ** قرار، نه تعداد قرارهای واقعی. سه منبع مستقلاند و
+ میتوانند همزمان وجود داشته باشند.
+* «قرار دستی» = ستون `visit_hold_until` (فیلد «تا تاریخ پیگیری نشود»).
+* «بازدید برنامه» = اقدام `bazdid` با وضعیت `waite` و تاریخ ≥ آغاز امروز.
+* «هماهنگی» = اقدام `hamahangi` (وضعیت `waite` یا `finish`) با تاریخ ≥ همین لحظه.
+* ترتیب اولویت برچسب: چندقراره ← دستی ← بازدید ← هماهنگی ← بدون قرار. (ستون
+ `sort=status` هم همین ترتیب را دارد: 0 بدون، 1 دستی، 2 بازدید، 3 هماهنگی، 4 چندقراره.)
+
+### ۶.۳ شواهد واقعی «۲ قرار»
+
+اعداد زیر روی **کل مشتریان فعالی** که در لحظهٔ ممیزی حداقل یک منبع قرار آینده دارند
+محاسبه شده است (نه فقط ردیفهای یک نما):
+
+| سنجه | مقدار |
+|---|---|
+| مشتریان با ۱ منبع | ۲٫۲۶۷ |
+| مشتریان با ۲ منبع | ۱۶ |
+| ترکیب `manual + coordination` | ۱۲ (۱۱ مورد تاریخهای **متفاوت**، ۱ مورد همروز) |
+| ترکیب `scheduled + coordination` | ۴ |
+| بیش از ۲ منبع | ۰ |
+
+نمونهها: `#833` دستی ۲۰۲۶-۱۱-۰۴ / هماهنگی ۲۰۲۶-۱۲-۳۰ — `#2888` هر دو ۲۰۲۶-۱۱-۱۱
+(همان یک هماهنگی هم «هماهنگی» شمرده شده هم «قرار دستی»).
+
+**پیامد:** «۲ قرار» همیشه یعنی «دو توافق» نیست. یک هماهنگیِ انجامشده که تاریخ تعویق
+همان روز را دارد، دو منبع تولید میکند و برچسب «۲ قرار» میگیرد. اگر هدف نمایش
+«تعداد قرارهای واقعی» است، باید منابعِ هممنشأ (هماهنگی + قرار دستیِ حاصل از همان اقدام)
+یک قرار شمرده شوند.
+
+### ۶.۴ «قرار دستی» — تعریف دقیق
+
+| جنبه | مقدار |
+|---|---|
+| منبع داده | `Customer.visit_hold_until` (پایانِ روز) |
+| نویسنده | فقط اقدام `hamahangi` (فیلد «تا تاریخ پیگیری نشود» / پیشفرض ۱۴ روز) |
+| شرط اعتبار | یک `hamahangi` فعال + «آخرین فعالیت همان هماهنگی» (`_valid_manual_holds`) |
+| شرط نمایش در `?view=today` | `visit_hold_until` داخل بازهٔ همان روز |
+| شرط نمایش در `sources` | `visit_hold_until > now` (یعنی قرارِ آینده) |
+| اثر | خروج از «تعیینتکلیفنشده»، ورود به فیلتر `manual`، قطع یادآوری SMS تا آن روز |
+
+عدد واقعی: ۵۴ مشتری ستون پر دارند (۵۱ معتبر)، فقط ۴ ردیف در نمای امروز با منبع
+`manual` دیده میشوند (۲ فقط-دستی، ۲ دستی+هماهنگی).
+
+
+---
+
+## ۷. باگ `?view=today&missed=1` — چرا این فهرست پر از مشتریِ بازدیدشده بود؟
+
+> این باگ **رفع شد** (بخش ۹): فهرست از ۱۱۲ ردیف به ۴۲ ردیف رسید و هیچ ردیف
+> متناقضی (قرار معتبر + عقبافتاده) باقی نماند. متن زیر تحلیل زمان کشف است.
+
+
+### ۷.۱ تعریف فعلی «عقبافتاده» در کد
+
+`visit_policy.overdue_followups` (۵۹۰-۶۴۴):
+
+```python
+# آخرین موعد هماهنگی هر مشتری (فقط اقدام hamahangi با waite/finish)
+when = as_aware(action_dt) or as_aware(finish_dt) # ← finish هم با actionDateTime
+latest[cid] = max(روز when)
+overdue = {cid: day for ... if day < today} # ← موعد در گذشته
+
+# تنها حذفکننده: بازدیدهای «در انتظار» از زودترین موعد به بعد
+pending_visits = _pending_visit_customer_ids(since=آغاز روزِ زودترین موعد)
+```
+
+و `_pending_visit_customer_ids` (۴۹۹-۵۱۹) فقط:
+
+```python
+actionType='bazdid', status='waite'
+```
+
+### ۷.۲ سه ریشهٔ خطا (همراستا با هم)
+
+| # | ریشه | چرا باعث حضور مشتریِ «انجامشده» میشود |
+|---|---|---|
+| R1 | **بازدیدِ `finish` هرگز دیده نمیشود** | قرار بازدید فقط با `waite` تعریف شده؛ بازدیدی که انجام و بسته شده (`finish`) هیچ اثری در حذف ندارد |
+| R2 | **ستون «تا تاریخ پیگیری نشود» نادیده گرفته میشود** | `overdue_followups` اصلاً `visit_hold_until` را نمیخواند، پس مشتریای که قرارِ آیندهٔ معتبر دارد هم در فهرست «عقبافتاده» میماند (و در شمارندهٔ قرمز هم شمرده میشود) |
+| R3 | **`finish` هم با `actionDateTime` سنجیده میشود** | برای هماهنگیِ انجامشده، `actionDateTime` «زمان تماس» است نه «قرار بعدی» — همان چیزی که `_future_actions` با `whole_day=False` مستند کرده. نتیجه: هر تماسِ تمامشده تا ابد «عقبافتاده» میماند تا هماهنگی تازهای ثبت شود |
+
+قاعدهٔ موجود فقط وقتی کار میکند که کارشناس **بازدیدِ در انتظار** ثبت کرده باشد؛ یعنی
+سناریوی واقعیِ «تماس گرفتم و فردا بازدید میکنم» درست عمل میکند، اما «تماس گرفتم،
+بازدید انجام شد و ثبت شد» هرگز از فهرست خارج نمیشود.
+
+### ۷.۳ اعداد واقعی (کپی دیتابیس ۲۰۲۶-۱۰-۲۱)
+
+`?view=today&missed=1` = **۱۱۲ ردیف**:
+
+| سنجه | تعداد | درصد |
+|---|---|---|
+| حداقل یک بازدیدِ `finish` دارند | ۹۹ | ۸۸٫۴٪ |
+| بازدید **پس از ثبت هماهنگی** (createdDT بزرگتر) | ۳۲ | ۲۸٫۶٪ |
+| آخرین بازدید **بعد از موعد هماهنگی** | ۲۴ | ۲۱٫۴٪ |
+| هیچ بازدیدی ندارند (واقعاً نیازمند پیگیری) | ۱۳ | ۱۱٫۶٪ |
+| آخرین هماهنگی `finish` (تماس انجام شده) | ۹۹ | ۸۸٫۴٪ |
+| آخرین هماهنگی `waite` | ۱۳ | ۱۱٫۶٪ |
+| «قرار دستی» **معتبرِ آینده** دارند و باز هم در فهرستاند | ۳۸ | ۳۳٫۹٪ |
+| `finish` بدون قرار دستیِ فعال | ۶۱ | ۵۴٫۵٪ |
+| بازدیدِ در انتظار (هر تاریخی) دارند | ۰ | ۰٪ |
+
+### ۷.۴ دو نمونهٔ واقعی (خط زمانی کامل)
+
+**#236 — خانم مرضیه جمالی** (`--customer 236`):
+
+```
+#77042 hamahangi finish action=2026-10-17 14:31 finish=2026-10-17 15:26 created=2026-10-15
+#77176 bazdid finish action=2026-10-18 15:26 finish=2026-10-19 16:12 created=2026-10-17
+```
+
+هماهنگی ۱۵ اکتبر ثبت شده، تماس ۱۷ اکتبر انجام شده، **بازدید ۱۸ اکتبر ثبت شده** — ولی
+مشتری هنوز در `missed` است، چون R1 و R3 (تاریخِ هماهنگی گذشته است و بازدید `finish`
+نادیده گرفته میشود).
+
+**#1431 — آقای حمید جمالی:**
+
+```
+#63749 hamahangi finish action=2026-10-16 finish=2026-10-17 created=2026-02-09
+#77122 bazdid finish action=2026-10-18 created=2026-10-17
+#77262 bazdid finish action=2026-10-20 created=2026-10-18
+```
+
+دو بازدیدِ انجامشده بعد از تماس؛ باز هم در `missed`.
+
+### ۷.۵ چرا «بازدید انجام شده» در ستون «آخرین بازدید» دیده میشود ولی چیزی را عوض نمیکند؟
+
+ستون «آخرین بازدید» از `last_visit_map` میآید (که `bazdid+finish` را میبیند)، اما
+تصمیم حضور در فهرست از `overdue_followups` میآید (که آن را نمیبیند). یعنی **دو
+تعریف واگرا**؛ دقیقاً همان چیزی که خود `visit_policy` در docstring ابتدایی میخواست از
+بین ببرد.
+
+### ۷.۶ نمای `today` هم آلوده است (به شکل دیگری)
+
+`?view=today` = **۱۳ ردیف**:
+
+| سنجه | مقدار |
+|---|---|
+| فقط `coordination` | ۹ |
+| فقط `manual` | ۲ |
+| `coordination + manual` | ۲ |
+| هماهنگیهای امروز با وضعیت `finish` (تماس انجامشده) | ۱ |
+| هماهنگیهای امروز با وضعیت `waite` | ۸ |
+| ردیفهایی که قرار دستیِ آینده هم دارند (بعداً دوباره میآیند) | ۴ |
+| ردیفهایی که آخرین بازدیدشان در/بعد از همان روز موعد است | ۰ |
+
+> «وضعیت» در دو سطر بالا، وضعیت **آخرین اقدام هماهنگی** همان مشتری است (ممکن است با
+> اقدامی که منبع `coordination` امروز را ساخته یکی نباشد).
+
+نتیجه: این نما «موعدِ تماس» را با «موعدِ پیگیری» مخلوط میکند؛ تماسِ انجامشدهٔ امروز هم
+یک ردیف میسازد، و اگر همان تماس قرار دستی هم داده باشد، مشتری دو بار (امروز و روزِ
+قرار) در فهرستهای کاری ظاهر میشود. همچنین `_pending_visit_customer_ids` فقط
+`waite` را حذف میکند، پس **بازدیدِ امروزِ انجامشده** هم ردیف امروز را حذف نمیکند.
+
+### ۷.۷ تناقض آشکار: یک مشتری همزمان «قرار دارد» و «عقبافتاده» است
+
+۳۸ مشتری از ۱۱۲ ردیف، در `appointment_sources_map` (و در فیلتر `?view=manual`) قرار
+معتبر دارند — یعنی از «تعیینتکلیفنشده» خارجاند — ولی همان لحظه در `missed=1` و در
+شمارندهٔ قرمز دیده میشوند. مثال: `#3339` قرار دستی ۲۰۲۶-۱۰-۲۲ (امروز) دارد و در
+`sources` هم منبع هماهنگی ۲۰۲۶-۱۲-۱۶ دارد؛ یعنی «۲ قرار» گرفته، در حالی که وضعیت
+پیگیریاش متناقض نمایش داده میشود.
+
+
+---
+
+## ۸. جمعبندی ریشهای و پیشنهاد رفع
+
+> وضعیت: **اعمال شد** — فهرست نهایی رفعها، کد تغییرکرده و اثر عددی در بخش ۹.
+
+### ۸.۱ دو تعریف واگرا از «تعیینتکلیف شده»
+
+| لایه | معیار فعلی |
+|---|---|
+| «تعیینتکلیفنشده» (کارتها/فیلترها) | اجتماع سه منبع قرارِ آینده + اعتبار قرار دستی |
+| «عقبافتاده» (`missed`) | فقط `hamahangi` + فقط بازدیدِ `waite` |
+
+رفع باگ = همترازکردن لایهٔ دوم با لایهٔ اول.
+
+### ۸.۲ حداقل تغییرِ ممکن (پیشنهاد، بدون اعمال)
+
+۱. **در `overdue_followups`** بعد از حذف `pending_visits`:
+
+```python
+# الف) هر بازدیدِ انجامشده که تاریخ مؤثرش از روزِ موعد عقبتر است، تکلیف را روشن کرده
+for customer_id, action_dt, finish_dt in Action.objects.filter(
+ actionType=Action.ActionType.BAZDID, status=Action.Status.FINISH,
+).values_list('customer_id', 'actionDateTime', 'finishDateTime').iterator():
+ eff = as_aware(action_dt) if action_dt and as_aware(action_dt) <= now else as_aware(finish_dt)
+ if eff and eff.date() >= overdue.get(customer_id, today):
+ overdue.pop(customer_id, None)
+
+# ب) قرار دستیِ معتبر همان چیزی است که کاربر صریحاً گفته «تا این تاریخ پیگیری نکن»
+for customer_id in manual_holds_map(now=now):
+ overdue.pop(customer_id, None)
+```
+
+۲. **در `followups_on_date`**: همان قاعدهٔ «بازدید انجامشدهٔ بعد از موعد» را هم اعمال
+ کنید (فیلتر سوم بعد از `pending_visits`)، و اگر مشتری هم `manual` دارد و هم
+ `coordination` از **همان اقدام**، فقط `manual` را نگه دارید تا ردیف تکراری/«۲ قرار»
+ کاذب ساخته نشود.
+
+۳. **معنای تاریخ `finish` در هماهنگی**: تصمیم محصولی لازم است؛ پیشنهاد: موعدِ
+ `finish` را `visit_hold_until` بدانید و `actionDateTime` را فقط برای `waite`
+ بهعنوان موعد بشمارید. این تنها راه حذف ۹۹ ردیفِ «تماس انجامشده» از فهرست
+ عقبافتادهها بدون از دست دادن قرارهای واقعی است.
+
+۴. **`_valid_manual_holds`**: ابطال قرار را به اقدامهای «واقعاً جایگزین قرار» محدود کنید
+ (`bazdid`/`nasb`) نه هر اقدام غیرهماهنگی؛ الان یک سند وصول یا تماس ورودی، قرار را
+ باطل میکند.
+
+۵. **برچسب «N قرار»**: تعداد را بر اساس **تاریخهای یکتا** یا **اقدامهای یکتا** حساب
+ کنید (نه تعداد کلیدهای منابع)، یا منابع هممنشأ را در `appointment_rows` ادغام کنید.
+
+۶. **نوشتن در GET**: بازنویسی `CacheTable('9mahbazdid')` را از ویو به یک دستور/سیگنال
+ منتقل کنید (کش باید مشتق باشد، نه منبع).
+
+### ۸.۳ ریسکها
+
+* هر تغییر در `overdue_followups` روی شمارندهٔ قرمزِ هدر (`entezarbazdid`/`followup_overdue`)
+ و هشدارهای صفحه اثر میگذارد؛ تست `test_overdue_counter_equals_missed_rows` را حفظ کنید.
+* `scan_visit` از `next_appointment_map` میخواند، نه از `overdue_followups`؛ پس تغییر
+ فهرست، پیامکها را جابهجا **نمیکند** (خوب است).
+* تستهای `MissedViewSimulationTests` رفتار فعلی را قفل کردهاند؛ بعد از رفع، باید
+ `assertIn` به `assertNotIn` تبدیل شود.
+
+---
+
+## ۹. باگهای شناساییشده و رفعشده
+
+> تاریخ رفع: ۲۰۲۶-۱۰-۲۲ · بدون تغییر در قرارداد دادهها (بدون migration) ·
+> ۲۶۹ + ۱۹۹ تست قبلی سبز ماند + ۴۶ تست شبیهسازی/رگرسیون.
+
+### ۹.۱ چرخهٔ کسبوکار (مبنا همهٔ تصمیمها)
+
+```
+نصب دستگاه ──► ۹ ماه ──► «موعد بازدید» (کارت عقبافتاده + پیامک visit)
+ │
+ ▼
+ تماس کارشناس (hamahangi)
+ ├── انجام نشده (waite) ──► موعد تماس = actionDateTime ──► نمای «امروز»
+ └── انجام شد (finish) ───► «تا تاریخ پیگیری نشود» ──► پنهان تا آن روز + قطع SMS
+ │
+ ▼
+ بازدید انجام شد (bazdid + finish)
+ ──► لنگر چرخه از نو (۹ ماه) + پیامک visit_created
+```
+
+اصل حاکم بر همهٔ رفعها: **قرار/بازدیدِ انجامشده باید فهرست کار را ببندد، ولی هیچ
+مشتریِ بدون قرار نباید بیصدا از فهرست کار ناپدید شود.**
+
+### ۹.۲ جدول باگها
+
+| # | نوع | باگ | ریشه | رفع |
+|---|---|---|---|---|
+| B1 | کسبوکار | فهرست `missed=1` پر از مشتریانی که بازدیدشان انجام شده بود (۹۹ از ۱۱۲) | `overdue_followups` فقط بازدیدِ `waite` را حذف میکرد | بازدیدِ `finish` با تاریخ مؤثر ≥ موعد ⇒ حذف از فهرست |
+| B2 | کسبوکار | ۳۸ ردیف همزمان «قرار معتبر آینده» و «عقبافتاده» بودند | ستون `visit_hold_until` در `overdue_followups` خوانده نمیشد | قرار دستیِ معتبر ⇒ حذف از فهرست |
+| B3 | کسبوکار | نمای «امروز» بعد از بازدیدِ همان روز، ردیف را نگه میداشت | `followups_on_date` فقط `waite` را حذف میکرد | بازدیدِ انجامشده از همان روز به بعد ⇒ حذف ردیف (فقط امروز/آینده) |
+| B4 | کسبوکار | یک سند وصول یا تماس ورودی، «قرار دستی» را بیصدا باطل میکرد | `_valid_manual_holds` هر اقدام غیرهماهنگی را «جایگزین قرار» میگرفت | فقط `bazdid`/`nasb` قرار را باطل میکنند (`_SUPERSEDING_TYPES`) |
+| B5 | کسبوکار | «۲ قرار» برای یک توافق (هماهنگی + تعویقِ همان روز) | `source_count` تعداد منابع بود، نه تعداد قرار | شمارش روزهای یکتا (و همان قاعده در `sort=status`) |
+| B6 | فنی | مرز «وظایف امروز» با رشتهٔ naive ساخته میشد (وابسته به تایمزون/DST) | `strftime('%Y-%m-%d 00:00:00')` در `ActionListToday`/`ActionListUserToday` | `_local_day_range()` صریح و aware با پوشش DST |
+| B7 | فنی | هر GET صفحهٔ انتظار، `CacheTable` را مینوشت (حتی بدون تغییر) | `save()` بیقید در ویو | فقط در صورت تغییر + `update_fields` |
+| B8 | فنی (تست) | تست `test_overdue_column_hidden_only_in_followup_view` به ساعت اجرا وابسته بود | هماهنگی «امروز ساعت ۱۲» میتوانست «آینده» شمرده شود | تست با مشتری بدون قرار، مستقل از ساعت |
+
+### ۹.۳ اثر روی دادهٔ واقعی (همان نسخهٔ پشتیبان)
+
+| سنجه | قبل | بعد |
+|---|---|---|
+| ردیفهای `?view=today&missed=1` | **۱۱۲** | **۴۲** |
+| ردیف با قرار دستیِ معتبر و همزمان در فهرست | ۳۸ | **۰** |
+| ردیف با بازدیدِ بعد از موعد (بستهشده) | ۶۲ (۳۲ + ۲۴ + …) | **۰** |
+| ردیف با بازدیدِ قدیمیتر از موعد (واقعاً نیازمند) | ۳۱ | ۳۱ |
+| ردیف بدون هیچ بازدید | ۱۳ | ۱۳ |
+
+نمونههای گزارششده: `#236` و `#1431` دیگر در فهرست نیستند (بازدیدشان بعد از موعد
+هماهنگی ثبت شده بود).
+
+### ۹.۴ چرا این رفعها چرخه را نمیشکنند
+
+* `next_appointment_map` (منبع تصمیم اسکنر پیامک) دست نخورده است ⇒ **هیچ پیامکی
+ جابهجا نشد**؛ فقط نمایش فهرست کار درست شد.
+* هیچ فیلد/معنای دادهای تغییر نکرد؛ `visit_hold_until` همچنان مشتق و بیاعتبار
+ بهتنهایی است.
+* هر رفع، «حذف از فهرست کار» است نه «حذف مشتری»: مشتریِ بدون قرار همچنان در
+ `?view=uncoordinated` (تعیینتکلیفنشده) دیده میشود.
+* قاعدهٔ «عدد هشدار = تعداد ردیف فهرست» حفظ شد (شمارنده از همان تابع میآید).
+* `followups_on_date` برای **روزهای گذشته** رفتار قبلی را نگه میدارد تا مرور
+ تقویم، سندِ کاریِ همان روز را بازنویسی نکند.
+
+### ۹.۵ باقیمانده (تصمیم محصولی، عمداً تغییر نکرد)
+
+* موعدِ هماهنگیِ `finish` همچنان «زمان تماس» است. اگر تعویق منقضی شود و بازدیدی
+ ثبت نشود، مشتری دوباره «نیازمند پیگیری» میشود. این عمدی است: «ناپدید شدن
+ بیصدای مشتریِ بیقرار» خطرناکتر از «یک ردیف اضافه» است. اگر کسبوکار تصمیم
+ بگیرد تماسِ انجامشده چرخه را ۹ ماه عقب بیندازد، تغییر فقط افزودن `hamahangi`
+ به `_CYCLE_RESET_TYPES` است — با اثر جانبی روی پیامکها.
+* `Customer.visit_hold_until` همچنان ستون مشتق است (بدون migration)؛ تنها
+ نویسندهاش باید `set_visit_hold` بماند.
+
+### ۹.۶ بازخورد شفاف + تأیید کاربر برای «عقببردن» تاریخ قرار
+
+> پیادهسازیشده · بدون migration · منطق تصمیمگیری عوض نشده (قاعدهٔ «فقط رو به جلو» پابرجاست).
+
+پیش از این، اگر تاریخ «تا تاریخ پیگیری نشود» از قرار فعلی عقبتر بود،
+`set_visit_hold` مقدار `False` برمیگرداند و لایهٔ فراخوان آن را دور میریخت:
+نه نوشتنی، نه پیامی، نه نشانهای. حالا زنجیره کامل و شفاف است:
+
+| لایه | تغییر |
+|---|---|
+| `visit_policy.set_visit_hold(..., force=False)` | پارامتر تأیید صریح؛ پیشفرض همان قاعدهٔ «فقط رو به جلو» |
+| `views/actions._apply_visit_hold(...)` | خروجی ساختیافته: `{'ok', 'reason', 'current', 'target', 'force_allowed'}` — `reason='forward_only'` یعنی نوشته نشد |
+| `views/actions._report_hold_result(...)` | `messages.warning` کوتاه: «تاریخ قرار ذخیره نشد؛ از قرار فعلی این مشتری (تاریخ) عقبتر است.» + مسیر انجام (تیک/ویرایش) |
+| `customerNewAction` / `customerEditAction` | مقدار `force_hold` فرم و گزارش نتیجه به کاربر |
+| `customerActionInc.html` | اعلان `holdBackwardNotice` + `force_hold` + تیک «تاریخ قرار را عقب ببر» (فقط در ویرایش) |
+| `customerBaseNew.html` | پیام `warning` با رنگ هشدار (کهربایی) نمایش داده میشود |
+
+### تجربهٔ کاربر
+
+1. تاریخ عقبتر را انتخاب میکند؛ همان لحظه (نه فقط هنگام ثبت) اعلان کوتاه باز میشود:
+ «این تاریخ از قرار فعلی عقبتر است» / «قرار فعلی: ۱۴۰۵/۰۸/۰۲».
+2. **در ویرایش**: تیک «تاریخ قرار را عقب ببر» ⇒ `force_hold=1` و ذخیره میشود.
+ بدون تیک، ثبت بلاک میشود و فوکوس روی همان اعلان میرود.
+3. **در ایجاد وظیفهٔ تازه**: تیک وجود ندارد؛ بهجای آن یک خط راهنما:
+ «این تاریخ ذخیره نمیشود. برای تغییر قرار، وظیفهٔ هماهنگی فعلی را ویرایش کنید.»
+ دلیل: در ایجاد، کاربر قصد اصلاح قرار موجود را ندارد و اجازه دادن میتوانست
+ ناخواسته قولِ ثبتشدهٔ قبلی (شاید کارِ کارشناس دیگر) را کوتاه کند. خودِ وظیفه
+ ثبت میشود؛ فقط تاریخ قرار عقب نمیرود.
+4. اگر جاوااسکریپت اجرا نشود یا کاربر مستقیم POST بزند، **سرور** هم مستقل بررسی
+ میکند: نه مینویسد، نه ساکت میماند — پیام هشدار روی صفحهٔ اقدام نمایش داده میشود
+ (برای ایجاد، پیام «همان وظیفه را ویرایش کنید» را نشان میدهد).
+5. همان روز ⇒ تغییر واقعی لازم نیست؛ هیچ پیامی هم ساخته نمیشود.
+
+### تستها (`HoldBackwardConfirmTests` — ۸ مورد)
+
+| تست | چه چیزی را تضمین میکند |
+|---|---|
+| `test_earlier_date_is_not_saved_and_user_is_told` | نوشته نمیشود + پیام شامل «ذخیره نشد»، قرار فعلی و مسیر تأیید؛ پیام در HTML صفحه هم دیده میشود |
+| `test_user_confirmation_writes_the_earlier_date` | با `force_hold` تاریخ عقبتر ذخیره میشود |
+| `test_forward_date_needs_no_confirmation` | تاریخ جلوتر مثل قبل بدون پرسش ذخیره میشود |
+| `test_same_day_is_silent` | همان روز ⇒ بدون پیام |
+| `test_create_does_not_move_the_hold_backward` | در ایجاد، حتی با `force_hold=1` ستون عقب نمیرود، ولی خودِ وظیفه ثبت میشود + پیام «ویرایش کنید» |
+| `test_apply_visit_hold_reports_reason` | قرارداد تابع: `reason='forward_only'` و `force_allowed=True` در ویرایش |
+| `test_apply_visit_hold_ignores_force_on_create` | `force_hold` در ایجاد نادیده گرفته میشود |
+| `test_modal_exposes_confirmation_controls` | قالب شامل `data-customer-hold`، `force_hold`، `hbForceRow`، `hbCreateHint` است |
+
+علاوه بر آن، منطق جاوااسکریپت با اجرای همان کدِ قالب (نه کپی) در Node آزموده شد:
+«عقبتر ⇒ هشدار»، «جلوتر/همان روز/بدون قرار/نوع غیرهماهنگی ⇒ بیهشدار».
+
+---
+
+## ۱۰. سناریوی «تعویق پیش از تاریخ کار» (۱۴۰۵/۰۸/۰۳ در برابر ۱۴۰۵/۰۸/۰۵)
+
+> بازتولید: `scripts/coordination_hold_order_probe.py --customer 5124`
+> قفل رفتاری: `tests/test_entezar_action_simulation.py::HoldBeforeWorkDateSimulationTests`
+
+### ۱۰.۱ چه اتفاقی میافتد
+
+| ورودی | مقدار ذخیرهشده |
+|---|---|
+| نوع = هماهنگی · «تاریخ و ساعتی که کار باید انجام شود» = ۱۴۰۵/۰۸/۰۵ | `Action.actionDateTime` = ۲۰۲۶-۱۰-۲۷ |
+| «تا تاریخ پیگیری نشود» = ۱۴۰۵/۰۸/۰۳ | `Customer.visit_hold_until` = ۲۰۲۶-۱۰-۲۵ ۲۳:۵۹:۵۹ |
+
+هر دو تاریخ در آیندهاند، پس `normalize_hold_until` قبول میکند:
+**هیچ اعتبارسنجیای «تعویقِ زودتر از تاریخ کار» را رد نمیکند** (نه در فرم، نه در سرور).
+
+نتیجه: دو تاریخ متفاوت برای یک مشتری ⇒ منبع `manual` (۱۰-۲۵) + منبع `coordination` (۱۰-۲۷).
+
+### ۱۰.۲ گاهشمار رفتار (شبیهسازیشده، هم با تیک و هم بدون تیک)
+
+| روز | منابع قرار | برچسب | «نیازمند پیگیری امروز» | «عقبافتاده» | یادآوری SMS |
+|---|---|---|---|---|---|
+| ۱۰-۲۲ تا ۱۰-۲۴ | manual + coordination | **۲ قرار** | خیر | خیر | ارسال نمیشود |
+| ۱۰-۲۵ (روزِ تعویق) | manual + coordination | ۲ قرار | **بله** ← ردیف کاری اضافه | خیر | ارسال نمیشود |
+| ۱۰-۲۶ | coordination | ۱ قرار | خیر | خیر | ارسال نمیشود |
+| ۱۰-۲۷ (تاریخ کار) | coordination | ۱ قرار | **بله** (روز قرار) | خیر | ارسال نمیشود |
+| ۱۰-۲۸ به بعد | — | بدون قرار | خیر | **بله** | (موعد چرخه) |
+
+مقایسه با حالت سالم (تعویق ۱۱-۰۱ ≥ تاریخ کار ۱۰-۲۷): از ۱۰-۲۸ به بعد
+`manual` باقی میماند، «۱ قرار» میشود و مشتری «عقبافتاده» نمیشود.
+
+### ۱۰.۳ اثر تیک «انجام شد»
+
+| مورد | با تیک | بدون تیک |
+|---|---|---|
+| `status` | `finish` | `waite` |
+| `finishDateTime` | اگر خالی ⇒ **همین حالا** (نه ۱۰-۲۷) | `NULL` |
+| `visit_hold_until` | همان ۱۰-۲۵ | همان ۱۰-۲۵ |
+| پنهانسازی/«۲ قرار»/SMS | **یکسان** | **یکسان** |
+| «لیست وظایف» و شمارندهٔ داشبورد (`status='waite'`) | خارج میشود | میماند (وظیفهٔ ۱۰-۲۷) |
+| صف «نظرسنجی» (`finish` و `nazarDT` خالی) | **وارد میشود** | وارد نمیشود |
+| تشخیص «قرار دستی» | معتبر (همهٔ شرطها) | معتبر |
+
+یعنی تیک روی «قرار» اثری ندارد؛ فقط «وظیفه بسته شد» را اعلام میکند و در عوض
+مشتری را به صف نظرسنجی میبرد.
+
+### ۱۰.۴ پیامدهای کسبوکاری این ورودی
+
+1. **ردیف کاری تکراری**: در ۱۰-۲۵ (روز تعویق) مشتری در «نیازمند پیگیری» ظاهر میشود،
+ در حالی که قرارِ توافقشده ۱۰-۲۷ است ⇒ تماسِ بیمورد.
+2. **برچسب گمراهکننده**: «۲ قرار» نشان میدهد دو توافق وجود دارد، ولی در واقع یک
+ توافق است با تعویقی که زودتر از موعدِ کار تمام میشود.
+3. **سقوطِ یکروزه به «عقبافتاده»**: فردای تاریخ کار، تعویق قبلاً منقضی شده، پس
+ «بدون قرار» و «عقبافتاده» میشود.
+4. **بدون ارسال پیامک در فاصلهٔ ۱۰-۲۵ تا ۱۰-۲۷**: چون `next_appointment_map` (که اسکنر
+ میخواند) هنوز منبع `coordination` را دارد؛ صفحه و پیامک واگرا نمیشوند.
+
+### ۱۰.۵ توصیه
+
+* برای یک توافق، **تاریخ تعویق = تاریخ کار** (یا دورتر) بگذارید؛ آنوقت «۱ قرار» میشود
+ و ردیف تکراری ساخته نمیشود.
+* اگر میخواهید سیستم جلوی این ورودی را بگیرد، حداقل تغییر: در
+ `normalize_hold_until`/ویو، اگر `hold < actionDateTime` بود، **هشدار** بدهید
+ (نه خطا) — چون در برخی سناریوها («تعویق تا این تاریخ، بعدش خودم تماس میگیرم»)
+ ممکن است عمدی باشد.
+
+---
+
+## ۱۱. بازتولید و اعتبارسنجی
+
+### ۱۱.۱ ممیزی روی دیتابیس واقعی (فقط خواندن)
+
+```bash
+.venv/Scripts/python.exe scripts/entezar_action_audit.py
+.venv/Scripts/python.exe scripts/entezar_action_audit.py --samples 10
+.venv/Scripts/python.exe scripts/entezar_action_audit.py --customer 236
+.venv/Scripts/python.exe scripts/entezar_action_audit.py --db data9/
.sqlite3
+```
+
+اسکریپت ابتدا کل فایل sqlite (بههمراه `-wal`/`-shm`) را در پوشهٔ موقت کپی میکند و
+همهٔ کوئریها/توابع `visit_policy` را روی همان کپی اجرا میکند؛ دیتابیس اصلی دستنخورده
+میماند. خروجی شامل پنج بخش است (missed، today، برچسبها، ستون قرار، هماهنگیها) و با
+`--customer` خط زمانی کامل یک مشتری.
+
+### ۱۱.۲ شبیهسازیها
+
+```bash
+.venv/Scripts/python.exe manage.py test tests.test_entezar_action_simulation -v 2
+```
+
+نتیجهٔ اجرا: **۵۵ تست، OK** (Django 3.2.25، SQLite حافظهای). پوشش:
+
+| کلاس | تعداد | چه چیزی را ثابت میکند |
+|---|---|---|
+| `ActionFormSimulationTests` | ۹ | نگاشت دو فیلد، پیشفرض ۱۴ روز، رد تاریخ گذشته/بیش از ۶ ماه، «فقط رو به جلو»، بیاثری روی بازدید |
+| `EntezarDecisionSimulationTests` | ۸ | «۲ قرار» فقط برای دو روز متفاوت، «قرار دستی»، ابطال با بازدید، روزِ پیگیری، خروج با بازدید آینده |
+| `AppointmentLifetimeSimulationTests` | ۴ | سند وصول/تماس قرار را باطل نمیکند؛ بازدید/نصب باطل میکند (B4) |
+| `FollowupDaySimulationTests` | ۳ | بازدیدِ امروز ردیف امروز را میبندد؛ بازدید دیروز نه؛ مرور روز گذشته دستنخورده (B3) |
+| `TaskListBoundaryTests` | ۲ | مرز ۰۰:۳۰/۲۳:۳۰ به وقت محلی؛ وظیفهٔ انجامشده در «امروز» نمیآید (B6) |
+| `CacheTableWriteTests` | ۱ | عدد کش داشبورد = شمارندهٔ «تعیینتکلیفنشده» (B7) |
+| `MissedViewSimulationTests` | ۹ | رفع B1/B2: بازدید `finish` و قرار دستی معتبر از فهرست خارج میکنند؛ قرارِ مرده نه |
+| `SmsSimulationTests` | ۱۱ | قطع/بازگشت یادآوری دورهای، بیاثری ستون تنها، `visit_created` با تاریخ، پاکشدن ستون با ثبت بازدید |
+| `HoldBeforeWorkDateSimulationTests` | ۹ | سناریوی «تعویق پیش از تاریخ کار»: پذیرش، «۲ قرار»، ردیف تکراری، سکوت SMS، سقوط به missed، تفاوت تیک/بدونتیک |
+
+> سناریوی ۱۴۰۵/۰۸/۰۳ در برابر ۱۴۰۵/۰۸/۰۵ در بخش ۱۰ بهتفصیل آمده و با
+> `scripts/coordination_hold_order_probe.py` قابل بازپخش است.
+
+### ۱۱.۳ محدودیتها / فرضها
+
+* همهٔ اعداد از **یک نسخهٔ پشتیبان** (۲۰۲۶-۱۰-۲۱) است؛ با دادهٔ فردا تغییر میکنند.
+* ممیزی «تاریخ مؤثر» را با قاعدهٔ `effective_visit_dt` بازسازی میکند؛ اگر رکوردی
+ `actionDateTime` آینده داشته باشد، `finishDateTime` مبنا میشود.
+* «بازدید انجامشده پس از هماهنگی» بر اساس `createdDT` (زمان ثبت) سنجیده شده، نه تاریخ
+ واقعی بازدید؛ چون `createdDT` تنها ستونی است که ترتیب ثبت را تضمین میکند.
+* هیچکدام از تستها به «امروزِ واقعی» وابسته نیستند مگر از طریق `timezone.now()` در
+ لحظهٔ اجرا؛ برای همین در هر تاریخ دیگری هم باید سبز بمانند (تاریخها نسبیاند).
+
+### ۱۱.۴ فایلهای مرتبط
+
+| فایل | نقش |
+|---|---|
+| `admin_panel/visit_policy.py` | تنها منبع حقیقت قرار/بازدید (`last_visit_map`، `appointment_sources_map`، `followups_on_date`، `overdue_followups`، `manual_holds_map`) |
+| `admin_panel/views/actions.py` | ایجاد/ویرایش/حذف اقدام، `_apply_visit_hold`، لیست وظایف |
+| `admin_panel/views/customers.py` | ویوی `entezar` و ساخت `appointment_rows` |
+| `admin_panel/sms/scanner.py` | `scan_visit` (یادآوری دورهای) |
+| `admin_panel/sms/triggers.py` | `notify_visit_scheduled` (پیامک ثبت بازدید) |
+| `admin_panel/templates/admin_panel/entezar.html` | برچسبهای وضعیت و ستونهای قرار |
+| `admin_panel/templates/admin_panel/customerActionInc.html` | فرم و JS دو فیلد + تأیید «هماهنگی انجام شد» |
+| `docs/visit_hold_entezar.md` | طراحیِ موردنظر (سند قبلی) — بخشهایی از آن در کد فعلی رعایت نشده (R2/R3) |
diff --git a/docs/visit_hold_entezar.md b/docs/visit_hold_entezar.md
index c73a7a4..b72fa83 100644
--- a/docs/visit_hold_entezar.md
+++ b/docs/visit_hold_entezar.md
@@ -43,13 +43,20 @@
>
> ۱. تاریخش از حالا جلوتر باشد،
> ۲. برای همان مشتری یک اقدام **هماهنگیِ فعال** (`waite` یا `finish`) وجود داشته باشد،
-> ۳. **آخرین فعالیتِ مشتری همان هماهنگی باشد** — اگر بعد از آن، اقدام دیگری
-> (بهویژه «بازدید») ثبت شده باشد، قرار منتفی است.
+> ۳. **آخرین اقدامِ جایگزینکنندهٔ قرار، همان هماهنگی باشد** — اگر بعد از آن
+> «بازدید» (`bazdid`) یا «نصب» (`nasb`) ثبت شده باشد، قرار منتفی است.
+> اقدامهای دیگر (وصول، تماس ورودی/خروجی، …) قرار را باطل **نمیکنند**.
شرط ۳ برای این است که «ثبت بازدید پس از هماهنگی» قرار را خودکار باطل کند.
بدون آن، قرار قدیمی تا تاریخش زنده میماند و مشتری در فهرست «با قرار» میماند،
هرچند بازدید جای هماهنگی را گرفته است.
+> **اصلاح ۲۰۲۶-۱۰-۲۲:** قبلاً «هر اقدام غیرهماهنگی» قرار را باطل میکرد؛ نتیجه
+> این بود که یک سند وصول یا یک تماس ثبتشده، قرارِ توافقشده را بیصدا از بین
+> میبرد و مشتری دوباره در فهرست تماس ظاهر میشد. حالا فقط `bazdid`/`nasb`
+> (یعنی اقدامهایی که واقعاً جای قرار مینشینند) قرار را باطل میکنند
+> (`visit_policy._SUPERSEDING_TYPES`).
+
نتیجهٔ این قاعده:
| رویداد | اثر روی قرار |
diff --git a/graphify-out/.graphify_labels.json b/graphify-out/.graphify_labels.json
index 83457a5..ee4ff7b 100644
--- a/graphify-out/.graphify_labels.json
+++ b/graphify-out/.graphify_labels.json
@@ -1,6 +1,6 @@
{
"0": "views/sms.py",
- "1": "run_all",
+ "1": "AutoSendTests",
"2": "django_db",
"3": "views/store.py",
"4": "ModelsPackageContractTests",
@@ -11,14 +11,14 @@
"9": "views/customers.py",
"10": "django_db_models_deletion",
"11": "PanelTestCase",
- "12": "make_customer",
+ "12": "mark_primary",
"13": "EventTriggerTests",
"14": "views/finance.py",
- "15": "django_contrib",
+ "15": "django_conf",
"16": "sms0098ws.py",
"17": "Admin Panel Customization",
- "18": "User",
- "19": "apply_timing",
+ "18": "views/actions.py",
+ "19": "timing_for",
"20": "InactiveCustomerHiddenTests",
"21": "normalize_mobile",
"22": "Login Template Forms",
@@ -26,12 +26,12 @@
"24": "factories.py",
"25": "Rip'n Fast. Fewer Tokens. Better Code.",
"26": "views/factors.py",
- "27": "sms0098.py",
+ "27": "SMSResult",
"28": "make_user",
"29": "QueueReviewTests",
"30": "NoticeMechanismTests",
"31": "name_fix.py",
- "32": "enqueue_sms",
+ "32": "scanner.py",
"33": "ZeroZeroNinetyEightWebServiceTests",
"34": "ScannerTests",
"35": "admin_panel/templates/admin_panel/baseHesabdari.html",
@@ -44,47 +44,47 @@
"42": "SMS Integration Docs",
"43": "enqueue.py",
"44": "django_shortcuts",
- "45": "test_inventory.py",
+ "45": "stock_in",
"46": "admin_panel/templates/admin_panel/customerAction.html",
"47": "SmsReportTests",
- "48": "write_tree",
+ "48": "generate_snapshot",
"49": "CustomerInfoCardUITests",
- "50": "TestCase",
+ "50": "enqueue_sms",
"51": "Phone Cleanup Guide",
- "52": "Customer",
+ "52": ".run_command",
"53": "project_tree.py",
"54": "Agent Operating Guidelines",
"55": "Command",
"56": "BehinSMSProvider",
- "57": "activate_sms",
+ "57": "run_all",
"58": "Financial Data Objects",
"59": "Hybrid Migration Command",
- "60": ".run_cmd",
+ "60": "PhoneNumber",
"61": "Resource Cleanup Handler",
"62": "SMS Timing Migration",
"63": "admin_panel/templates/admin_panel/bedehi.html",
"64": "customerIDFactorBackup.html",
- "65": "bale/providers.py",
- "66": "os",
+ "65": "VisitHoldTests",
+ "66": "reconciliation_tool.py",
"67": "Graphify Tooling Docs",
"68": "Command",
"69": "ZeroZeroNinetyEightLinkTests",
"70": "SMS Template Seeding",
"71": "admin_panel/entezar.html",
"72": "customerNewBackup.html",
- "73": "django_conf",
+ "73": "SMSProvider",
"74": "0070_sms_types_seven.py",
- "75": "Endpoint Smoke Tests",
+ "75": "ViewSmokeTests",
"76": "Timezone Migration History",
"77": "Command",
- "78": "UrlContractTests",
+ "78": "TodayFollowupTests",
"79": "Brand Scrubbing Migration",
"80": "SMS Dedup Migration",
"81": "Installation Guide",
"82": "Project Honesty Contract",
"83": "Flow Control Migration",
"84": "Permission Label Migration",
- "85": "PhoneNumber",
+ "85": "visit_policy.py",
"86": "Output Format Specs",
"87": "Performance Benchmarks",
"88": "SMS Integration Overview",
@@ -94,25 +94,25 @@
"92": "Infrastructure Services",
"93": "Agent Setup Guide",
"94": "First Use Guide",
- "95": "ConsoleEncodingTests",
- "96": "test_factors.py",
+ "95": "console.py",
+ "96": "test_sms.py",
"97": "تجمیع شمارههای پیامک مشتریان",
"98": "Command",
"99": "Base Template",
- "100": "test_sms.py",
- "101": "Command",
+ "100": "test_customer_name_fix.py",
+ "101": "Customer",
"102": "WorkerTests",
"103": "manage.py",
"104": "افزودن پیشوند «آقای/خانم» به نام مشتریان",
"105": "Command",
- "106": "QueueStatsTests",
- "107": "signals.py",
+ "106": "make_customer",
+ "107": "admin_panel/apps.py",
"108": "SMS Domain Logic",
"109": "Customer Communication Partials",
"110": "SMS Template Management",
"111": "Documentation and Graphing",
"112": "Admin Migration Package",
- "113": "recipients.py",
+ "113": "resolve_recipients",
"114": "Customer Action Templates",
"115": "Customer Feedback Components",
"116": "Customer Invoice Details",
@@ -152,7 +152,7 @@
"150": "Transfer Processing Endpoint",
"151": "Satisfaction Edit Endpoint",
"152": "Customer Deletion Endpoint",
- "153": "HttpProviderRobustnessTests",
+ "153": "HTTPSMSProvider",
"154": "Django Permissions System",
"155": "Static Asset Management",
"156": "Data Cleanup Documentation",
@@ -163,10 +163,10 @@
"161": "Project Dependencies",
"162": "Test Suite Package",
"163": "Customer",
- "164": "factory.py",
+ "164": "EntezarPageControlsTests",
"165": "0075_cheque_funding_before_due.py",
"166": "توقف و ادامه (Resume / Checkpoint)",
- "167": "Command",
+ "167": "RecalcCommandTests",
"168": "0071_sms_settings_permission.py",
"169": "0056_auto_20260731_1824.py",
"170": "الگوریتم تصمیم (به همین ترتیب)",
@@ -174,36 +174,52 @@
"172": "0073_alter_action_actiontype_choices.py",
"173": "stop.sh",
"174": "0074_alter_action_status_choices.py",
- "175": "remove_query_param",
+ "175": "at",
"176": "اجرای محلی (توسعه)",
- "177": ".test_deleted_visit_reminder_rearms_next_period_only",
+ "177": "apply_timing",
"178": "اجرا روی سرور Production",
"179": "اجرای محلی (توسعه)",
"180": "ادامهٔ job پس از crash / قطع برق",
"181": "اجرا روی سرور Production",
- "182": "wsgi.py",
+ "182": "os",
"183": "0005_auto_20220121_2054.py",
- "184": "0009_remove_storeroom_user.py",
+ "184": "test_visit_policy.py",
"185": "0014_auto_20220123_1348.py",
- "186": "0015_auto_20220124_0858.py",
- "187": "0017_factoriteam_garantyesh.py",
- "188": "0022_article_typepy.py",
- "189": "0026_auto_20220209_1924.py",
+ "186": "ActionSoftDeleteTests",
+ "187": "last_visit_map",
+ "188": "CoordinationDoneConfirmTests",
+ "189": "BaseTemplateStructureTests",
"190": "0028_action_grade.py",
- "191": "0032_article_paystatus.py",
- "192": "0033_article_nahvepardakht.py",
+ "191": "validate_action_payload",
+ "192": "NextAppointmentTests",
"193": "0034_alter_article_sarresid.py",
- "194": "0035_alter_article_sarresid.py",
- "195": "0040_customer_laghv_bazdid.py",
+ "194": "django_contrib_auth_models",
+ "195": "entezar",
"196": "0041_auto_20240216_1607.py",
- "197": "0048_alter_productinstore_options.py",
- "198": "0053_storeroom_owners.py",
- "199": "0055_action_nextnazardt.py",
+ "197": "Action",
+ "198": "بخش دوم — امکانات جدید رابط کاربری صفحهٔ انتظار",
+ "199": "بازمحاسبهٔ «آخرین بازدید» مشتریان",
"200": "0062_sms_link_provider.py",
- "201": "0063_alter_smsqueue_status.py",
- "202": "0068_auto_20261010_1305.py",
+ "201": "TestCase",
+ "202": "newFactor",
"203": "0072_customernamefixjob.py",
"204": "0077_alter_smstemplate_text_balance_help.py",
- "205": ".test_visit_period_comes_from_template",
- "206": ".test_period_start_filters_out_very_old_customers"
+ "205": "«قرار بعدی» بازدید و صفحهٔ انتظار — توضیح کامل عملکرد",
+ "206": "SMSDeliveryCommandTests",
+ "207": "ActionModalTemplateTests",
+ "208": "customer_balance",
+ "209": "VisitDecisionTests",
+ "210": "EntezarScannerConsistencyTests",
+ "211": "EntezarLayoutTests",
+ "212": "signals.py",
+ "213": "۴. فرم ثبت اقدام (UI)",
+ "214": "۱.۱ نمای «نیازمند پیگیری امروز» (`?view=today`) — **پیشفرض صفحه**",
+ "215": "cleanup_visit_dedup.py",
+ "216": "۶.۱ حذف یک اقدام (soft-delete)",
+ "217": "active_deferral",
+ "218": "index",
+ "219": "0025_action_actiontype.py",
+ "220": "0027_alter_action_nazar.py",
+ "221": "0029_alter_article_options.py",
+ "222": "0030_auto_20220414_1047.py"
}
diff --git a/graphify-out/.graphify_labels.json.sig b/graphify-out/.graphify_labels.json.sig
index 12fd07c..2820c16 100644
--- a/graphify-out/.graphify_labels.json.sig
+++ b/graphify-out/.graphify_labels.json.sig
@@ -1 +1 @@
-{"0": "1dfdd2cf317807f3", "1": "17cefe4501ef5349", "2": "b498606dcac72b73", "3": "bd6e67d4a5f28a70", "4": "28cdf3baa198be4d", "5": "fc9b619d52672e4e", "6": "50f8029c1ec04918", "7": "97d81e46229f8a49", "8": "313943d53fc2bd9f", "9": "627a6196750653eb", "10": "b61d115e59860e03", "11": "9e67a33b989286ec", "12": "e88164ad40862751", "13": "3b56f304a0c6c939", "14": "6dce744bd0131ce7", "15": "e41d0c7172d175f1", "16": "7d8afbd7fea7cf7b", "17": "9eec9953ee3ca67f", "18": "d947d20b351370eb", "19": "d26d5b0aeca2417c", "20": "a8997e767700eb26", "21": "80296933c90021fc", "22": "9701f1591d8fda6e", "23": "8f0f51ff611eb425", "24": "2fdec3396c6901a3", "25": "75866dc8db907971", "26": "bcc559e489b72153", "27": "9b6b55a8d8dcb4c4", "28": "35722e3c74f50804", "29": "e0f6e325bac28363", "30": "bf1e337abf82f763", "31": "e4282689f6251d51", "32": "e5bd8cd35998e36a", "33": "3746e81c1256465f", "34": "817f04c7b50c9b76", "35": "0928f3aa4e597117", "36": "4e9567703b727de7", "37": "d36df3c85181f806", "38": "bc695fcee2050806", "39": "7b572aa5bf8298b3", "40": "4cc746f3f10c71a6", "41": "59b1c6760dbaf1b9", "42": "bbd8a9066e48dada", "43": "b6e84eea788411f9", "44": "4b603c000aa79db9", "45": "8cda0f1879c40078", "46": "f38c1f1dc0ead50f", "47": "93e8d94e7c998882", "48": "9d46bbe4641a8543", "49": "df2dbdd967da3eaf", "50": "28183579704b35fd", "51": "afc6d855b9fc1b76", "52": "5e7f77f5e6dac074", "53": "e97ba10a1e6a2df3", "54": "34401beb9f60ebfd", "55": "ffa54e3d7035005e", "56": "330644cc56a0e06c", "57": "9e17a2dab584e04c", "58": "90146abe85d9419f", "59": "88f880da5a61f9dc", "60": "356d13eb0dd615ee", "61": "dbfbf8557361f5ea", "62": "6e90b41fbd82c265", "63": "6af26e59b0e8c354", "64": "8ae1cb991df55ba5", "65": "7b932cfef527d130", "66": "3de85332d944dae4", "67": "0f632fc57e9feeb0", "68": "92d7e4efe25770db", "69": "2937001282a8a8b6", "70": "df7ea8ce3fa52be3", "71": "fd47ef3914adf11e", "72": "9b3249f4a4e6fbfe", "73": "39cd7adefdc0d3ce", "74": "a534e43d03671a7c", "75": "5290f7a7558a9c3e", "76": "a19c211702401a5b", "77": "35aa0fc2a0626f70", "78": "996c5e480613f739", "79": "da683f642f8c61ac", "80": "40313433c93b7054", "81": "81af0a28b68936d9", "82": "25287261c3d1c44b", "83": "bda9be183e0f933e", "84": "63526d01888c4f22", "85": "ad4f4a79134b4346", "86": "694a38d63caf7217", "87": "f220eaa4fac26e3e", "88": "d78c8e21a45613e9", "89": "e5ca736ea1d10113", "90": "03f8896e9b60ca96", "91": "5b7e9b89423f4164", "92": "4bfa64a1cd8c6a47", "93": "057eae5a523d2004", "94": "5de74655cbc56e06", "95": "7767ac1c95366fb0", "96": "f36a8effec98632a", "97": "de6816239e70e6e8", "98": "7508d3e84c588617", "99": "15f6a40009e539bb", "100": "f4ecac15615b820e", "101": "4b7f81f0e7e4c044", "102": "7c85c7a24cf3cbed", "103": "3ec6277e63307488", "104": "756a405f723988ce", "105": "d55977ec0e3ae213", "106": "64fb409201f034b8", "107": "19d9362d6aa40786", "108": "b738c5aefe939cff", "109": "d2001d7d283134ac", "110": "6b8e6100893a6b6a", "111": "66ed76491a412a72", "112": "e67668b62ee6c62c", "113": "6bb5ee4a70642036", "114": "68aee39cd24db354", "115": "054401d27ffaa4b2", "116": "dace81c1995fb321", "117": "19e301aeace95bb2", "118": "d6fd0cec4634a0b0", "119": "0230d355a6dabb54", "120": "5cebbb5528321795", "121": "536fe28d24c361f5", "122": "b08bdc2712cb0f12", "123": "6ce8444f504022cc", "124": "8021f52559633383", "125": "d8e34a3b3eb43ee0", "126": "171c34c74a95481b", "127": "73e9eca6f2ca1c9b", "128": "dcc2230ae751c29a", "129": "2a617b932f5d92b2", "130": "d55cb638532fdfeb", "131": "06cae4389208269f", "132": "e9c0a7be255e9b26", "133": "455b4ca39e55ecb0", "134": "8105c55538c34081", "135": "74304670b54c018f", "136": "a27f876eabb044b2", "137": "73f74dd7a40776d6", "138": "29d433d164b99f89", "139": "3f44aa48aa05d597", "140": "ca8a7efba0a3e579", "141": "5cc770af9d184d77", "142": "2d31fa723128b164", "143": "be720fc0071f0c3d", "144": "9d814c2520329846", "145": "f97d1a5c3a066598", "146": "14f84a6e4cd311d6", "147": "3a592d27b1f4ccc9", "148": "0bf6f9d0aeab4fd8", "149": "8b491bb3ff0649dd", "150": "638812cee15d1b9b", "151": "3dd16d68928a479c", "152": "03a0ee43e18f7934", "153": "b672f7977ddfa52e", "154": "435331945d6aad17", "155": "968e1a135dc062b7", "156": "d1a987a548e55a76", "157": "a9bf69b3d550abef", "158": "b039e1c69c8fb7b9", "159": "60029bf0948fc02a", "160": "72dad91d66d471ce", "161": "353a9d09eaae47ea", "162": "43bbf35b96423d46", "163": "4faac8c35fa8b703", "164": "76d6488c0976adbc", "165": "652eabd3db52041c", "166": "bca6f6760a942d10", "167": "ce80aac5f4c439da", "168": "1abe2e7b0bcbeca9", "169": "cfbcf8cef78c5757", "170": "c9f25e20d3a7d5d8", "171": "7e818e8715c414bd", "172": "8d68a258e8ec738b", "173": "827420b645a55301", "174": "19e1beef5d40e7ac", "175": "d6451fb8a1e2d3ca", "176": "adac855b05ea55ee", "177": "95a53382a8e217be", "178": "c67bd11d1973d6ac", "179": "63c999649e523e73", "180": "56f7f3573c34f651", "181": "f8f5346df7d0a239", "182": "9c5614d4fee3efc0", "183": "6886ad7a5751faca", "184": "a1a1f3c1cdff72cf", "185": "32f7ccd6bd4068c7", "186": "1d89c741fb1f9b27", "187": "a800ecffc01fc9d5", "188": "f97753a176d20dae", "189": "9f8c26fb5fbe89a4", "190": "edb619bf5ac0fc49", "191": "d5c0f5f6714c9991", "192": "9948fb8b548535ed", "193": "c02bc9595eaedd36", "194": "7714d721fe893ef5", "195": "ef8221c5fdeee7f8", "196": "8d20e5c1bce8916d", "197": "bf60a8731033e467", "198": "7ca812ef30c0752b", "199": "ef1669fc78d119b0", "200": "f0242773793fa7bf", "201": "84208a7b8507da66", "202": "9144923ab31b52a3", "203": "1e2795525cbc6fa2", "204": "e298c218ccbada05", "205": "79d00401c99b2005", "206": "55d209d7b64c2954"}
\ No newline at end of file
+{"0": "0b739897e89c7b41", "1": "c845994d0f12d034", "2": "8c7679f720857bf1", "3": "9653c364b823290d", "4": "ea39680823c06133", "5": "1c7a6b6536eca5f4", "6": "6b788d0721a913f7", "7": "97d81e46229f8a49", "8": "313943d53fc2bd9f", "9": "da38f92f51e93c79", "10": "0153b3a4e04dd75a", "11": "41d17fe9560c47f6", "12": "d6d2ce2d7d595ba2", "13": "e799ebae9c509cb3", "14": "48d0d033c0d2964f", "15": "58e9c0768a2c889d", "16": "e4b58f9f9e36d64a", "17": "9eec9953ee3ca67f", "18": "1b6aefd19ce25cbe", "19": "a7ba24b2eb2edbbe", "20": "a8997e767700eb26", "21": "0020ce7d1908ad7a", "22": "9701f1591d8fda6e", "23": "7cd46ce4c8fb3ca4", "24": "2ffddab549a70d4b", "25": "90da597ad2a7919f", "26": "c2574e654e1d0322", "27": "994c5e2378052e94", "28": "9a4550f6e7e78e08", "29": "ff4922906a507758", "30": "e4f88099ea3ccfa4", "31": "4d4f780fb4e7f8ea", "32": "293ee6609d9f939d", "33": "0564a8d4ebd069cc", "34": "d92b5a81599860af", "35": "0928f3aa4e597117", "36": "b584145a0a6af86a", "37": "d36df3c85181f806", "38": "bc695fcee2050806", "39": "b5fb0ddc2a912cb0", "40": "ef568e093f9cb1b3", "41": "18750a5fb3366fa5", "42": "bbd8a9066e48dada", "43": "0822c3c7bdd37717", "44": "159d7e82897e35fc", "45": "a7e9b25451da94c4", "46": "f38c1f1dc0ead50f", "47": "9004ecf9c7067aa0", "48": "67d2b4075e544a13", "49": "df2dbdd967da3eaf", "50": "8a2fd7414c9ee9e6", "51": "afc6d855b9fc1b76", "52": "5b43fbf8fcf8716d", "53": "e97ba10a1e6a2df3", "54": "34401beb9f60ebfd", "55": "ffa54e3d7035005e", "56": "330644cc56a0e06c", "57": "e2cb064067d950bd", "58": "90146abe85d9419f", "59": "88f880da5a61f9dc", "60": "306fc8a49c104841", "61": "dbfbf8557361f5ea", "62": "6e90b41fbd82c265", "63": "6af26e59b0e8c354", "64": "8ae1cb991df55ba5", "65": "870e90323abfd449", "66": "8f5a6aa62c618b5f", "67": "0f632fc57e9feeb0", "68": "92d7e4efe25770db", "69": "edda1f9fc3ddc6f9", "70": "df7ea8ce3fa52be3", "71": "fd47ef3914adf11e", "72": "9b3249f4a4e6fbfe", "73": "12a25396ede4f1e9", "74": "a534e43d03671a7c", "75": "0d14f943dc48c339", "76": "a19c211702401a5b", "77": "35aa0fc2a0626f70", "78": "fed2ae936b50d86d", "79": "da683f642f8c61ac", "80": "40313433c93b7054", "81": "81af0a28b68936d9", "82": "25287261c3d1c44b", "83": "bda9be183e0f933e", "84": "63526d01888c4f22", "85": "0bf99e0a63147db6", "86": "694a38d63caf7217", "87": "f220eaa4fac26e3e", "88": "d78c8e21a45613e9", "89": "e5ca736ea1d10113", "90": "03f8896e9b60ca96", "91": "4d4f5e897228ae79", "92": "4bfa64a1cd8c6a47", "93": "057eae5a523d2004", "94": "5de74655cbc56e06", "95": "b4f836f96422aa82", "96": "8d8fa0ac595a0ab7", "97": "19b821f40f591523", "98": "7508d3e84c588617", "99": "15f6a40009e539bb", "100": "634340879ffb956e", "101": "e752861d6bd6141e", "102": "9bb0051cf12f369d", "103": "85ecb40ed558ed43", "104": "756a405f723988ce", "105": "d55977ec0e3ae213", "106": "5970abbcf384c31d", "107": "06b5a3c2baa12fdb", "108": "b738c5aefe939cff", "109": "d2001d7d283134ac", "110": "6b8e6100893a6b6a", "111": "66ed76491a412a72", "112": "e67668b62ee6c62c", "113": "38b87928585aa997", "114": "68aee39cd24db354", "115": "054401d27ffaa4b2", "116": "dace81c1995fb321", "117": "19e301aeace95bb2", "118": "d6fd0cec4634a0b0", "119": "0230d355a6dabb54", "120": "5cebbb5528321795", "121": "536fe28d24c361f5", "122": "b08bdc2712cb0f12", "123": "6ce8444f504022cc", "124": "8021f52559633383", "125": "d8e34a3b3eb43ee0", "126": "171c34c74a95481b", "127": "73e9eca6f2ca1c9b", "128": "dcc2230ae751c29a", "129": "2a617b932f5d92b2", "130": "d55cb638532fdfeb", "131": "06cae4389208269f", "132": "e9c0a7be255e9b26", "133": "455b4ca39e55ecb0", "134": "8105c55538c34081", "135": "74304670b54c018f", "136": "a27f876eabb044b2", "137": "73f74dd7a40776d6", "138": "29d433d164b99f89", "139": "3f44aa48aa05d597", "140": "ca8a7efba0a3e579", "141": "5cc770af9d184d77", "142": "2d31fa723128b164", "143": "be720fc0071f0c3d", "144": "9d814c2520329846", "145": "f97d1a5c3a066598", "146": "14f84a6e4cd311d6", "147": "3a592d27b1f4ccc9", "148": "0bf6f9d0aeab4fd8", "149": "8b491bb3ff0649dd", "150": "638812cee15d1b9b", "151": "3dd16d68928a479c", "152": "03a0ee43e18f7934", "153": "1b4ee66550701d94", "154": "435331945d6aad17", "155": "968e1a135dc062b7", "156": "d1a987a548e55a76", "157": "a9bf69b3d550abef", "158": "b039e1c69c8fb7b9", "159": "60029bf0948fc02a", "160": "72dad91d66d471ce", "161": "353a9d09eaae47ea", "162": "43bbf35b96423d46", "163": "4faac8c35fa8b703", "164": "4af634c3c14ff03b", "165": "652eabd3db52041c", "166": "bca6f6760a942d10", "167": "29cb511ef6daa0c1", "168": "1abe2e7b0bcbeca9", "169": "cfbcf8cef78c5757", "170": "c9f25e20d3a7d5d8", "171": "7e818e8715c414bd", "172": "8d68a258e8ec738b", "173": "827420b645a55301", "174": "19e1beef5d40e7ac", "175": "43e1b3409f1becc6", "176": "adac855b05ea55ee", "177": "237c0746be1bce16", "178": "c67bd11d1973d6ac", "179": "63c999649e523e73", "180": "56f7f3573c34f651", "181": "f8f5346df7d0a239", "182": "43b977f2b2e6524e", "183": "6886ad7a5751faca", "184": "bfad11052c54a1cc", "185": "32f7ccd6bd4068c7", "186": "3f88bb4be9a0294a", "187": "f3756e86a6b2bee6", "188": "5bb9e4f75573b97c", "189": "bcc36ee1c288bd57", "190": "edb619bf5ac0fc49", "191": "459ed3078aed4df0", "192": "3b37db72fb202ee6", "193": "c02bc9595eaedd36", "194": "116a2c0fd7aa31c1", "195": "ed9e56af6e3d83df", "196": "8d20e5c1bce8916d", "197": "de8b3df94a8b37a1", "198": "e7edd1d6deb5308c", "199": "69bb788e4cabbf39", "200": "f0242773793fa7bf", "201": "1b3921662ec9c2e1", "202": "24b1632435476856", "203": "1e2795525cbc6fa2", "204": "e298c218ccbada05", "205": "b2b31f89e4953b6c", "206": "1cac82bc309473ef", "207": "a54e1d456d8a1722", "208": "8ea7df32a2131a33", "209": "de2620566f7a6d0a", "210": "b26bdb07adcd2a21", "211": "dd200315cb868787", "212": "d0c2310f7d93caf6", "213": "a41120359da2fa5d", "214": "8e5febc9c9183aeb", "215": "1a9f7bd7ba52809b", "216": "e5e89af901265c29", "217": "2c95216072dc8f04", "218": "ce5f237abed01c9e", "219": "9cc7a507cfecd8fb", "220": "be1b722cf1a01583", "221": "cde232c2541ef2d0", "222": "8cb6b3db3aadb973"}
\ No newline at end of file
diff --git a/graphify-out/GRAPH_REPORT.md b/graphify-out/GRAPH_REPORT.md
index dc1d52c..9c2accd 100644
--- a/graphify-out/GRAPH_REPORT.md
+++ b/graphify-out/GRAPH_REPORT.md
@@ -1,23 +1,23 @@
-# Graph Report - khatibi (2026-10-20)
+# Graph Report - khatibi (2026-10-22)
## Corpus Check
-- 197 files · ~287,853 words
+- 204 files · ~314,556 words
- Verdict: corpus is large enough that graph structure adds value.
- Unclassified: 8 file(s) not represented in the graph (top: (none) 5, .env 2, .csv 1)
## Summary
-- 1991 nodes · 3632 edges · 207 communities (87 shown, 120 thin omitted)
-- Extraction: 95% EXTRACTED · 5% INFERRED · 0% AMBIGUOUS · INFERRED: 188 edges (avg confidence: 0.89)
+- 2509 nodes · 4876 edges · 223 communities (104 shown, 119 thin omitted)
+- Extraction: 95% EXTRACTED · 5% INFERRED · 0% AMBIGUOUS · INFERRED: 234 edges (avg confidence: 0.91)
- Token cost: 0 input · 0 output
## Graph Freshness
-- Built from commit: `1fd27d04`
+- Built from commit: `99c25a82`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
## Community Hubs (Navigation)
- views/sms.py
-- run_all
+- AutoSendTests
- django_db
- views/store.py
- ModelsPackageContractTests
@@ -27,13 +27,14 @@
- views/customers.py
- django_db_models_deletion
- PanelTestCase
-- make_customer
+- mark_primary
- EventTriggerTests
- views/finance.py
+- django_conf
- sms0098ws.py
- Admin Panel Customization
-- User
-- apply_timing
+- views/actions.py
+- timing_for
- InactiveCustomerHiddenTests
- normalize_mobile
- Login Template Forms
@@ -41,12 +42,12 @@
- factories.py
- Rip'n Fast. Fewer Tokens. Better Code.
- views/factors.py
-- sms0098.py
+- SMSResult
- make_user
- QueueReviewTests
- NoticeMechanismTests
- name_fix.py
-- enqueue_sms
+- scanner.py
- ZeroZeroNinetyEightWebServiceTests
- ScannerTests
- admin_panel/templates/admin_panel/baseHesabdari.html
@@ -59,47 +60,47 @@
- SMS Integration Docs
- enqueue.py
- django_shortcuts
-- test_inventory.py
+- stock_in
- admin_panel/templates/admin_panel/customerAction.html
- SmsReportTests
-- write_tree
+- generate_snapshot
- CustomerInfoCardUITests
-- TestCase
+- enqueue_sms
- Phone Cleanup Guide
-- Customer
+- .run_command
- project_tree.py
- Agent Operating Guidelines
- Command
- BehinSMSProvider
-- activate_sms
+- run_all
- Financial Data Objects
- Hybrid Migration Command
-- .run_cmd
+- PhoneNumber
- Resource Cleanup Handler
- SMS Timing Migration
- admin_panel/templates/admin_panel/bedehi.html
- customerIDFactorBackup.html
-- bale/providers.py
-- os
+- VisitHoldTests
+- reconciliation_tool.py
- Graphify Tooling Docs
- Command
- ZeroZeroNinetyEightLinkTests
- SMS Template Seeding
- admin_panel/entezar.html
- customerNewBackup.html
-- django_conf
+- SMSProvider
- 0070_sms_types_seven.py
-- Endpoint Smoke Tests
+- ViewSmokeTests
- Timezone Migration History
- Command
-- UrlContractTests
+- TodayFollowupTests
- Brand Scrubbing Migration
- SMS Dedup Migration
- Installation Guide
- Project Honesty Contract
- Flow Control Migration
- Permission Label Migration
-- PhoneNumber
+- visit_policy.py
- Output Format Specs
- Performance Benchmarks
- SMS Integration Overview
@@ -109,23 +110,23 @@
- Infrastructure Services
- Agent Setup Guide
- First Use Guide
-- ConsoleEncodingTests
-- test_factors.py
+- console.py
+- test_sms.py
- تجمیع شمارههای پیامک مشتریان
- Command
- Base Template
-- test_sms.py
-- Command
+- test_customer_name_fix.py
+- Customer
- WorkerTests
- manage.py
- افزودن پیشوند «آقای/خانم» به نام مشتریان
- Command
-- QueueStatsTests
-- signals.py
+- make_customer
+- admin_panel/apps.py
- Customer Communication Partials
- SMS Template Management
- Documentation and Graphing
-- recipients.py
+- resolve_recipients
- Customer Action Templates
- Customer Feedback Components
- Customer Invoice Details
@@ -155,14 +156,14 @@
- Store Transfer Interface
- Store Directory Template
- Currency Formatting Utility
-- HttpProviderRobustnessTests
+- HTTPSMSProvider
- Data Cleanup Documentation
- Project Dependencies
- Customer
-- factory.py
+- EntezarPageControlsTests
- 0075_cheque_funding_before_due.py
- توقف و ادامه (Resume / Checkpoint)
-- Command
+- RecalcCommandTests
- 0071_sms_settings_permission.py
- 0056_auto_20260731_1824.py
- الگوریتم تصمیم (به همین ترتیب)
@@ -170,59 +171,78 @@
- 0073_alter_action_actiontype_choices.py
- stop.sh
- 0074_alter_action_status_choices.py
-- remove_query_param
+- at
- اجرای محلی (توسعه)
-- .test_deleted_visit_reminder_rearms_next_period_only
+- apply_timing
- اجرا روی سرور Production
- اجرای محلی (توسعه)
- ادامهٔ job پس از crash / قطع برق
- اجرا روی سرور Production
+- os
- 0005_auto_20220121_2054.py
-- 0009_remove_storeroom_user.py
+- test_visit_policy.py
- 0014_auto_20220123_1348.py
-- 0015_auto_20220124_0858.py
-- 0017_factoriteam_garantyesh.py
-- 0022_article_typepy.py
-- 0026_auto_20220209_1924.py
+- ActionSoftDeleteTests
+- last_visit_map
+- CoordinationDoneConfirmTests
+- BaseTemplateStructureTests
- 0028_action_grade.py
-- 0032_article_paystatus.py
-- 0033_article_nahvepardakht.py
+- validate_action_payload
+- NextAppointmentTests
- 0034_alter_article_sarresid.py
-- 0035_alter_article_sarresid.py
-- 0040_customer_laghv_bazdid.py
+- django_contrib_auth_models
+- entezar
- 0041_auto_20240216_1607.py
-- 0048_alter_productinstore_options.py
-- 0053_storeroom_owners.py
-- 0055_action_nextnazardt.py
+- Action
+- بخش دوم — امکانات جدید رابط کاربری صفحهٔ انتظار
+- بازمحاسبهٔ «آخرین بازدید» مشتریان
- 0062_sms_link_provider.py
-- 0063_alter_smsqueue_status.py
-- 0068_auto_20261010_1305.py
+- TestCase
+- newFactor
- 0072_customernamefixjob.py
- 0077_alter_smstemplate_text_balance_help.py
+- «قرار بعدی» بازدید و صفحهٔ انتظار — توضیح کامل عملکرد
+- SMSDeliveryCommandTests
+- ActionModalTemplateTests
+- customer_balance
+- VisitDecisionTests
+- EntezarScannerConsistencyTests
+- EntezarLayoutTests
+- signals.py
+- ۴. فرم ثبت اقدام (UI)
+- ۱.۱ نمای «نیازمند پیگیری امروز» (`?view=today`) — **پیشفرض صفحه**
+- cleanup_visit_dedup.py
+- ۶.۱ حذف یک اقدام (soft-delete)
+- active_deferral
+- index
+- 0025_action_actiontype.py
+- 0027_alter_action_nazar.py
+- 0029_alter_article_options.py
+- 0030_auto_20220414_1047.py
## God Nodes (most connected - your core abstractions)
-1. `make_customer()` - 89 edges
-2. `run_all()` - 28 edges
-3. `make_user()` - 28 edges
-4. `normalize_mobile()` - 27 edges
-5. `Customer` - 24 edges
-6. `PhoneNumber` - 24 edges
-7. `Command` - 24 edges
-8. `enqueue_sms()` - 24 edges
-9. `SMSResult` - 23 edges
-10. `Rip'n Fast. Fewer Tokens. Better Code.` - 23 edges
+1. `make_customer()` - 111 edges
+2. `as_aware()` - 50 edges
+3. `make_user()` - 42 edges
+4. `at()` - 42 edges
+5. `VisitHoldTests` - 41 edges
+6. `TodayFollowupTests` - 38 edges
+7. `Customer` - 37 edges
+8. `PhoneNumber` - 30 edges
+9. `make_visit()` - 29 edges
+10. `EntezarPageControlsTests` - 28 edges
## Surprising Connections (you probably didn't know these)
- `نکتههای مهم` --references--> `Action` [INFERRED]
recalc_last_bazdid.md → admin_panel/models/actions.py
-- `مدل وضعیت job` --references--> `CustomerNameFixJob` [INFERRED]
- fix_customer_names.md → admin_panel/models/system.py
-- `چرا؟` --references--> `CustomerNameFixJob` [INFERRED]
- fix_customer_names.md → admin_panel/models/system.py
-- `سریعترین مسیر روی Production (اگر همین یک دستور را میخواهی)` --references--> `CustomerNameFixJob` [INFERRED]
- normalize_customer_sms_phones.md → admin_panel/models/system.py
- `تستها` --references--> `resolve_recipients()` [INFERRED]
normalize_customer_sms_phones.md → admin_panel/sms/recipients.py
+- `۷. سناریوهای تستشده` --references--> `next_appointment_map()` [INFERRED]
+ docs/visit_hold_entezar.md → admin_panel/visit_policy.py
+- `ایمنی ویرایش رکوردهای قدیمی` --references--> `manual_holds_map()` [INFERRED]
+ docs/visit_hold_entezar.md → admin_panel/visit_policy.py
+- `هشدار «پیگیریهای عقبافتاده»` --references--> `overdue_followups()` [INFERRED]
+ docs/visit_hold_entezar.md → admin_panel/visit_policy.py
## Import Cycles
- None detected.
@@ -255,15 +275,15 @@
- **New Factor Creation Flow** — admin_panel_templates_admin_panel_customeridbackup_html, django_url_admin_panel_productsjson, api_endpoint_panel_productmojodi, admin_panel_templates_admin_panel_customeridbackup_html_orderlist, admin_panel_templates_admin_panel_customeridbackup_html_buildorderlist, django_url_admin_panel_newfactor [INFERRED]
- **Store and Inventory Management Flow** — admin_panel_templates_admin_panel_basestore, concept_url_admin_panel_stores, concept_url_admin_panel_productadd, concept_url_admin_panel_operations, concept_url_admin_panel_store_transfers [INFERRED]
-## Communities (207 total, 120 thin omitted)
+## Communities (223 total, 119 thin omitted)
### Community 0 - "views/sms.py"
Cohesion: 0.12
-Nodes (18): _approve(), customer_sms_history(), _customer_url(), _notice(), parse_jalali_date(), parse_scan_since(), resolve_recipients_test(), sms_customer_toggle() (+10 more)
+Nodes (19): _approve(), customer_sms_history(), _customer_url(), _notice(), parse_jalali_date(), parse_scan_since(), _remember_keys(), resolve_recipients_test() (+11 more)
### Community 2 - "django_db"
-Cohesion: 0.05
-Nodes (20): Migration, Migration, Migration, Migration, Migration, Migration, Migration, Migration (+12 more)
+Cohesion: 0.04
+Nodes (28): Migration, Migration, Migration, Migration, Migration, Migration, Migration, Migration (+20 more)
### Community 3 - "views/store.py"
Cohesion: 0.05
@@ -273,105 +293,101 @@ Nodes (32): Product, Meta, ProductCategory, ProductInStore, Meta, MovementType,
Cohesion: 0.08
Nodes (9): Command, Migration, scrub_legacy_phones(), _append_note(), clean_all(), clean_customer(), clean_legacy_cell(), _numeric_tokens() (+1 more)
-### Community 6 - ".load"
-Cohesion: 0.19
-Nodes (4): 12. Supported languages and formats, Languages, SettingsPanelTests, TestMessageSettingsTests
-
### Community 7 - "models/sms.py"
Cohesion: 0.06
Nodes (15): CustomerSMSSetting, Meta, SMSDedup, Meta, SMSLog, Meta, SMSProviderConfig, Meta (+7 more)
### Community 9 - "views/customers.py"
Cohesion: 0.10
-Nodes (18): normalize_primary_values(), customer_toggle_map(), notify_welcome(), customer(), customerDel(), customerEdit(), customerFactor(), customerFactors() (+10 more)
+Nodes (15): customer_toggle_map(), notify_welcome(), customer(), customerDel(), customerEdit(), customerFactor(), customerFactors(), customerNew() (+7 more)
### Community 10 - "django_db_models_deletion"
-Cohesion: 0.05
-Nodes (19): Migration, Migration, Migration, Migration, Migration, Migration, Migration, Migration (+11 more)
+Cohesion: 0.07
+Nodes (13): Migration, Migration, Migration, Migration, Migration, Migration, Migration, Migration (+5 more)
### Community 11 - "PanelTestCase"
-Cohesion: 0.15
-Nodes (3): EmptyQueueTests, PanelTestCase, SentQueueProtectionTests
+Cohesion: 0.18
+Nodes (3): DeletedStaysDeletedTests, PanelTestCase, SentQueueProtectionTests
-### Community 12 - "make_customer"
-Cohesion: 0.10
-Nodes (7): resolve_recipients(), تستها, make_customer(), DeletedStaysDeletedTests, mark_primary(), PrimaryNumberPanelTests, RecipientTests
+### Community 12 - "mark_primary"
+Cohesion: 0.14
+Nodes (3): mark_primary(), RecipientTests, WelcomeTriggerTests
### Community 14 - "views/finance.py"
-Cohesion: 0.20
-Nodes (18): Article, Meta, addSarresid(), bedehi(), bedehiKhadamat(), bedehiNasb(), financial_dashboard(), hesabdari() (+10 more)
+Cohesion: 0.17
+Nodes (20): Article, Meta, notify_payment_thanks(), customerHesabdariAdd(), addSarresid(), bedehi(), bedehiKhadamat(), bedehiNasb() (+12 more)
+
+### Community 15 - "django_conf"
+Cohesion: 0.08
+Nodes (11): Migration, Migration, Migration, Migration, Migration, Migration, Migration, Migration (+3 more)
### Community 16 - "sms0098ws.py"
-Cohesion: 0.11
-Nodes (5): to_local(), classify_delivery(), DeliveryCode, describe_send_code(), _as_text()
+Cohesion: 0.13
+Nodes (5): classify_delivery(), DeliveryCode, describe_send_code(), _as_text(), ZeroZeroNinetyEightWebServiceProvider
### Community 17 - "Admin Panel Customization"
Cohesion: 0.12
Nodes (10): CustomerSMSSettingAdmin, SMSLogAdmin, SMSProviderConfigAdmin, SMSProviderConfigForm, Meta, SMSQueueAdmin, SMSTemplateAdmin, StoreRoomAdmin (+2 more)
-### Community 18 - "User"
-Cohesion: 0.07
-Nodes (34): User, Action, ActionType, Status, VisitQueue, Factor, Meta, StoreRoom (+26 more)
+### Community 18 - "views/actions.py"
+Cohesion: 0.10
+Nodes (25): User, notify_visit_scheduled(), _action_payload_from_request(), ActionList(), ActionListAll(), ActionListToday(), ActionListUser(), ActionListUserToday() (+17 more)
-### Community 19 - "apply_timing"
+### Community 19 - "timing_for"
Cohesion: 0.12
-Nodes (7): apply_timing(), current_timing_value(), timing_for(), sms_template_edit(), sms_templates(), _templates_redirect(), TimingsTests
+Nodes (7): current_timing_value(), timing_for(), _sample_context(), sms_template_edit(), sms_templates(), _templates_redirect(), TimingsTests
### Community 20 - "InactiveCustomerHiddenTests"
Cohesion: 0.12
Nodes (3): CustomerCrudTests, CustomerDeleteGuardTests, InactiveCustomerHiddenTests
### Community 21 - "normalize_mobile"
-Cohesion: 0.10
-Nodes (13): customer_phone_entries(), _iter_sources(), PhoneEntry, resolved_recipients(), save_primary_normalized(), stored_primary_normalized(), to_display(), usable_entries() (+5 more)
+Cohesion: 0.09
+Nodes (13): customer_phone_entries(), _iter_sources(), normalize_primary_values(), PhoneEntry, primary_phones(), save_primary_normalized(), stored_primary_normalized(), to_display() (+5 more)
### Community 22 - "Login Template Forms"
Cohesion: 0.11
Nodes (17): Django CSRF Token, Django Custom Tags, Django Static Files, Django URL: admin_panel:newStore, Django URL: admin_panel:operations, Django URL: admin_panel:p_login, Django URL: admin_panel:store, Form Field: password (+9 more)
### Community 23 - "cheque_inquiry.py"
-Cohesion: 0.17
+Cohesion: 0.19
Nodes (6): api_check_sabt_inquiry(), api_check_status_inquiry(), parse_cheque_result_html(), run_bpi_cheque_inquiry_logic(), run_cbi_sayad_inquiry_logic(), solve_base64_captcha()
### Community 24 - "factories.py"
-Cohesion: 0.31
+Cohesion: 0.20
Nodes (5): ProductCategory, make_category(), make_product(), make_store(), _next()
### Community 25 - "Rip'n Fast. Fewer Tokens. Better Code."
Cohesion: 0.11
-Nodes (18): Against the leading graph-database code-context MCP server, Better Code: It automates the review judgments nobody has time to make — every lens from published research, Documentation, Graph-Ranked Retrieval: It finds the right files more often than the alternatives, If it works on your codebase, tell us what it got wrong, Improve it with your agent, Licence, Quickstart (+10 more)
+Nodes (19): Against the leading graph-database code-context MCP server, Better Code: It automates the review judgments nobody has time to make — every lens from published research, Documentation, Graph-Ranked Retrieval: It finds the right files more often than the alternatives, If it works on your codebase, tell us what it got wrong, Improve it with your agent, Languages, Licence (+11 more)
### Community 26 - "views/factors.py"
-Cohesion: 0.12
-Nodes (10): FactorIteam, notify_factor_survey(), notify_payment_thanks(), FactorDel(), newFactor(), newFactor2(), InventoryException, _parse_jalali_date() (+2 more)
+Cohesion: 0.18
+Nodes (6): notify_factor_survey(), newFactor2(), InventoryException, _parse_jalali_date(), _parse_timestamp(), _safe_int()
-### Community 27 - "sms0098.py"
-Cohesion: 0.20
-Nodes (3): _configured_codes(), _response_code(), ZeroZeroNinetyEightSMSProvider
+### Community 27 - "SMSResult"
+Cohesion: 0.18
+Nodes (5): SMSResult, _configured_codes(), _response_code(), ZeroZeroNinetyEightSMSProvider, _FailingProvider
### Community 30 - "NoticeMechanismTests"
Cohesion: 0.17
Nodes (3): dashBoxData(), resolve(), NoticeMechanismTests
### Community 31 - "name_fix.py"
-Cohesion: 0.13
-Nodes (6): build_name_index(), DatasetError, load_name_index(), normalize_name_key(), validate_dataset(), NameIndexTests
-
-### Community 32 - "enqueue_sms"
-Cohesion: 0.21
-Nodes (7): enqueue_sms(), _aware(), _Budget, scan_cheque_funding(), scan_nesiye_due(), scan_visit(), _template()
-
-### Community 33 - "ZeroZeroNinetyEightWebServiceTests"
Cohesion: 0.12
-Nodes (4): SMSDeliveryResult, ZeroZeroNinetyEightWebServiceProvider, SMSDeliveryCommandTests, ZeroZeroNinetyEightWebServiceTests
+Nodes (8): build_name_index(), DatasetError, load_name_index(), NameIndex, normalize_name_key(), validate_dataset(), NameIndexTests, sha256_of()
+
+### Community 32 - "scanner.py"
+Cohesion: 0.12
+Nodes (12): _aware(), _Budget, scan_cheque_funding(), scan_nesiye_due(), scan_visit(), _template(), cycle_anchor_dt(), cycle_anchor_map() (+4 more)
### Community 35 - "admin_panel/templates/admin_panel/baseHesabdari.html"
Cohesion: 0.13
Nodes (14): URL: admin_panel:ActionListUserToday, URL: admin_panel:bedehi, URL: admin_panel:entezar, URL: admin_panel:hesabdari, URL: admin_panel:hesabdari_residnashone, URL: admin_panel:index, URL: admin_panel:p_logout, URL: admin_panel:rezayat (+6 more)
### Community 36 - "Reference guide"
-Cohesion: 0.14
-Nodes (14): 10. Quality evaluation, 11. Agent integration, 13. Performance, 14. Known limits, 15. Documentation, 16. Improvement, 17. License, 1. Purpose and function (+6 more)
+Cohesion: 0.13
+Nodes (15): 10. Quality evaluation, 11. Agent integration, 12. Supported languages and formats, 13. Performance, 14. Known limits, 15. Documentation, 16. Improvement, 17. License (+7 more)
### Community 37 - "Factor Creation UI"
Cohesion: 0.15
@@ -382,27 +398,23 @@ Cohesion: 0.19
Nodes (11): admin_panel:customerNew, admin_panel:pardakhtNesiye, /panel/passCheck/, all_sarresid, mandehesabKP, admin_panel.ViewReportMali, jQuery, pDatepicker JS Library (+3 more)
### Community 39 - "requests"
-Cohesion: 0.25
-Nodes (4): AnnouncementRejected, place_announcement_call(), place_voip_call(), VoipCallResult
+Cohesion: 0.13
+Nodes (8): AnnouncementRejected, place_announcement_call(), BaleBotProvider, ConsoleBaleProvider, get_bale_provider(), NotificationResult, place_voip_call(), VoipCallResult
### Community 40 - "CustomerSmsHistoryTests"
-Cohesion: 0.15
-Nodes (3): customer_phones(), toJalaliDT(), CustomerSmsHistoryTests
+Cohesion: 0.09
+Nodes (5): customer_phones(), row_for(), toJalaliDT(), CustomerSmsHistoryTests, QueueStatsTests
### Community 42 - "SMS Integration Docs"
Cohesion: 0.15
Nodes (12): **جدول کدهای خطای لینک ارتباطی**, **جدول کدهای خطای وبسرویس و وضعیتهای دلیوری**, **راهنمای استفاده از Web Service**, **راهنمای استفاده از لینک ارتباطی (URL GET)**, **راهنمای استفاده از لینک ارتباطی و وبسرویس سامانه پیامک (0098vip.com و 0098sms.com)**, **فهرست مطالب**, **مقدمات اتصال**, **نمونه کدهای پیادهسازی** (+4 more)
### Community 43 - "enqueue.py"
-Cohesion: 0.13
-Nodes (6): customer_balance(), format_amount(), format_jalali(), render_template(), _SafeDict, _sample_context()
+Cohesion: 0.21
+Nodes (4): format_amount(), format_jalali(), render_template(), _SafeDict
-### Community 44 - "django_shortcuts"
-Cohesion: 0.13
-Nodes (5): ArticleType, Sarresid, index(), `nest=` is a max, so it cannot tell a long function from a tangled one, The quality panel
-
-### Community 45 - "test_inventory.py"
-Cohesion: 0.11
+### Community 45 - "stock_in"
+Cohesion: 0.12
Nodes (11): Factor, FactorIteam, Product, ProductInStore, StoreRoom, VisitQueue, current_stock(), stock_in() (+3 more)
### Community 46 - "admin_panel/templates/admin_panel/customerAction.html"
@@ -413,21 +425,21 @@ Nodes (9): admin_panel/templates/admin_panel/customerActionInc.html, admin_panel
Cohesion: 0.10
Nodes (7): describe(), detect_encoding(), encoded_length(), is_gsm7(), parts_count(), SmsPartsTests, SmsReportTests
-### Community 48 - "write_tree"
-Cohesion: 0.21
-Nodes (6): Counts, format_header(), generate_snapshot(), normalized_path(), write_tree(), visit()
+### Community 48 - "generate_snapshot"
+Cohesion: 0.32
+Nodes (3): Counts, format_header(), generate_snapshot()
-### Community 50 - "TestCase"
-Cohesion: 0.20
-Nodes (3): is_sms_enabled(), EnqueueIdempotencyTests, RuleTests
+### Community 50 - "enqueue_sms"
+Cohesion: 0.21
+Nodes (4): enqueue_sms(), is_sms_enabled(), EnqueueIdempotencyTests, RuleTests
### Community 51 - "Phone Cleanup Guide"
Cohesion: 0.17
Nodes (11): اجرا روی سرور اصلی (production), اجرای دستور, اصلاح دستی شمارههای قدیمی مشتریان, راه جایگزین: مهاجرت (migration), قاعدهٔ پاکسازی, مشاهدهٔ نتیجه, نکتهها, چرا؟ (+3 more)
-### Community 52 - "Customer"
-Cohesion: 0.10
-Nodes (8): Customer, ApplyTests, CommandTestMixin, DatasetFixtureMixin, DryRunTests, ResilienceTests, flaky(), sha256_of()
+### Community 52 - ".run_command"
+Cohesion: 0.13
+Nodes (5): ApplyTests, CommandTestMixin, DryRunTests, ResilienceTests, flaky()
### Community 53 - "project_tree.py"
Cohesion: 0.23
@@ -437,13 +449,17 @@ Nodes (6): is_excluded_file(), main(), non_negative_integer(), parse_args(), sor
Cohesion: 0.18
Nodes (8): 1. Codebase Navigation & Analysis (Ripwire vs. Graphify), 2. Dynamic Help, Self-Correction & Staleness, 3. Git Operations & Safety, 4. Development Server Lifecycle, 5. Environment, Frontend & PWA Safety, 6. End-to-End Browser Tests (Playwright), Project Operating & Agent Guidelines, Routing Gate & Pipeline
+### Community 57 - "run_all"
+Cohesion: 0.25
+Nodes (3): run_all(), activate_sms(), ColdStartTests
+
### Community 58 - "Financial Data Objects"
Cohesion: 0.20
Nodes (9): Customer Data Object, Factor Data Object, Sarresid Data Object, admin_panel:customerNew URL, admin_panel:pardakhtNesiye URL, /panel/passCheck/ URL, admin_panel.ViewReportMali Permission, jQuery JS Library (+1 more)
-### Community 60 - ".run_cmd"
-Cohesion: 0.10
-Nodes (6): ApplyTests, BatchAndCheckpointTests, flaky(), CommandTestCase, DryRunTests, make_phone()
+### Community 60 - "PhoneNumber"
+Cohesion: 0.11
+Nodes (8): PhoneNumber, BatchAndCheckpointTests, flaky(), CommandTestCase, DryRunTests, IntegrationAndEfficiencyTests, make_phone(), OverflowTests
### Community 62 - "SMS Timing Migration"
Cohesion: 0.22
@@ -457,13 +473,9 @@ Nodes (8): Articl Model, Customer Model, articls, toJalali, admin_panel:bedehi,
Cohesion: 0.20
Nodes (7): Client-side script in customerIDFactorBackup.html, URL: admin_panel:customerEdit, Factor Data Model, Product Data Model, Product Category Data Model, User Data Model, URL: /panel/productMojodi/
-### Community 65 - "bale/providers.py"
-Cohesion: 0.36
-Nodes (4): BaleBotProvider, ConsoleBaleProvider, get_bale_provider(), NotificationResult
-
-### Community 66 - "os"
-Cohesion: 0.20
-Nodes (3): check_financial_discrepancy(), run_360_audit(), run_audit()
+### Community 66 - "reconciliation_tool.py"
+Cohesion: 0.19
+Nodes (4): remove_query_param(), check_financial_discrepancy(), run_360_audit(), run_audit()
### Community 67 - "Graphify Tooling Docs"
Cohesion: 0.22
@@ -477,9 +489,9 @@ Nodes (6): Customer Data Structure, kamaDatepicker Library, URL: admin_panel:cus
Cohesion: 0.25
Nodes (7): admin_panel:newFactor URL, Category Data, Client-side Form Logic, Customer Data, CSRF Token, custom_tags, static
-### Community 73 - "django_conf"
+### Community 73 - "SMSProvider"
Cohesion: 0.13
-Nodes (7): SMSProvider, SMSResult, webotp_message(), console_write(), ConsoleSMSProvider, HTTPSMSProvider, _FailingProvider
+Nodes (4): SMSProvider, get_provider_class(), get_sms_provider(), reset_sms_provider()
### Community 76 - "Timezone Migration History"
Cohesion: 0.29
@@ -493,9 +505,9 @@ Nodes (5): 3.1 Install a prebuilt binary, 3.2 Build from source, 3.3 Install the
Cohesion: 0.40
Nodes (5): In the numbers, In the output, In the tests, The honesty contract, What it misses, and what to run next
-### Community 85 - "PhoneNumber"
-Cohesion: 0.20
-Nodes (4): PhoneNumber, IntegrationAndEfficiencyTests, OverflowTests, WelcomeTriggerTests
+### Community 85 - "visit_policy.py"
+Cohesion: 0.08
+Nodes (20): _action_appointments(), ActionDeleteError, appointment_sources_map(), cancel_pending_sms_for_action(), coordination_holds_map(), effective_visit_dt(), followups_on_date(), _future_actions() (+12 more)
### Community 86 - "Output Format Specs"
Cohesion: 0.50
@@ -518,8 +530,8 @@ Cohesion: 0.67
Nodes (3): createCS click handler, openRow click handler, Customer Modals UI
### Community 91 - "decide_prefix"
-Cohesion: 0.18
-Nodes (4): decide_prefix(), Decision, NameIndex, DecidePrefixTests
+Cohesion: 0.14
+Nodes (4): decide_prefix(), Decision, DatasetFixtureMixin, DecidePrefixTests
### Community 92 - "Infrastructure Services"
Cohesion: 0.67
@@ -533,46 +545,58 @@ Nodes (3): 1. (Optional) Register the MCP server, 2. Install the skills, Set it
Cohesion: 0.67
Nodes (3): 4.1 Make the first map, 4.2 Use the common commands, 4. First use
+### Community 95 - "console.py"
+Cohesion: 0.15
+Nodes (4): webotp_message(), console_write(), ConsoleSMSProvider, ConsoleEncodingTests
+
### Community 97 - "تجمیع شمارههای پیامک مشتریان"
-Cohesion: 0.14
-Nodes (13): الگوریتم (برای هر مشتری), بازگردانی (اگر نتیجه را نخواستی), بررسی صحت داده (SQL), تجمیع شمارههای پیامک مشتریان, تعریف کلیدهای گزارش, خطاها و اقدام درست, عملکرد و بار روی دیتابیس, نمونهها (+5 more)
+Cohesion: 0.12
+Nodes (15): الگوریتم (برای هر مشتری), بازگردانی (اگر نتیجه را نخواستی), بررسی صحت داده (SQL), تجمیع شمارههای پیامک مشتریان, تستها, تعریف کلیدهای گزارش, خطاها و اقدام درست, عملکرد و بار روی دیتابیس (+7 more)
### Community 99 - "Base Template"
Cohesion: 0.15
Nodes (11): Base Template, Customer Base Template, New Customer Base Template, Create Factor View, Customer List View, Django Custom Tags: custom_tags, URL: admin_panel:newFactor, URL: admin_panel:operations (+3 more)
-### Community 100 - "test_sms.py"
-Cohesion: 0.10
+### Community 100 - "test_customer_name_fix.py"
+Cohesion: 0.11
Nodes (3): digit_string(), is_landline(), PureHelperTests
+### Community 101 - "Customer"
+Cohesion: 0.23
+Nodes (3): Customer, Command, ActionDeleteUITests
+
### Community 103 - "manage.py"
-Cohesion: 0.22
+Cohesion: 0.24
Nodes (5): entrypoint.sh script, main(), run.sh script, start_service(), usage()
### Community 104 - "افزودن پیشوند «آقای/خانم» به نام مشتریان"
Cohesion: 0.15
Nodes (12): SQLite، قفل دیتابیس و بار روی سایت, افزودن پیشوند «آقای/خانم» به نام مشتریان, بررسی صحت داده (SQL، پیش از/پس از اجرا), بکاپ/بازیابی خودکار نیست, بکاپ پیش از اجرای واقعی, تستها, تعریف کلیدهای آماری, خطاهای رایج و اقدام درست (+4 more)
-### Community 107 - "signals.py"
-Cohesion: 0.12
-Nodes (4): AdminConfig, AdminPanelConfig, make_std_streams_unicode_safe(), configure_sqlite_connection()
+### Community 106 - "make_customer"
+Cohesion: 0.09
+Nodes (4): make_customer(), EmptyQueueTests, ApplyTests, PrimaryNumberPanelTests
-### Community 113 - "recipients.py"
-Cohesion: 0.33
-Nodes (3): primary_phones(), primary_phones(), ⚠️ هشدار معنای داده (مهمترین نکتهٔ این سند)
+### Community 107 - "admin_panel/apps.py"
+Cohesion: 0.18
+Nodes (3): AdminConfig, AdminPanelConfig, make_std_streams_unicode_safe()
+
+### Community 113 - "resolve_recipients"
+Cohesion: 0.16
+Nodes (5): resolved_recipients(), primary_phones(), resolve_recipients(), قالبِ ذخیرهشده: `+989xxxxxxxxx`, قواعد اعتبارسنجی و قالب شماره
### Community 163 - "Customer"
Cohesion: 0.29
Nodes (3): Customer, Meta, PhoneNumber
-### Community 164 - "factory.py"
-Cohesion: 0.29
-Nodes (3): get_provider_class(), get_sms_provider(), reset_sms_provider()
-
### Community 166 - "توقف و ادامه (Resume / Checkpoint)"
Cohesion: 0.40
Nodes (5): اتمیک بودن, توقف و ادامه (Resume / Checkpoint), سلسلهمراتب تصمیم نقطهٔ شروع, فایل وضعیت, گزینههای ادامه
+### Community 167 - "RecalcCommandTests"
+Cohesion: 0.10
+Nodes (5): Command, action_snapshot(), plan_command(), RecalcCommandTests, run_command()
+
### Community 170 - "الگوریتم تصمیم (به همین ترتیب)"
Cohesion: 0.50
Nodes (4): الگوریتم تصمیم (به همین ترتیب), برخورد با نامهای تکراری و متناقض, نرمالسازی (فقط برای کلید جستوجو), نمونههای رفتاری
@@ -585,6 +609,10 @@ Nodes (4): اگر وسط کار قطع شد, بررسی نتیجه, سریع
Cohesion: 0.73
Nodes (5): is_our_process(), say(), stop.sh script, stop_pid(), windows_cleanup()
+### Community 175 - "at"
+Cohesion: 0.17
+Nodes (6): last_visit_dt(), at(), EffectiveVisitDateTests, HamahangiIsolationTests, make_visit(), RefreshLastBazdidTests
+
### Community 176 - "اجرای محلی (توسعه)"
Cohesion: 0.50
Nodes (4): اجرای محلی (توسعه), ۱) شبیهسازی — همیشه قدم اول, ۲) تنظیم سرعت batch, ۳) اجرای واقعی
@@ -605,30 +633,90 @@ Nodes (3): ادامهٔ job پس از crash / قطع برق, دستورهای د
Cohesion: 0.67
Nodes (3): اجرا روی سرور Production, ترتیب دقیق, نکتهها برای Production
+### Community 182 - "os"
+Cohesion: 0.18
+Nodes (3): normalized_path(), write_tree(), visit()
+
+### Community 184 - "test_visit_policy.py"
+Cohesion: 0.16
+Nodes (6): as_aware(), visit_cycle_index(), due_date(), visit_due_date(), NextAppointmentEquivalenceTests, VisitCadenceTests
+
+### Community 187 - "last_visit_map"
+Cohesion: 0.13
+Nodes (5): _bucket(), last_visit_map(), _last_visit_map_python(), _visit_rows(), LastVisitMapEquivalenceTests
+
+### Community 194 - "django_contrib_auth_models"
+Cohesion: 0.15
+Nodes (5): VisitQueue, StoreRoom, Meta, StoreRoomOwner, UserProfile
+
+### Community 195 - "entezar"
+Cohesion: 0.19
+Nodes (9): entezar(), _apply_search(), _followup_ids(), _followup_link(), _qs(), _sort_value(), _source_date(), visit_period_days() (+1 more)
+
+### Community 197 - "Action"
+Cohesion: 0.17
+Nodes (9): Action, ActionType, Meta, Status, رفتار دادهٔ موجود, قاعدهٔ طلایی: ستون، «مرجع» نیست, ۲. راهحل: ستون «قرار بعدی», ۴. مرتبسازی (ستونها و دراپداون) (+1 more)
+
+### Community 198 - "بخش دوم — امکانات جدید رابط کاربری صفحهٔ انتظار"
+Cohesion: 0.17
+Nodes (11): بخش دوم — امکانات جدید رابط کاربری صفحهٔ انتظار, تفکیک نوع قرار — در دراپداون، نه روی کارت, ۱. نوار آمار (بالای صفحه), ۱۰. پارامترهای URL, ۲. فیلترها (نوار زیر کارتها), ۳. تفکیک مدت تأخیر (کلیکپذیر), ۵. جستوجو, ۵. چیدمان ستونها (بازطراحی) (+3 more)
+
+### Community 199 - "بازمحاسبهٔ «آخرین بازدید» مشتریان"
+Cohesion: 0.18
+Nodes (10): اجرا روی production, بازمحاسبهٔ «آخرین بازدید» مشتریان, بررسی صحت داده (SQL، پیش از اجرا), دستور, دقیقاً چه میکند؟, نمونهٔ خروجی, نکتههای مهم, گزینهها (+2 more)
+
+### Community 202 - "newFactor"
+Cohesion: 0.22
+Nodes (5): Factor, Meta, FactorIteam, FactorDel(), newFactor()
+
+### Community 205 - "«قرار بعدی» بازدید و صفحهٔ انتظار — توضیح کامل عملکرد"
+Cohesion: 0.20
+Nodes (9): ستونهای جدول, شمارندهٔ داشبورد, فیلترها (قابل بوکمارک و ترکیب با جستوجو), «قرار بعدی» بازدید و صفحهٔ انتظار — توضیح کامل عملکرد, ۱. صورت مسئله, ۳. سه منبع «قرار بعدی», ۵. صفحهٔ انتظار: چهار گروه و فیلترها, ۷. سناریوهای تستشده (+1 more)
+
+### Community 210 - "EntezarScannerConsistencyTests"
+Cohesion: 0.29
+Nodes (3): active_deferrals(), EntezarScannerConsistencyTests, is_stale()
+
+### Community 213 - "۴. فرم ثبت اقدام (UI)"
+Cohesion: 0.29
+Nodes (7): ایمنی ویرایش رکوردهای قدیمی, بارگذاری مقدار ذخیرهشده در حالت ویرایش, قاعدهٔ «تمدید مجاز، کوتاهکردن ممنوع» (max), قواعد اعتبارسنجی (سرور), ۴. فرم ثبت اقدام (UI), ۴.۲ هشدار زمانبندی و پیشفرض «انجام شده» (فقط هماهنگی), ۴.۳ راهنمای بازشونده (فقط «هماهنگی»)
+
+### Community 214 - "۱.۱ نمای «نیازمند پیگیری امروز» (`?view=today`) — **پیشفرض صفحه**"
+Cohesion: 0.29
+Nodes (7): این فهرست از کجا میآید؟, تفاوت با فهرست «عقبافتاده», تقویم شمسی (گذشته و آینده), ستون «تأخیر» در این نما نمایش داده نمیشود, قاعدهٔ قطعی: «بازدید» اینجا نمیآید, هشدار «پیگیریهای عقبافتاده», ۱.۱ نمای «نیازمند پیگیری امروز» (`?view=today`) — **پیشفرض صفحه**
+
+### Community 216 - "۶.۱ حذف یک اقدام (soft-delete)"
+Cohesion: 0.40
+Nodes (5): اثر روی مصرفکنندهها, امنیت, قاعدهٔ محصولی: اقدام دارای نظرسنجی حذف نمیشود, پیامدهای جانبی (در همان تراکنش), ۶.۱ حذف یک اقدام (soft-delete)
+
+### Community 218 - "index"
+Cohesion: 0.67
+Nodes (3): index(), `nest=` is a max, so it cannot tell a long function from a tangled one, The quality panel
+
## Ambiguous Edges - Review These
- `admin_panel/templates/admin_panel/baseHesabdari.html` → `Base Template` [AMBIGUOUS]
admin_panel/templates/admin_panel/baseHesabdari.html · relation: conceptually_related_to
## Knowledge Gaps
- **118 isolated node(s):** `base.html`, `toCurrency`, `DataTables Library`, `Customer Data Object`, `Factor Data Object` (+113 more)
- These have ≤1 connection - possible missing edges or undocumented components. (Counts symbols only; 858 node(s) total have ≤1 connection when file, concept and rationale nodes are included.)
-- **120 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
+ These have ≤1 connection - possible missing edges or undocumented components. (Counts symbols only; 1048 node(s) total have ≤1 connection when file, concept and rationale nodes are included.)
+- **119 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
-- **Why does `make_customer()` connect `make_customer` to `run_all`, `PhoneCleanupTests`, `PanelTestCase`, `EventTriggerTests`, `InactiveCustomerHiddenTests`, `factories.py`, `make_user`, `QueueReviewTests`, `NoticeMechanismTests`, `CustomerSmsHistoryTests`, `test_inventory.py`, `CustomerInfoCardUITests`, `TestCase`, `.test_deleted_visit_reminder_rearms_next_period_only`, `Customer`, `activate_sms`, `.run_cmd`, `.test_visit_period_comes_from_template`, `.test_period_start_filters_out_very_old_customers`, `PhoneNumber`, `ConsoleEncodingTests`, `test_factors.py`, `test_sms.py`, `WorkerTests`, `QueueStatsTests`?**
- _High betweenness centrality (0.071) - this node is a cross-community bridge._
+- **Why does `make_customer()` connect `make_customer` to `AutoSendTests`, `PhoneCleanupTests`, `PanelTestCase`, `mark_primary`, `EventTriggerTests`, `InactiveCustomerHiddenTests`, `factories.py`, `make_user`, `QueueReviewTests`, `NoticeMechanismTests`, `ScannerTests`, `EntezarPageControlsTests`, `CustomerSmsHistoryTests`, `stock_in`, `at`, `CustomerInfoCardUITests`, `enqueue_sms`, `apply_timing`, `test_visit_policy.py`, `run_all`, `ActionSoftDeleteTests`, `last_visit_map`, `PhoneNumber`, `CoordinationDoneConfirmTests`, `NextAppointmentTests`, `VisitHoldTests`, `TestCase`, `TodayFollowupTests`, `ActionModalTemplateTests`, `VisitDecisionTests`, `EntezarScannerConsistencyTests`, `EntezarLayoutTests`, `decide_prefix`, `console.py`, `test_sms.py`, `test_customer_name_fix.py`, `Customer`, `WorkerTests`, `resolve_recipients`?**
+ _High betweenness centrality (0.105) - this node is a cross-community bridge._
- **Are the 2 inferred relationships involving `make_customer()` (e.g. with `Customer` and `PhoneNumber`) actually correct?**
_`make_customer()` has 2 INFERRED edges - model-reasoned connections that need verification._
- **What connects `base.html`, `toCurrency`, `DataTables Library` to the rest of the system?**
_118 weakly-connected nodes found - possible documentation gaps or missing edges._
- **Should `views/sms.py` be split into smaller, more focused modules?**
- _Cohesion score 0.11746031746031746 - nodes in this community are weakly interconnected._
-- **Why does `Rip'n Fast. Fewer Tokens. Better Code.` connect `Rip'n Fast. Fewer Tokens. Better Code.` to `Reference guide`, `.load`, `django_shortcuts`, `Project Honesty Contract`, `Agent Operating Guidelines`, `Performance Benchmarks`, `Agent Setup Guide`?**
- _High betweenness centrality (0.053) - this node is a cross-community bridge._
-- **Are the 2 inferred relationships involving `normalize_mobile()` (e.g. with `الگوریتم (برای هر مشتری)` and `قالبِ ذخیرهشده: `+989xxxxxxxxx``) actually correct?**
- _`normalize_mobile()` has 2 INFERRED edges - model-reasoned connections that need verification._
+ _Cohesion score 0.11711711711711711 - nodes in this community are weakly interconnected._
+- **Why does `Customer` connect `Customer` to `AutoSendTests`, `PhoneCleanupTests`, `mark_primary`, `InactiveCustomerHiddenTests`, `ScannerTests`, `EntezarPageControlsTests`, `RecalcCommandTests`, `stock_in`, `at`, `.run_command`, `Command`, `test_visit_policy.py`, `ActionSoftDeleteTests`, `PhoneNumber`, `NextAppointmentTests`, `VisitHoldTests`, `reconciliation_tool.py`, `Command`, `TodayFollowupTests`, `VisitDecisionTests`, `EntezarScannerConsistencyTests`, `EntezarLayoutTests`, `make_customer`?**
+ _High betweenness centrality (0.040) - this node is a cross-community bridge._
+- **Should `django_db` be split into smaller, more focused modules?**
+ _Cohesion score 0.03508771929824561 - nodes in this community are weakly interconnected._
### Low-confidence Hints
_AMBIGUOUS edges — the extractor was unsure. Verify before acting on these._
diff --git a/graphify-out/graph.html b/graphify-out/graph.html
index ed62b1b..d3bae75 100644
--- a/graphify-out/graph.html
+++ b/graphify-out/graph.html
@@ -63,12 +63,12 @@