Django 3.2 · SQLite ۳۲ شبیه‌سازی سبز ممیزی روی کپی دیتابیس واقعی کدِ تولیدی بدون تغییر

دو فیلد تاریخِ «اقدام»، صفحهٔ انتظار و پیامک‌های بازدید

تحلیل کامل رفتار Action نوع «هماهنگی» و «بازدید»، اثرشان بر /panel/entezar/?view=today و ?view=today&missed=1، به‌همراه اعداد واقعی، خط زمانی نمونه‌ها و ریشهٔ باگ.

دیتابیس ممیزی
khatibi92669266_…_20261021_223009.sqlite3
مصنوعات
audit script · simulation tests · md report
داده
۱۰٬۸۲۲ مشتری · ۲٬۵۴۱ هماهنگی فعال · ۵۰٬۰۹۰ بازدید انجام‌شده

۰ پاسخ کوتاه

«تاریخ و ساعتی که کار باید انجام شود»

actionDateTime

زمانِ انجام وظیفه. بسته به نوع اقدام سه معنا دارد: برای بازدید در انتظار = زمان قرار بازدید، برای هماهنگی = زمان تماس، برای بقیه = زمان وظیفه. ۸ مصرف‌کننده دارد.

لیست وظایف آخرین بازدید لنگر SMS قرار scheduled قرار coordination فهرست روز missed متن visit_created

«تا تاریخ پیگیری نشود»

visit_hold_until

قرارِ بعدی مشتری روی خودِ Customer، پایانِ همان روز. فقط برای «هماهنگی» نوشته می‌شود، پیش‌فرض ۱۴ روز، سقف ۱۸۳ روز، فقط رو به جلو. ۳ مصرف‌کننده دارد و اعتبارش وابسته به «آخرین فعالیت = هماهنگی» است.

برچسب/ستون «قرار دستی» فهرست پیگیری همان روز قطع یادآوری SMS
تیک «انجام شد» در هماهنگی
finish + تاریخ اتمام (خالی ⇒ الان) + تعویقِ واردشده؛ اگر خالی باشد +۱۴ روز
بدون تیک
waite و بدون پیش‌فرض؛ فقط اگر تاریخ تعویق پر شود قرار ثبت می‌شود
«۲ قرار» یعنی
بیش از یک منبعِ قرار (دستی / بازدید برنامه / هماهنگی) — نه لزوماً دو توافق
باگ missed
۸۸٪ ردیف‌ها بازدید انجام‌شده دارند؛ ۳۸ ردیف قرارِ معتبر آینده دارند

۱ نقشهٔ داده: از فرم تا تصمیم

هر فیلد کجا ذخیره می‌شود و کدام تابع آن را می‌خواند.

۱. فرم مودال
تاریخ و ساعتی که کار باید انجام شود
تا تاریخ پیگیری نشود
تیک «این کار انجام شده است»
تاریخ و ساعت اتمام کار
customerActionInc.html
۲. نرمال‌سازی و اعتبارسنجی
_parse_form_datetime
ms → datetime aware
normalize_hold_until
گذشته ✗ · بیش از ۱۸۳ روز ✗ · پایان روز
normalize_finish_datetime
خالی ⇒ now
validate_action_payload
بازدید انجام‌شده: حداکثر +۳۰ روز
views/actions.py:84-146
۳. دیتابیس
Action.actionDateTime
زمان انجام / تماس / قرار
Customer.visit_hold_until
قرار بعدی (پایان روز)
status / finishDateTime
waite / finish / cancell
createdDT
ترتیب ثبت (قاعدهٔ آخرین فعالیت)
۴. مصرف‌کننده‌های نهایی
visit_policy
last_visit_map · cycle_anchor_map · appointment_sources_map · followups_on_date · overdue_followups
views/customers.py
entezar + appointment_rows + counts
sms/scanner.py
scan_visit → enqueue(visit)
sms/triggers.py
notify_visit_scheduled → visit_created

۲ فیلد «تاریخ و ساعتی که کار باید انجام شود»

یک ستون، سه معنا — تفکیک آن در visit_policy._future_actions صریح پیاده شده است.

