improve bazdid 4
This commit is contained in:
1 parent
f86d060e4d
commit
8fd881e0ea
10 files changed
+1752
-320
No files matched your search
+194
-19
@@ -402,33 +402,24 @@ def coordination_holds_map(*, now=None):
|
||||
)
|
||||
|
||||
|
||||
def manual_holds_map(*, now=None):
|
||||
"""قرارهای دستی (ستون ``visit_hold_until``) — **با اعتبارسنجی**.
|
||||
def _valid_manual_holds(holds):
|
||||
"""اعتبارسنجی قرارهای دستی — قاعدهٔ قطعی «ستون، مرجع نیست».
|
||||
|
||||
قاعدهٔ قطعی: ستون بهتنهایی معتبر نیست. یک قرار دستی فقط وقتی اثر دارد که:
|
||||
ورودی ``{customer_id: visit_hold_until}`` و خروجی همان نگاشت، فقط برای
|
||||
قرارهایی که معتبرند:
|
||||
|
||||
۱. تاریخش از حالا جلوتر باشد،
|
||||
۲. یک اقدام «هماهنگی» فعال (``waite``/``finish``) برای مشتری وجود داشته باشد،
|
||||
۳. و **آخرین فعالیتِ مشتری همان هماهنگی باشد** — اگر پس از هماهنگی، اقدام
|
||||
۱. یک اقدام «هماهنگی» فعال (``waite``/``finish``) برای مشتری وجود دارد،
|
||||
۲. و **آخرین فعالیتِ مشتری همان هماهنگی است** — اگر پس از هماهنگی، اقدام
|
||||
دیگری (بهویژه «بازدید») ثبت شده باشد، قرار باطل است.
|
||||
|
||||
بند ۳ همان چیزی است که «ثبت بازدید پس از هماهنگی» را خودکار بیاثر میکند؛
|
||||
بدون آن، قرارِ قدیمی تا تاریخش باقی میماند و مشتری در فهرست «با قرار»
|
||||
میماند، هرچند بازدید (یا برنامهٔ بازدید) جای هماهنگی را گرفته است.
|
||||
بند ۲ همان چیزی است که «ثبت بازدید پس از هماهنگی» را خودکار بیاثر میکند.
|
||||
|
||||
نتیجه: حذف/لغو هماهنگی، یا هر اقدام جدیدتر از آن ⇒ قرار بیاثر میشود؛
|
||||
و کهنهماندن ستون هیچ اثر مخربی ندارد.
|
||||
این تابع جدا شده تا هم :func:`manual_holds_map` (قرارهای آینده) و هم
|
||||
:func:`followups_on_date` (قرارهای یک روز مشخص، حتی گذشته) از **یک قاعدهٔ
|
||||
واحد** استفاده کنند و دو تعریف واگرا شکل نگیرد.
|
||||
"""
|
||||
now = _now(now)
|
||||
rows = (
|
||||
Customer.objects.filter(visit_hold_until__gt=now)
|
||||
.values_list("id", "visit_hold_until")
|
||||
.iterator()
|
||||
)
|
||||
holds = {customer_id: as_aware(until) for customer_id, until in rows}
|
||||
if not holds:
|
||||
return {}
|
||||
|
||||
customer_ids = list(holds)
|
||||
|
||||
# مرجع اعتبار: وجود یک اقدام «هماهنگیِ فعال» برای همان مشتری (نه لزوماً با
|
||||
@@ -469,6 +460,190 @@ def manual_holds_map(*, now=None):
|
||||
return valid
|
||||
|
||||
|
||||
def manual_holds_map(*, now=None):
|
||||
"""قرارهای دستی **آینده** (ستون ``visit_hold_until``) — با اعتبارسنجی.
|
||||
|
||||
برای فهرست انتظار و آمار؛ قاعدهٔ اعتبار در :func:`_valid_manual_holds`.
|
||||
"""
|
||||
now = _now(now)
|
||||
rows = (
|
||||
Customer.objects.filter(visit_hold_until__gt=now)
|
||||
.values_list("id", "visit_hold_until")
|
||||
.iterator()
|
||||
)
|
||||
return _valid_manual_holds(
|
||||
{customer_id: as_aware(until) for customer_id, until in rows}
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# «نیازمند پیگیری» در یک روز مشخص
|
||||
# ---------------------------------------------------------------------------
|
||||
def _local_day_bounds(target_date):
|
||||
"""بازهٔ ``[آغاز روز، پایان روز]`` به وقت محلی (aware)."""
|
||||
current_tz = timezone.get_current_timezone()
|
||||
start = datetime.combine(target_date, time.min)
|
||||
end = datetime.combine(target_date, time.max)
|
||||
try:
|
||||
return (
|
||||
timezone.make_aware(start, current_tz),
|
||||
timezone.make_aware(end, current_tz),
|
||||
)
|
||||
except Exception:
|
||||
return (
|
||||
timezone.make_aware(start, current_tz, is_dst=False),
|
||||
timezone.make_aware(end, current_tz, is_dst=False),
|
||||
)
|
||||
|
||||
|
||||
def _pending_visit_customer_ids(*, since):
|
||||
"""مشتریانی که **قرار بازدید** دارند و نباید در فهرست پیگیری بیایند.
|
||||
|
||||
قاعدهٔ محصولی: «اگر برای مشتری بازدید گذاشته شده، دیگر لازم نیست تماس
|
||||
بگیریم». پس هر مشتری با اقدام ``bazdid + waite`` که تاریخش از ``since``
|
||||
(آغاز روز مورد نظر) عقبتر نباشد، از فهرست پیگیری **حذف** میشود.
|
||||
|
||||
چرا ``>= since`` و نه «هر بازدیدی»؟ بازدیدِ در انتظارِ بسیار قدیمی
|
||||
(backlog بستهنشده) قرار فعال نیست؛ آن مشتری هنوز نیاز به تماس دارد.
|
||||
"""
|
||||
return set(
|
||||
Action.objects.filter(
|
||||
actionType=Action.ActionType.BAZDID,
|
||||
status=Action.Status.WAITE,
|
||||
)
|
||||
.filter(
|
||||
models.Q(actionDateTime__gte=since)
|
||||
| models.Q(actionDateTime__isnull=True, finishDateTime__gte=since)
|
||||
)
|
||||
.values_list("customer_id", flat=True)
|
||||
)
|
||||
|
||||
|
||||
def followups_on_date(target_date, *, now=None):
|
||||
"""مشتریانِ «نیازمند پیگیری» در یک تاریخ مشخص (تقویم شمسی/میلادی).
|
||||
|
||||
«نیازمند پیگیری» یعنی موعدی که با مشتری توافق شده **همان روز** است:
|
||||
|
||||
* **قرار دستی** — ``Customer.visit_hold_until`` که تاریخ آن همان روز است
|
||||
(با همان اعتبارسنجی :func:`_valid_manual_holds`).
|
||||
* **هماهنگی** — اقدام ``hamahangi`` با ``actionDateTime`` (وگرنه
|
||||
``finishDateTime``) در همان روز، با وضعیت ``waite`` یا ``finish``.
|
||||
|
||||
تفاوت با :func:`appointment_sources_map`: آن تابع فقط **آینده** را میبیند
|
||||
(برای تصمیم «خارج از فهرست انتظار»)، ولی این یکی **هر روزی** — گذشته،
|
||||
امروز یا آینده — را میبیند تا کارشناس بتواند با تقویم عقب/جلو برود.
|
||||
|
||||
خروجی: ``{customer_id: {منبع: تاریخ}}`` مرتب بر اساس زودترین ساعت موعد.
|
||||
مشتریانی که قرار **بازدید** دارند در خروجی نیستند.
|
||||
"""
|
||||
day_start, day_end = _local_day_bounds(target_date)
|
||||
|
||||
result = {}
|
||||
|
||||
# ۱) قرارهای دستی همان روز (مستقل از گذشته/آینده بودن).
|
||||
rows = (
|
||||
Customer.objects.filter(
|
||||
visit_hold_until__gte=day_start, visit_hold_until__lte=day_end,
|
||||
)
|
||||
.values_list("id", "visit_hold_until")
|
||||
.iterator()
|
||||
)
|
||||
for customer_id, until in _valid_manual_holds(
|
||||
{cid: as_aware(when) for cid, when in rows}
|
||||
).items():
|
||||
result.setdefault(customer_id, {})[SOURCE_MANUAL] = until
|
||||
|
||||
# ۲) اقدامهای «هماهنگی» همان روز.
|
||||
coordination_rows = (
|
||||
Action.objects.filter(
|
||||
actionType=Action.ActionType.HAMAHANGI,
|
||||
status__in=(Action.Status.WAITE, Action.Status.FINISH),
|
||||
)
|
||||
.filter(
|
||||
models.Q(actionDateTime__gte=day_start, actionDateTime__lte=day_end)
|
||||
| models.Q(
|
||||
actionDateTime__isnull=True,
|
||||
finishDateTime__gte=day_start,
|
||||
finishDateTime__lte=day_end,
|
||||
)
|
||||
)
|
||||
.values_list("customer_id", "actionDateTime", "finishDateTime")
|
||||
.iterator()
|
||||
)
|
||||
for customer_id, action_dt, finish_dt in coordination_rows:
|
||||
when = as_aware(action_dt) or as_aware(finish_dt)
|
||||
if when is None:
|
||||
continue
|
||||
bucket = result.setdefault(customer_id, {})
|
||||
current = bucket.get(SOURCE_COORDINATION)
|
||||
if current is None or when < current:
|
||||
bucket[SOURCE_COORDINATION] = when
|
||||
|
||||
# ۳) حذف مشتریانی که برایشان «بازدید» گذاشته شده است.
|
||||
if result:
|
||||
for customer_id in _pending_visit_customer_ids(since=day_start):
|
||||
result.pop(customer_id, None)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def overdue_followups(*, now=None, target_date=None):
|
||||
"""پیگیریهای **عقبافتاده**: موعدشان گذشته و هنوز تعیین تکلیف نشدهاند.
|
||||
|
||||
«عقبافتاده» یعنی برای هر مشتری، **آخرین** اقدام هماهنگی (``waite``/``finish``)
|
||||
تاریخی پیش از امروز دارد؛ یعنی موعد توافقشده گذشته و هیچ موعد تازهای
|
||||
جایگزینش نشده است.
|
||||
|
||||
قواعد:
|
||||
|
||||
* اگر مشتری هماهنگیِ **امروز یا آینده** داشته باشد، عقبافتاده نیست
|
||||
(موعدش تازه است).
|
||||
* اگر برایش **قرار بازدید** ثبت شده باشد، از فهرست خارج میشود — همان قاعدهٔ
|
||||
:func:`followups_on_date` («بازدید گذاشته شده ⇒ تماس لازم نیست»).
|
||||
* هماهنگیِ لغوشده (``cancell``) اصلاً دیده نمیشود.
|
||||
|
||||
خروجی: ``{customer_id: تاریخِ موعدِ ازدسترفته}`` — تا هم شمارش کنیم و هم
|
||||
بتوانیم کاربر را به همان روز ببریم.
|
||||
"""
|
||||
now = _now(now)
|
||||
today = target_date or now.astimezone(timezone.get_current_timezone()).date()
|
||||
|
||||
rows = (
|
||||
Action.objects.filter(
|
||||
actionType=Action.ActionType.HAMAHANGI,
|
||||
status__in=(Action.Status.WAITE, Action.Status.FINISH),
|
||||
)
|
||||
.values_list("customer_id", "actionDateTime", "finishDateTime")
|
||||
.iterator()
|
||||
)
|
||||
|
||||
# آخرین موعد هر مشتری (تاریخ، نه لحظه).
|
||||
latest = {}
|
||||
for customer_id, action_dt, finish_dt in rows:
|
||||
when = as_aware(action_dt) or as_aware(finish_dt)
|
||||
if when is None:
|
||||
continue
|
||||
day = when.astimezone(timezone.get_current_timezone()).date()
|
||||
current = latest.get(customer_id)
|
||||
if current is None or day > current:
|
||||
latest[customer_id] = day
|
||||
|
||||
overdue = {cid: day for cid, day in latest.items() if day < today}
|
||||
if not overdue:
|
||||
return {}
|
||||
|
||||
# حذف مشتریانی که برایشان بازدید گذاشته شده است (قرار بازدید از آن روز).
|
||||
# «از آن روز» = زودترین موعدِ ازدسترفته؛ بازدیدی که بعد از آن گذاشته شده
|
||||
# تکلیف مشتری را روشن کرده است.
|
||||
pending_visits = _pending_visit_customer_ids(
|
||||
since=_local_day_bounds(min(overdue.values()))[0]
|
||||
)
|
||||
for customer_id in pending_visits:
|
||||
overdue.pop(customer_id, None)
|
||||
|
||||
return overdue
|
||||
|
||||
|
||||
def appointment_sources_map(*, now=None):
|
||||
"""نگاشت ``customer_id → {منبع: تاریخ}`` برای هر سه منبع (فقط آینده).
|
||||
|
||||
|
||||
Reference in new issue
Block a user