331 lines
16 KiB
Python
331 lines
16 KiB
Python
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, clear_visit_hold, default_hold_until,
|
|
normalize_finish_datetime, normalize_hold_until,
|
|
refresh_last_bazdid, set_visit_hold,
|
|
validate_action_payload)
|
|
|
|
|
|
@login_required(login_url='/panel/p_login/')
|
|
def customerAction(request, c_id):
|
|
# استفاده از get_object_or_404 برای امنیت بیشتر
|
|
customer = get_object_or_404(Customer, id=c_id)
|
|
|
|
# دریافت پارامترهای فیلتر از URL (پیشفرض: all)
|
|
filter_type = request.GET.get('actionType', 'all')
|
|
filter_status = request.GET.get('status', 'all')
|
|
|
|
# کوئری پایه برای اقدامات این مشتری
|
|
actions = Action.objects.filter(customer=customer)
|
|
|
|
# اعمال فیلتر بر اساس نوع اقدام (بازدید، نصب و ...)
|
|
if filter_type != 'all':
|
|
actions = actions.filter(actionType=filter_type)
|
|
|
|
# اعمال فیلتر بر اساس وضعیت (انجام شده، در انتظار و ...)
|
|
if filter_status != 'all':
|
|
actions = actions.filter(status=filter_status)
|
|
|
|
# مرتبسازی نزولی بر اساس تاریخ ایجاد
|
|
actions = actions.order_by("-createdDT")
|
|
|
|
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
|
|
|
|
data = {
|
|
'customer': customer,
|
|
'actions': actions,
|
|
'allUsers': allUsers,
|
|
# ارسال مقادیر فیلتر به قالب برای حفظ وضعیت انتخابشده در دراپداونها
|
|
'filter_type': filter_type,
|
|
'filter_status': filter_status
|
|
}
|
|
|
|
return render(request, 'admin_panel/customerAction.html', data)
|
|
|
|
@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")
|
|
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)
|
|
|
|
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')),
|
|
# فقط برای «هماهنگی» معنا دارد؛ تاریخِ «تا کِی پیگیری نشود».
|
|
'holdRaw': _parse_form_datetime(request.POST.get('visitHoldUntil')),
|
|
'desc': request.POST.get('desc', None),
|
|
'result': request.POST.get('result', None),
|
|
'userID': request.POST.get('userID', None),
|
|
'is_done': is_done,
|
|
}
|
|
|
|
|
|
def _apply_visit_hold(customer_obj, payload, *, status):
|
|
"""همراستاکردن «قرار بعدی» با اقدام ثبت/ویرایششده.
|
|
|
|
قواعد (یکسان برای ایجاد و ویرایش — قبلاً ایجاد این را انجام نمیداد):
|
|
|
|
* فقط برای اقدام نوع «هماهنگی» معنا دارد؛
|
|
- اگر تاریخ قرار داده شده باشد ⇒ ``set_visit_hold`` (فقط رو به جلو)
|
|
- اگر «انجام شد» تیک خورده و تاریخ خالی است ⇒ پیشفرض ۱۴ روز
|
|
- اگر هماهنگی لغو شده باشد ⇒ قرار پاک میشود
|
|
* **هر اقدام دیگری** (از جمله «بازدید») ⇒ قرار دستی پاک میشود؛ چون هدفِ
|
|
قرار (پیگیری در آینده) با انجام/برنامهریزی بازدید منتفی است.
|
|
"""
|
|
is_coordination = payload['actionType'] == Action.ActionType.HAMAHANGI
|
|
if not is_coordination or status == Action.Status.CANCELL:
|
|
return clear_visit_hold(customer_obj)
|
|
|
|
raw = payload.get('holdRaw')
|
|
if raw is not None:
|
|
set_visit_hold(customer_obj, normalize_hold_until(raw))
|
|
return True
|
|
if payload['status'] == Action.Status.FINISH:
|
|
set_visit_hold(customer_obj, default_hold_until())
|
|
return True
|
|
|
|
|
|
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):
|
|
payload = _action_payload_from_request(request)
|
|
try:
|
|
validate_action_payload(payload)
|
|
# اعتبارسنجی «قرار بعدی» پیش از هر نوشتنی (سقف ۶ ماه و تاریخ گذشته).
|
|
if payload['actionType'] == Action.ActionType.HAMAHANGI and payload['holdRaw']:
|
|
normalize_hold_until(payload['holdRaw'])
|
|
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 = payload['actionType']
|
|
newAction.actionDateTime = payload['actionDateTime']
|
|
newAction.desc = payload['desc']
|
|
newAction.forUser = User.objects.get(id=int(payload['userID']))
|
|
newAction.customer = customer_obj
|
|
if payload['is_done']:
|
|
newAction.status = Action.Status.FINISH
|
|
# تاریخ اتمام خالی نباید None ذخیره شود (سابقه را پاک میکرد).
|
|
newAction.finishDateTime = normalize_finish_datetime(payload['finishDateTime'])
|
|
newAction.result = payload['result']
|
|
else:
|
|
newAction.status = Action.Status.WAITE
|
|
newAction.save()
|
|
|
|
# last_bazdid_dt فقط از تابع واحد بازمحاسبه میشود؛ نه از finishDateTime.
|
|
refresh_last_bazdid(customer_obj, reason=REASON_CREATE, user=request.user)
|
|
# «قرار بعدی» فقط برای هماهنگی.
|
|
_apply_visit_hold(customer_obj, payload, status=newAction.status)
|
|
|
|
# پیامک «بازدید شما برای تاریخ … ثبت شد» فقط برای اقدام از نوع بازدید.
|
|
if newAction.actionType == Action.ActionType.BAZDID:
|
|
notify_visit_scheduled(customer_obj, newAction, created_by=request.user)
|
|
|
|
return redirect('/panel/customer/'+str(c_id)+'/action')
|
|
|
|
|
|
@login_required(login_url='/panel/p_login/')
|
|
def ActionList(request):
|
|
actions = Action.objects.filter(forUser__id=request.user.id, status='waite').order_by("-createdDT")
|
|
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
|
|
data = {'actions': actions, 'allUsers': allUsers}
|
|
return render(request, 'admin_panel/ActionList.html', data)
|
|
|
|
@login_required(login_url='/panel/p_login/')
|
|
def ActionListUser(request, u_id):
|
|
actions = Action.objects.filter(forUser__id=u_id, status='waite').order_by("-createdDT")
|
|
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
|
|
data = {'actions': actions, 'allUsers': allUsers}
|
|
return render(request, 'admin_panel/ActionList.html', data)
|
|
|
|
@login_required(login_url='/panel/p_login/')
|
|
def customerEditAction(request ,c_id, action_id):
|
|
|
|
payload = _action_payload_from_request(request)
|
|
try:
|
|
validate_action_payload(payload)
|
|
if payload['actionType'] == Action.ActionType.HAMAHANGI and payload['holdRaw']:
|
|
normalize_hold_until(payload['holdRaw'])
|
|
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)
|
|
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 payload['is_done']:
|
|
newAction.status = Action.Status.FINISH
|
|
newAction.finishDateTime = normalize_finish_datetime(payload['finishDateTime'])
|
|
newAction.result = payload['result']
|
|
else:
|
|
newAction.status = Action.Status.WAITE
|
|
newAction.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)
|
|
# «قرار بعدی»: با ویرایش همراستا میشود؛ اگر اقدام دیگر هماهنگی نباشد یا
|
|
# هماهنگی لغو شود، قرار دستی پاک میشود.
|
|
_apply_visit_hold(customer_obj, payload, status=newAction.status)
|
|
|
|
#return redirect('/panel/customer/'+str(c_id)+'/action')
|
|
return redirect(request.META.get('HTTP_REFERER', 'redirect_if_referer_not_found'))
|
|
|
|
|
|
@login_required(login_url='/panel/p_login/')
|
|
def ActionListAll(request):
|
|
actions = Action.objects.filter().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)
|
|
|
|
@login_required(login_url='/panel/p_login/')
|
|
def ActionListToday(request):
|
|
startTodayDT= datetime.datetime.today().strftime('%Y-%m-%d 00:00:00')
|
|
endTodayDT= datetime.datetime.today().strftime('%Y-%m-%d 23:59:59')
|
|
#startTodayDT = datetime.datetime.strptime(startTodayDT, '%Y-%m-%d-00:00:00')
|
|
#endTodayDT = datetime.datetime.strptime(endTodayDT, '%Y-%m-%d-23:59:59')
|
|
#return HttpResponse(todayDT)
|
|
actions = Action.objects.filter(actionDateTime__gte=startTodayDT,actionDateTime__lte=endTodayDT, forUser__id=request.user.id, status='waite').order_by("-actionDateTime")[:50]
|
|
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
|
|
data = {'actions': actions, 'allUsers': allUsers}
|
|
return render(request, 'admin_panel/ActionList.html', data)
|
|
|
|
@login_required(login_url='/panel/p_login/')
|
|
def ActionListUserToday(request, u_id):
|
|
dateTimeAction = request.GET.get('dateTime', None)
|
|
status = request.GET.get('status', None)
|
|
unixADT = dateTimeAction
|
|
if dateTimeAction:
|
|
dateTimeAction = datetime.datetime.fromtimestamp(int(dateTimeAction)/1e3)
|
|
startTodayDT = dateTimeAction.strftime('%Y-%m-%d 00:00:00')
|
|
endTodayDT= dateTimeAction.strftime('%Y-%m-%d 23:59:59')
|
|
else:
|
|
startTodayDT= datetime.datetime.today().strftime('%Y-%m-%d 00:00:00')
|
|
endTodayDT= datetime.datetime.today().strftime('%Y-%m-%d 23:59:59')
|
|
#startTodayDT = datetime.datetime.strptime(startTodayDT, '%Y-%m-%d-00:00:00')
|
|
#endTodayDT = datetime.datetime.strptime(endTodayDT, '%Y-%m-%d-23:59:59')
|
|
#return HttpResponse(todayDT)
|
|
if status == 'waite' or status == None or status == '' or status == ' ':
|
|
status = 'waite'
|
|
actions = Action.objects.filter(actionDateTime__gte=startTodayDT,actionDateTime__lte=endTodayDT, forUser__id=u_id, status=status).order_by("-actionDateTime")[:50]
|
|
actionPr = Action.objects.filter(actionDateTime__lt=startTodayDT, forUser__id=u_id, status=status).order_by("-actionDateTime")[:200]
|
|
elif status == 'finish':
|
|
actionPr = None
|
|
actions = Action.objects.filter(actionDateTime__gte=startTodayDT,actionDateTime__lte=endTodayDT, forUser__id=u_id, status=status).order_by("-actionDateTime")[:50]
|
|
|
|
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
|
|
data = {'actions': actions, 'allUsers': allUsers, 'u_id': u_id, "unixADT": unixADT, "status": status, 'actionPr': actionPr}
|
|
return render(request, 'admin_panel/ActionList.html', data)
|
|
|
|
@login_required(login_url='/panel/p_login/')
|
|
def rezayat(request):
|
|
statusRezayat = request.GET.get('status', 'waite')
|
|
now = timezone.now()
|
|
|
|
# محاسبه بازههای زمانی ۳۰ روز و ۶۰ روز قبل
|
|
dt_30_days_ago = now - datetime.timedelta(days=30)
|
|
dt_60_days_ago = now - datetime.timedelta(days=60)
|
|
|
|
if statusRezayat == 'finish':
|
|
# نمایش نظرسنجیهای تعیینتکلیف شده (ثبت شده یا کنسل شده)
|
|
actions = Action.objects.exclude(actionType='vosol').filter(
|
|
status='finish',
|
|
nazarDT__isnull=False
|
|
).order_by("-nazarDT")[:500]
|
|
else:
|
|
statusRezayat = 'waite'
|
|
# نمایش نظرسنجیهای در انتظار با اعمال شرط عدم انقضای زمان
|
|
actions = Action.objects.exclude(actionType='vosol').filter(
|
|
status='finish',
|
|
nazarDT__isnull=True, # نظرسنجی هنوز نهایی نشده
|
|
createdDT__gte=dt_60_days_ago # حداکثر ۶۰ روز از ایجاد گذشته باشد
|
|
).filter(
|
|
Q(finishDateTime__isnull=True) | Q(finishDateTime__gte=dt_30_days_ago) # حداکثر ۳۰ روز از اتمام گذشته باشد
|
|
).filter(
|
|
Q(nextNazarDT__isnull=True) | Q(nextNazarDT__lte=now) # موعد پیگیری رسیده یا تعویقی نداشته باشد
|
|
).order_by("-id")[:500]
|
|
|
|
data = {'actions': actions, "status": statusRezayat}
|
|
return render(request, 'admin_panel/customerRezayat.html', data)
|
|
|
|
|
|
@login_required(login_url='/panel/p_login/')
|
|
def rezayatEdit(request, ac_id):
|
|
csGrade = request.POST.get('csGrade', None)
|
|
csDesc = request.POST.get('csDesc', None)
|
|
|
|
if csDesc and csGrade:
|
|
action = get_object_or_404(Action, id=ac_id)
|
|
|
|
if csGrade == '-1':
|
|
# عدم موفقیت: تنظیم nextNazarDT برای ۳ روز بعد (nazarDT پر نمیشود تا نهایی نشود)
|
|
action.nazar = csDesc
|
|
action.grade = int(csGrade)
|
|
action.nextNazarDT = timezone.now() + datetime.timedelta(days=3)
|
|
|
|
elif csGrade == '-2':
|
|
# کنسل کردن: پر کردن nazarDT برای خروج کامل از لیست کارهای در انتظار
|
|
action.nazar = csDesc
|
|
action.grade = int(csGrade)
|
|
action.nazarDT = timezone.now()
|
|
action.nextNazarDT = None
|
|
|
|
else:
|
|
# ثبت امتیاز عادی
|
|
action.nazar = csDesc
|
|
action.grade = int(csGrade)
|
|
action.nazarDT = timezone.now()
|
|
action.nextNazarDT = None
|
|
|
|
action.save()
|
|
return redirect('/panel/rezayat/')
|
|
else:
|
|
return HttpResponse('لطفا همه فیلد ها را پر کنید')
|