بازدید + در انتظار
زمان قرار بازدید
مرز: پایان روز — قرار امروز تا ۲۳:۵۹ معتبر می‌ماند.
whole_day=True
هماهنگی (waite / finish)
زمان تماس
مرز: لحظه‌ای — تماسِ امروز نباید مشتری را تا آخر امروز پنهان کند.
whole_day=False
بازدید + انجام‌شده
تاریخ مؤثر بازدید
اگر از حالا جلوتر نباشد، مبنای «آخرین بازدید»؛ وگرنه تاریخ اتمام.
effective_visit_dt
# مصرف‌کننده تابع / فایل اثر
۱ لیست وظایف ActionListToday · ActionListUserToday امروز ۰۰:۰۰ تا ۲۳:۵۹ + وضعیت باز ⇒ «وظایف امروز»؛ قبل از امروز ⇒ «عقب‌افتاده»
۲ آخرین بازدید last_visit_map فقط bazdid + finish؛ «هماهنگی» هرگز وارد نمی‌شود
۳ لنگر چرخه و SMS cycle_anchor_map → scan_visit آخرین بازدید (یا تاریخ نصب) + دورهٔ ۹ ماه ⇒ موعد یادآوری دوره‌ای
۴ قرار بازدید برنامه scheduled_visits_map bazdid + waite با تاریخ ≥ آغاز امروز ⇒ خروج از «تعیین‌تکلیف‌نشده» و قطع یادآوری
۵ قرار هماهنگی coordination_holds_map hamahangi با تاریخ ≥ همین لحظه ⇒ همان اثر با مرز لحظه‌ای
۶ فهرست پیگیری روز followups_on_date هماهنگی همان روز (یا تاریخ اتمام اگر تاریخ اقدام خالی باشد) + قرار دستی همان روز
۷ فهرست عقب‌افتاده‌ها overdue_followups آخرین هماهنگی با تاریخ قبل از امروز ⇒ «موعد ازدست‌رفته» (منشأ باگ)
۸ متن پیامک ثبت بازدید notify_visit_scheduled متغیر {date} از همین فیلد پر می‌شود (تبدیل شمسی)
اعتبارسنجی: فقط برای bazdid + finish سقف «۳۰ روز آینده» اعمال می‌شود (FINISH_MAX_FUTURE_DAYS). برای هماهنگی هیچ سقفی وجود ندارد.

۳ فیلد «تا تاریخ پیگیری نشود» visit_hold_until

قرار دستی؛ ستون هست، ولی مرجع نیست.

قاعده پیاده‌سازی اثر
فقط هماهنگی_apply_visit_holdبرای بازدید/نصب/وصول بی‌اثر
پایانِ روزend_of_local_dayمعتبر تا ۲۳:۵۹:۵۹ همان روز
خالی + تیک انجام شدdefault_hold_until()+۱۴ روز خودکار
خالی + بدون تیک—هیچ نوشتنی؛ مشتری در فهرست می‌ماند
تاریخ گذشتهnormalize_hold_untilرد با پیام فارسی
بیش از ۱۸۳ روزHOLD_MAX_DAYSرد (جلوگیری از تعویق بی‌پایان)
فقط رو به جلوset_visit_holdقرار دورتر با درخواست کوتاه‌تر عقب نمی‌رود
ثبت اقدام غیرهماهنگی (ایجاد)clear_visit_holdستون پاک می‌شود
ویرایش اقدامis_new=Falseعمداً دست نمی‌زند
قاعدهٔ اعتبار — سه شرط هم‌زمان
  1. ۱ تاریخ از حالا جلوتر باشد
  2. ۲ یک اقدام هماهنگی فعال (waite/finish) وجود داشته باشد
  3. ۳ آخرین فعالیتِ مشتری همان هماهنگی باشد — هر اقدام بعدی (به‌ویژه بازدید) قرار را باطل می‌کند
