62 KiB
تحلیل کامل: دو فیلد تاریخِ «اقدام»، صفحهٔ انتظار و پیامکهای بازدید
دامنه:
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 (۴۰۵-۴۶۰): یک قرار دستی فقط وقتی معتبر است که
- برای مشتری یک اقدام
hamahangiبا وضعیتwaiteیاfinishوجود داشته باشد، و - آخرین فعالیتِ مشتری همان هماهنگی باشد — اگر بعد از آن، اقدامِ غیرهماهنگی
(بازدید، وصول، تماس، …) با
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}) |
install_created (رویدادی) |
بلافاصله پس از ثبت اقدام نصب — همان دو مسیر (ایجاد و تغییر نوع در ویرایش) | notify_install_scheduled |
فقط actionDateTime (برای متن {date}) |
۴.۲ ماتریس اثر (نتیجهٔ شبیهسازی، همه با scan_visit)
| سناریو | یادآوری visit |
پیامک visit_created |
|---|---|---|
| مشتری عقبافتاده، هیچ اقدامی | ✅ ارسال | — |
| «تا تاریخ پیگیری نشود» = +۱۰ روز (با هماهنگی فعال) | ⛔ تا پایان روزِ دهم؛ از فردای آن ✅ | — |
ستون visit_hold_until پر، ولی بدون اقدام هماهنگی |
✅ ارسال (ستون بیاثر) | — |
بازدید waite با تاریخ +۵ روز |
⛔ (تا پایان آن روز) | — |
| هماهنگی با تاریخ +۵ روز (بدون تعویق) | ⛔ | — |
| هماهنگی «انجامشده» با تاریخ گذشته و بدون تعویق | ✅ ارسال | — |
| ثبت اقدام بازدید (هر وضعیتی) | — | ✅ با {date} = actionDateTime |
| ثبت اقدام نصب (هر وضعیتی) | — | ✅ install_created، مستقل از visit_created |
| ثبت اقدام هماهنگی | — | ❌ |
| قرار دستی فعال + ثبت بازدید | — | ✅ (فیلد دوم مانع نیست) |
تستهای متناظر: 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، تابع _sync_action_event_sms) فقط
برای actionType == bazdid صدا زده میشود، و اصلاً از next_appointment_map عبور
نمیکند. یعنی اگر هماهنگی بگذارید و بعد بازدیدی ثبت کنید، پیامکِ «بازدید شما برای
تاریخ … ثبت شد» میرود حتی اگر «تا تاریخ پیگیری نشود» فعال باشد. همین قاعده برای
install_created (اقدام از نوع نصب) هم برقرار است. (اگر رفتار مطلوب چیز دیگری است،
باید در notify_visit_scheduled/notify_install_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 (۳۷۲-۸۵۳):
now,due_cut = visit_due_at(now)(امروز − ۹ ماه)،period_months.visits = last_visit_map()— منبع «آخرین بازدید».sources = appointment_sources_map()— سه منبع قرارِ آینده:manual(ستون تعویق معتبر)،scheduled(bazdid+waiteبا تاریخ ≥ آغاز امروز)،coordination(hamahangiبا تاریخ ≥ همین لحظه).- پایهٔ نمای «عقبافتاده» (سایر
viewها): مشتریانlaghv_bazdid=False، نامدار،installDate ≤ due_cut، و سپسstale_ids= کسانی که «بازدید معتبر ندارند یا بازدیدشان ≤due_cutاست». - نمای
todayجدا است:today_followups = followups_on_date(today)— یعنی «قرار دستیِ همان روز» + «هماهنگی با تاریخ همان روز»؛ منهای مشتریانی که بازدیدِ در انتظار از آغاز امروز دارند. _followup_ids: فیلترis_active،laghv_bazdid=False، نام غیرتهی + جستوجو؛ مرتبسازی بر اساس زودترین موعد.appointment_rowsبرای هر ردیف: ادغامsources[cid]باfollowups[cid]، سپسsource_count= تعداد کلیدهای موجود از میانmanual/scheduled/coordination.- شمارندهها:
counts['followup_today']وcounts['followup_overdue']از همان مسیر فهرست ⇒ «عدد = تعداد ردیف». - عارضهٔ جانبی: ویو در هر GET مقدار
CacheTable('9mahbazdid')را باcounts['uncoordinated']بازنویسی میکند (خط ۶۹۹-۷۰۵) — یعنی یک صفحهٔ خواندنی، نوشتن در دیتابیس دارد.
۶.۲ ستون «وضعیت قرار» — معنی برچسبها
قالب entezar.html:502-515:
{% if appt.source_count > 1 %} <span>{{ appt.source_count }} قرار</span> ← «۲ قرار»
{% 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 (۵۹۰-۶۴۴):
# آخرین موعد هماهنگی هر مشتری (فقط اقدام 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 (۴۹۹-۵۱۹) فقط:
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:
# الف) هر بازدیدِ انجامشده که تاریخ مؤثرش از روزِ موعد عقبتر است، تکلیف را روشن کرده
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 به ساعت اجرا وابسته بود |
هماهنگی «امروز ساعت ۱۲» میتوانست «آینده» شمرده شود | تست با مشتری بدون قرار، مستقل از ساعت |
| B9 | کسبوکار | اقدام «نصب» ردیف را از فهرستهای کار (?view=today و ?view=today&missed=1) حذف نمیکرد؛ فقط «بازدید» حذف میکرد |
followups_on_date/overdue_followups در دو مرحلهٔ «قرار در انتظار» و «انجامشده» فقط bazdid را میدیدند (_pending_visit_customer_ids و last_visit_map) |
آن دو مرحله به _pending_appointment_ids (با _CYCLE_RESET_TYPES = bazdid/nasb) و cycle_anchor_map منتقل شدند؛ last_visit_map دستنخورده ماند |
| B10 | رویداد | تغییر نوع یک اقدام از «هماهنگی» به «بازدید» در فرم ویرایش، پیامک visit_created نمیفرستاد |
تریگر notify_visit_scheduled فقط در customerNewAction صدا زده میشد (یک فراخوان در کل کد) |
در customerEditAction گذار نوع بررسی میشود: غیربازدید → بازدید ⇒ تریگر؛ بازدید → غیربازدید ⇒ cancel_pending_sms_for_action(action_id=…). تکرار با dedup_key=visit_created:{action.pk} مهار میشود |
| F1 | قابلیت | نوع پیامک «ثبت نصب» (اقدام از نوع نصب) وجود نداشت | فرمهای اقدام از نوع نصب هیچ تریگر پیامکی نداشتند | SMSType.INSTALL_CREATED + notify_install_scheduled + قالب قابل ویرایش در /panel/sms/templates + تریگر در هر دو ویوی ایجاد/ویرایش (مهاجرت ۰۰۸۱) |
F1 — قیدهای ایمنی مهاجرت ۰۰۸۱: مهاجرت فقط
choicesرا گسترش میدهد (AlterField= متادیتا، بدون تغییر داده) و یک ردیف تازه باget_or_createمیسازد. هیچupdate/saveروی قالبهای موجود ندارد؛ اگر قالبinstall_createdاز قبل باشد هم دست نمیخورد. قالب تازه:is_enabled=True(در پنل دیده شود) وauto_send=False⇒ پیامکها در صف «در انتظار تأیید» میمانند تا مدیر خودش تأیید کند. تستهای نگهبان:InstallTemplateMigrationTestsوtest_install_template_is_seeded_with_auto_send_offدرtests/test_sms.py.
B9 — جزئیات (۲۰۲۶-۱۰-۲۷): «نصب» از قبل در
_SUPERSEDING_TYPES(ابطال قرار دستی) و_CYCLE_RESET_TYPES(ریست چرخهٔ ۹ ماهه در اسکنر پیامک) بود، ولی فهرست کار روزانه آن را نمیدید. حالا:
nasb + waiteبا تاریخ از آغاز روز مورد نظر به بعد ⇒ ردیف پنهان میشود (همان قاعدهٔbazdid + waite؛ «قرار امروز» تا پایان امروز معتبر است). نصبِ در انتظارِ قدیمیتر از آن روز، قرار فعال نیست و ردیف را باز نمیگذارد.nasb + finishبا تاریخ مؤثر از موعد به بعد ⇒ ردیف بسته میشود (همان قاعدهٔbazdid + finish).last_visit_map(تاریخ آخرین بازدید، ستون جدول،last_bazdid_dtو پیامک) عمداً تغییر نکرد؛ «نصب» فقط لنگر چرخه است، نه بازدید.
۹.۳ اثر روی دادهٔ واقعی (همان نسخهٔ پشتیبان)
| سنجه | قبل | بعد |
|---|---|---|
ردیفهای ?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 با رنگ هشدار (کهربایی) نمایش داده میشود |
تجربهٔ کاربر
- تاریخ عقبتر را انتخاب میکند؛ همان لحظه (نه فقط هنگام ثبت) اعلان کوتاه باز میشود: «این تاریخ از قرار فعلی عقبتر است» / «قرار فعلی: ۱۴۰۵/۰۸/۰۲».
- در ویرایش: تیک «تاریخ قرار را عقب ببر» ⇒
force_hold=1و ذخیره میشود. بدون تیک، ثبت بلاک میشود و فوکوس روی همان اعلان میرود. - در ایجاد وظیفهٔ تازه: تیک وجود ندارد؛ بهجای آن یک خط راهنما: «این تاریخ ذخیره نمیشود. برای تغییر قرار، وظیفهٔ هماهنگی فعلی را ویرایش کنید.» دلیل: در ایجاد، کاربر قصد اصلاح قرار موجود را ندارد و اجازه دادن میتوانست ناخواسته قولِ ثبتشدهٔ قبلی (شاید کارِ کارشناس دیگر) را کوتاه کند. خودِ وظیفه ثبت میشود؛ فقط تاریخ قرار عقب نمیرود.
- اگر جاوااسکریپت اجرا نشود یا کاربر مستقیم POST بزند، سرور هم مستقل بررسی میکند: نه مینویسد، نه ساکت میماند — پیام هشدار روی صفحهٔ اقدام نمایش داده میشود (برای ایجاد، پیام «همان وظیفه را ویرایش کنید» را نشان میدهد).
- همان روز ⇒ تغییر واقعی لازم نیست؛ هیچ پیامی هم ساخته نمیشود.
تستها (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 خالی) |
وارد میشود | وارد نمیشود |
| تشخیص «قرار دستی» | معتبر (همهٔ شرطها) | معتبر |
یعنی تیک روی «قرار» اثری ندارد؛ فقط «وظیفه بسته شد» را اعلام میکند و در عوض مشتری را به صف نظرسنجی میبرد.
۱۰.۴ پیامدهای کسبوکاری این ورودی
- ردیف کاری تکراری: در ۱۰-۲۵ (روز تعویق) مشتری در «نیازمند پیگیری» ظاهر میشود، در حالی که قرارِ توافقشده ۱۰-۲۷ است ⇒ تماسِ بیمورد.
- برچسب گمراهکننده: «۲ قرار» نشان میدهد دو توافق وجود دارد، ولی در واقع یک توافق است با تعویقی که زودتر از موعدِ کار تمام میشود.
- سقوطِ یکروزه به «عقبافتاده»: فردای تاریخ کار، تعویق قبلاً منقضی شده، پس «بدون قرار» و «عقبافتاده» میشود.
- بدون ارسال پیامک در فاصلهٔ ۱۰-۲۵ تا ۱۰-۲۷: چون
next_appointment_map(که اسکنر میخواند) هنوز منبعcoordinationرا دارد؛ صفحه و پیامک واگرا نمیشوند.
۱۰.۵ توصیه
- برای یک توافق، تاریخ تعویق = تاریخ کار (یا دورتر) بگذارید؛ آنوقت «۱ قرار» میشود و ردیف تکراری ساخته نمیشود.
- اگر میخواهید سیستم جلوی این ورودی را بگیرد، حداقل تغییر: در
normalize_hold_until/ویو، اگرhold < actionDateTimeبود، هشدار بدهید (نه خطا) — چون در برخی سناریوها («تعویق تا این تاریخ، بعدش خودم تماس میگیرم») ممکن است عمدی باشد.
۱۱. بازتولید و اعتبارسنجی
۱۱.۱ ممیزی روی دیتابیس واقعی (فقط خواندن)
.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) را در پوشهٔ موقت کپی میکند و
همهٔ کوئریها/توابع visit_policy را روی همان کپی اجرا میکند؛ دیتابیس اصلی دستنخورده
میماند. خروجی شامل پنج بخش است (missed، today، برچسبها، ستون قرار، هماهنگیها) و با
--customer خط زمانی کامل یک مشتری.
۱۱.۲ شبیهسازیها
.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) |