improve bazdid 6
This commit is contained in:
1 parent
99c25a8216
commit
fa73a734a0
21 files changed
+32760
-7985
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -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 %} <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` (۵۹۰-۶۴۴):
|
||||
|
||||
```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/<backup>.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) |
|
||||
@@ -43,13 +43,20 @@
|
||||
>
|
||||
> ۱. تاریخش از حالا جلوتر باشد،
|
||||
> ۲. برای همان مشتری یک اقدام **هماهنگیِ فعال** (`waite` یا `finish`) وجود داشته باشد،
|
||||
> ۳. **آخرین فعالیتِ مشتری همان هماهنگی باشد** — اگر بعد از آن، اقدام دیگری
|
||||
> (بهویژه «بازدید») ثبت شده باشد، قرار منتفی است.
|
||||
> ۳. **آخرین اقدامِ جایگزینکنندهٔ قرار، همان هماهنگی باشد** — اگر بعد از آن
|
||||
> «بازدید» (`bazdid`) یا «نصب» (`nasb`) ثبت شده باشد، قرار منتفی است.
|
||||
> اقدامهای دیگر (وصول، تماس ورودی/خروجی، …) قرار را باطل **نمیکنند**.
|
||||
|
||||
شرط ۳ برای این است که «ثبت بازدید پس از هماهنگی» قرار را خودکار باطل کند.
|
||||
بدون آن، قرار قدیمی تا تاریخش زنده میماند و مشتری در فهرست «با قرار» میماند،
|
||||
هرچند بازدید جای هماهنگی را گرفته است.
|
||||
|
||||
> **اصلاح ۲۰۲۶-۱۰-۲۲:** قبلاً «هر اقدام غیرهماهنگی» قرار را باطل میکرد؛ نتیجه
|
||||
> این بود که یک سند وصول یا یک تماس ثبتشده، قرارِ توافقشده را بیصدا از بین
|
||||
> میبرد و مشتری دوباره در فهرست تماس ظاهر میشد. حالا فقط `bazdid`/`nasb`
|
||||
> (یعنی اقدامهایی که واقعاً جای قرار مینشینند) قرار را باطل میکنند
|
||||
> (`visit_policy._SUPERSEDING_TYPES`).
|
||||
|
||||
نتیجهٔ این قاعده:
|
||||
|
||||
| رویداد | اثر روی قرار |
|
||||
|
||||
Reference in new issue
Block a user