پیاده‌سازی: _valid_manual_holds — ستون پر، ولی «مرده» ممکن است.
۵۴
مشتری با ستون پرشده
۵۱
قرارِ آیندهٔ معتبر
۳
بی‌اعتبار (۱ بدون هماهنگی · ۲ با اقدام بعدی)
مصرف‌کننده‌ها
منبع manual manual_holds_map → appointment_sources_map خروج از «تعیین‌تکلیف‌نشده» + فیلتر ?view=manual
فهرست پیگیری روز followups_on_date دقیقاً در همان روز (حتی گذشته) در ?view=today
قطع یادآوری SMS next_appointment_map → scan_visit تا پایان آن روز هیچ پیامک دوره‌ای visit صف نمی‌شود
نمایش و ویرایش entezar.html · customerActionInc.html برچسب/ستون «قرار دستی» + بارگذاری مقدار در مودال ویرایش

۴ اثر دو فیلد بر پیامک‌ها

دو نوع پیامکِ بازدید، دو منطق کاملاً متفاوت.

یادآوری دوره‌ای بازدید
visit

اسکنر وقتی موعدِ «آخرین بازدید + ۹ ماه» رسید صف می‌کند. تصمیم‌گیر: cycle_anchor_map + next_appointment_map.

هر دو فیلد اثر دارند: فیلد اول به‌واسطهٔ لنگر چرخه، فیلد دوم به‌واسطهٔ قرار.
ثبت بازدید
visit_created

بلافاصله پس از ثبت اقدام بازدید ساخته می‌شود؛ متن آن {date} را از actionDateTime می‌گیرد.

فیلد تعویق هیچ اثری ندارد: این پیامک از مسیر next_appointment_map عبور نمی‌کند.
جریان تصمیم scan_visit
مشتریان فعال → قرار بعدی دارد؟ بله ⇒ توقف → لنگر = آخرین بازدید یا نصب → موعدِ ۹/۱۸/۲۷ ماه رسیده؟ → صف با کلید دوره‌ای

«قرار بعدی» اجتماع سه منبع است (دستی / بازدید برنامه‌ریزی‌شده / هماهنگی)؛ پس صفحه و پیامک هرگز دو جواب نمی‌دهند. کلید یکتاسازی visit:{customer}:{YYYY-MM} است؛ حذف از صف فقط همان دوره را کنسل می‌کند.

سناریو یادآوری visit پیامک visit_created
مشتری عقب‌افتاده، هیچ اقدامیارسال—
«تا تاریخ پیگیری نشود» = +۱۰ روزقطع تا پایان روز دهم—
ستون پر، ولی بدون اقدام هماهنگیارسال ستون بی‌اثر—
بازدید در انتظار با تاریخ +۵ روزقطع—
هماهنگی با تاریخ +۵ روزقطع—
هماهنگی انجام‌شده با تاریخ گذشته و بدون تعویقارسال—
ثبت اقدام بازدید (هر وضعیتی)—ارسال {date} = تاریخ انجام
ثبت اقدام هماهنگی—نه
قرار دستی فعال + ثبت بازدید—ارسال فیلد دوم مانع نیست

همهٔ سطرها با scan_visit و notify_visit_scheduled واقعی شبیه‌سازی شده‌اند (tests/test_entezar_action_simulation.py).

۵ تیک «این کار انجام شده است» در هماهنگی

فرم status finishDateTime visit_hold_until اثر
تیک + تاریخ تعویق پرfinishخالی ⇒ الانهمان تاریخپنهان تا آن روز؛ همان روز در نمای امروز برمی‌گردد
تیک + تاریخ تعویق خالیfinishالان+۱۴ روزهمان؛ تعویقِ خودکار
بدون تیک + تاریخ تعویق پرwaiteNULLهمان تاریخهمان
بدون تیک + تاریخ خالیwaiteNULLبدون تغییرمشتری در فهرست می‌ماند
تأیید دو مرحله‌ای UI
برای هماهنگیِ تیک‌خورده، پیش از submit هشدار «آیا هماهنگی را انجام دادی؟» نمایش داده می‌شود؛ «نه» تیک را سمت کلاینت برمی‌دارد.
اثر بر «آخرین بازدید»
هیچ. هماهنگی هرگز وارد last_visit_map نمی‌شود؛ تیک هم آن را تغییر نمی‌دهد.
ریسک رفتاری
«انجام شد» بدون تاریخ تعویق همیشه ۱۴ روز پنهان می‌کند؛ اگر کارشناس فقط می‌خواست تماس را لاگ کند، این پیش‌فرض ناخواسته است.

