fix sms
This commit is contained in:
1 parent
1dea3509a7
commit
e3a6d1f9d4
42 files changed
+33086
-15779
No files matched your search
@@ -7,6 +7,7 @@ from django.contrib.auth.models import User
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
from ..models import Action, Customer
|
||||
from ..sms.triggers import notify_visit_scheduled
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@@ -91,6 +92,10 @@ def customerNewAction(request ,c_id):
|
||||
customer_obj.last_bazdid_dt = newAction.finishDateTime
|
||||
customer_obj.save()
|
||||
|
||||
# پیامک «بازدید شما برای تاریخ … ثبت شد» فقط برای اقدام از نوع بازدید.
|
||||
if newAction.actionType == 'bazdid':
|
||||
notify_visit_scheduled(customer_obj, newAction, created_by=request.user)
|
||||
|
||||
return redirect('/panel/customer/'+str(c_id)+'/action')
|
||||
|
||||
|
||||
|
||||
@@ -11,11 +11,10 @@ from django.contrib.auth.decorators import login_required, permission_required
|
||||
from persiantools.jdatetime import JalaliDate
|
||||
|
||||
from ..models import (Customer, PhoneNumber, ProductCategory, Product, Factor,
|
||||
FactorIteam, Article, Sarresid, Action, CacheTable, SMSType)
|
||||
from ..sms.enqueue import enqueue_sms
|
||||
from ..sms.phones import (customer_phone_entries, normalize_primary_values,
|
||||
FactorIteam, Article, Sarresid, Action, CacheTable)
|
||||
from ..sms.phones import (normalize_primary_values,
|
||||
save_primary_normalized)
|
||||
from ..sms.recipients import resolve_recipients
|
||||
from ..sms.triggers import notify_payment_thanks, notify_welcome
|
||||
from ..sms.rules import customer_toggle_map
|
||||
from ._helpers import logger, _parse_jalali_date, _parse_phone_rows
|
||||
|
||||
@@ -122,15 +121,10 @@ def customerNew2(request ,c_id):
|
||||
#return JsonResponse(all_prd,safe = False)
|
||||
dateNow = datetime.datetime.now().date
|
||||
sms_toggles = customer_toggle_map(customer)
|
||||
# همهٔ شمارهها از هر منبعی که ذخیره شدهاند (PhoneNumber و phone1/2/3 قدیمی).
|
||||
sms_phone_entries = customer_phone_entries(customer)
|
||||
sms_effective = {n for n in resolve_recipients(customer)}
|
||||
data = {
|
||||
'customer': customer, 'categorys': categorys, 'dateNow': dateNow,
|
||||
'factorha': factorha, 'mandehesabCs': mandehesabCs,
|
||||
'sms_toggles': sms_toggles, 'sms_phone_entries': sms_phone_entries,
|
||||
'sms_effective': sms_effective,
|
||||
'sms_recipients': sorted(sms_effective),
|
||||
'sms_toggles': sms_toggles,
|
||||
}
|
||||
return render(request, 'admin_panel/customerNew2.html', data)
|
||||
|
||||
@@ -271,12 +265,7 @@ def customersAdd(request):
|
||||
)
|
||||
|
||||
# پیامک خوشآمدگویی (در صورت فعال بودن قالب و تنظیم مشتری)
|
||||
enqueue_sms(
|
||||
customer=newCs, sms_type=SMSType.WELCOME,
|
||||
context={'name': newCs.name_family},
|
||||
dedup_key=f'welcome:{newCs.id}',
|
||||
created_by=request.user,
|
||||
)
|
||||
notify_welcome(newCs, created_by=request.user)
|
||||
|
||||
return redirect('/panel/customers/')
|
||||
|
||||
@@ -494,4 +483,9 @@ def customerHesabdariAdd(request ,c_id):
|
||||
newArticl.user = user_op
|
||||
newArticl.nahvePardakht = nahvePardakhat
|
||||
newArticl.save()
|
||||
# سند بستانکار (credit>0 و debit=0) یعنی مشتری پول پرداخت کرده ⇒ پیامک تشکر.
|
||||
if type == 'credit':
|
||||
notify_payment_thanks(newArticl.customer, newArticl.credit,
|
||||
article_id=newArticl.id, method=nahvePardakhat,
|
||||
created_by=user_op)
|
||||
return redirect('/panel/customer/'+str(c_id)+'/hesabdari')
|
||||
@@ -12,6 +12,7 @@ from persiantools import digits
|
||||
|
||||
from ..models import (Customer, StoreRoom, Factor, FactorIteam, Product,
|
||||
ProductInStore, Article, Sarresid)
|
||||
from ..sms.triggers import notify_factor_survey, notify_payment_thanks
|
||||
from ._helpers import logger, _safe_int, _parse_jalali_date, _parse_timestamp, InventoryException
|
||||
|
||||
|
||||
@@ -196,6 +197,8 @@ def newFactor2(request, c_id):
|
||||
if not str(item.get('garanty_Esh', '')).strip()
|
||||
)
|
||||
|
||||
new_factor = None
|
||||
payment_articles = []
|
||||
try:
|
||||
with transaction.atomic():
|
||||
|
||||
@@ -231,7 +234,7 @@ def newFactor2(request, c_id):
|
||||
|
||||
if key in payment_map:
|
||||
nahve_pardakht = payment_map[key]
|
||||
Article.objects.create(
|
||||
payment_articles.append((Article.objects.create(
|
||||
debit=0,
|
||||
credit=amount,
|
||||
customer=customer_obj,
|
||||
@@ -240,7 +243,7 @@ def newFactor2(request, c_id):
|
||||
nahvePardakht=nahve_pardakht,
|
||||
payStatus="true",
|
||||
user=user_op
|
||||
)
|
||||
), nahve_pardakht))
|
||||
|
||||
elif key in ['l_nesiye', 'l_checkPrice']:
|
||||
is_nesiye = (key == 'l_nesiye')
|
||||
@@ -359,6 +362,16 @@ def newFactor2(request, c_id):
|
||||
logger.error(f"Error creating factor for customer {c_id}: {str(e)}", exc_info=True)
|
||||
return HttpResponse("خطایی غیرمنتظره در سرور رخ داد. اطلاعات ذخیره نشد.", status=500)
|
||||
|
||||
# تریگرهای پیامک پس از تأیید تراکنش (خارج از atomic تا در صورت rollback ساخته نشوند).
|
||||
if new_factor is not None:
|
||||
# نظرسنجی بیواسطه پس از ثبت فاکتور.
|
||||
notify_factor_survey(customer_obj, new_factor, created_by=user_op)
|
||||
# تشکر برای هر پرداخت نقدی/کارتیِ ثبتشده روی همین فاکتور (تخفیف مستثناست).
|
||||
for art, method in payment_articles:
|
||||
notify_payment_thanks(customer_obj, art.credit,
|
||||
article_id=art.id, method=method,
|
||||
created_by=user_op)
|
||||
|
||||
return redirect(f'/panel/customer/{c_id}/factors/')
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ from persiantools.jdatetime import JalaliDate
|
||||
|
||||
from ..models import (Article, Sarresid, Customer, Factor, ProductInStore,
|
||||
StoreRoom, Action)
|
||||
from ..sms.triggers import notify_payment_thanks
|
||||
from ._helpers import logger, _safe_int
|
||||
|
||||
|
||||
@@ -713,6 +714,11 @@ def passCheck(request, articl_id):
|
||||
sarresidINC.desc = desc
|
||||
sarresidINC.save()
|
||||
|
||||
# سند وصول (credit>0 و debit=0) یعنی مشتری پول داده ⇒ پیامک تشکر از پرداخت.
|
||||
notify_payment_thanks(articleHesabDari2.customer, articleHesabDari2.credit,
|
||||
article_id=articleHesabDari2.id,
|
||||
method=articleHesabDari2.nahvePardakht, created_by=user_op)
|
||||
|
||||
return HttpResponse("ok")
|
||||
#return redirect(request.META.get('HTTP_REFERER'))
|
||||
|
||||
@@ -916,6 +922,11 @@ def pardakhtNesiye(request, cs_id):
|
||||
articleHesabDariObj.user = user_op
|
||||
articleHesabDariObj.save()
|
||||
|
||||
# پرداخت نسیه (بستانکار) ⇒ پیامک تشکر از پرداخت (تخفیف مستثناست).
|
||||
notify_payment_thanks(customerObj, articleHesabDariObj.credit,
|
||||
article_id=articleHesabDariObj.id,
|
||||
method=nahvePardakht, created_by=user_op)
|
||||
|
||||
return redirect(request.META.get('HTTP_REFERER'))
|
||||
|
||||
except:
|
||||
|
||||
+217
-30
@@ -1,11 +1,11 @@
|
||||
"""ویوهای مدیریت پیامک در پنل (نیازمند پرمیشن ``admin_panel.ManageSMS``)."""
|
||||
import re
|
||||
from datetime import datetime, time
|
||||
from datetime import datetime, time, timedelta
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Count
|
||||
from django.db.models import Count, Q
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
@@ -13,12 +13,34 @@ from django.utils import timezone
|
||||
from ..models import (Customer, SMSDedup, SMSProviderConfig, SMSQueue,
|
||||
SMSTemplate, SMSType)
|
||||
from ..sms.enqueue import enqueue_sms
|
||||
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.timing import apply_timing, current_timing_value, timing_for
|
||||
|
||||
_manage_required = permission_required("admin_panel.ManageSMS", raise_exception=True)
|
||||
# صفحات «تنظیمات سامانه» و «قالبها» یک سطح دسترسی جداگانه دارند تا مدیر بتواند
|
||||
# فقط به کاربران موردنظرش اجازهٔ دیدن/ویرایش بدهد (نه هر کاربر دارای ManageSMS).
|
||||
_settings_required = permission_required(
|
||||
"admin_panel.ManageSMSSettings", raise_exception=True,
|
||||
)
|
||||
|
||||
# گزارش هزینه فقط روی پیامکهایی است که از مرحلهٔ «تأیید مدیر» گذشته و واقعاً به
|
||||
# سامانه/اپراتور سپرده شدهاند. موارد «در انتظار تأیید» (هنوز ارسال نشده) و
|
||||
# «لغوشده» (هرگز ارسال نمیشود) نباید در تحلیل هزینه بیایند.
|
||||
SENT_STATUSES = [
|
||||
SMSQueue.Status.APPROVED,
|
||||
SMSQueue.Status.PROCESSING,
|
||||
SMSQueue.Status.SENT,
|
||||
SMSQueue.Status.FAILED,
|
||||
]
|
||||
|
||||
# وضعیتهای «پایانی» که دیگر کاری روی آنها لازم نیست. صفحهٔ «صف ارسال» فقط مواردِ
|
||||
# نیازمند اقدام را نشان میدهد؛ پیامکِ ارسالشده از صف بیرون میرود و سابقهاش در
|
||||
# صفحهٔ «گزارش ارسال» (/panel/sms/report) دیده میشود. این کار جلوی حذفِ ناخواستهٔ
|
||||
# تاریخچه از روی صف را میگیرد و صف را به فهرستِ کاریِ تمیز تبدیل میکند.
|
||||
DONE_STATUSES = [SMSQueue.Status.SENT]
|
||||
|
||||
|
||||
def _notice(url, code, **extra):
|
||||
@@ -47,14 +69,13 @@ TEMPLATE_VARIABLES = [
|
||||
|
||||
# فقط برای نمایش: آیکن و توضیح کوتاه «چه زمانی ارسال میشود» هر نوع (لایهٔ UI، نه دامنه).
|
||||
SMS_TYPE_UI = {
|
||||
SMSType.WELCOME: {"icon": "👋", "when": "بیدرنگ پس از ثبت مشتری"},
|
||||
SMSType.CHEQUE_DUE: {"icon": "🧾", "when": "پیش از سررسید چک"},
|
||||
SMSType.WELCOME: {"icon": "👋", "when": "بیدرنگ پس از ثبت مشتری"},
|
||||
SMSType.SURVEY: {"icon": "⭐", "when": "بیدرنگ پس از ثبت فاکتور"},
|
||||
SMSType.CHEQUE_REGISTRATION: {"icon": "🧾", "when": "چند روز پس از سررسید چک"},
|
||||
SMSType.VISIT: {"icon": "🔧", "when": "پس از گذشت دورهٔ بازدید دستگاه"},
|
||||
SMSType.VISIT_CREATED: {"icon": "📅", "when": "پس از ثبت اقدام بازدید"},
|
||||
SMSType.PAYMENT_THANKS: {"icon": "🙏", "when": "پس از ثبت پرداخت"},
|
||||
SMSType.NESIYE_DUE: {"icon": "💳", "when": "پیش از سررسید نسیه"},
|
||||
SMSType.CHEQUE_REGISTRATION: {"icon": "⏳", "when": "تا ثبتشدن اطلاعات چک"},
|
||||
SMSType.DEBT_DUE: {"icon": "💰", "when": "روز سررسید وصول مطالبات"},
|
||||
SMSType.SURVEY: {"icon": "⭐", "when": "پس از صدور فاکتور"},
|
||||
SMSType.VISIT: {"icon": "🔧", "when": "پس از گذشت دورهٔ بازدید"},
|
||||
SMSType.CUSTOM: {"icon": "✍️", "when": "ارسال دستی از صفحهٔ مشتری"},
|
||||
}
|
||||
|
||||
# نمونهٔ داده برای «پیشنمایش زندهٔ» متن در پنل (هیچ پیامکی با این مقادیر ارسال نمیشود).
|
||||
@@ -70,7 +91,7 @@ SAMPLE_CONTEXT = {
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@_manage_required
|
||||
@_settings_required
|
||||
def sms_settings(request):
|
||||
config = SMSProviderConfig.load()
|
||||
if request.method == "POST":
|
||||
@@ -82,7 +103,7 @@ def sms_settings(request):
|
||||
config.test_message = test_text
|
||||
config.save(update_fields=["test_message"])
|
||||
enqueue_sms(
|
||||
customer=None, sms_type=SMSType.CUSTOM,
|
||||
customer=None, sms_type="",
|
||||
message=test_text or config.test_message,
|
||||
recipients=resolve_recipients_test(phone), created_by=request.user,
|
||||
approved=True, # ارسال دستیِ مدیر؛ نیاز به بازبینی صف ندارد.
|
||||
@@ -192,7 +213,7 @@ def _sample_context():
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@_manage_required
|
||||
@_settings_required
|
||||
def sms_templates(request):
|
||||
raw_sample, text_sample = _sample_context()
|
||||
rows = []
|
||||
@@ -236,7 +257,7 @@ def _templates_redirect(anchor="", notice="", **extra):
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@_manage_required
|
||||
@_settings_required
|
||||
def sms_template_edit(request, t_id):
|
||||
"""ویرایش یک قالب. فیلد زمانی بر اساس نوع پیامک انتخاب و اعتبارسنجی میشود.
|
||||
|
||||
@@ -321,22 +342,17 @@ def sms_primary_toggle(request, c_id):
|
||||
def sms_send_custom(request, c_id):
|
||||
"""ارسال پیامک با متن سفارشی برای همین مشتری.
|
||||
|
||||
اگر کادر متن خالی بماند، متن قالب «پیام سفارشی» استفاده میشود. متغیرهایی مثل
|
||||
متن باید توسط مدیر نوشته شود (قالب پیشفرضی وجود ندارد)؛ متغیرهایی مثل
|
||||
``{name}`` در متن واردشده هم رندر میشوند.
|
||||
"""
|
||||
customer = get_object_or_404(Customer, id=c_id)
|
||||
if request.method == "POST":
|
||||
text = (request.POST.get("text", "") or "").strip()
|
||||
if not text:
|
||||
template = SMSTemplate.objects.filter(
|
||||
sms_type=SMSType.CUSTOM, is_enabled=True,
|
||||
).first()
|
||||
text = (template.text if template else "").strip()
|
||||
rendered = render_template(text, {"name": customer.name_family}).strip()
|
||||
if not rendered:
|
||||
return _notice(_customer_url(c_id), "custom_empty")
|
||||
created = enqueue_sms(
|
||||
customer=customer, sms_type=SMSType.CUSTOM, message=rendered,
|
||||
customer=customer, sms_type="", message=rendered,
|
||||
created_by=request.user,
|
||||
approved=True, # متن را خودِ مدیر نوشته و ارسال را خواسته است.
|
||||
)
|
||||
@@ -348,9 +364,19 @@ def sms_send_custom(request, c_id):
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@_manage_required
|
||||
def sms_queue(request):
|
||||
"""صف ارسال: فقط مواردی که هنوز «کارِ باقیمانده» دارند.
|
||||
|
||||
پیامکهای ارسالشده (``sent``) از این فهرست بیرون میمانند چون کارشان تمام است و
|
||||
سابقهشان در «گزارش ارسال» دیده میشود؛ ماندنشان در صف فقط باعث میشد دکمهٔ
|
||||
«حذف» روی تاریخچه ظاهر شود. بقیهٔ وضعیتها (در انتظار تأیید، تأییدشده، در حال
|
||||
پردازش، ناموفق) در صف میمانند چون هنوز نیاز به اقدام دارند.
|
||||
"""
|
||||
status = request.GET.get("status", "")
|
||||
delivery = request.GET.get("delivery", "")
|
||||
qs = SMSQueue.objects.all().select_related("customer").order_by("-created_at")
|
||||
qs = (
|
||||
SMSQueue.objects.exclude(status__in=DONE_STATUSES)
|
||||
.select_related("customer").order_by("-created_at")
|
||||
)
|
||||
if status:
|
||||
qs = qs.filter(status=status)
|
||||
if delivery:
|
||||
@@ -364,24 +390,177 @@ def sms_queue(request):
|
||||
counts = dict(
|
||||
SMSQueue.objects.order_by().values_list("status").annotate(n=Count("id"))
|
||||
)
|
||||
# «کل صف» = موارد باقیمانده (بدون ارسالشدهها).
|
||||
queue_total = sum(n for code, n in counts.items() if code not in DONE_STATUSES)
|
||||
data = {
|
||||
"queue": page,
|
||||
"status": status,
|
||||
"delivery": delivery,
|
||||
"status_choices": SMSQueue.Status.choices,
|
||||
# گزینههای فیلتر وضعیت: فقط وضعیتهای درون صف (ارسالشده حذف میشود).
|
||||
"status_choices": [
|
||||
(code, label) for code, label in SMSQueue.Status.choices
|
||||
if code not in DONE_STATUSES
|
||||
],
|
||||
"delivery_choices": SMSQueue.DeliveryStatus.choices,
|
||||
"pending_count": counts.get(SMSQueue.Status.PENDING, 0),
|
||||
"total_count": sum(counts.values()),
|
||||
"total_count": queue_total,
|
||||
# تعداد ارسالشدهها جداگانه نمایش داده میشود (برای رفتن به گزارش).
|
||||
"sent_count": counts.get(SMSQueue.Status.SENT, 0),
|
||||
"filtered_count": paginator.count,
|
||||
# (کد وضعیت، برچسب، تعداد) — به ترتیب تعریف Status.
|
||||
# (کد وضعیت، برچسب، تعداد) — فقط وضعیتهای درون صف.
|
||||
"status_stats": [
|
||||
(code, label, counts.get(code, 0))
|
||||
for code, label in SMSQueue.Status.choices
|
||||
if code not in DONE_STATUSES
|
||||
],
|
||||
}
|
||||
return render(request, "admin_panel/sms_queue.html", data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@_manage_required
|
||||
def customer_sms_history(request, c_id):
|
||||
"""سوابق پیامکهای یک مشتری روی صفحهٔ خود مشتری.
|
||||
|
||||
همهٔ پیامکهایی که برای شمارههای این مشتری در صف قرار گرفته/ارسال شدهاند
|
||||
(هر وضعیتی) با متن دقیق، تاریخ شمسی و وضعیت تحویل (دلیوری) نمایش داده میشوند و
|
||||
فهرست صفحهبندی میشود. با ``?status=`` و ``?delivery=`` میتوان فیلتر کرد.
|
||||
"""
|
||||
customer = get_object_or_404(Customer, id=c_id)
|
||||
status = request.GET.get("status", "")
|
||||
delivery = request.GET.get("delivery", "")
|
||||
qs = (
|
||||
SMSQueue.objects.filter(customer=customer).order_by("-created_at")
|
||||
)
|
||||
if status:
|
||||
qs = qs.filter(status=status)
|
||||
if delivery:
|
||||
qs = qs.filter(delivery_status=delivery)
|
||||
paginator = Paginator(qs, 20)
|
||||
page = paginator.get_page(request.GET.get("page"))
|
||||
data = {
|
||||
"customer": customer,
|
||||
"items": page,
|
||||
"status": status,
|
||||
"delivery": delivery,
|
||||
"status_choices": SMSQueue.Status.choices,
|
||||
"delivery_choices": SMSQueue.DeliveryStatus.choices,
|
||||
"total_count": SMSQueue.objects.filter(customer=customer).count(),
|
||||
"filtered_count": paginator.count,
|
||||
}
|
||||
return render(request, "admin_panel/customerSmsHistory.html", data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@_manage_required
|
||||
def sms_report(request):
|
||||
"""گزارش پیامکهای ارسالشده برای تحلیل هزینه.
|
||||
|
||||
امکان فیلتر بر اساس **بازهٔ تاریخ شمسی** (از/تا)، وضعیت ارسال، وضعیت تحویل و
|
||||
نوع پیامک، بههمراه جستجوی متنی روی متن پیامک/شماره/نام مشتری.
|
||||
|
||||
برای هر ردیف، «تعداد پیامکی که اپراتور محاسبه کرده» (parts) بر اساس کدگذاری
|
||||
واقعی متن (GSM-7 برای انگلیسی/عدد، UCS-2 برای فارسی/یونیکد) محاسبه و نمایش
|
||||
داده میشود؛ جمع کل بخشها هم برای برآورد هزینه گزارش میشود.
|
||||
"""
|
||||
date_from_raw = (request.GET.get("date_from") or "").strip()
|
||||
date_to_raw = (request.GET.get("date_to") or "").strip()
|
||||
status = request.GET.get("status", "")
|
||||
delivery = request.GET.get("delivery", "")
|
||||
sms_type = request.GET.get("sms_type", "")
|
||||
query = (request.GET.get("q") or "").strip()
|
||||
|
||||
qs = (
|
||||
SMSQueue.objects.select_related("customer")
|
||||
.filter(status__in=SENT_STATUSES)
|
||||
.order_by("-created_at")
|
||||
)
|
||||
|
||||
# بازهٔ تاریخ شمسی روی «زمان ایجاد» (ثبت در سیستم) اعمال میشود. بازهٔ «تا» تا
|
||||
# پایان همان روز در نظر گرفته میشود تا پیامکهای همان روز هم بیایند.
|
||||
parsed_from = parse_jalali_date(date_from_raw) if date_from_raw else None
|
||||
parsed_to = parse_jalali_date(date_to_raw) if date_to_raw else None
|
||||
if parsed_from is not None:
|
||||
qs = qs.filter(created_at__gte=parsed_from)
|
||||
if parsed_to is not None:
|
||||
qs = qs.filter(created_at__lt=parsed_to + timedelta(days=1))
|
||||
|
||||
if status:
|
||||
qs = qs.filter(status=status)
|
||||
if delivery:
|
||||
qs = qs.filter(delivery_status=delivery)
|
||||
if sms_type:
|
||||
qs = qs.filter(sms_type=sms_type)
|
||||
if query:
|
||||
qs = qs.filter(
|
||||
Q(message__icontains=query)
|
||||
| Q(phone_number__icontains=query)
|
||||
| Q(customer__name_family__icontains=query)
|
||||
)
|
||||
|
||||
# --- آمار کلی روی کل نتیجهٔ فیلترشده (نه فقط صفحهٔ جاری) ---
|
||||
status_counts = dict(
|
||||
qs.order_by().values_list("status").annotate(n=Count("id"))
|
||||
)
|
||||
total_records = sum(status_counts.values())
|
||||
# جمع بخشها (parts) باید در پایتون حساب شود چون به کدگذاری هر متن بستگی دارد.
|
||||
total_parts = 0
|
||||
gsm7_parts = ucs2_parts = 0
|
||||
for message in qs.values_list("message", flat=True).iterator(chunk_size=500):
|
||||
info = describe(message)
|
||||
total_parts += info["parts"]
|
||||
if info["encoding"] == "gsm7":
|
||||
gsm7_parts += info["parts"]
|
||||
else:
|
||||
ucs2_parts += info["parts"]
|
||||
|
||||
paginator = Paginator(qs, 50)
|
||||
page = paginator.get_page(request.GET.get("page"))
|
||||
|
||||
# برای هر ردیف صفحه، محاسبهٔ کدگذاری/طول/بخش (نمایشی).
|
||||
rows = []
|
||||
for item in page:
|
||||
info = describe(item.message)
|
||||
rows.append({"item": item, "parts": info["parts"],
|
||||
"encoding": info["encoding_label"], "length": info["length"]})
|
||||
|
||||
# برای لینک صفحهبندی که همهٔ فیلترها را حفظ کند (بدون خودِ page).
|
||||
params = request.GET.copy()
|
||||
params.pop("page", None)
|
||||
base_query = params.urlencode()
|
||||
|
||||
data = {
|
||||
"rows": rows,
|
||||
"page": page,
|
||||
"status": status,
|
||||
"delivery": delivery,
|
||||
"sms_type": sms_type,
|
||||
"query": query,
|
||||
"date_from": date_from_raw,
|
||||
"date_to": date_to_raw,
|
||||
"status_choices": [
|
||||
(code, label) for code, label in SMSQueue.Status.choices
|
||||
if code in SENT_STATUSES
|
||||
],
|
||||
"delivery_choices": SMSQueue.DeliveryStatus.choices,
|
||||
"type_choices": SMSType.choices,
|
||||
"total_records": total_records,
|
||||
"total_parts": total_parts,
|
||||
"gsm7_parts": gsm7_parts,
|
||||
"ucs2_parts": ucs2_parts,
|
||||
"encoding_labels": ENCODING_LABELS,
|
||||
"status_stats": [
|
||||
(code, label, status_counts.get(code, 0))
|
||||
for code, label in SMSQueue.Status.choices
|
||||
if code in SENT_STATUSES
|
||||
],
|
||||
"filtered": paginator.count,
|
||||
"base_query": base_query,
|
||||
"has_filter": any([date_from_raw, date_to_raw, status, delivery, sms_type, query]),
|
||||
}
|
||||
return render(request, "admin_panel/sms_report.html", data)
|
||||
|
||||
|
||||
def _approve(queryset):
|
||||
"""پیامکهای «در انتظار تأیید» را تأیید و آمادهٔ ارسال میکند."""
|
||||
return queryset.filter(status=SMSQueue.Status.PENDING).update(
|
||||
@@ -403,9 +582,15 @@ def sms_queue_approve(request, q_id):
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@_manage_required
|
||||
def sms_queue_delete(request, q_id):
|
||||
"""حذف یک پیامک از صف (مثلاً پس از بازبینی و رد آن)."""
|
||||
"""حذف یک پیامک از صف (مثلاً پس از بازبینی و رد آن).
|
||||
|
||||
پیامکِ ارسالشده حذف نمیشود؛ سابقهٔ آن در «گزارش ارسال» میماند و حذفش یعنی
|
||||
پاککردنِ تاریخچه. برای چنین موردی فقط پیام هشدار برمیگردد.
|
||||
"""
|
||||
item = get_object_or_404(SMSQueue, id=q_id)
|
||||
if request.method == "POST":
|
||||
if item.status in DONE_STATUSES:
|
||||
return _notice(reverse("admin_panel:sms_queue"), "delete_done_blocked")
|
||||
item.delete()
|
||||
return _notice(reverse("admin_panel:sms_queue"), "deleted")
|
||||
return redirect("admin_panel:sms_queue")
|
||||
@@ -427,6 +612,8 @@ def sms_queue_bulk(request):
|
||||
count = _approve(selected)
|
||||
code, extra = "bulk_approved", {"count": count}
|
||||
elif action == "delete_selected":
|
||||
# تاریخچهٔ ارسالشدهها از طریق صف حذف نمیشود.
|
||||
selected = selected.exclude(status__in=DONE_STATUSES)
|
||||
count = selected.count()
|
||||
selected.delete()
|
||||
code, extra = "bulk_deleted", {"count": count}
|
||||
@@ -439,14 +626,14 @@ def sms_queue_bulk(request):
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@_manage_required
|
||||
def sms_queue_empty(request):
|
||||
"""خالی کردن کل صف — هر وضعیتی، بدون بازگشت.
|
||||
"""خالی کردن صف — فقط موارد باقیمانده (بدون بازگشت).
|
||||
|
||||
پیش از حذف، کلید یکتاسازیِ هر رکورد در دفتر ``SMSDedup`` تضمین میشود تا اسکنر
|
||||
در اجرای بعدی همان پیامکها را از نو نسازد. رکوردهایی که کلید ندارند (مثلاً
|
||||
پیامک دستی) کلید مشتق از خودشان را میگیرند.
|
||||
پیامکهای ارسالشده جزو صف نیستند و حذف نمیشوند؛ تاریخچهشان در «گزارش ارسال»
|
||||
لازم است. پیش از حذف، کلید یکتاسازیِ هر رکورد در دفتر ``SMSDedup`` تضمین میشود
|
||||
تا اسکنر در اجرای بعدی همان پیامکها را از نو نسازد.
|
||||
"""
|
||||
if request.method == "POST":
|
||||
rows = SMSQueue.objects.all()
|
||||
rows = SMSQueue.objects.exclude(status__in=DONE_STATUSES)
|
||||
keys = [k for k in rows.values_list("dedup_key", flat=True) if k]
|
||||
SMSDedup.objects.bulk_create(
|
||||
[SMSDedup(key=k) for k in keys], ignore_conflicts=True,
|
||||
|
||||
Reference in new issue
Block a user