improve bazdid 6
This commit is contained in:
1 parent
99c25a8216
commit
fa73a734a0
21 files changed
+32760
-7985
No files matched your search
+75
-12
@@ -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),
|
||||
|
||||
Reference in new issue
Block a user