improve bazdid 6

This commit is contained in:
javad committed 2026-10-22 14:01:08 +03:30
1 parent 99c25a8216
commit fa73a734a0
21 files changed
+32760 -7985

No files matched your search

+114 -26
View File
@@ -8,6 +8,7 @@ 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 persiantools.jdatetime import JalaliDate
from ..models import Action, Customer
from ..sms.triggers import notify_visit_scheduled
@@ -91,6 +92,8 @@ def _action_payload_from_request(request):
'finishDateTime': _parse_form_datetime(request.POST.get('FinishDateTime')),
# فقط برای «هماهنگی» معنا دارد؛ تاریخِ «تا کِی پیگیری نشود».
'holdRaw': _parse_form_datetime(request.POST.get('visitHoldUntil')),
# تأیید صریح کاربر برای «عقب بردن» تاریخ قرار دستی.
'force_hold': bool(request.POST.get('force_hold')),
'desc': request.POST.get('desc', None),
'result': request.POST.get('result', None),
'userID': request.POST.get('userID', None),
@@ -98,13 +101,13 @@ def _action_payload_from_request(request):
}
def _apply_visit_hold(customer_obj, payload, *, status, is_new):
def _apply_visit_hold(customer_obj, payload, *, status, is_new, force_hold=False):
"""هم‌راستاکردن «قرار بعدی» با اقدام ثبت/ویرایش‌شده.
قواعد:
* برای اقدام نوع «هماهنگی»:
- تاریخ قرار داده شده ⇒ ``set_visit_hold`` (فقط رو به جلو)
- تاریخ قرار داده شده ⇒ ``set_visit_hold`` (فقط رو به جلو، مگر تأیید کاربر)
- «انجام شد» تیک خورده و تاریخ خالی ⇒ پیش‌فرض ۱۴ روز
- لغو شده ⇒ (در ایجاد) پاک می‌شود
* برای **هر اقدام دیگری** (از جمله «بازدید»):
@@ -114,18 +117,77 @@ def _apply_visit_hold(customer_obj, payload, *, status, is_new):
هماهنگیِ *جدیدتر* آمده باشد؛ پس دست نمی‌زنیم. اعتبارسنجی
:func:`manual_holds_map` (شرط «آخرین فعالیت = هماهنگی») خودش تصمیم
می‌گیرد. بدون این تفکیک، ویرایش یک اقدام قدیمی قرار معتبر را پاک می‌کرد.
خروجی (دیکشنری، برای بازخورد شفاف به کاربر)::
{'ok': bool, 'reason': 'written'|'default'|'forward_only'|'cleared'|'',
'current': datetime|None, 'target': datetime|None}
``reason='forward_only'`` یعنی تاریخ درخواستی از قرار فعلی عقب‌تر است و
نوشته **نشد**؛ لایهٔ ویو باید همین را با تاریخ‌ها به کاربر نشان بدهد و اگر
خودش تأیید کرد، با ``force_hold=True`` دوباره صدا بزند.
"""
result = {'ok': True, 'reason': '', 'current': None, 'target': None,
'force_allowed': not is_new}
# «عقب‌بردن قرار» فقط در ویرایش مجاز است. در ایجادِ وظیفهٔ تازه، کاربر قصد
# اصلاح قرارِ موجود را ندارد و اجازه دادن به آن می‌تواند قولِ قبلیِ ثبت‌شده
# (شاید توسط کارشناس دیگر) را بی‌آنکه کسی بخواهد کوتاه کند.
force_hold = bool(force_hold) and not is_new
is_coordination = payload['actionType'] == Action.ActionType.HAMAHANGI
if not is_coordination or status == Action.Status.CANCELL:
return clear_visit_hold(customer_obj) if is_new else True
if is_new:
result['ok'] = bool(clear_visit_hold(customer_obj))
result['reason'] = 'cleared' if result['ok'] else ''
return result
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
target = normalize_hold_until(raw)
reason = 'written'
elif payload['status'] == Action.Status.FINISH:
target = default_hold_until()
reason = 'default'
else:
return result
current = as_aware(customer_obj.visit_hold_until)
result['current'] = current
result['target'] = target
written = set_visit_hold(customer_obj, target, force=force_hold)
result['ok'] = written
result['reason'] = reason if written else 'forward_only'
return result
def _jalali_day(value):
"""تاریخ شمسی روز (به وقت محلی) برای پیام‌های کاربر."""
if value is None:
return '—'
return JalaliDate(timezone.localtime(value)).strftime('%Y/%m/%d')
def _report_hold_result(request, result):
"""بازخورد شفافِ نتیجهٔ نوشتن «تا تاریخ پیگیری نشود» به کاربر.
قاعدهٔ «فقط رو به جلو» بی‌صدا نیست: اگر تاریخ درخواستی از قرار فعلی عقب‌تر
باشد، اینجا گفته می‌شود که ذخیره نشد و کاربر چطور می‌تواند خودش انجامش دهد.
"""
if result.get('reason') != 'forward_only':
return
current, target = result.get('current'), result.get('target')
tz = timezone.get_current_timezone()
if current is not None and target is not None \
and target.astimezone(tz).date() >= current.astimezone(tz).date():
# همان روز ⇒ تغییر واقعی‌ای لازم نیست؛ پیام دادن گمراه‌کننده است.
return
tail = ('برای ثبت، گزینهٔ «تاریخ قرار را عقب ببر» را تیک بزنید.'
if result.get('force_allowed')
else 'برای تغییر قرار، همان وظیفهٔ هماهنگی را ویرایش کنید.')
messages.warning(
request,
'تاریخ قرار ذخیره نشد؛ از قرار فعلی این مشتری (%s) عقب‌تر است. %s'
% (_jalali_day(current), tail)
)
def _reject(request, error, redirect_to):
@@ -164,7 +226,11 @@ def customerNewAction(request ,c_id):
# last_bazdid_dt فقط از تابع واحد بازمحاسبه می‌شود؛ نه از finishDateTime.
refresh_last_bazdid(customer_obj, reason=REASON_CREATE, user=request.user)
# «قرار بعدی» فقط برای هماهنگی.
_apply_visit_hold(customer_obj, payload, status=newAction.status, is_new=True)
hold_result = _apply_visit_hold(
customer_obj, payload, status=newAction.status, is_new=True,
force_hold=payload.get('force_hold', False),
)
_report_hold_result(request, hold_result)
# پیامک «بازدید شما برای تاریخ … ثبت شد» فقط برای اقدام از نوع بازدید.
if newAction.actionType == Action.ActionType.BAZDID:
@@ -225,7 +291,11 @@ def customerEditAction(request ,c_id, action_id):
# «قرار بعدی»: با ویرایش هم‌راستا می‌شود. برای اقدام غیرهماهنگی عمداً
# پاک نمی‌کنیم؛ رکورد ویرایش‌شده ممکن است قدیمی‌تر از یک هماهنگیِ جدیدتر
# باشد و پاک‌کردن، قرار معتبر را از بین ببرد.
_apply_visit_hold(customer_obj, payload, status=newAction.status, is_new=False)
hold_result = _apply_visit_hold(
customer_obj, payload, status=newAction.status, is_new=False,
force_hold=payload.get('force_hold', False),
)
_report_hold_result(request, hold_result)
#return redirect('/panel/customer/'+str(c_id)+'/action')
return redirect(request.META.get('HTTP_REFERER', 'redirect_if_referer_not_found'))
@@ -272,14 +342,35 @@ def ActionListAll(request):
data = {'actions': actions, 'allUsers': allUsers}
return render(request, 'admin_panel/ActionList.html', data)
def _local_day_range(day=None):
"""بازهٔ ``[آغاز روز، پایان روز]`` به وقت محلی و aware.
قبلاً این دو ویو رشته‌های naive می‌ساختند (``'%Y-%m-%d 00:00:00'``) و Django
آن‌ها را با منطقهٔ زمانی پیش‌فرض تفسیر می‌کرد؛ هر تغییر ساعت/تایم‌زون باعث
جابه‌جایی مرز «وظایف امروز» می‌شد. حالا مرز صریح و aware است.
در روزهای تغییر ساعت (مثلاً ۲۲ مارس در تهران) نیمه‌شب ممکن است وجود نداشته
باشد؛ آن حالت با ``is_dst=False`` پوشش داده می‌شود تا یک تاریخ مرزی، صفحهٔ
وظایف را نشکند (همان کاری که ``visit_policy.as_aware`` می‌کند).
"""
day = day or timezone.localdate()
bounds = []
for moment in (datetime.datetime.combine(day, datetime.time.min),
datetime.datetime.combine(day, datetime.time.max)):
try:
bounds.append(timezone.make_aware(moment))
except Exception:
bounds.append(timezone.make_aware(moment, is_dst=False))
return bounds[0], bounds[1]
@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]
startTodayDT, endTodayDT = _local_day_range()
actions = Action.objects.filter(
actionDateTime__range=(startTodayDT, 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)
@@ -290,22 +381,19 @@ def ActionListUserToday(request, u_id):
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')
# timestamp میلی‌ثانیه‌ای GET → تاریخ محلی (نه naive UTC).
moment = as_aware(datetime.datetime.fromtimestamp(int(dateTimeAction) / 1e3))
day = moment.astimezone(timezone.get_current_timezone()).date()
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)
day = timezone.localdate()
startTodayDT, endTodayDT = _local_day_range(day)
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]
actions = Action.objects.filter(actionDateTime__range=(startTodayDT, 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]
actions = Action.objects.filter(actionDateTime__range=(startTodayDT, 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}
+14 -5
View File
@@ -628,7 +628,8 @@ def entezar(request):
entry = sources.get(cid) or {}
if not entry:
return 0
if len(entry) > 1:
days = {value.astimezone(current_tz).date() for value in entry.values()}
if len(days) > 1:
return 4
if SOURCE_MANUAL in entry:
return 1
@@ -699,8 +700,10 @@ def entezar(request):
try:
entezar_bazdid = CacheTable.objects.get(key='9mahbazdid')
# کارت داشبورد = «کارِ فوری» = تعیین تکلیف نشده.
entezar_bazdid.value = counts['uncoordinated']
entezar_bazdid.save()
# فقط در صورت تغییر می‌نویسیم تا یک GET ساده بی‌دلیل دیتابیس را تغییر ندهد.
if str(entezar_bazdid.value) != str(counts['uncoordinated']):
entezar_bazdid.value = str(counts['uncoordinated'])
entezar_bazdid.save(update_fields=['value'])
except CacheTable.DoesNotExist:
pass
@@ -812,14 +815,20 @@ def entezar(request):
entry.update(followup_entry)
flags = [key for key in (SOURCE_MANUAL, SOURCE_SCHEDULED,
SOURCE_COORDINATION) if key in entry]
# «N قرار» = تعداد **روزهای یکتای** قرار، نه تعداد منابع. یک هماهنگی
# که تاریخ تعویقش همان روز است، هم‌زمان «manual» و «coordination»
# تولید می‌کند ولی دو قرارِ متفاوت نیست (برچسب «۲ قرار» گمراه‌کننده بود).
appointment_days = {
entry[key].astimezone(current_tz).date() for key in flags
}
appointment_rows[customer.id] = {
'manual': entry.get(SOURCE_MANUAL),
'scheduled': entry.get(SOURCE_SCHEDULED),
'coordination': entry.get(SOURCE_COORDINATION),
'overdue_days': overdue_days.get(customer.id, 0),
'last_visit': visits.get(customer.id),
# تعداد منابع (۰ تا ۳) — قالب بر اساس آن برچسب وضعیت را می‌سازد.
'source_count': len(flags),
# تعداد قرارهای یکتا (۱ تا ۳ روز متمایز) — قالب بر اساس آن برچسب می‌سازد.
'source_count': len(appointment_days),
# تاریخ پیگیری این نمای خاص (برای ستون اختصاصی نمای «امروز»).
'followup_at': (min(followup_entry.values())
if followup_entry else None),