improve bazdid 2
This commit is contained in:
1 parent
534a84f977
commit
f8d768a724
6 files changed
+374
-92
No files matched your search
@@ -11,29 +11,55 @@
|
||||
@media (min-width: 769px) { .ez-wrap { padding: 1rem 1.25rem; } }
|
||||
|
||||
/* ---------- نوار آمار ---------- */
|
||||
.ez-stats-head { margin: 0 0 .55rem; }
|
||||
.ez-stats-title { font-size: .92rem; font-weight: 800; color: #0f172a; margin: 0; }
|
||||
.ez-stats-hint {
|
||||
font-size: .72rem; color: #64748b; margin: .2rem 0 0; line-height: 1.7;
|
||||
}
|
||||
.ez-stats-hint b { color: #334155; }
|
||||
|
||||
.ez-stats {
|
||||
display: grid; grid-auto-flow: column; grid-auto-columns: minmax(8.5rem, 1fr);
|
||||
gap: .5rem; overflow-x: auto; padding-bottom: .25rem;
|
||||
display: grid; grid-auto-flow: column; grid-auto-columns: minmax(11rem, 1fr);
|
||||
gap: .5rem; overflow-x: auto; padding-bottom: .35rem;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.ez-stats { grid-auto-flow: row; grid-template-columns: repeat(5, 1fr); overflow: visible; }
|
||||
}
|
||||
.ez-stat {
|
||||
display: block; border: 1px solid #e2e8f0; border-radius: .625rem;
|
||||
background: #fff; padding: .55rem .7rem; text-decoration: none; color: #334155;
|
||||
transition: border-color .15s ease, background-color .15s ease;
|
||||
display: flex; flex-direction: column; gap: .18rem;
|
||||
border: 1px solid #e2e8f0; border-radius: .625rem; background: #fff;
|
||||
padding: .55rem .65rem .6rem; text-decoration: none; color: #334155;
|
||||
border-top: 3px solid #cbd5e1;
|
||||
transition: border-color .15s ease, background-color .15s ease, box-shadow .15s ease;
|
||||
}
|
||||
.ez-stat:hover { border-color: #94a3b8; box-shadow: 0 1px 6px rgba(15, 23, 42, .08); }
|
||||
/* نوار رنگی بالای هر کارت = همان رنگ برچسب وضعیت در جدول */
|
||||
.ez-stat.total { border-top-color: #64748b; }
|
||||
.ez-stat.danger { border-top-color: #dc2626; }
|
||||
.ez-stat.success { border-top-color: #16a34a; }
|
||||
.ez-stat.warning { border-top-color: #d97706; }
|
||||
.ez-stat.link { border-top-color: #0284c7; }
|
||||
|
||||
.ez-stat-top {
|
||||
display: flex; align-items: baseline; justify-content: space-between; gap: .4rem;
|
||||
}
|
||||
.ez-stat .ez-lbl {
|
||||
font-size: .74rem; font-weight: 700; color: #334155; line-height: 1.35;
|
||||
}
|
||||
.ez-stat:hover { border-color: #94a3b8; }
|
||||
.ez-stat .ez-num {
|
||||
display: block; font-size: 1.2rem; font-weight: 800; line-height: 1.1;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-size: 1.25rem; font-weight: 800; line-height: 1.1;
|
||||
font-variant-numeric: tabular-nums; flex: 0 0 auto;
|
||||
}
|
||||
.ez-stat .ez-desc {
|
||||
font-size: .68rem; color: #7c8798; line-height: 1.6;
|
||||
}
|
||||
.ez-stat .ez-lbl { display: block; margin-top: .15rem; font-size: .7rem; color: #64748b; }
|
||||
.ez-stat.is-active {
|
||||
border-color: #33b5d3; background: #f1fbff; box-shadow: inset 0 -3px 0 #33b5d3;
|
||||
border-color: #33b5d3; background: #f1fbff;
|
||||
box-shadow: inset 0 -3px 0 #33b5d3, 0 1px 6px rgba(51, 181, 211, .18);
|
||||
}
|
||||
.ez-stat.is-active .ez-lbl { color: #0e7490; font-weight: 700; }
|
||||
.ez-stat.is-active .ez-lbl { color: #0e7490; }
|
||||
.ez-stat.is-active .ez-desc { color: #0e7490; }
|
||||
|
||||
/* ---------- تفکیک تأخیر ---------- */
|
||||
.ez-legend {
|
||||
@@ -169,40 +195,78 @@
|
||||
<div class="columns m-0">
|
||||
<div class="column is-12 p-0">
|
||||
{# ===== نوار آمار: هر عدد لینک فیلتر ===== #}
|
||||
<div class="ez-stats-head">
|
||||
<p class="ez-stats-title">وضعیت مشتریانِ عقبافتاده</p>
|
||||
<p class="ez-stats-hint">
|
||||
«عقبافتاده» یعنی از آخرین بازدید
|
||||
<b>بیش از {{ counts.period_months }} ماه</b> گذشته باشد.
|
||||
روی هر کارت بزنید تا فقط همان گروه نمایش داده شود؛
|
||||
<b>کل = بدون قرار + با قرار</b>.
|
||||
{% if counts.both %}
|
||||
({{ counts.both }} مشتری هم قرار هماهنگی دارند و هم بازدید
|
||||
برنامهریزیشده، پس در هر دو کارت شمرده میشوند.)
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="ez-stats">
|
||||
<a href="?view=all&sort={{ sort }}{% if searchVal %}&searchVal={{ searchVal }}&searchOn={{ searchOn }}{% endif %}"
|
||||
class="ez-stat {% if view == 'all' %}is-active{% endif %}">
|
||||
<span class="ez-num">{{ counts.total }}</span>
|
||||
<span class="ez-lbl">کل معوق</span>
|
||||
class="ez-stat total {% if view == 'all' %}is-active{% endif %}"
|
||||
title="همهٔ مشتریان عقبافتاده، با هر وضعیت قرار">
|
||||
<span class="ez-stat-top">
|
||||
<span class="ez-lbl">کل عقبافتادهها</span>
|
||||
<span class="ez-num">{{ counts.total }}</span>
|
||||
</span>
|
||||
<span class="ez-desc">همه، فارغ از داشتن یا نداشتن قرار</span>
|
||||
</a>
|
||||
|
||||
<a href="?view=uncoordinated&sort={{ sort }}{% if searchVal %}&searchVal={{ searchVal }}&searchOn={{ searchOn }}{% endif %}"
|
||||
class="ez-stat {% if view == 'uncoordinated' %}is-active{% endif %}">
|
||||
<span class="ez-num has-text-danger">{{ counts.uncoordinated }}</span>
|
||||
<span class="ez-lbl">بدون قرار</span>
|
||||
class="ez-stat danger {% if view == 'uncoordinated' %}is-active{% endif %}"
|
||||
title="مشتریانی که هنوز هیچ قرار بعدی برایشان ثبت نشده است">
|
||||
<span class="ez-stat-top">
|
||||
<span class="ez-lbl">بدون قرار</span>
|
||||
<span class="ez-num has-text-danger">{{ counts.uncoordinated }}</span>
|
||||
</span>
|
||||
<span class="ez-desc">هنوز وقت بازدید تعیین نشده — نیاز به تماس</span>
|
||||
</a>
|
||||
|
||||
<a href="?view=coordinated&sort={{ sort }}{% if searchVal %}&searchVal={{ searchVal }}&searchOn={{ searchOn }}{% endif %}"
|
||||
class="ez-stat {% if view == 'coordinated' %}is-active{% endif %}">
|
||||
<span class="ez-num has-text-success">{{ counts.coordinated }}</span>
|
||||
<span class="ez-lbl">با قرار</span>
|
||||
class="ez-stat success {% if view == 'coordinated' %}is-active{% endif %}"
|
||||
title="مشتریانی که وقت بازدید بعدی برایشان مشخص شده است">
|
||||
<span class="ez-stat-top">
|
||||
<span class="ez-lbl">با قرار</span>
|
||||
<span class="ez-num has-text-success">{{ counts.coordinated }}</span>
|
||||
</span>
|
||||
<span class="ez-desc">وقت بازدیدشان مشخص است (هر دو نوع قرار)</span>
|
||||
</a>
|
||||
|
||||
<a href="?view=manual&sort={{ sort }}{% if searchVal %}&searchVal={{ searchVal }}&searchOn={{ searchOn }}{% endif %}"
|
||||
class="ez-stat {% if view == 'manual' %}is-active{% endif %}">
|
||||
<span class="ez-num has-text-warning-dark">{{ counts.manual }}</span>
|
||||
<span class="ez-lbl">قرار هماهنگی</span>
|
||||
class="ez-stat warning {% if view == 'manual' %}is-active{% endif %}"
|
||||
title="قرار تعویق که از فرم «هماهنگی» ثبت شده است">
|
||||
<span class="ez-stat-top">
|
||||
<span class="ez-lbl">قرار هماهنگی</span>
|
||||
<span class="ez-num has-text-warning-dark">{{ counts.manual }}</span>
|
||||
</span>
|
||||
<span class="ez-desc">تعویق دستی از فرم هماهنگی (مثلاً «ماه بعد زنگ بزنید»)</span>
|
||||
</a>
|
||||
|
||||
<a href="?view=scheduled&sort={{ sort }}{% if searchVal %}&searchVal={{ searchVal }}&searchOn={{ searchOn }}{% endif %}"
|
||||
class="ez-stat {% if view == 'scheduled' %}is-active{% endif %}">
|
||||
<span class="ez-num has-text-link">{{ counts.scheduled }}</span>
|
||||
<span class="ez-lbl">بازدید برنامهریزیشده</span>
|
||||
class="ez-stat link {% if view == 'scheduled' %}is-active{% endif %}"
|
||||
title="اقدام «بازدید» با تاریخ آینده">
|
||||
<span class="ez-stat-top">
|
||||
<span class="ez-lbl">بازدید برنامهریزیشده</span>
|
||||
<span class="ez-num has-text-link">{{ counts.scheduled }}</span>
|
||||
</span>
|
||||
<span class="ez-desc">اقدام «بازدید» با تاریخ آینده ثبت شده است</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# تفکیک مدت تأخیر در نمای جاری #}
|
||||
<div class="ez-legend">
|
||||
<span>تأخیر:</span>
|
||||
<span><b>{{ counts.buckets.lt30 }}</b> زیر ۳۰ روز</span>
|
||||
<span><b>{{ counts.buckets.30to90 }}</b> ۳۰–۹۰ روز</span>
|
||||
<span><b>{{ counts.buckets.90to180 }}</b> ۹۰–۱۸۰ روز</span>
|
||||
<span>میزان تأخیر در همین نما:</span>
|
||||
<span><b>{{ counts.buckets.lt30 }}</b> تا ۳۰ روز</span>
|
||||
<span><b>{{ counts.buckets.30to90 }}</b> ۳۰ تا ۹۰ روز</span>
|
||||
<span><b>{{ counts.buckets.90to180 }}</b> ۹۰ تا ۱۸۰ روز</span>
|
||||
<span><b>{{ counts.buckets.gt180 }}</b> بیش از ۶ ماه</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -91,24 +91,29 @@ def _action_payload_from_request(request):
|
||||
}
|
||||
|
||||
|
||||
def _apply_visit_hold(customer_obj, payload):
|
||||
"""نوشتن/پاککردن «قرار بعدی» بر اساس اقدام ثبت/ویرایششده.
|
||||
def _apply_visit_hold(customer_obj, payload, *, status):
|
||||
"""همراستاکردن «قرار بعدی» با اقدام ثبت/ویرایششده.
|
||||
|
||||
قواعد:
|
||||
* فقط برای اقدام نوع «هماهنگی» معنا دارد.
|
||||
* اگر تاریخ قرار داده شده باشد ⇒ ``set_visit_hold`` (فقط رو به جلو).
|
||||
* اگر هماهنگی «انجام شد» تیک خورده و تاریخ قرار خالی است ⇒ پیشفرض ۱۴ روز.
|
||||
* اگر هماهنگی لغو شد (``cancell``) یا نوع عوض شد ⇒ قرار پاک میشود.
|
||||
قواعد (یکسان برای ایجاد و ویرایش — قبلاً ایجاد این را انجام نمیداد):
|
||||
|
||||
* فقط برای اقدام نوع «هماهنگی» معنا دارد؛
|
||||
- اگر تاریخ قرار داده شده باشد ⇒ ``set_visit_hold`` (فقط رو به جلو)
|
||||
- اگر «انجام شد» تیک خورده و تاریخ خالی است ⇒ پیشفرض ۱۴ روز
|
||||
- اگر هماهنگی لغو شده باشد ⇒ قرار پاک میشود
|
||||
* **هر اقدام دیگری** (از جمله «بازدید») ⇒ قرار دستی پاک میشود؛ چون هدفِ
|
||||
قرار (پیگیری در آینده) با انجام/برنامهریزی بازدید منتفی است.
|
||||
"""
|
||||
if payload['actionType'] != Action.ActionType.HAMAHANGI:
|
||||
return
|
||||
is_coordination = payload['actionType'] == Action.ActionType.HAMAHANGI
|
||||
if not is_coordination or status == Action.Status.CANCELL:
|
||||
return clear_visit_hold(customer_obj)
|
||||
|
||||
raw = payload.get('holdRaw')
|
||||
if raw is not None:
|
||||
target = normalize_hold_until(raw)
|
||||
set_visit_hold(customer_obj, target)
|
||||
return
|
||||
set_visit_hold(customer_obj, normalize_hold_until(raw))
|
||||
return True
|
||||
if payload['status'] == Action.Status.FINISH:
|
||||
set_visit_hold(customer_obj, default_hold_until())
|
||||
return True
|
||||
|
||||
|
||||
def _reject(request, error, redirect_to):
|
||||
@@ -147,7 +152,7 @@ def customerNewAction(request ,c_id):
|
||||
# last_bazdid_dt فقط از تابع واحد بازمحاسبه میشود؛ نه از finishDateTime.
|
||||
refresh_last_bazdid(customer_obj, reason=REASON_CREATE, user=request.user)
|
||||
# «قرار بعدی» فقط برای هماهنگی.
|
||||
_apply_visit_hold(customer_obj, payload)
|
||||
_apply_visit_hold(customer_obj, payload, status=newAction.status)
|
||||
|
||||
# پیامک «بازدید شما برای تاریخ … ثبت شد» فقط برای اقدام از نوع بازدید.
|
||||
if newAction.actionType == Action.ActionType.BAZDID:
|
||||
@@ -207,10 +212,7 @@ def customerEditAction(request ,c_id, action_id):
|
||||
refresh_last_bazdid(customer_obj, reason=reason, user=request.user)
|
||||
# «قرار بعدی»: با ویرایش همراستا میشود؛ اگر اقدام دیگر هماهنگی نباشد یا
|
||||
# هماهنگی لغو شود، قرار دستی پاک میشود.
|
||||
if payload['actionType'] == Action.ActionType.HAMAHANGI and newAction.status != Action.Status.CANCELL:
|
||||
_apply_visit_hold(customer_obj, payload)
|
||||
else:
|
||||
clear_visit_hold(customer_obj)
|
||||
_apply_visit_hold(customer_obj, payload, status=newAction.status)
|
||||
|
||||
#return redirect('/panel/customer/'+str(c_id)+'/action')
|
||||
return redirect(request.META.get('HTTP_REFERER', 'redirect_if_referer_not_found'))
|
||||
|
||||
@@ -492,12 +492,20 @@ def entezar(request):
|
||||
counts = {
|
||||
'total': len(stale_ids),
|
||||
'uncoordinated': len(groups['A']),
|
||||
'manual': len(groups['B']),
|
||||
'scheduled': len(groups['C']),
|
||||
# توجه: عددِ هر کارت باید **همان چیزی** باشد که با کلیک روی آن کارت
|
||||
# دیده میشود. فیلتر ``view=manual`` گروههای B و D را نشان میدهد و
|
||||
# ``view=scheduled`` گروههای C و D را؛ پس اینجا هم همان اجتماع
|
||||
# شمرده میشود (قبلاً فقط B/C شمرده میشد و عدد با فهرست نمیخواند).
|
||||
'manual': len(groups['B']) + len(groups['D']),
|
||||
'scheduled': len(groups['C']) + len(groups['D']),
|
||||
'manual_only': len(groups['B']),
|
||||
'scheduled_only': len(groups['C']),
|
||||
'both': len(groups['D']),
|
||||
'coordinated': len(groups['B']) + len(groups['C']) + len(groups['D']),
|
||||
'selected': len(selected),
|
||||
'buckets': buckets,
|
||||
# برای متن راهنمای بالای صفحه («بیش از N ماه از بازدید گذشته»).
|
||||
'period_months': period_months,
|
||||
}
|
||||
|
||||
try:
|
||||
|
||||
+87
-29
@@ -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).
|
||||
|
||||
|
||||
Reference in new issue
Block a user