۶ صفحهٔ ?view=today — برچسب‌ها و اعداد

«۲ قرار» و «قرار دستی» دقیقاً از کجا می‌آیند.

منطق برچسب وضعیت
۲ قرار source_count > 1
قرار دستی manual
بازدید برنامه scheduled
هماهنگی coordination
بدون قرار هیچ منبعی

اولویت: چندقراره ← دستی ← بازدید ← هماهنگی ← بدون قرار. یک هماهنگیِ انجام‌شده با تاریخ تعویق، هم‌زمان «هماهنگی» و «قرار دستی» تولید می‌کند.

ترکیب منابع در دادهٔ واقعی
۱ منبع۲٬۲۶۷
۲ منبع۱۶
ترکیب دو‌منبعی‌ها: manual + coordination = ۱۲ (۱۱ مورد تاریخ متفاوت، ۱ مورد هم‌روز) · scheduled + coordination = ۴
نمونهٔ #2888: دستی ۲۰۲۶-۱۱-۱۱ و هماهنگی ۲۰۲۶-۱۱-۱۱ — همان یک توافق، دو منبع ⇒ برچسب «۲ قرار».
۱۳
ردیف نمای امروز
۱۲
دارای بازدید انجام‌شده (۹۲٪)
۴
قرار دستیِ آینده هم دارند
۰
آخرین بازدید در/بعد از روز موعد
مشکل این نما: «موعدِ تماس» با «موعدِ پیگیری» مخلوط شده است؛ تماسِ انجام‌شدهٔ امروز هم ردیف می‌سازد، و حذف‌کنندهٔ ردیف فقط بازدیدِ waite است — پس بازدیدِ امروزِ انجام‌شده چیزی را حذف نمی‌کند.

۷ باگ ?view=today&missed=1

رفع شد · ۱۱۲ → ۴۲ ردیف

فهرست «نیازمند پیگیری از روزهای قبل» پر از مشتریانی بود که بازدیدشان انجام شده (یا قرارِ معتبر آینده داشتند). تحلیل زیر وضعیت زمان کشف است؛ رفع در بخش ۸.

۱۱۲ ردیف — علتِ حضور
کپی دیتابیس ۲۰۲۶-۱۰-۲۱
بازدید انجام‌شده، ولی قدیمی‌تر از هماهنگی۶۷ · ۵۹.۸٪
بازدید انجام‌شده پس از ثبت هماهنگی۳۲ · ۲۸.۶٪
بدون هیچ بازدید انجام‌شده۱۳ · ۱۱.۶٪
فقط ۱۳ ردیف واقعاً نیازمند پیگیری‌اند؛ ۹۹ ردیف (۸۸.۴٪) حداقل یک بازدید انجام‌شده دارند.
سنجه‌های تکمیلی
۳۲
بازدید بعد از هماهنگی (createdDT)
۲۴
آخرین بازدید بعد از موعد هماهنگی
۹۹
آخرین هماهنگی با وضعیت finish
۱۳
آخرین هماهنگی با وضعیت waite
۶۱
finish بدون قرار دستی فعال
۳۸
قرار دستیِ معتبر آینده + حضور در missed
تناقض: همان مشتری در appointment_sources_map «قرار معتبر» دارد (از تعیین‌تکلیف‌نشده خارج است) و هم‌زمان در فهرست قرمز «عقب‌افتاده» شمرده می‌شود.

ریشه‌ها — سه خطای هم‌راستا

R1بازدید finish دیده نمی‌شود

حذف از فهرست فقط بر اساس اقدام bazdid + waite انجام می‌شود. بازدیدی که انجام و بسته شده (finish) هیچ اثری در حذف ندارد.

_pending_visit_customer_ids
R2قرار دستی نادیده گرفته می‌شود

