improve bazdid 3

This commit is contained in:
javad committed 2026-10-21 12:24:52 +03:30
1 parent f8d768a724
commit f86d060e4d
6 files changed
+337 -121

No files matched your search

+19 -13
View File
@@ -91,21 +91,26 @@ def _action_payload_from_request(request):
}
def _apply_visit_hold(customer_obj, payload, *, status):
def _apply_visit_hold(customer_obj, payload, *, status, is_new):
"""هم‌راستاکردن «قرار بعدی» با اقدام ثبت/ویرایش‌شده.
قواعد (یکسان برای ایجاد و ویرایش — قبلاً ایجاد این را انجام نمی‌داد):
قواعد:
* فقط برای اقدام نوع «هماهنگی» معنا دارد؛
- اگر تاریخ قرار داده شده باشد ⇒ ``set_visit_hold`` (فقط رو به جلو)
- اگر «انجام شد» تیک خورده و تاریخ خالی است ⇒ پیش‌فرض ۱۴ روز
- اگر هماهنگی لغو شده باشد ⇒ قرار پاک می‌شود
* **هر اقدام دیگری** (از جمله «بازدید») ⇒ قرار دستی پاک می‌شود؛ چون هدفِ
قرار (پیگیری در آینده) با انجام/برنامه‌ریزی بازدید منتفی است.
* برای اقدام نوع «هماهنگی»:
- تاریخ قرار داده شده ⇒ ``set_visit_hold`` (فقط رو به جلو)
- «انجام شد» تیک خورده و تاریخ خالی ⇒ پیش‌فرض ۱۴ روز
- لغو شده ⇒ (در ایجاد) پاک می‌شود
* برای **هر اقدام دیگری** (از جمله «بازدید»):
- در حالت **ایجاد**، این اقدام آخرین فعالیت مشتری است ⇒ قرار دستی پاک
می‌شود (سناریوی گزارش‌شده: ثبت بازدید پس از هماهنگی).
- در حالت **ویرایش**، رکوردی تاریخی است و ممکن است قرارِ فعلی از یک
هماهنگیِ *جدیدتر* آمده باشد؛ پس دست نمی‌زنیم. اعتبارسنجی
:func:`manual_holds_map` (شرط «آخرین فعالیت = هماهنگی») خودش تصمیم
می‌گیرد. بدون این تفکیک، ویرایش یک اقدام قدیمی قرار معتبر را پاک می‌کرد.
"""
is_coordination = payload['actionType'] == Action.ActionType.HAMAHANGI
if not is_coordination or status == Action.Status.CANCELL:
return clear_visit_hold(customer_obj)
return clear_visit_hold(customer_obj) if is_new else True
raw = payload.get('holdRaw')
if raw is not None:
@@ -152,7 +157,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, status=newAction.status)
_apply_visit_hold(customer_obj, payload, status=newAction.status, is_new=True)
# پیامک «بازدید شما برای تاریخ … ثبت شد» فقط برای اقدام از نوع بازدید.
if newAction.actionType == Action.ActionType.BAZDID:
@@ -210,9 +215,10 @@ def customerEditAction(request ,c_id, action_id):
# (تا مقداری که این رکورد ساخته بود با منبع حقیقت هم‌راستا شود).
reason = REASON_CANCEL if (was_done and not payload['is_done']) else REASON_EDIT
refresh_last_bazdid(customer_obj, reason=reason, user=request.user)
# «قرار بعدی»: با ویرایش هم‌راستا می‌شود؛ اگر اقدام دیگر هماهنگی نباشد یا
# هماهنگی لغو شود، قرار دستی پاک می‌شود.
_apply_visit_hold(customer_obj, payload, status=newAction.status)
# «قرار بعدی»: با ویرایش هم‌راستا می‌شود. برای اقدام غیرهماهنگی عمداً
# پاک نمی‌کنیم؛ رکورد ویرایش‌شده ممکن است قدیمی‌تر از یک هماهنگیِ جدیدتر
# باشد و پاک‌کردن، قرار معتبر را از بین ببرد.
_apply_visit_hold(customer_obj, payload, status=newAction.status, is_new=False)
#return redirect('/panel/customer/'+str(c_id)+'/action')
return redirect(request.META.get('HTTP_REFERER', 'redirect_if_referer_not_found'))
+47 -47
View File
@@ -24,7 +24,8 @@ from ..visit_policy import (SOURCE_COORDINATION, SOURCE_MANUAL, SOURCE_SCHEDULED
# نماهای صفحهٔ «انتظار» (فیلتر ?view=). پیش‌فرض = فقط «بدون قرار» تا رفتار
# قبلی صفحه برای کاربران حفظ شود.
ENTEZAR_VIEWS = ('uncoordinated', 'manual', 'scheduled', 'coordinated', 'all')
ENTEZAR_VIEWS = ('uncoordinated', 'manual', 'scheduled', 'coordination',
'coordinated', 'all')
ENTEZAR_DEFAULT_VIEW = 'uncoordinated'
# حالت‌های مرتب‌سازی (?sort=):
# oldest ⇒ قدیمی‌ترین بازدید اول (پیش‌فرض/اولویت کارشناس)
@@ -341,18 +342,17 @@ def customerEdit(request, c_id):
@login_required(login_url='/panel/p_login/')
def entezar(request):
"""فهرست «منتظر بازدید» + تفکیک «با قرار» و «بدون قرار».
"""فهرست «منتظر بازدید» + تفکیک بر اساس **نوع قرار**.
منبع حقیقت تاریخ بازدید ``visit_policy.last_visit_map`` است و منبع «قرار بعدی»
``appointment_sources_map`` (سه منبع: قرار دستی، بازدید برنامه‌ریزی‌شده،
هماهنگیِ در انتظار). هر مشتری دقیقاً در یکی از چهار گروه زیر است:
منبع حقیقت تاریخ بازدید ``visit_policy.last_visit_map`` است و منبع قرارها
``appointment_sources_map`` که سه منبع مستقل دارد:
* ``A`` — معوق بدون هیچ قرار (پیش‌فرض نمایش؛ رفتار قبلی صفحه)
* ``B`` — معوق با قرار دستی (تعویق از فرم هماهنگی)
* ``C`` — معوق با بازدید برنامه‌ریزی‌شده یا هماهنگی در انتظار
* ``D`` — معوق با بیش از یک منبعِ قرار
* ``manual`` — قرار دستی روی مشتری (تعویق از فرم هماهنگی)
* ``scheduled`` — اقدام «بازدید» با تاریخ آینده
* ``coordination`` — اقدام «هماهنگی» با تاریخ پیگیری در آینده
فیلتر با ``?view=`` و قابل ترکیب با جست‌وجوی آدرس است.
هر فیلتر فقط منبع خودش را نشان می‌دهد (یک مشتری می‌تواند چند منبع داشته
باشد و در چند فیلتر دیده شود). فیلتر با ``?view=`` و قابل ترکیب با جست‌وجو.
"""
searchVal = request.GET.get('searchVal', '')
searchOn = request.GET.get('searchOn', '')
@@ -403,29 +403,35 @@ def entezar(request):
if visits.get(customer_id) is None or visits[customer_id] <= due_cut
]
# تفکیک چهارگروهی (MECE): هر مشتری فقط در یک گروه.
groups = {'A': [], 'B': [], 'C': [], 'D': []}
# تفکیک بر اساس **منبعِ** قرار: هر مشتری می‌تواند صفر تا سه منبع داشته باشد
# (قرار دستی، بازدید برنامه‌ریزی‌شده، هماهنگی). قبلاً «هماهنگی» با «بازدید»
# در یک گروه جمع می‌شد و در نمای «بازدید برنامه‌ریزی‌شده» دیده می‌شد؛ حالا
# هر منبع گروه نمایشیِ خودش را دارد.
with_manual, with_scheduled, with_coordination = set(), set(), set()
for customer_id in stale_ids:
customer_sources = sources.get(customer_id) or {}
manual = SOURCE_MANUAL in customer_sources
scheduled = (SOURCE_SCHEDULED in customer_sources
or SOURCE_COORDINATION in customer_sources)
if manual and scheduled:
groups['D'].append(customer_id)
elif manual:
groups['B'].append(customer_id)
elif scheduled:
groups['C'].append(customer_id)
else:
groups['A'].append(customer_id)
if SOURCE_MANUAL in customer_sources:
with_manual.add(customer_id)
if SOURCE_SCHEDULED in customer_sources:
with_scheduled.add(customer_id)
if SOURCE_COORDINATION in customer_sources:
with_coordination.add(customer_id)
selected = {
'uncoordinated': groups['A'],
'manual': groups['B'] + groups['D'],
'scheduled': groups['C'] + groups['D'],
'coordinated': groups['B'] + groups['C'] + groups['D'],
coordinated_ids = [cid for cid in stale_ids if cid in (
with_manual | with_scheduled | with_coordination)]
selected_map = {
# نداشتن هیچ قرار
'uncoordinated': [cid for cid in stale_ids if cid not in (
with_manual | with_scheduled | with_coordination)],
# هر مشتری که این منبع را دارد (حتی اگر منبع دیگری هم داشته باشد)
'manual': [cid for cid in stale_ids if cid in with_manual],
'scheduled': [cid for cid in stale_ids if cid in with_scheduled],
'coordination': [cid for cid in stale_ids if cid in with_coordination],
# اجتماع هر سه منبع
'coordinated': coordinated_ids,
'all': stale_ids,
}[view]
}
selected = selected_map[view]
# «مدت تأخیر» = از چه زمانی بازدید عقب افتاده است (به روز).
# مبنای محاسبه «موعد بازدید» است، نه خود بازدید: موعد = بازدید + دوره.
@@ -491,17 +497,14 @@ def entezar(request):
counts = {
'total': len(stale_ids),
'uncoordinated': len(groups['A']),
# توجه: عددِ هر کارت باید **همان چیزی** باشد که با کلیک روی آن کارت
# دیده می‌شود. فیلتر ``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']),
'uncoordinated': len(selected_map['uncoordinated']),
# عددِ هر کارت دقیقاً همان چیزی است که با کلیک روی آن دیده می‌شود؛
# چون یک مشتری می‌تواند چند منبع داشته باشد، جمع کارت‌ها ممکن است
# از «با قرار» بیشتر شود. در راهنمای بالای صفحه توضیح داده می‌شود.
'manual': len(selected_map['manual']),
'scheduled': len(selected_map['scheduled']),
'coordination': len(selected_map['coordination']),
'coordinated': len(selected_map['coordinated']),
'selected': len(selected),
'buckets': buckets,
# برای متن راهنمای بالای صفحه («بیش از N ماه از بازدید گذشته»).
@@ -524,19 +527,16 @@ def entezar(request):
appointment_rows = {}
for customer in customerHa.object_list:
entry = sources.get(customer.id) or {}
flags = [key for key in (SOURCE_MANUAL, SOURCE_SCHEDULED,
SOURCE_COORDINATION) if key in entry]
appointment_rows[customer.id] = {
'manual': entry.get(SOURCE_MANUAL),
'scheduled': entry.get(SOURCE_SCHEDULED),
'coordination': entry.get(SOURCE_COORDINATION),
'overdue_days': overdue_days.get(customer.id, 0),
'last_visit': visits.get(customer.id),
'group': (
'D' if (SOURCE_MANUAL in entry and (
SOURCE_SCHEDULED in entry or SOURCE_COORDINATION in entry))
else 'B' if SOURCE_MANUAL in entry
else 'C' if (SOURCE_SCHEDULED in entry or SOURCE_COORDINATION in entry)
else 'A'
),
# تعداد منابع (۰ تا ۳) — قالب بر اساس آن برچسب وضعیت را می‌سازد.
'source_count': len(flags),
}
return render(request, 'admin_panel/entezar.html', {