improve bazdid 6

This commit is contained in:
javad committed 2026-10-22 14:01:08 +03:30
1 parent 99c25a8216
commit fa73a734a0
21 files changed
+32760 -7985

No files matched your search

+75 -12
View File
@@ -337,6 +337,14 @@ SOURCE_MANUAL = "manual"
SOURCE_SCHEDULED = "scheduled"
SOURCE_COORDINATION = "coordination"
# اقدام‌هایی که **واقعاً** یک «قرار بعدی» را جایگزین می‌کنند.
# چرا فقط این‌ها؟ قرار دستی یعنی «تا این تاریخ تماس نگیر». تنها چیزی که این
# قرار را منتفی می‌کند، خودِ بازدیدی است که به‌جای تماس انجام شده (یا نصب که
# چرخه را از نو شروع می‌کند). یک سند وصول، یک تماس ورودی یا خروجی، یا هر
# اقدام دیگر، قرار را باطل نمی‌کند؛ در غیر این‌صورت قرارِ ثبت‌شده بی‌صدا می‌مرد
# و مشتری دوباره در فهرست تماس ظاهر می‌شد.
_SUPERSEDING_TYPES = (Action.ActionType.BAZDID, Action.ActionType.NASB)
def _future_actions(action_type, statuses, *, now_utc, whole_day=False):
"""اقدام‌های آیندهٔ یک نوع با تاریخ مؤثر (تاریخ اقدام، وگرنه تاریخ اتمام).
@@ -409,10 +417,13 @@ def _valid_manual_holds(holds):
قرارهایی که معتبرند:
۱. یک اقدام «هماهنگی» فعال (``waite``/``finish``) برای مشتری وجود دارد،
۲. و **آخرین فعالیتِ مشتری همان هماهنگی است** — اگر پس از هماهنگی، اقدام
دیگری (به‌ویژه «بازدید») ثبت شده باشد، قرار باطل است.
۲. و **آخرین فعالیتِ جایگزین‌کنندهٔ قرار** همان هماهنگی است — یعنی پس از
هماهنگی، بازدید (``bazdid``) یا نصب (``nasb``) ثبت نشده باشد.
بند ۲ همان چیزی است که «ثبت بازدید پس از هماهنگی» را خودکار بی‌اثر می‌کند.
ولی فقط اقدام‌های جایگزین‌کننده (``_SUPERSEDING_TYPES``) قرار را باطل
می‌کنند: سند وصول، تماس ورودی/خروجی و بقیهٔ اقدام‌ها قرار را نمی‌کشند —
وگرنه یک پرداخت ساده، قرارِ توافق‌شده را بی‌صدا از بین می‌برد.
این تابع جدا شده تا هم :func:`manual_holds_map` (قرارهای آینده) و هم
:func:`followups_on_date` (قرارهای یک روز مشخص، حتی گذشته) از **یک قاعدهٔ
@@ -436,10 +447,12 @@ def _valid_manual_holds(holds):
.annotate(last=Max("createdDT"))
.values_list("customer_id", "last")
)
# آخرین فعالیتِ غیرهماهنگی (بازدید، نصب، تماس، …) برای هر مشتری.
# آخرین فعالیتِ **جایگزین‌کنندهٔ قرار** (بازدید/نصب) برای هر مشتری.
# عمداً فقط این دو نوع: یک سند وصول یا تماس ورودی نباید قرار را باطل کند.
other_latest = dict(
Action.objects.filter(customer_id__in=customer_ids)
.exclude(actionType=Action.ActionType.HAMAHANGI)
Action.objects.filter(
customer_id__in=customer_ids, actionType__in=_SUPERSEDING_TYPES,
)
.values("customer_id")
.annotate(last=Max("createdDT"))
.values_list("customer_id", "last")
@@ -534,7 +547,8 @@ def followups_on_date(target_date, *, now=None):
امروز یا آینده — را می‌بیند تا کارشناس بتواند با تقویم عقب/جلو برود.
خروجی: ``{customer_id: {منبع: تاریخ}}`` مرتب بر اساس زودترین ساعت موعد.
مشتریانی که قرار **بازدید** دارند در خروجی نیستند.
مشتریانی که قرار **بازدید** دارند، یا بازدیدشان از همان روز موعد به بعد
**انجام شده**، در خروجی نیستند (فهرست، «کارِ باقی‌مانده» است نه آرشیو).
"""
day_start, day_end = _local_day_bounds(target_date)
@@ -584,6 +598,25 @@ def followups_on_date(target_date, *, now=None):
for customer_id in _pending_visit_customer_ids(since=day_start):
result.pop(customer_id, None)
# ۴) حذف مشتریانی که بازدیدشان از همان روزِ موعد به بعد **انجام شده** است.
# قاعدهٔ کسب‌وکار: اگر بازدید انجام شده، تماسِ آن روز لازم نیست. قبلاً فقط
# بازدیدِ «در انتظار» دیده می‌شد، پس مشتریِ بازدیدشده باز هم در فهرست
# «نیازمند پیگیری امروز» می‌ماند (۹۲٪ ردیف‌های دادهٔ واقعی).
# فقط برای امروز/آینده اعمال می‌شود تا مرور روزهای گذشته (تقویم) سندِ کاریِ
# همان روز را با داده‌های بعدی بازنویسی نکند.
if result:
now = _now(now)
current_tz = timezone.get_current_timezone()
if target_date >= now.astimezone(current_tz).date():
visits = last_visit_map(now=now, customer_ids=list(result))
for customer_id, visited_at in visits.items():
bucket = result.get(customer_id)
if not bucket or visited_at is None:
continue
due_day = min(bucket.values()).astimezone(current_tz).date()
if visited_at.astimezone(current_tz).date() >= due_day:
result.pop(customer_id, None)
return result
@@ -600,6 +633,10 @@ def overdue_followups(*, now=None, target_date=None):
(موعدش تازه است).
* اگر برایش **قرار بازدید** ثبت شده باشد، از فهرست خارج می‌شود — همان قاعدهٔ
:func:`followups_on_date` («بازدید گذاشته شده ⇒ تماس لازم نیست»).
* اگر بازدیدش **از همان موعد به بعد انجام شده** باشد، از فهرست خارج می‌شود
(پیگیری با بازدید بسته شده است).
* اگر **قرار دستیِ معتبر** داشته باشد («تا تاریخ پیگیری نشود»)، از فهرست
خارج می‌شود؛ چون خودِ کاربر صریحاً خواسته تا آن تاریخ تماس نگیریم.
* هماهنگیِ لغوشده (``cancell``) اصلاً دیده نمی‌شود.
خروجی: ``{customer_id: تاریخِ موعدِ ازدست‌رفته}`` — تا هم شمارش کنیم و هم
@@ -641,6 +678,24 @@ def overdue_followups(*, now=None, target_date=None):
for customer_id in pending_visits:
overdue.pop(customer_id, None)
current_tz = timezone.get_current_timezone()
# بازدیدِ **انجام‌شده** از همان موعد به بعد ⇒ این پیگیری انجام شده است.
# قبلاً فقط بازدیدِ «در انتظار» دیده می‌شد و همین، فهرست «از روزهای قبل»
# را پر از مشتریانی می‌کرد که بازدیدشان ثبت شده بود (۸۸٪ دادهٔ واقعی).
visits = last_visit_map(now=now, customer_ids=list(overdue))
for customer_id, visited_at in visits.items():
if visited_at is None:
continue
if visited_at.astimezone(current_tz).date() >= overdue[customer_id]:
overdue.pop(customer_id, None)
# «قرار دستیِ معتبر» یعنی کاربر صریحاً گفته «تا این تاریخ پیگیری نشود»؛
# چنین مشتری‌ای نمی‌تواند هم‌زمان «عقب‌افتاده» شمرده شود. قبلاً این ستون
# در این محاسبه دیده نمی‌شد (۳۸ ردیف متناقض در دادهٔ واقعی).
for customer_id in manual_holds_map(now=now):
overdue.pop(customer_id, None)
return overdue
@@ -726,12 +781,20 @@ def default_hold_until(*, now=None, days=HOLD_DEFAULT_DAYS):
return end_of_local_day(base)
def set_visit_hold(customer, until, *, now=None):
"""ثبت قرار دستی؛ فقط اگر **جلوتر** از مقدار فعلی باشد می‌نویسد.
def set_visit_hold(customer, until, *, now=None, force=False):
"""ثبت قرار دستی؛ به‌طور پیش‌فرض فقط اگر **جلوتر** از مقدار فعلی باشد می‌نویسد.
قاعدهٔ «فقط رو به جلو» (مثل ``refresh_last_bazdid``) جلوی این را می‌گیرد که
ثبت هماهنگی توسط کارشناس دوم، قرارِ دورترِ قبلی را عقب بکشد.
خروجی: ``True`` اگر نوشته شد.
قاعدهٔ «فقط رو به جلو» جلوی این را می‌گیرد که ثبت هماهنگی توسط کارشناس دوم،
قرارِ دورترِ قبلی را عقب بکشد.
``force=True`` برای وقتی است که کاربر **خودش** صریحاً تأیید کرده تاریخ عقب
برود (تیک «تاریخ قرار را عقب ببر» در مودالِ **ویرایش** ⇒ فیلد ``force_hold``).
در «ایجاد وظیفهٔ تازه» ویو این پارامتر را نمی‌فرستد. این پارامتر فقط از مسیر
ویو و پس از تأیید کاربر استفاده می‌شود؛ منطق نمایش/فهرست‌ها تغییری نمی‌کند و
ستون همچنان همان جای ذخیرهٔ تاریخ است.
خروجی: ``True`` اگر نوشته شد، وگرنه ``False`` (یا مقدار فعلی جلوتر است، یا
تاریخ نامعتبر).
"""
if customer is None or customer.pk is None:
return False
@@ -739,7 +802,7 @@ def set_visit_hold(customer, until, *, now=None):
if target is None:
return False
current = as_aware(customer.visit_hold_until)
if current is not None and current >= target:
if current is not None and current >= target and not force:
return False
Customer.objects.filter(pk=customer.pk).update(
visit_hold_until=target, visit_hold_set_at=_now(now),