overdue_followups اصلاً ستون visit_hold_until را نمی‌خواند؛ پس مشتری با قرارِ آیندهٔ معتبر هم در فهرست «عقب‌افتاده» می‌ماند.

overdue_followups
R3زمان تماس جای موعد نشست

برای هماهنگیِ انجام‌شده، actionDateTime «زمان تماس» است نه «قرار بعدی»؛ ولی فهرست آن را موعد می‌گیرد ⇒ هر تماسِ تمام‌شده تا ابد عقب‌افتاده می‌ماند.

whole_day=False
visit_policy.py — تصمیم فعلی «عقب‌افتاده» :590-644
# آخرین موعد هماهنگی هر مشتری
when = as_aware(action_dt) or as_aware(finish_dt)   # ← finish هم با actionDateTime
latest[cid] = max(day)
overdue = {cid: day for cid, day in latest.items() if day < today}

# تنها حذف‌کننده: بازدیدهای «در انتظار»
pending_visits = _pending_visit_customer_ids(since=...)
# ↑ داخلش فقط:
actionType='bazdid', status='waite'   # ← finish هرگز

دو نمونهٔ واقعی با خط زمانی

#236 — خانم مرضیه جمالی
هنوز در missed
هماهنگی · finish
ثبت ۱۵ اکتبر · تماس ۱۷ اکتبر
→
بازدید · finish
۱۸ اکتبر (ثبت ۱۷ اکتبر)
→
missed
بازدید انجام‌شده نادیده گرفته شد
#1431 — آقای حمید جمالی
هنوز در missed
هماهنگی · finish
تماس ۱۶ اکتبر
→
بازدید · finish
۱۸ اکتبر
→
بازدید · finish
۲۰ اکتبر
→
missed
دو بازدید انجام‌شده، بی‌اثر
سناریوی «تماس گرفتم و فردا بازدید می‌کنم» (بازدید در انتظار) درست کار می‌کند؛ آنچه خراب است سناریوی «تماس گرفتم، بازدید انجام و ثبت شد» است.

۸ جمع‌بندی و رفع‌های اعمال‌شده

هر مورد پیاده شد، با تست قفل شد و روی دادهٔ واقعی سنجیده شد. ۲۶۹ + ۱۹۹ تست قبلی سبز ماند · ۴۶ تست شبیه‌سازی/رگرسیون OK

# نوع باگ رفع
B1کسب‌وکارفهرست missed پر از مشتریِ بازدیدشده (۹۹ از ۱۱۲)بازدید finish با تاریخ ≥ موعد ⇒ حذف
B2کسب‌وکار۳۸ ردیف هم‌زمان «قرار معتبر» و «عقب‌افتاده»قرار دستیِ معتبر ⇒ حذف از فهرست
B3کسب‌وکارنمای امروز بعد از بازدیدِ همان روز ردیف را نگه می‌داشتبازدید ≥ موعد ⇒ حذف (فقط امروز/آینده)
B4کسب‌وکارسند وصول یا تماس، قرار را بی‌صدا باطل می‌کردفقط bazdid/nasb قرار را باطل می‌کنند
B5کسب‌وکار«۲ قرار» برای یک توافق (هماهنگی + تعویقِ همان روز)شمارش روزهای یکتا
B6فنیمرز «وظایف امروز» با رشتهٔ naive و وابسته به تایم‌زون_local_day_range() aware + پوشش DST
B7فنیهر GET صفحهٔ انتظار در کش می‌نوشتفقط در صورت تغییر + update_fields
B8فنی/تستیک تست به ساعت اجرا وابسته بودمستقل از ساعت
B9کسب‌وکار/UXتاریخ عقب‌ترِ «تا تاریخ پیگیری نشود» بی‌صدا ذخیره نمی‌شدهشدار با هر دو تاریخ + تأیید صریح کاربر (force_hold)
missed قبل
۱۱۲
missed بعد
۴۲
ردیف متناقض (قرار + عقب‌افتاده)
۰
تناقض ساختاری
«تعیین‌تکلیف‌نشده»: اجتماع سه منبع قرار آینده + اعتبار قرار دستی
«عقب‌افتاده»: فقط hamahangi + فقط بازدید waite

