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): """نوشتن/پاک‌کردن «قرار بعدی» بر اساس اقدام ثبت/ویرایش‌شده. قواعد: * فقط برای اقدام نوع «هماهنگی» معنا دارد. * اگر تاریخ قرار داده شده باشد ⇒ ``set_visit_hold`` (فقط رو به جلو). * اگر هماهنگی «انجام شد» تیک خورده و تاریخ قرار خالی است ⇒ پیش‌فرض ۱۴ روز. * اگر هماهنگی لغو شد (``cancell``) یا نوع عوض شد ⇒ قرار پاک می‌شود. """ if payload['actionType'] != Action.ActionType.HAMAHANGI: return raw = payload.get('holdRaw') if raw is not None: target = normalize_hold_until(raw) set_visit_hold(customer_obj, target) return if payload['status'] == Action.Status.FINISH: set_visit_hold(customer_obj, default_hold_until()) 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) # پیامک «بازدید شما برای تاریخ … ثبت شد» فقط برای اقدام از نوع بازدید. 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) # «قرار بعدی»: با ویرایش هم‌راستا می‌شود؛ اگر اقدام دیگر هماهنگی نباشد یا # هماهنگی لغو شود، قرار دستی پاک می‌شود. if payload['actionType'] == Action.ActionType.HAMAHANGI and newAction.status != Action.Status.CANCELL: _apply_visit_hold(customer_obj, payload) else: clear_visit_hold(customer_obj) #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('لطفا همه فیلد ها را پر کنید')