improve bazdid 2

This commit is contained in:
javad committed 2026-10-21 11:28:01 +03:30
1 parent 534a84f977
commit f8d768a724
6 files changed
+374 -92

No files matched your search

+87 -29
View File
@@ -338,24 +338,35 @@ SOURCE_SCHEDULED = "scheduled"
SOURCE_COORDINATION = "coordination"
def _future_actions(action_type, statuses, *, now_utc):
"""اقدام‌های آیندهٔ یک نوع با تاریخ مؤثر (تاریخ اقدام، وگرنه تاریخ اتمام)."""
def _future_actions(action_type, statuses, *, now_utc, whole_day=False):
"""اقدام‌های آیندهٔ یک نوع با تاریخ مؤثر (تاریخ اقدام، وگرنه تاریخ اتمام).
``whole_day=True`` فقط برای «بازدید برنامه‌ریزی‌شده» استفاده می‌شود: در آنجا
``actionDateTime`` یک **قرارِ آینده** است که کاربر با تقویم روزانه انتخاب
می‌کند، پس «قرار امروز» باید تا پایان امروز معتبر بماند. مقایسهٔ لحظه‌ای
باعث می‌شد بازدیدِ امروز بلافاصله پس از گذشتن ساعتش ناپدید شود.
برای «هماهنگی» عمداً ``whole_day=False`` است: ``actionDateTime`` آنجا
**زمانِ تماس** است، نه قرار آینده؛ تماسِ گذشته نباید مشتری را تا آخر امروز
پنهان کند (وگرنه گروه «قرار دستی» در سناریوی رایج خالی می‌شد).
"""
threshold = start_of_local_day(now_utc) if whole_day else now_utc
return (
Action.objects.filter(actionType=action_type, status__in=statuses)
.filter(
models.Q(actionDateTime__gt=now_utc)
| models.Q(actionDateTime__isnull=True, finishDateTime__gt=now_utc)
models.Q(actionDateTime__gte=threshold)
| models.Q(actionDateTime__isnull=True, finishDateTime__gte=threshold)
)
.values_list("customer_id", "actionDateTime", "finishDateTime")
)
def _action_appointments(action_type, statuses, *, now=None):
def _action_appointments(action_type, statuses, *, now=None, whole_day=False):
"""نگاشت ``customer_id → زودترین تاریخِ آینده`` برای یک نوع اقدام."""
now_utc = _now(now).astimezone(dt_timezone.utc)
result = {}
for customer_id, action_dt, finish_dt in _future_actions(
action_type, statuses, now_utc=now_utc
action_type, statuses, now_utc=now_utc, whole_day=whole_day
).iterator():
when = as_aware(action_dt) or as_aware(finish_dt)
if when is None:
@@ -367,9 +378,13 @@ def _action_appointments(action_type, statuses, *, now=None):
def scheduled_visits_map(*, now=None):
"""قرارهای «بازدید برنامه‌ریزی‌شده»: ``bazdid + waite`` با تاریخ آینده."""
"""قرارهای «بازدید برنامه‌ریزی‌شده»: ``bazdid + waite`` با تاریخ آینده.
مقایسه در سطح **روز** است (``whole_day=True``): بازدیدی که برای «امروز»
گذاشته شده تا پایان امروز معتبر می‌ماند.
"""
return _action_appointments(
Action.ActionType.BAZDID, (Action.Status.WAITE,), now=now,
Action.ActionType.BAZDID, (Action.Status.WAITE,), now=now, whole_day=True,
)
@@ -390,17 +405,19 @@ def coordination_holds_map(*, now=None):
def manual_holds_map(*, now=None):
"""قرارهای دستی (ستون ``visit_hold_until``) — **با اعتبارسنجی**.
قاعدهٔ قطعی: ستون به‌تنهایی معتبر نیست. فقط وقتی اثر دارد که یک اقدام
«هماهنگی» با وضعیت ``waite`` یا ``finish`` و **تاریخ آینده** برای همان
مشتری وجود داشته باشد. بنابراین:
قاعدهٔ قطعی: ستون به‌تنهایی معتبر نیست. یک قرار دستی فقط وقتی اثر دارد که:
* حذف هماهنگی ⇒ قرار خودکار بی‌اثر می‌شود،
* لغو هماهنگی (``cancell``) ⇒ بی‌اثر می‌شود،
* هماهنگیِ گذشته ⇒ بی‌اثر می‌شود،
* کهنه‌ماندن ستون هیچ اثر مخربی ندارد.
۱. تاریخش از حالا جلوتر باشد،
۲. یک اقدام «هماهنگی» فعال (``waite``/``finish``) برای مشتری وجود داشته باشد،
۳. و **آخرین فعالیتِ مشتری همان هماهنگی باشد** — اگر پس از هماهنگی، اقدام
دیگری (به‌ویژه «بازدید») ثبت شده باشد، قرار باطل است.
مرجع هماهنگی: هر ``hamahangi`` فعالی با تاریخ آینده (نه لزوماً برابر با ستون)،
چون کاربر می‌تواند هماهنگی را ویرایش کند و تاریخ دیگری بگذارد.
بند ۳ همان چیزی است که «ثبت بازدید پس از هماهنگی» را خودکار بی‌اثر می‌کند؛
بدون آن، قرارِ قدیمی تا تاریخش باقی می‌ماند و مشتری در فهرست «با قرار»
می‌ماند، هرچند بازدید (یا برنامهٔ بازدید) جای هماهنگی را گرفته است.
نتیجه: حذف/لغو هماهنگی، یا هر اقدام جدیدتر از آن ⇒ قرار بی‌اثر می‌شود؛
و کهنه‌ماندن ستون هیچ اثر مخربی ندارد.
"""
now = _now(now)
rows = (
@@ -412,23 +429,44 @@ def manual_holds_map(*, now=None):
if not holds:
return {}
customer_ids = list(holds)
# مرجع اعتبار: وجود یک اقدام «هماهنگیِ فعال» برای همان مشتری (نه لزوماً با
# تاریخ آینده). این نکته حیاتی است: سناریوی رایج «همین حالا تماس گرفتم و قرار
# دو هفتهٔ بعد گذاشتم» تاریخ اقدام را «امروز» می‌ذارد؛ اگر شرط آینده بودن تاریخ
# اقدام را بگذاریم، همین مورد (که هدف اصلی این قابلیت است) بی‌اثر می‌شد.
# پس فقط وضعیت مهم است: ``waite`` (در انتظار) یا ``finish`` (انجام شد).
# ``cancell`` حساب نمی‌شود و حذف رکورد هم خودکار بی‌اثر می‌کند.
active_coordination = set(
# تاریخ آینده). سناریوی رایج «همین حالا تماس گرفتم و قرار دو هفتهٔ بعد
# گذاشتم» تاریخ اقدام را «امروز» می‌ذارد؛ اگر شرط آینده‌بودن تاریخ اقدام را
# بگذاریم، همین مورد (هدف اصلی این قابلیت) بی‌اثر می‌شد. پس فقط وضعیت مهم است.
coordination_latest = dict(
Action.objects.filter(
customer_id__in=customer_ids,
actionType=Action.ActionType.HAMAHANGI,
status__in=(Action.Status.WAITE, Action.Status.FINISH),
).values_list("customer_id", flat=True)
)
.values("customer_id")
.annotate(last=Max("createdDT"))
.values_list("customer_id", "last")
)
return {
customer_id: until
for customer_id, until in holds.items()
if customer_id in active_coordination and until is not None
}
# آخرین فعالیتِ غیرهماهنگی (بازدید، نصب، تماس، …) برای هر مشتری.
other_latest = dict(
Action.objects.filter(customer_id__in=customer_ids)
.exclude(actionType=Action.ActionType.HAMAHANGI)
.values("customer_id")
.annotate(last=Max("createdDT"))
.values_list("customer_id", "last")
)
valid = {}
for customer_id, until in holds.items():
if until is None:
continue
coordinated_at = coordination_latest.get(customer_id)
if coordinated_at is None:
continue
activity_at = other_latest.get(customer_id)
if activity_at is not None and as_aware(activity_at) > as_aware(coordinated_at):
# پس از هماهنگی، اقدام دیگری ثبت شده ⇒ قرار منتفی است.
continue
valid[customer_id] = until
return valid
def appointment_sources_map(*, now=None):
@@ -467,6 +505,26 @@ HOLD_DEFAULT_DAYS = 14
HOLD_MAX_DAYS = 183 # ≈ ۶ ماه
def start_of_local_day(moment):
"""آغاز همان روز به وقت محلی (به‌صورت aware).
قرارها با تقویم روزانه انتخاب می‌شوند؛ پس «قرار امروز» باید تا پایان امروز
معتبر بماند. مقایسهٔ لحظه‌ای باعث می‌شد قرارِ امروز بلافاصله پس از گذشتن
ساعتِ آن، ناپدید شود.
"""
moment = as_aware(moment)
if moment is None:
return None
local = moment.astimezone(timezone.get_current_timezone())
start_of_day = datetime.combine(local.date(), time.min)
try:
return timezone.make_aware(start_of_day, timezone.get_current_timezone())
except Exception:
return timezone.make_aware(
start_of_day, timezone.get_current_timezone(), is_dst=False,
)
def end_of_local_day(moment):
"""پایان همان روز به وقت محلی (به‌صورت aware).