رفع باگ = هم‌ترازکردن لایهٔ دوم با لایهٔ اول.

پیاده‌شده — overdue_followups (B1 + B2)
for customer_id, action_dt, finish_dt in Action.objects.filter(
        actionType=BAZDID, status=FINISH,
).values_list(...):
    eff = action_dt if action_dt <= now else 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)
B3 · followups_on_date
قاعدهٔ «بازدید انجام‌شده از همان روز موعد به بعد» اعمال شد؛ ردیفِ امروز پس از بازدید حذف می‌شود (روزهای گذشته دست‌نخورده می‌مانند).
B4 · ابطال قرار دستی
فقط bazdid/nasb قرار را باطل می‌کنند؛ سند وصول و تماس دیگر قرارِ توافق‌شده را نمی‌کشند.
B5 · برچسب «N قرار»
بر اساس روزهای یکتا حساب می‌شود؛ هماهنگی + تعویقِ همان روز = «۱ قرار».
B7 · نوشتن در GET
کش فقط وقتی مقدارش تغییر کند نوشته می‌شود (update_fields).
B9 · بازخورد شفاف عقب‌بردن قرار
اگر تاریخ «تا تاریخ پیگیری نشود» از قرار فعلی عقب‌تر باشد، به‌جای سکوت: اعلان کوتاه «این تاریخ از قرار فعلی عقب‌تر است» + «قرار فعلی: …» و تیک «تاریخ قرار را عقب ببر» (فیلد force_hold) — فقط در ویرایش. در «ایجاد وظیفهٔ تازه» تیک وجود ندارد و راهنمای «وظیفه را ویرایش کنید» نشان داده می‌شود، تا قولِ ثبت‌شدهٔ قبلی ناخواسته کوتاه نشود. بدون جاوااسکریپت هم سرور همان دلیل را روی صفحه می‌نویسد.
باقی‌مانده (تصمیم محصولی)
موعدِ هماهنگیِ finish همچنان «زمان تماس» است؛ بعد از انقضای تعویق، اگر بازدیدی ثبت نشود، مشتری دوباره «نیازمند پیگیری» می‌شود — این رفتار عمدی است تا مشتریِ بی‌قرار از فهرست کار ناپدید نشود.
ریسک
هر تغییر در overdue_followups روی شمارندهٔ قرمز و هشدارهای صفحه اثر می‌گذارد؛ تست «عدد هشدار = تعداد ردیف» باید حفظ شود.
نکته: scan_visit از next_appointment_map می‌خواند، نه از overdue_followups؛ پس اصلاح فهرست، پیامک‌ها را جابه‌جا نمی‌کند.

۹ بازتولید و اعتبارسنجی

ممیزی فقط-خواندنی روی کپی دیتابیس
.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/<backup>.sqlite3

فایل sqlite + -wal/-shm در پوشهٔ موقت کپی می‌شود؛ دیتابیس اصلی دست‌نخورده می‌ماند.

شبیه‌سازی‌ها
.venv/Scripts/python.exe manage.py test \
    tests.test_entezar_action_simulation -v 2
۳۲ تست · OK Django 3.2.25 · SQLite حافظه‌ای
۹
تست فرم: نگاشت فیلدها، ۱۴ روز، گذشته/۶ ماه، فقط رو به جلو
۶
تست تصمیم انتظار: «۲ قرار»، قرار دستی، ابطال با بازدید
۶
تست بازتولید باگ missed (رفتار فعلی قفل شده)
۱۱
تست SMS: قطع/بازگشت، بی‌اثری ستون، متن visit_created
محدودیت‌ها: اعداد از یک نسخهٔ پشتیبان (۲۰۲۶-۱۰-۲۱) هستند · «بازدید بعد از هماهنگی» با createdDT سنجیده شده (تنها ستون تضمین‌کنندهٔ ترتیب ثبت) · تاریخ‌های شبیه‌سازی نسبی‌اند تا تست به روزِ اجرا وابسته نباشد.