improve bazdid 4

This commit is contained in:
javad committed 2026-10-21 16:34:13 +03:30
1 parent 8fd881e0ea
commit 825f2a5973
13 files changed
+585 -15

No files matched your search

+47 -6
View File
@@ -12,9 +12,10 @@ from django.utils import timezone
from ..models import Action, Customer
from ..sms.triggers import notify_visit_scheduled
from ..visit_policy import (REASON_CANCEL, REASON_CREATE, REASON_EDIT,
as_aware, clear_visit_hold, default_hold_until,
normalize_finish_datetime, normalize_hold_until,
refresh_last_bazdid, set_visit_hold,
ActionDeleteError, as_aware, clear_visit_hold,
default_hold_until, normalize_finish_datetime,
normalize_hold_until, refresh_last_bazdid,
set_visit_hold, soft_delete_action,
validate_action_payload)
@@ -27,8 +28,12 @@ def customerAction(request, c_id):
filter_type = request.GET.get('actionType', 'all')
filter_status = request.GET.get('status', 'all')
# کوئری پایه برای اقدامات این مشتری
# کوئری پایه برای اقدامات این مشتری.
# اقدام‌های حذف‌شده (soft-delete) به‌طور پیش‌فرض نمایش داده نمی‌شوند؛ با
# فیلتر صریح `status=cancell` می‌توان آن‌ها را دید (برای حسابرسی).
actions = Action.objects.filter(customer=customer)
if filter_status != 'cancell':
actions = actions.filter(deleted_at__isnull=True)
# اعمال فیلتر بر اساس نوع اقدام (بازدید، نصب و ...)
if filter_type != 'all':
@@ -57,7 +62,9 @@ def customerAction(request, c_id):
@login_required(login_url='/panel/p_login/')
def customerNazar(request ,c_id):
customer = Customer.objects.get(id=c_id)
actions = Action.objects.filter(customer=customer, nazarDT__isnull=False).order_by("-createdDT")
actions = Action.objects.filter(
customer=customer, nazarDT__isnull=False, deleted_at__isnull=True,
).order_by("-createdDT")
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
data = {'customer': customer, 'actions': actions, 'allUsers': allUsers}
return render(request, 'admin_panel/customer/nazar.html', data)
@@ -224,9 +231,43 @@ def customerEditAction(request ,c_id, action_id):
return redirect(request.META.get('HTTP_REFERER', 'redirect_if_referer_not_found'))
@login_required(login_url='/panel/p_login/')
def customerActionDelete(request, c_id, action_id):
"""«حذف» یک اقدام (soft-delete).
فقط POST مجاز است (تغییر وضعیت داده). اقدام فیزیکی حذف **نمی‌شود**؛
``status`` به ``cancell`` می‌رود و ``deleted_at``/``deleted_by`` پر می‌شوند.
چرا soft-delete؟ چون سابقهٔ CRM باید قابل حسابرسی بماند و منطق محاسبات
(entezar / ActionList / داشبورد) از قبل فقط ``waite``/``finish`` را می‌بیند؛
پس همین مقدار، اقدام را از همهٔ خروجی‌ها کنار می‌گذارد.
"""
customer_obj = get_object_or_404(Customer, id=c_id)
action = get_object_or_404(Action, id=action_id, customer=customer_obj)
back_url = '/panel/customer/%d/action' % customer_obj.pk
if request.method != 'POST':
# دسترسی مستقیم با GET نباید چیزی را عوض کند.
return redirect(back_url)
try:
cancelled = soft_delete_action(action, user=request.user)
except ActionDeleteError as error:
messages.error(request, str(error))
return redirect(back_url)
label = action.get_actionType_display() or 'اقدام'
message = '«%s» حذف شد و از فهرست‌های انتظار/وظایف خارج شد.' % label
if cancelled:
message += ' %d پیامک ارسال‌نشدهٔ مرتبط هم لغو شد.' % cancelled
messages.success(request, message)
return redirect(back_url)
@login_required(login_url='/panel/p_login/')
def ActionListAll(request):
actions = Action.objects.filter().order_by("-id")
# اقدام‌های حذف‌شده (soft-delete) در فهرست «همه» نمایش داده نمی‌شوند.
actions = Action.objects.filter(deleted_at__isnull=True).order_by("-id")
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
data = {'actions': actions, 'allUsers': allUsers}
return render(request, 'admin_panel/ActionList.html', data)
+7 -4
View File
@@ -156,9 +156,10 @@ def report_debtors_no_vosol(request):
).values('customer').annotate(c=Count('id')).values('c')
# استخراج آخرین سابقه اکشن وصول
# نکته: اقدام حذف‌شده/لغوشده نباید «آخرین اقدام وصول» به‌حساب بیاید.
latest_vosol_qs = Action.objects.filter(
customer=OuterRef('pk'), actionType='vosol'
).order_by('-createdDT')
customer=OuterRef('pk'), actionType='vosol',
).exclude(status='cancell').order_by('-createdDT')
# --- ۳. ساخت کوئری پایه (فقط مشتریان فعال و دارای معوقه) ---
base_qs = Customer.objects.filter(is_active=True).annotate(
@@ -939,7 +940,8 @@ def rezayat_dashboard(request):
# دریافت تمامی نظرسنجی‌های ثبت شده معتبر (که تاریخ نظرسنجی دارند)
valid_surveys = Action.objects.filter(
nazarDT__isnull=False,
grade__in=[5, 10, 15, 20]
grade__in=[5, 10, 15, 20],
deleted_at__isnull=True,
)
# ۱. محاسبات شاخص‌های کلیدی (KPIs)
@@ -965,7 +967,8 @@ def rezayat_dashboard(request):
# ۴. واکشی لیست مشتریان نیازمند پیگیری (شامل متوسط ۱۵، ضعیف ۱۰ و بد ۵)
low_score_actions = Action.objects.filter(
nazarDT__isnull=False,
grade__in=[5, 10, 15] # <-- امتیاز ۱۵ (متوسط) نیز اضافه شد
grade__in=[5, 10, 15], # <-- امتیاز ۱۵ (متوسط) نیز اضافه شد
deleted_at__isnull=True,
).order_by('-nazarDT')
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
+3 -1
View File
@@ -40,7 +40,9 @@ SENT_STATUSES = [
# نیازمند اقدام را نشان می‌دهد؛ پیامکِ ارسال‌شده از صف بیرون می‌رود و سابقه‌اش در
# صفحهٔ «گزارش ارسال» (/panel/sms/report) دیده می‌شود. این کار جلوی حذفِ ناخواستهٔ
# تاریخچه از روی صف را می‌گیرد و صف را به فهرستِ کاریِ تمیز تبدیل می‌کند.
DONE_STATUSES = [SMSQueue.Status.SENT]
# پیامک «لغوشده» هم دیگر کاری برای انجام ندارد (مثلاً چون اقدامِ مرتبطش حذف شده)،
# پس از صف بیرون می‌رود.
DONE_STATUSES = [SMSQueue.Status.SENT, SMSQueue.Status.CANCELLED]
def _notice(url, code, **extra):