improve entezar bazdid

This commit is contained in:
javad committed 2026-10-20 19:02:34 +03:30
1 parent 1fd27d0445
commit 2aee85593f
18 files changed
+10271 -7558

No files matched your search

+83 -60
View File
@@ -1,13 +1,19 @@
import datetime
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.shortcuts import render, redirect, get_object_or_404
from django.http import HttpResponse
from django.contrib import messages
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
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, normalize_finish_datetime,
refresh_last_bazdid, validate_action_payload)
@login_required(login_url='/panel/p_login/')
@@ -54,46 +60,68 @@ def customerNazar(request ,c_id):
data = {'customer': customer, 'actions': actions, 'allUsers': allUsers}
return render(request, 'admin_panel/customer/nazar.html', data)
def _parse_form_datetime(raw):
"""timestamp میلی‌ثانیه‌ای فرم → دیتاتایم aware (یا ``None``).
کد قبلی ``datetime.datetime.fromtimestamp`` (naive) می‌ساخت که هنگام ذخیره
با ``USE_TZ=True`` هشدار ``RuntimeWarning`` می‌داد؛ اینجا صریح aware می‌شود.
"""
try:
return as_aware(datetime.datetime.fromtimestamp(int(raw) / 1e3))
except (TypeError, ValueError, OverflowError, OSError):
return None
def _action_payload_from_request(request):
"""استخراج و نرمال‌سازی ورودی فرم اقدام (مشترک ایجاد و ویرایش)."""
is_done = bool(request.POST.get('is_done'))
return {
'actionType': (request.POST.get('actionType') or '').strip(),
'status': Action.Status.FINISH if is_done else Action.Status.WAITE,
'actionDateTime': _parse_form_datetime(request.POST.get('dateTimeAnjam')),
'finishDateTime': _parse_form_datetime(request.POST.get('FinishDateTime')),
'desc': request.POST.get('desc', None),
'result': request.POST.get('result', None),
'userID': request.POST.get('userID', None),
'is_done': is_done,
}
def _reject(request, error, redirect_to):
"""پیام فارسی خطا به کاربر و بازگشت بدون ذخیرهٔ چیزی."""
messages.error(request, '؛ '.join(error.messages))
return redirect(redirect_to)
@login_required(login_url='/panel/p_login/')
def customerNewAction(request ,c_id):
actionType = request.POST.get('actionType', None)
dateTimeAnjam = request.POST.get('dateTimeAnjam', None)
#return HttpResponse(float(dateTimeAnjam+".0"))
payload = _action_payload_from_request(request)
try:
dateTimeAnjam = datetime.datetime.fromtimestamp(int(dateTimeAnjam)/1e3)
except:
pass
desc = request.POST.get('desc', None)
userID = request.POST.get('userID', None)
is_done = request.POST.get('is_done', None)
result = request.POST.get('result', None)
FinishDateTime = request.POST.get('FinishDateTime', None)
try:
FinishDateTime = datetime.datetime.fromtimestamp(int(FinishDateTime)/1e3)
except:
pass
validate_action_payload(payload)
except ValidationError as error:
return _reject(request, error, '/panel/customer/' + str(c_id) + '/action')
customer_obj = Customer.objects.get(id=int(c_id))
newAction = Action()
newAction.actionType = actionType
newAction.actionDateTime = dateTimeAnjam
newAction.desc = desc
newAction.forUser = User.objects.get(id=int(userID))
newAction.actionType = payload['actionType']
newAction.actionDateTime = payload['actionDateTime']
newAction.desc = payload['desc']
newAction.forUser = User.objects.get(id=int(payload['userID']))
newAction.customer = customer_obj
if is_done:
newAction.status = 'finish'
newAction.finishDateTime = FinishDateTime
newAction.result = result
if payload['is_done']:
newAction.status = Action.Status.FINISH
# تاریخ اتمام خالی نباید None ذخیره شود (سابقه را پاک می‌کرد).
newAction.finishDateTime = normalize_finish_datetime(payload['finishDateTime'])
newAction.result = payload['result']
else:
newAction.status = 'waite'
newAction.status = Action.Status.WAITE
newAction.save()
if newAction.status == 'finish' and newAction.actionType == 'bazdid':
customer_obj.last_bazdid_dt = newAction.finishDateTime
customer_obj.save()
# last_bazdid_dt فقط از تابع واحد بازمحاسبه می‌شود؛ نه از finishDateTime.
refresh_last_bazdid(customer_obj, reason=REASON_CREATE, user=request.user)
# پیامک «بازدید شما برای تاریخ … ثبت شد» فقط برای اقدام از نوع بازدید.
if newAction.actionType == 'bazdid':
if newAction.actionType == Action.ActionType.BAZDID:
notify_visit_scheduled(customer_obj, newAction, created_by=request.user)
return redirect('/panel/customer/'+str(c_id)+'/action')
@@ -116,42 +144,37 @@ def ActionListUser(request, u_id):
@login_required(login_url='/panel/p_login/')
def customerEditAction(request ,c_id, action_id):
actionType = request.POST.get('actionType', None)
dateTimeAnjam = request.POST.get('dateTimeAnjam', None)
print("ok")
#return HttpResponse(float(dateTimeAnjam+".0"))
payload = _action_payload_from_request(request)
try:
dateTimeAnjam = datetime.datetime.fromtimestamp(int(dateTimeAnjam)/1e3)
except:
pass
desc = request.POST.get('desc', None)
userID = request.POST.get('userID', None)
is_done = request.POST.get('is_done', None)
result = request.POST.get('result', None)
FinishDateTime = request.POST.get('FinishDateTime', None)
try:
FinishDateTime = datetime.datetime.fromtimestamp(int(FinishDateTime)/1e3)
except:
pass
validate_action_payload(payload)
except ValidationError as error:
return _reject(
request, error,
request.META.get('HTTP_REFERER', '/panel/customer/' + str(c_id) + '/action'),
)
customer_obj = Customer.objects.get(id=int(c_id))
newAction = Action.objects.get(id=action_id)
newAction.actionType = actionType
newAction.actionDateTime = dateTimeAnjam
newAction.desc = desc
newAction.forUser = User.objects.get(id=int(userID))
was_done = newAction.status == Action.Status.FINISH
newAction.actionType = payload['actionType']
newAction.actionDateTime = payload['actionDateTime']
newAction.desc = payload['desc']
newAction.forUser = User.objects.get(id=int(payload['userID']))
newAction.customer = customer_obj
if is_done:
newAction.status = 'finish'
newAction.finishDateTime = FinishDateTime
newAction.result = result
if payload['is_done']:
newAction.status = Action.Status.FINISH
newAction.finishDateTime = normalize_finish_datetime(payload['finishDateTime'])
newAction.result = payload['result']
else:
newAction.status = 'waite'
newAction.status = Action.Status.WAITE
newAction.save()
if newAction.status == 'finish' and newAction.actionType == 'bazdid':
customer_obj.last_bazdid_dt = newAction.finishDateTime
customer_obj.save()
# بازمحاسبهٔ واحد: هم پس از ویرایش، هم وقتی رکورد از «انجام‌شده» برگشت می‌خورد
# (تا مقداری که این رکورد ساخته بود با منبع حقیقت هم‌راستا شود).
reason = REASON_CANCEL if (was_done and not payload['is_done']) else REASON_EDIT
refresh_last_bazdid(customer_obj, reason=reason, user=request.user)
#return redirect('/panel/customer/'+str(c_id)+'/action')
return redirect(request.META.get('HTTP_REFERER', 'redirect_if_referer_not_found'))
@@ -205,7 +228,7 @@ def ActionListUserToday(request, u_id):
@login_required(login_url='/panel/p_login/')
def rezayat(request):
statusRezayat = request.GET.get('status', 'waite')
now = datetime.datetime.now()
now = timezone.now()
# محاسبه بازه‌های زمانی ۳۰ روز و ۶۰ روز قبل
dt_30_days_ago = now - datetime.timedelta(days=30)
@@ -246,20 +269,20 @@ def rezayatEdit(request, ac_id):
# عدم موفقیت: تنظیم nextNazarDT برای ۳ روز بعد (nazarDT پر نمی‌شود تا نهایی نشود)
action.nazar = csDesc
action.grade = int(csGrade)
action.nextNazarDT = datetime.datetime.now() + datetime.timedelta(days=3)
action.nextNazarDT = timezone.now() + datetime.timedelta(days=3)
elif csGrade == '-2':
# کنسل کردن: پر کردن nazarDT برای خروج کامل از لیست کارهای در انتظار
action.nazar = csDesc
action.grade = int(csGrade)
action.nazarDT = datetime.datetime.now()
action.nazarDT = timezone.now()
action.nextNazarDT = None
else:
# ثبت امتیاز عادی
action.nazar = csDesc
action.grade = int(csGrade)
action.nazarDT = datetime.datetime.now()
action.nazarDT = timezone.now()
action.nextNazarDT = None
action.save()
+35 -34
View File
@@ -5,6 +5,7 @@ from django.db import models
from django.db.models import Sum, Q
from django.shortcuts import redirect, render, get_object_or_404
from django.core.paginator import Paginator
from django.utils import timezone
from django.views.decorators.csrf import csrf_exempt
from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required
@@ -17,6 +18,7 @@ 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 (last_visit_map, next_appointment_map, visit_due_at)
from ._helpers import logger, _parse_jalali_date, _parse_phone_rows
@@ -327,56 +329,55 @@ def customerEdit(request, c_id):
@login_required(login_url='/panel/p_login/')
def entezar(request):
a = datetime.datetime.now()
"""فهرست «منتظر بازدید».
منبع حقیقت تاریخ بازدید، ``visit_policy.last_visit_map`` است (نه ستون کش‌شدهٔ
``last_bazdid_dt``) تا این صفحه و اسکنر پیامک هرگز واگرا نشوند. دورهٔ بازدید هم
از همان قالب پیامک خوانده می‌شود. مشتری دارای «تعویق فعال» (اقدام هماهنگی
در انتظار با تاریخ آینده) از فهرست خارج و به صف خدمات پس از فروش سپرده می‌شود.
"""
searchVal = request.GET.get('searchVal', '')
searchOn = request.GET.get('searchOn', '')
searchStatus = True
full_path = request.get_full_path()
from django.db.models import Q
now = timezone.now()
due_cut = visit_due_at(now=now)
# مرز تاریخ به وقت محلی (نه UTC) تا صفحه و اسکنر پیامک یک جواب بدهند.
due_cut_date = due_cut.astimezone(timezone.get_current_timezone()).date()
bazdid = os.environ.get('bazdid', 9)
bazdid = int ( int(bazdid) * 4.3 )
dateTime6MonthAgo = a - datetime.timedelta(weeks=bazdid)
''' customers = Customer.objects.filter(name_family__isnull=False)
m6mbazdid = []
for customerObj in customers:
try:
actionAa = Action.objects.filter(customer=customerObj, actionType='bazdid', status='finish').last()
if actionAa.status == 'finish':
customerObj.last_bazdid_dt = actionAa.actionDateTime
customerObj.save()
if actionAa.actionDateTime.date() < dateTime6MonthAgo.date():
m6mbazdid.append(actionAa)
except:
pass '''
visits = last_visit_map(now=now)
appointments = next_appointment_map(now=now)
customers_objj = Customer.objects.filter(
laghv_bazdid=False, name_family__isnull=False,
installDate__lte=due_cut_date,
)
if searchOn == 'address':
print("addressaddressaddressaddressaddress")
customers_objj = Customer.objects.filter(Q(last_bazdid_dt__lte=dateTime6MonthAgo) | Q(last_bazdid_dt__isnull=True),
installDate__lte=dateTime6MonthAgo,
laghv_bazdid=False, name_family__isnull=False, address__contains=searchVal).order_by('last_bazdid_dt')
customers_objj = customers_objj.filter(address__contains=searchVal)
else:
searchStatus = False
customers_objj = Customer.objects.filter(Q(last_bazdid_dt__lte=dateTime6MonthAgo) | Q(last_bazdid_dt__isnull=True),
installDate__lte=dateTime6MonthAgo,
laghv_bazdid=False, name_family__isnull=False).order_by('last_bazdid_dt')
#customers_objj = Customer.objects.filter(name_family__isnull=False, last_bazdid_dt__lte=dateTime6MonthAgo)
# مشتریان عقب‌افتاده: بدون بازدید معتبر، یا بازدید معتبرشان پیش از مقطع دوره.
stale_ids = [
customer_id for customer_id in customers_objj.values_list('id', flat=True)
if visits.get(customer_id) is None or visits[customer_id] <= due_cut
]
customers_objj = customers_objj.filter(id__in=stale_ids)
# «قرار بعدی» (هماهنگی توافق‌شده یا بازدید برنامه‌ریزی‌شده) یعنی این مشتری
# در این فهرست جایی ندارد تا آن تاریخ.
pending_ids = [cid for cid in stale_ids if cid in appointments]
if pending_ids:
customers_objj = customers_objj.exclude(id__in=pending_ids)
customers_objj = customers_objj.order_by('last_bazdid_dt', 'id')
try:
entezar_bazdid = CacheTable.objects.get(key='9mahbazdid')
entezar_bazdid.value = Customer.objects.filter(
Q(last_bazdid_dt__lte=dateTime6MonthAgo) | Q(last_bazdid_dt__isnull=True),
installDate__lte=dateTime6MonthAgo,
laghv_bazdid=False, name_family__isnull=False).count()
entezar_bazdid.value = len(stale_ids) - len(pending_ids)
entezar_bazdid.save()
except:
except CacheTable.DoesNotExist:
pass
paginator = Paginator(customers_objj, 20) # Show 25 contacts per page.
+19 -4
View File
@@ -597,6 +597,10 @@ def sms_queue_delete(request, q_id):
if request.method == "POST":
if item.status in DONE_STATUSES:
return _notice(reverse("admin_panel:sms_queue"), "delete_done_blocked")
# تصمیم محصولی: حذف توسط مدیر = «کنسل همین دوره». برای همین کلید یکتاسازی
# پیش از حذف در دفتر ثبت می‌شود؛ وگرنه اسکنر در اجرای بعدی همان پیامک را
# دوباره می‌ساخت.
_remember_keys([item])
item.delete()
return _notice(reverse("admin_panel:sms_queue"), "deleted")
return redirect("admin_panel:sms_queue")
@@ -621,6 +625,7 @@ def sms_queue_bulk(request):
# تاریخچهٔ ارسال‌شده‌ها از طریق صف حذف نمی‌شود.
selected = selected.exclude(status__in=DONE_STATUSES)
count = selected.count()
_remember_keys(selected)
selected.delete()
code, extra = "bulk_deleted", {"count": count}
else:
@@ -629,6 +634,19 @@ def sms_queue_bulk(request):
return redirect("admin_panel:sms_queue")
def _remember_keys(rows):
"""کلیدهای یکتاسازیِ رکوردهای صف را پیش از حذف در دفتر ``SMSDedup`` ثبت می‌کند.
قرارداد: وقتی مدیر یادآوری را حذف می‌کند، یعنی «همین دوره کنسل شد»؛ وگرنه
اسکنر در اجرای بعدی همان پیامک را از نو می‌سازد و حذفِ مدیر بی‌اثر می‌شود.
"""
keys = [k for k in (row.dedup_key for row in rows) if k]
if keys:
SMSDedup.objects.bulk_create(
[SMSDedup(key=key) for key in keys], ignore_conflicts=True,
)
@login_required(login_url='/panel/p_login/')
@_manage_required
def sms_queue_empty(request):
@@ -640,10 +658,7 @@ def sms_queue_empty(request):
"""
if request.method == "POST":
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,
)
_remember_keys(rows)
count = rows.count()
rows.delete()
return _notice(reverse("admin_panel:sms_queue"), "queue_emptied", count=count)