some improve
This commit is contained in:
1 parent
fa73a734a0
commit
2e5d0bcdd3
17 files changed
+1512
-226
No files matched your search
@@ -11,9 +11,10 @@ from django.utils import timezone
|
||||
from persiantools.jdatetime import JalaliDate
|
||||
|
||||
from ..models import Action, Customer
|
||||
from ..sms.triggers import notify_visit_scheduled
|
||||
from ..sms.triggers import notify_install_scheduled, notify_visit_scheduled
|
||||
from ..visit_policy import (REASON_CANCEL, REASON_CREATE, REASON_EDIT,
|
||||
ActionDeleteError, as_aware, clear_visit_hold,
|
||||
ActionDeleteError, as_aware,
|
||||
cancel_pending_sms_for_action, clear_visit_hold,
|
||||
default_hold_until, normalize_finish_datetime,
|
||||
normalize_hold_until, refresh_last_bazdid,
|
||||
set_visit_hold, soft_delete_action,
|
||||
@@ -159,6 +160,45 @@ def _apply_visit_hold(customer_obj, payload, *, status, is_new, force_hold=False
|
||||
return result
|
||||
|
||||
|
||||
# نگاشت «نوع اقدام → (تریگر پیامک، نوع پیامک)». تنها منبعِ تصمیم است تا مسیر
|
||||
# «ایجاد» و «ویرایش» هرگز واگرا نشوند (باگ قبلی: ویرایش تریگر را صدا نمیزد).
|
||||
_ACTION_EVENT_SMS = {
|
||||
Action.ActionType.BAZDID: (notify_visit_scheduled, 'visit_created'),
|
||||
Action.ActionType.NASB: (notify_install_scheduled, 'install_created'),
|
||||
}
|
||||
|
||||
|
||||
def _sync_action_event_sms(customer, action, *, previous_type=None, user=None):
|
||||
"""همراستاسازی پیامک رویدادی با نوع اقدام (مشترکِ ایجاد و ویرایش).
|
||||
|
||||
* نوع جدید **رویدادی** است (بازدید/نصب):
|
||||
- اگر نوع عوض شده باشد (یا رکورد تازه باشد) ⇒ تریگر همان نوع صدا زده
|
||||
میشود. تکرار ممکن نیست: کلید یکتاسازی ``{sms_type}:{action.pk}`` است.
|
||||
- اگر نوع همان بوده باشد (ویرایش عادی متن/تاریخ) ⇒ هیچ پیامکی ساخته
|
||||
نمیشود، پس ویرایشهای پیاپی اسپم تولید نمیکنند.
|
||||
* نوع جدید **غیررویدادی** است ولی قبلاً رویدادی بود ⇒ پیامک معلقِ همان رکورد
|
||||
لغو میشود (کاربر عملاً بازدید/نصب را پس گرفته است).
|
||||
|
||||
خروجی: تعداد پیامکهای ساختهشده.
|
||||
"""
|
||||
current = _ACTION_EVENT_SMS.get(action.actionType)
|
||||
previous = _ACTION_EVENT_SMS.get(previous_type) if previous_type else None
|
||||
|
||||
# نوع رویدادی عوض شده (بازدید ↔ نصب) ⇒ پیامک معلقِ نوع قبلی همین رکورد لغو شود،
|
||||
# وگرنه هر دو پیامک در صف میمانند.
|
||||
if previous is not None and previous_type != action.actionType:
|
||||
cancel_pending_sms_for_action(
|
||||
customer, action_id=action.pk, sms_type=previous[1],
|
||||
)
|
||||
|
||||
if current is None:
|
||||
# نوع جدید رویدادی نیست؛ اگر قبلاً بود، پیامکش همین بالا لغو شد.
|
||||
return 0
|
||||
if previous_type == action.actionType:
|
||||
return 0 # ویرایش عادی همان نوع ⇒ پیامک تازهای لازم نیست
|
||||
return current[0](customer, action, created_by=user)
|
||||
|
||||
|
||||
def _jalali_day(value):
|
||||
"""تاریخ شمسی روز (به وقت محلی) برای پیامهای کاربر."""
|
||||
if value is None:
|
||||
@@ -232,9 +272,8 @@ def customerNewAction(request ,c_id):
|
||||
)
|
||||
_report_hold_result(request, hold_result)
|
||||
|
||||
# پیامک «بازدید شما برای تاریخ … ثبت شد» فقط برای اقدام از نوع بازدید.
|
||||
if newAction.actionType == Action.ActionType.BAZDID:
|
||||
notify_visit_scheduled(customer_obj, newAction, created_by=request.user)
|
||||
# پیامک «بازدید/نصب شما برای تاریخ … ثبت شد» (برای اقدام از نوع بازدید یا نصب).
|
||||
_sync_action_event_sms(customer_obj, newAction, user=request.user)
|
||||
|
||||
return redirect('/panel/customer/'+str(c_id)+'/action')
|
||||
|
||||
@@ -270,6 +309,9 @@ def customerEditAction(request ,c_id, action_id):
|
||||
customer_obj = Customer.objects.get(id=int(c_id))
|
||||
newAction = Action.objects.get(id=action_id)
|
||||
was_done = newAction.status == Action.Status.FINISH
|
||||
# نوع **قبلی** رکورد (پیش از بازنویسی) — برای تشخیص گذار «غیربازدید → بازدید»
|
||||
# و برعکس. اگر بعد از ``save()`` خوانده شود، همیشه مقدار جدید است.
|
||||
previous_type = newAction.actionType
|
||||
|
||||
newAction.actionType = payload['actionType']
|
||||
newAction.actionDateTime = payload['actionDateTime']
|
||||
@@ -297,6 +339,15 @@ def customerEditAction(request ,c_id, action_id):
|
||||
)
|
||||
_report_hold_result(request, hold_result)
|
||||
|
||||
# پیامک «بازدید/نصب شما برای تاریخ … ثبت شد».
|
||||
# فقط *گذار* نوع اقدام بررسی میشود، نه هر ویرایش (جزئیات در
|
||||
# ``_sync_action_event_sms``):
|
||||
# * غیررویدادی → بازدید/نصب ⇒ تریگر همان نوع صدا زده میشود.
|
||||
# * بازدید/نصب → نوع دیگر ⇒ پیامک معلقِ همان رکورد لغو میشود.
|
||||
_sync_action_event_sms(
|
||||
customer_obj, newAction, previous_type=previous_type, user=request.user,
|
||||
)
|
||||
|
||||
#return redirect('/panel/customer/'+str(c_id)+'/action')
|
||||
return redirect(request.META.get('HTTP_REFERER', 'redirect_if_referer_not_found'))
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ from ..sms.balance import customer_balance
|
||||
from ..sms.phones import (normalize_primary_values,
|
||||
save_primary_normalized)
|
||||
from ..sms.triggers import notify_payment_thanks, notify_welcome
|
||||
from ..sms.rules import customer_toggle_map
|
||||
from ..visit_policy import (SOURCE_COORDINATION, SOURCE_MANUAL, SOURCE_SCHEDULED,
|
||||
appointment_sources_map, as_aware,
|
||||
followups_on_date, last_visit_map, overdue_followups,
|
||||
@@ -164,11 +163,9 @@ def customerNew2(request ,c_id):
|
||||
#all_prd = serializers.serialize("json", all_prda)
|
||||
#return JsonResponse(all_prd,safe = False)
|
||||
dateNow = datetime.datetime.now().date
|
||||
sms_toggles = customer_toggle_map(customer)
|
||||
data = {
|
||||
'customer': customer, 'categorys': categorys, 'dateNow': dateNow,
|
||||
'factorha': factorha, 'mandehesabCs': mandehesabCs,
|
||||
'sms_toggles': sms_toggles,
|
||||
}
|
||||
return render(request, 'admin_panel/customerNew2.html', data)
|
||||
|
||||
@@ -494,8 +491,8 @@ def entezar(request):
|
||||
|
||||
# ---------- نمای «نیازمند پیگیری» (یک تاریخ مشخص) ----------
|
||||
# این نما **مستقل** از «عقبافتاده» است: مشتری سالمی که با او قرار تماس
|
||||
# گذاشته شده هم باید دیده شود. قاعدهٔ حذف «بازدید» در خود
|
||||
# ``followups_on_date`` اعمال میشود (مشتریِ دارای قرار بازدید نمیآید).
|
||||
# گذاشته شده هم باید دیده شود. قاعدهٔ حذفِ «بازدید/نصب» در خود
|
||||
# ``followups_on_date`` اعمال میشود (مشتریِ دارای قرار بازدید یا نصب نمیآید).
|
||||
today_followups = followups_on_date(today_date, now=now)
|
||||
|
||||
# پیگیریهای عقبافتاده: موعدشان گذشته و هنوز تعیین تکلیف نشدهاند.
|
||||
|
||||
+79
-13
@@ -17,6 +17,7 @@ from ..sms.parts import ENCODING_LABELS, describe, parts_count
|
||||
from ..sms.phones import (customer_phone_entries, save_primary_normalized,
|
||||
stored_primary_normalized)
|
||||
from ..sms.render import format_jalali, render_template
|
||||
from ..sms.rules import customer_toggle_map
|
||||
from ..sms.timing import apply_timing, current_timing_value, timing_for
|
||||
|
||||
_manage_required = permission_required("admin_panel.ManageSMS", raise_exception=True)
|
||||
@@ -58,6 +59,16 @@ def _notice(url, code, **extra):
|
||||
def _customer_url(c_id):
|
||||
return reverse("admin_panel:customer", kwargs={"c_id": c_id})
|
||||
|
||||
|
||||
def _customer_sms_url(c_id):
|
||||
"""صفحهٔ «سوابق و تنظیمات پیامک» مشتری — مقصد بازگشتِ عملیات پیامکی.
|
||||
|
||||
تنظیمات پیامک از صفحهٔ فاکتور به این صفحه منتقل شده؛ پس بعد از هر تغییر
|
||||
(تیک نوع پیامک، شمارهٔ اصلی، ارسال سفارشی) کاربر باید همینجا بماند، نه به
|
||||
صفحهٔ اصلی مشتری پرتاب شود.
|
||||
"""
|
||||
return reverse("admin_panel:customerSmsHistory", kwargs={"c_id": c_id})
|
||||
|
||||
# متغیرهای قابل استفاده در متن قالبها (برای راهنمای پنل).
|
||||
TEMPLATE_VARIABLES = [
|
||||
("{name}", "نام مشتری"),
|
||||
@@ -79,6 +90,7 @@ SMS_TYPE_UI = {
|
||||
SMSType.CHEQUE_REGISTRATION: {"icon": "🧾", "when": "چند روز پیش از سررسید چک"},
|
||||
SMSType.VISIT: {"icon": "🔧", "when": "پس از گذشت دورهٔ بازدید دستگاه"},
|
||||
SMSType.VISIT_CREATED: {"icon": "📅", "when": "پس از ثبت اقدام بازدید"},
|
||||
SMSType.INSTALL_CREATED: {"icon": "🛠️", "when": "پس از ثبت اقدام نصب"},
|
||||
SMSType.PAYMENT_THANKS: {"icon": "🙏", "when": "پس از ثبت پرداخت"},
|
||||
SMSType.NESIYE_DUE: {"icon": "💳", "when": "پیش از سررسید نسیه"},
|
||||
}
|
||||
@@ -313,8 +325,12 @@ def sms_customer_toggle(request, c_id):
|
||||
)
|
||||
setting.is_enabled = enabled
|
||||
setting.save()
|
||||
return _notice(_customer_url(c_id), "customer_sms_saved")
|
||||
return redirect("admin_panel:customer", c_id=c_id)
|
||||
# ``open=settings`` ⇒ صفحه با همان مودال تنظیمات باز میشود تا کاربر
|
||||
# چند نوع را پشتسرهم روشن/خاموش کند و هر بار مودال را باز نکند.
|
||||
return _notice(
|
||||
_customer_sms_url(c_id), "customer_sms_saved", open="settings",
|
||||
)
|
||||
return redirect("admin_panel:customerSmsHistory", c_id=c_id)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@@ -336,13 +352,13 @@ def sms_primary_toggle(request, c_id):
|
||||
"",
|
||||
)
|
||||
if not normalized:
|
||||
return _notice(_customer_url(c_id), "primary_not_found")
|
||||
return _notice(_customer_sms_url(c_id), "primary_not_found")
|
||||
current = [n for n in stored_primary_normalized(customer) if n != normalized]
|
||||
if enabled:
|
||||
current.append(normalized)
|
||||
save_primary_normalized(customer, current)
|
||||
return _notice(_customer_url(c_id), "primary_updated")
|
||||
return redirect("admin_panel:customer", c_id=c_id)
|
||||
return _notice(_customer_sms_url(c_id), "primary_updated")
|
||||
return redirect("admin_panel:customerSmsHistory", c_id=c_id)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@@ -358,15 +374,16 @@ def sms_send_custom(request, c_id):
|
||||
text = (request.POST.get("text", "") or "").strip()
|
||||
rendered = render_template(text, {"name": customer.name_family}).strip()
|
||||
if not rendered:
|
||||
return _notice(_customer_url(c_id), "custom_empty")
|
||||
# خطا ⇒ همان مودال ارسال سفارشی دوباره باز شود تا متن از دست نرود.
|
||||
return _notice(_customer_sms_url(c_id), "custom_empty", open="custom")
|
||||
created = enqueue_sms(
|
||||
customer=customer, sms_type="", message=rendered,
|
||||
created_by=request.user,
|
||||
approved=True, # متن را خودِ مدیر نوشته و ارسال را خواسته است.
|
||||
)
|
||||
code = "custom_queued" if created else "custom_no_phone"
|
||||
return _notice(_customer_url(c_id), code)
|
||||
return redirect("admin_panel:customer", c_id=c_id)
|
||||
return _notice(_customer_sms_url(c_id), code)
|
||||
return redirect("admin_panel:customerSmsHistory", c_id=c_id)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@@ -428,11 +445,17 @@ def sms_queue(request):
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@_manage_required
|
||||
def customer_sms_history(request, c_id):
|
||||
"""سوابق پیامکهای یک مشتری روی صفحهٔ خود مشتری.
|
||||
"""سوابق + تنظیمات پیامک یک مشتری (همه در یک صفحه).
|
||||
|
||||
همهٔ پیامکهایی که برای شمارههای این مشتری در صف قرار گرفته/ارسال شدهاند
|
||||
(هر وضعیتی) با متن دقیق، تاریخ شمسی و وضعیت تحویل (دلیوری) نمایش داده میشوند و
|
||||
فهرست صفحهبندی میشود. با ``?status=`` و ``?delivery=`` میتوان فیلتر کرد.
|
||||
ساختار صفحه عمداً دو لایه است تا **تنظیمات، سوابق را شلوغ نکند**:
|
||||
|
||||
* لایهٔ اول (همیشه دیده میشود): آمار کوتاه، فیلترها و فهرست سوابق.
|
||||
* لایهٔ دوم (فقط با کلیک): تنظیمات پیامک، شمارههای اصلی و ارسال سفارشی،
|
||||
همه داخل مودالهای Bulma.
|
||||
|
||||
تنظیماتی که قبلاً داخل باکس «پیامک» صفحهٔ ایجاد فاکتور بود
|
||||
(``_customer_sms.html``) حالا اینجاست؛ آن باکس حذف شد تا فاکتور و سوابق هر
|
||||
دو تمامعرض بمانند.
|
||||
"""
|
||||
customer = get_object_or_404(Customer, id=c_id)
|
||||
status = request.GET.get("status", "")
|
||||
@@ -446,6 +469,34 @@ def customer_sms_history(request, c_id):
|
||||
qs = qs.filter(delivery_status=delivery)
|
||||
paginator = Paginator(qs, 20)
|
||||
page = paginator.get_page(request.GET.get("page"))
|
||||
|
||||
# --- آمار کارتهای بالای صفحه (کل تاریخچهٔ همین مشتری، بدون فیلتر) ---
|
||||
all_items = SMSQueue.objects.filter(customer=customer)
|
||||
status_counts = dict(
|
||||
all_items.order_by().values_list("status").annotate(n=Count("id"))
|
||||
)
|
||||
total_count = all_items.count()
|
||||
|
||||
# --- تنظیمات: نوعهای پیامک با وضعیت مؤثر همین مشتری + «ارسال خودکار» قالب ---
|
||||
# ``auto_send`` تنظیمی سراسری است (صفحهٔ قالبها)، ولی دانستنش اینجا کمک
|
||||
# میکند کاربر بداند پیامکِ این نوع بیدرنگ میرود یا منتظر تأیید میماند.
|
||||
auto_send = dict(
|
||||
SMSTemplate.objects.values_list("sms_type", "auto_send")
|
||||
)
|
||||
type_labels = dict(SMSType.choices)
|
||||
sms_toggles = [
|
||||
{
|
||||
"code": item["code"],
|
||||
"label": item["label"],
|
||||
"enabled": item["enabled"],
|
||||
"auto_send": bool(auto_send.get(item["code"])),
|
||||
"icon": SMS_TYPE_UI.get(item["code"], {}).get("icon", "✉️"),
|
||||
"when": SMS_TYPE_UI.get(item["code"], {}).get("when", ""),
|
||||
}
|
||||
for item in customer_toggle_map(customer)
|
||||
]
|
||||
enabled_count = sum(1 for item in sms_toggles if item["enabled"])
|
||||
|
||||
data = {
|
||||
"customer": customer,
|
||||
"items": page,
|
||||
@@ -453,8 +504,23 @@ def customer_sms_history(request, c_id):
|
||||
"delivery": delivery,
|
||||
"status_choices": SMSQueue.Status.choices,
|
||||
"delivery_choices": SMSQueue.DeliveryStatus.choices,
|
||||
"total_count": SMSQueue.objects.filter(customer=customer).count(),
|
||||
"total_count": total_count,
|
||||
"filtered_count": paginator.count,
|
||||
# تنظیمات (مودال)
|
||||
"sms_toggles": sms_toggles,
|
||||
"sms_enabled_count": enabled_count,
|
||||
"sms_type_total": len(type_labels),
|
||||
"phone_entries": customer_phone_entries(customer),
|
||||
# آمار (نوار بالای صفحه)
|
||||
"status_counts": status_counts,
|
||||
"stat_sent": status_counts.get(SMSQueue.Status.SENT, 0),
|
||||
# «در صف» = هر چیزی که هنوز ارسال نشده و منتظر اقدام/worker است.
|
||||
"stat_pending": (
|
||||
status_counts.get(SMSQueue.Status.PENDING, 0)
|
||||
+ status_counts.get(SMSQueue.Status.APPROVED, 0)
|
||||
+ status_counts.get(SMSQueue.Status.PROCESSING, 0)
|
||||
),
|
||||
"stat_failed": status_counts.get(SMSQueue.Status.FAILED, 0),
|
||||
}
|
||||
return render(request, "admin_panel/customerSmsHistory.html", data)
|
||||
|
||||
|
||||
Reference in new issue
Block a user