۰ پاسخ کوتاه
«تاریخ و ساعتی که کار باید انجام شود»
actionDateTimeزمانِ انجام وظیفه. بسته به نوع اقدام سه معنا دارد: برای بازدید در انتظار = زمان قرار بازدید، برای هماهنگی = زمان تماس، برای بقیه = زمان وظیفه. ۸ مصرفکننده دارد.
«تا تاریخ پیگیری نشود»
visit_hold_until
قرارِ بعدی مشتری روی خودِ Customer، پایانِ همان روز.
فقط برای «هماهنگی» نوشته میشود، پیشفرض ۱۴ روز، سقف ۱۸۳ روز، فقط رو به جلو.
۳ مصرفکننده دارد و اعتبارش وابسته به «آخرین فعالیت = هماهنگی» است.
finish + تاریخ اتمام (خالی ⇒ الان) +
تعویقِ واردشده؛ اگر خالی باشد +۱۴ روز
waite و بدون پیشفرض؛ فقط اگر تاریخ تعویق پر شود قرار ثبت میشود
۱ نقشهٔ داده: از فرم تا تصمیم
هر فیلد کجا ذخیره میشود و کدام تابع آن را میخواند.
۲ فیلد «تاریخ و ساعتی که کار باید انجام شود»
یک ستون، سه معنا — تفکیک آن در visit_policy._future_actions صریح پیاده شده است.
| # | مصرفکننده | تابع / فایل | اثر |
|---|---|---|---|
| ۱ | لیست وظایف | 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} از همین فیلد پر میشود (تبدیل شمسی) |
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 | عمداً دست نمیزند |
- ۱ تاریخ از حالا جلوتر باشد
- ۲ یک اقدام هماهنگی فعال (
waite/finish) وجود داشته باشد - ۳ آخرین فعالیتِ مشتری همان هماهنگی باشد — هر اقدام بعدی (بهویژه بازدید) قرار را باطل میکند
_valid_manual_holds — ستون پر، ولی «مرده» ممکن است.
manual
manual_holds_map → appointment_sources_map
خروج از «تعیینتکلیفنشده» + فیلتر ?view=manual
?view=today
visit صف نمیشود
۴ اثر دو فیلد بر پیامکها
دو نوع پیامکِ بازدید، دو منطق کاملاً متفاوت.
اسکنر وقتی موعدِ «آخرین بازدید + ۹ ماه» رسید صف میکند.
تصمیمگیر: cycle_anchor_map + next_appointment_map.
بلافاصله پس از ثبت اقدام بازدید ساخته میشود؛ متن آن
{date} را از actionDateTime میگیرد.
next_appointment_map عبور نمیکند.
«قرار بعدی» اجتماع سه منبع است (دستی / بازدید برنامهریزیشده / هماهنگی)؛ پس صفحه و پیامک هرگز دو جواب نمیدهند.
کلید یکتاسازی 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 | الان | +۱۴ روز | همان؛ تعویقِ خودکار |
| بدون تیک + تاریخ تعویق پر | waite | NULL | همان تاریخ | همان |
| بدون تیک + تاریخ خالی | waite | NULL | بدون تغییر | مشتری در فهرست میماند |
برای هماهنگیِ تیکخورده، پیش از submit هشدار «آیا هماهنگی را انجام دادی؟» نمایش داده میشود؛ «نه» تیک را سمت کلاینت برمیدارد.
هیچ. هماهنگی هرگز وارد
last_visit_map نمیشود؛ تیک هم آن را تغییر نمیدهد.
«انجام شد» بدون تاریخ تعویق همیشه ۱۴ روز پنهان میکند؛ اگر کارشناس فقط میخواست تماس را لاگ کند، این پیشفرض ناخواسته است.
۶
صفحهٔ ?view=today — برچسبها و اعداد
«۲ قرار» و «قرار دستی» دقیقاً از کجا میآیند.
اولویت: چندقراره ← دستی ← بازدید ← هماهنگی ← بدون قرار. یک هماهنگیِ انجامشده با تاریخ تعویق، همزمان «هماهنگی» و «قرار دستی» تولید میکند.
waite است — پس بازدیدِ امروزِ انجامشده چیزی را حذف نمیکند.
۷
باگ ?view=today&missed=1
رفع شد · ۱۱۲ → ۴۲ ردیف
فهرست «نیازمند پیگیری از روزهای قبل» پر از مشتریانی بود که بازدیدشان انجام شده (یا قرارِ معتبر آینده داشتند). تحلیل زیر وضعیت زمان کشف است؛ رفع در بخش ۸.
appointment_sources_map «قرار معتبر» دارد
(از تعیینتکلیفنشده خارج است) و همزمان در فهرست قرمز «عقبافتاده» شمرده میشود.
ریشهها — سه خطای همراستا
حذف از فهرست فقط بر اساس اقدام bazdid + waite انجام میشود.
بازدیدی که انجام و بسته شده (finish) هیچ اثری در حذف ندارد.
overdue_followups اصلاً ستون visit_hold_until را نمیخواند؛
پس مشتری با قرارِ آیندهٔ معتبر هم در فهرست «عقبافتاده» میماند.
برای هماهنگیِ انجامشده، actionDateTime «زمان تماس» است نه «قرار بعدی»؛
ولی فهرست آن را موعد میگیرد ⇒ هر تماسِ تمامشده تا ابد عقبافتاده میماند.
# آخرین موعد هماهنگی هر مشتری
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 هرگز
دو نمونهٔ واقعی با خط زمانی
۸ جمعبندی و رفعهای اعمالشده
هر مورد پیاده شد، با تست قفل شد و روی دادهٔ واقعی سنجیده شد. ۲۶۹ + ۱۹۹ تست قبلی سبز ماند · ۴۶ تست شبیهسازی/رگرسیون 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) |
رفع باگ = همترازکردن لایهٔ دوم با لایهٔ اول.
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)
قاعدهٔ «بازدید انجامشده از همان روز موعد به بعد» اعمال شد؛ ردیفِ امروز پس از بازدید حذف میشود (روزهای گذشته دستنخورده میمانند).
فقط
bazdid/nasb قرار را باطل میکنند؛ سند وصول و تماس
دیگر قرارِ توافقشده را نمیکشند.
بر اساس روزهای یکتا حساب میشود؛ هماهنگی + تعویقِ همان روز = «۱ قرار».
کش فقط وقتی مقدارش تغییر کند نوشته میشود (
update_fields).
اگر تاریخ «تا تاریخ پیگیری نشود» از قرار فعلی عقبتر باشد، بهجای سکوت: اعلان کوتاه «این تاریخ از قرار فعلی عقبتر است» + «قرار فعلی: …» و تیک «تاریخ قرار را عقب ببر» (فیلد
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
createdDT سنجیده شده (تنها ستون تضمینکنندهٔ ترتیب ثبت) · تاریخهای شبیهسازی نسبیاند
تا تست به روزِ اجرا وابسته نباشد.