965 lines
47 KiB
Python
965 lines
47 KiB
Python
import datetime
|
|
import os
|
|
|
|
from django.db import models
|
|
from django.db.models import Sum, Q, Case, When, Value
|
|
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
|
|
from persiantools.jdatetime import JalaliDate
|
|
|
|
from ..models import (Customer, PhoneNumber, ProductCategory, Product, Factor,
|
|
FactorIteam, Article, Sarresid, Action, CacheTable)
|
|
from ..sms.balance import customer_balance
|
|
from ..sms.phones import (normalize_primary_values,
|
|
save_primary_normalized)
|
|
from ..sms.triggers import notify_payment_thanks, notify_welcome
|
|
from ..visit_policy import (SOURCE_COORDINATION, SOURCE_MANUAL, SOURCE_SCHEDULED,
|
|
appointment_sources_map, as_aware,
|
|
followups_on_date, last_visit_map, overdue_followups,
|
|
visit_due_at, visit_due_date, visit_period_months)
|
|
|
|
# نماهای صفحهٔ «انتظار» (فیلتر ?view=).
|
|
# پیشفرض = «نیازمند پیگیری امروز»: فهرست کاریِ روزانهٔ کارشناس (موعد تماسهای
|
|
# امروز). سایر نماها با کارتها/دراپداون «نوع قرار» در دسترساند.
|
|
# ``today`` یک نمای **مستقل** است: پایهٔ آن «عقبافتاده بودن» نیست — مشتری
|
|
# سالمی هم که با او قرار تماس گذاشته شده باید در این فهرست دیده شود.
|
|
ENTEZAR_VIEWS = ('today', 'uncoordinated', 'manual', 'scheduled', 'coordination',
|
|
'coordinated', 'all')
|
|
ENTEZAR_DEFAULT_VIEW = 'today'
|
|
# نمای «نیازمند پیگیری» — برای بررسی «آیا این نما فعال است؟» در قالب/ویو.
|
|
ENTEZAR_FOLLOWUP_VIEW = 'today'
|
|
|
|
# تعداد ردیف در هر صفحه. فهرست کاری روزانه با یک نگاه مرور میشود، پس صفحهبندی
|
|
# دیرتر فعال شود.
|
|
ENTEZAR_PAGE_SIZE = 100
|
|
|
|
# فیلتر «میزان تأخیر» (?delay=). مرزها با «مدت تأخیر» (روز) مقایسه میشوند.
|
|
ENTEZAR_DELAYS = ('lt30', '30to90', '90to180', 'gt180')
|
|
|
|
# ستونهای قابل مرتبسازی: نام فیلد ⇒ جهت پیشفرضِ اولین کلیک.
|
|
# ``oldest``/``newest``/``overdue`` مقادیر قدیمیاند و برای سازگاری با
|
|
# بوکمارکهای قبلی نگه داشته شدهاند (به ``last_visit``/``overdue`` نگاشت میشوند).
|
|
ENTEZAR_SORT_COLUMNS = {
|
|
'name': 'asc',
|
|
'status': 'asc',
|
|
'manual': 'desc',
|
|
'scheduled': 'desc',
|
|
'coordination': 'desc',
|
|
'last_visit': 'asc',
|
|
'overdue': 'desc',
|
|
'phone': 'asc',
|
|
'address': 'asc',
|
|
}
|
|
_ENTEZAR_LEGACY_SORTS = {
|
|
'oldest': ('last_visit', 'asc'),
|
|
'newest': ('last_visit', 'desc'),
|
|
'overdue': ('overdue', 'desc'),
|
|
}
|
|
from ._helpers import logger, _parse_jalali_date, _parse_phone_rows
|
|
|
|
|
|
@csrf_exempt
|
|
@login_required(login_url='/panel/p_login/')
|
|
def customers(request):
|
|
searchVal = request.GET.get('searchVal', '')
|
|
searchOn = request.GET.get('searchOn', '')
|
|
searchStatus = True
|
|
full_path = request.get_full_path()
|
|
|
|
# مشتریان غیرفعال هرگز نمایش داده نمیشوند — نه در فهرست کامل و نه در جستجو.
|
|
all_customers = Customer.objects.filter(is_active=True)
|
|
if searchOn == 'code':
|
|
all_customers = all_customers.filter(idCode__contains=searchVal)
|
|
elif searchOn == 'phone':
|
|
all_customers = all_customers.filter(Q(phone1__contains=searchVal) | Q(phone2__contains=searchVal) | Q(phone3__contains=searchVal))
|
|
elif searchOn == 'name':
|
|
all_customers = all_customers.filter(name_family__contains=searchVal)
|
|
elif searchOn == 'address':
|
|
all_customers = all_customers.filter(address__contains=searchVal)
|
|
elif searchOn == 'laghv':
|
|
all_customers = all_customers.filter(laghv_bazdid=True)
|
|
else:
|
|
searchStatus = False
|
|
all_customers = all_customers.order_by('-create_date')
|
|
|
|
paginator = Paginator(all_customers, 25) # Show 25 contacts per page.
|
|
page_number = request.GET.get('page', None)
|
|
all_customers = paginator.get_page(page_number)
|
|
|
|
|
|
data = {'customersD': all_customers, 'searchStatus': searchStatus, 'full_path': full_path, 'searchVal': searchVal, 'searchOn':searchOn}
|
|
return render(request, 'admin_panel/customers.html', data)
|
|
|
|
|
|
@login_required(login_url='/panel/p_login/')
|
|
def customersSearch(request, searchVal,searchOn):
|
|
searchStatus = True
|
|
|
|
# همان قاعدهٔ فهرست اصلی: مشتری غیرفعال در هیچ نتیجهای نمایش داده نمیشود.
|
|
all_customers = Customer.objects.filter(is_active=True)
|
|
if searchOn == 'code':
|
|
all_customers = all_customers.filter(idCode__contains=searchVal)
|
|
elif searchOn == 'phone':
|
|
all_customers = all_customers.filter(phone1__contains=searchVal)
|
|
elif searchOn == 'name':
|
|
all_customers = all_customers.filter(name_family__contains=searchVal)
|
|
elif searchOn == 'address':
|
|
all_customers = all_customers.filter(address__contains=searchVal)
|
|
else:
|
|
searchStatus = False
|
|
all_customers = all_customers.order_by('-create_date')
|
|
|
|
paginator = Paginator(all_customers, 25) # Show 25 contacts per page.
|
|
page_number = request.GET.get('page', None)
|
|
all_customers = paginator.get_page(page_number)
|
|
|
|
|
|
data = {'customersD': all_customers, 'searchStatus': searchStatus}
|
|
return render(request, 'admin_panel/customers.html', data)
|
|
|
|
@login_required(login_url='/panel/p_login/')
|
|
def customer(request ,c_id):
|
|
customer = Customer.objects.get(id=c_id)
|
|
categorys = ProductCategory.objects.all()
|
|
factorha = Factor.objects.filter(customer__id = c_id)
|
|
#all_prd = serializers.serialize("json", all_prda)
|
|
#return JsonResponse(all_prd,safe = False)
|
|
dateNow = datetime.datetime.now().date
|
|
data = {'customer': customer, 'categorys': categorys, 'dateNow': dateNow, 'factorha': factorha}
|
|
return render(request, 'admin_panel/customerID.html', data)
|
|
|
|
@login_required(login_url='/panel/p_login/')
|
|
def customerNew(request ,c_id):
|
|
customer = Customer.objects.get(id=c_id)
|
|
categorys = ProductCategory.objects.all()
|
|
factorha = Factor.objects.filter(customer__id = c_id)
|
|
#all_prd = serializers.serialize("json", all_prda)
|
|
#return JsonResponse(all_prd,safe = False)
|
|
dateNow = datetime.datetime.now().date
|
|
data = {'customer': customer, 'categorys': categorys, 'dateNow': dateNow, 'factorha': factorha}
|
|
return render(request, 'admin_panel/customerNew.html', data)
|
|
|
|
|
|
@login_required(login_url='/panel/p_login/')
|
|
def customerNew2(request ,c_id):
|
|
customer = get_object_or_404(Customer, id=c_id, is_active=True)
|
|
categorys = ProductCategory.objects.all()
|
|
factorha = Factor.objects.filter(customer__id = c_id)
|
|
|
|
debit = Article.objects.filter(customer=customer).aggregate(Sum('debit'))['debit__sum']
|
|
credit = Article.objects.filter(customer=customer).aggregate(Sum('credit'))['credit__sum']
|
|
mandehesabCs = 0
|
|
try:
|
|
if (debit - credit) < 0:
|
|
mandehesabCs = abs(debit - credit)
|
|
except:
|
|
pass
|
|
|
|
|
|
#all_prd = serializers.serialize("json", all_prda)
|
|
#return JsonResponse(all_prd,safe = False)
|
|
dateNow = datetime.datetime.now().date
|
|
data = {
|
|
'customer': customer, 'categorys': categorys, 'dateNow': dateNow,
|
|
'factorha': factorha, 'mandehesabCs': mandehesabCs,
|
|
}
|
|
return render(request, 'admin_panel/customerNew2.html', data)
|
|
|
|
|
|
@login_required(login_url='/panel/p_login/')
|
|
def customerFactors(request, c_id):
|
|
customer = get_object_or_404(Customer, id=c_id)
|
|
categorys = ProductCategory.objects.all()
|
|
|
|
# کوئری پایه برای دریافت همه فاکتورهای این مشتری
|
|
factor_qs = Factor.objects.filter(customer=customer)
|
|
|
|
# دریافت پارامترهای فیلتر و جستجو از آدرس URL
|
|
search_query = request.GET.get('search_factor', '').strip()
|
|
filter_status = request.GET.get('status', 'all')
|
|
filter_type = request.GET.get('type', 'all')
|
|
az_tarikh = request.GET.get('az_tarikh', '').strip()
|
|
ta_tarikh = request.GET.get('ta_tarikh', '').strip()
|
|
|
|
# ۱. اعمال فیلتر جستجو (شناسه فاکتور یا متن توضیحات)
|
|
if search_query:
|
|
if search_query.isdigit():
|
|
factor_qs = factor_qs.filter(models.Q(id=int(search_query)) | models.Q(desc__icontains=search_query))
|
|
else:
|
|
factor_qs = factor_qs.filter(models.Q(desc__icontains=search_query))
|
|
|
|
# ۲. اعمال فیلتر وضعیت پرداخت و حذف نرم
|
|
if filter_status != 'all':
|
|
if filter_status == 'deleted':
|
|
factor_qs = factor_qs.filter(is_active=False)
|
|
else:
|
|
factor_qs = factor_qs.filter(pay_status=filter_status, is_active=True)
|
|
|
|
# ۳. اعمال فیلتر نوع فاکتور (نصب، خدمات، گارانتی)
|
|
if filter_type != 'all':
|
|
factor_qs = factor_qs.filter(FactorType=filter_type)
|
|
|
|
# ۴. اعمال فیلتر تاریخ شروع (از تاریخ)
|
|
if az_tarikh:
|
|
az_gregorian = _parse_jalali_date(az_tarikh)
|
|
if az_gregorian:
|
|
az_datetime = datetime.datetime.combine(az_gregorian, datetime.time.min)
|
|
factor_qs = factor_qs.filter(create_date__gte=az_datetime)
|
|
|
|
# ۵. اعمال فیلتر تاریخ پایان (تا تاریخ)
|
|
if ta_tarikh:
|
|
ta_gregorian = _parse_jalali_date(ta_tarikh)
|
|
if ta_gregorian:
|
|
ta_datetime = datetime.datetime.combine(ta_gregorian, datetime.time.max)
|
|
factor_qs = factor_qs.filter(create_date__lte=ta_datetime)
|
|
|
|
# مرتبسازی بر اساس آخرین تاریخ ایجاد
|
|
factorha = factor_qs.order_by('-create_date')
|
|
|
|
dateNow = datetime.datetime.now().date
|
|
|
|
data = {
|
|
'customer': customer,
|
|
'categorys': categorys,
|
|
'dateNow': dateNow,
|
|
'factorha': factorha,
|
|
'search_query': search_query,
|
|
'filter_status': filter_status,
|
|
'filter_type': filter_type,
|
|
'az_tarikh': az_tarikh,
|
|
'ta_tarikh': ta_tarikh
|
|
}
|
|
return render(request, 'admin_panel/customerIDFactor.html', data)
|
|
|
|
@login_required(login_url='/panel/p_login/')
|
|
def customerFactor(request ,c_id, f_id):
|
|
customer = Customer.objects.get(id=c_id)
|
|
categorys = ProductCategory.objects.all()
|
|
factorO = Factor.objects.get(customer=customer, id=f_id)
|
|
fiteam = FactorIteam.objects.filter(factor=factorO)
|
|
#all_prd = serializers.serialize("json", all_prda)
|
|
#return JsonResponse(all_prd,safe = False)
|
|
dateNow = datetime.datetime.now().date
|
|
data = {'customer': customer, 'categorys': categorys, 'dateNow': dateNow, 'factorO': factorO, 'fiteam': fiteam}
|
|
return render(request, 'admin_panel/customerFactor.html', data)
|
|
|
|
|
|
def productsJson(request):
|
|
from django.core import serializers
|
|
from django.http import JsonResponse
|
|
all_prd = Product.objects.all()
|
|
all_prd = serializers.serialize("json", all_prd)
|
|
return JsonResponse(all_prd,safe = False)
|
|
|
|
def ActionJson(request, action_id):
|
|
from django.core import serializers
|
|
from django.http import JsonResponse
|
|
AcJson = Action.objects.filter(id=action_id)
|
|
AcJson = serializers.serialize("json", AcJson)
|
|
return JsonResponse(AcJson,safe = False)
|
|
|
|
def CustomerJson(request, c_id):
|
|
from django.core import serializers
|
|
from django.http import JsonResponse
|
|
customerJson = Customer.objects.filter(id=c_id)
|
|
customerJson = serializers.serialize("json", customerJson)
|
|
return JsonResponse(customerJson,safe = False)
|
|
|
|
|
|
@login_required(login_url='/panel/p_login/')
|
|
def customersAdd(request):
|
|
an_name = request.POST.get('an_name', '')
|
|
an_address = request.POST.get('an_address', '')
|
|
installDate = request.POST.get('installDate', '')
|
|
idnCode = request.POST.get('idnCode', '')
|
|
|
|
# آرایههای عنوان/شمارهٔ فرم + ایندکس ردیفهای علامتخورده بهعنوان «اصلی»
|
|
valid_phones, phones_list, primary_indices = _parse_phone_rows(request)
|
|
|
|
year, month, day = map(int, installDate.split('/'))
|
|
installDate_gregorian = JalaliDate(year, month, day).to_gregorian()
|
|
|
|
newCs = Customer()
|
|
newCs.name_family = an_name
|
|
newCs.address = an_address
|
|
newCs.installDate = installDate_gregorian
|
|
newCs.idCode = idnCode
|
|
|
|
# حفظ سازگاری با کدهای قدیمی (ریختن ۳ شماره اول در فیلدهای قدیمی)
|
|
newCs.phone1 = valid_phones[0]['phone'] if len(valid_phones) > 0 else ''
|
|
newCs.phone2 = valid_phones[1]['phone'] if len(valid_phones) > 1 else ''
|
|
newCs.phone3 = valid_phones[2]['phone'] if len(valid_phones) > 2 else ''
|
|
# شمارههای «اصلی» پیامک، بر مبنای مقدارِ نرمالشدهٔ خودِ شمارهها (پایدار در بازسازیها).
|
|
newCs.sms_primary_phones = normalize_primary_values(
|
|
phones_list[i] for i in primary_indices if i < len(phones_list)
|
|
)
|
|
newCs.save()
|
|
|
|
# ذخیره شمارهها به صورت نامحدود در جدول PhoneNumber
|
|
for item in valid_phones:
|
|
PhoneNumber.objects.create(
|
|
customer=newCs, title=item['title'], phone_number=item['phone'],
|
|
)
|
|
|
|
# پیامک خوشآمدگویی (در صورت فعال بودن قالب و تنظیم مشتری)
|
|
notify_welcome(newCs, created_by=request.user)
|
|
|
|
return redirect('/panel/customers/')
|
|
|
|
|
|
@login_required(login_url='/panel/p_login/')
|
|
def customerEdit(request, c_id):
|
|
# این مسیر فقط endpoint فرم ویرایش است؛ دسترسی مستقیم با GET نباید ۵۰۰ بدهد.
|
|
if request.method != 'POST':
|
|
return redirect('/panel/customer/' + str(c_id))
|
|
|
|
an_name = request.POST.get('an_name', '')
|
|
an_address = request.POST.get('an_address', '')
|
|
installDate = request.POST.get('installDate', '')
|
|
idnCode = request.POST.get('idnCode', '')
|
|
laghv_bazdid = request.POST.get('laghv_bazdid', False)
|
|
|
|
# آرایههای عنوان/شمارهٔ فرم + ایندکس ردیفهای علامتخورده بهعنوان «اصلی»
|
|
valid_phones, phones_list, primary_indices = _parse_phone_rows(request)
|
|
|
|
year, month, day = map(int, installDate.split('/'))
|
|
installDate_gregorian = JalaliDate(year, month, day).to_gregorian()
|
|
|
|
newCs = get_object_or_404(Customer, id=c_id)
|
|
newCs.name_family = an_name
|
|
newCs.address = an_address
|
|
newCs.installDate = installDate_gregorian
|
|
newCs.idCode = idnCode
|
|
newCs.laghv_bazdid = True if laghv_bazdid == 'true' else False
|
|
# فقط وقتی فرم فیلد توضیحات را فرستاده باشد مقدارش عوض میشود؛ فرمهای قدیمی
|
|
# که این فیلد را ندارند نباید یادداشتهای پاکسازی را پاک کنند.
|
|
if 'notes' in request.POST:
|
|
newCs.notes = request.POST.get('notes', '')
|
|
|
|
# آپدیت فیلدهای قدیمی جهت حفظ سازگاری (تنها وقتی شمارهای ارسال شده باشد)
|
|
if valid_phones:
|
|
newCs.phone1 = valid_phones[0]['phone'] if len(valid_phones) > 0 else ''
|
|
newCs.phone2 = valid_phones[1]['phone'] if len(valid_phones) > 1 else ''
|
|
newCs.phone3 = valid_phones[2]['phone'] if len(valid_phones) > 2 else ''
|
|
newCs.save()
|
|
|
|
# بروزرسانی جدول شمارهها فقط در صورت ارسال شمارههای جدید.
|
|
# اگر فرم شمارهای نفرستاده باشد (فیلدهای غیرفعال)، شمارههای فعلی دستنخورده میمانند.
|
|
if valid_phones:
|
|
PhoneNumber.objects.filter(customer=newCs).delete()
|
|
for item in valid_phones:
|
|
PhoneNumber.objects.create(
|
|
customer=newCs, title=item['title'], phone_number=item['phone'],
|
|
)
|
|
|
|
# اگر فرم ویرایش ردیفهای «اصلی» را مشخص کرده باشد، همانها ذخیره میشوند
|
|
# (مبتنی بر خودِ شماره، نه ایندکس رکورد).
|
|
if primary_indices:
|
|
save_primary_normalized(
|
|
newCs, [phones_list[i] for i in primary_indices if i < len(phones_list)],
|
|
)
|
|
|
|
return redirect('/panel/customer/'+str(c_id))
|
|
|
|
@login_required(login_url='/panel/p_login/')
|
|
def entezar(request):
|
|
"""فهرست «منتظر بازدید» + تفکیک بر اساس **نوع قرار**.
|
|
|
|
منبع حقیقت تاریخ بازدید ``visit_policy.last_visit_map`` است و منبع قرارها
|
|
``appointment_sources_map`` که سه منبع مستقل دارد:
|
|
|
|
* ``manual`` — قرار دستی روی مشتری (تعویق از فرم هماهنگی)
|
|
* ``scheduled`` — اقدام «بازدید» با تاریخ آینده
|
|
* ``coordination`` — اقدام «هماهنگی» با تاریخ پیگیری در آینده
|
|
|
|
هر فیلتر فقط منبع خودش را نشان میدهد (یک مشتری میتواند چند منبع داشته
|
|
باشد و در چند فیلتر دیده شود). فیلتر با ``?view=`` و قابل ترکیب با جستوجو.
|
|
"""
|
|
searchVal = request.GET.get('searchVal', '')
|
|
searchOn = request.GET.get('searchOn', '')
|
|
view = (request.GET.get('view') or ENTEZAR_DEFAULT_VIEW).strip().lower()
|
|
if view not in ENTEZAR_VIEWS:
|
|
view = ENTEZAR_DEFAULT_VIEW # مقدار نامعتبر ⇒ پیشفرض، بدون خطا
|
|
delay = (request.GET.get('delay') or '').strip().lower()
|
|
if delay not in ENTEZAR_DELAYS:
|
|
delay = '' # '' یعنی بدون فیلتر تأخیر
|
|
|
|
# مرتبسازی: ``?sort=<ستون>&dir=asc|desc``؛ مقادیر قدیمی هم پذیرفته میشوند.
|
|
# نکته: اگر ``dir`` صریح آمده باشد، مقدم است — وگرنه کلیک روی سرستون
|
|
# «تأخیر» با جهت صعودی، بهخاطر نگاشت قدیمی، نادیده گرفته میشد.
|
|
raw_sort = (request.GET.get('sort') or '').strip().lower()
|
|
raw_dir = (request.GET.get('dir') or '').strip().lower()
|
|
if raw_sort in ENTEZAR_SORT_COLUMNS:
|
|
sort_field = raw_sort
|
|
if raw_dir in ('asc', 'desc'):
|
|
sort_dir = raw_dir
|
|
else:
|
|
sort_dir = 'desc' if raw_sort in _ENTEZAR_LEGACY_SORTS else \
|
|
ENTEZAR_SORT_COLUMNS[raw_sort]
|
|
elif raw_sort in _ENTEZAR_LEGACY_SORTS:
|
|
sort_field, sort_dir = _ENTEZAR_LEGACY_SORTS[raw_sort]
|
|
else:
|
|
sort_field, sort_dir = _ENTEZAR_LEGACY_SORTS['oldest']
|
|
searchStatus = True
|
|
full_path = request.get_full_path()
|
|
|
|
now = timezone.now()
|
|
due_cut = visit_due_at(now=now)
|
|
# دوره فقط یک بار خوانده میشود (جلوگیری از N+1 در حلقهٔ محاسبهٔ تأخیر).
|
|
period_months = visit_period_months()
|
|
# مرز تاریخ به وقت محلی (نه UTC) تا صفحه و اسکنر پیامک یک جواب بدهند.
|
|
current_tz = timezone.get_current_timezone()
|
|
today_date = now.astimezone(current_tz).date()
|
|
due_cut_date = due_cut.astimezone(current_tz).date()
|
|
|
|
# تاریخ نمای «نیازمند پیگیری»: از تقویم شمسی میآید (``?fdate=1405/08/29``).
|
|
# خالی/نامعتبر ⇒ امروز. مقدار گذشته و آینده هر دو مجازند.
|
|
followup_date_raw = (request.GET.get('fdate') or '').strip()
|
|
followup_date = _parse_jalali_date(followup_date_raw) or today_date
|
|
followup_date_jalali = JalaliDate(followup_date).strftime('%Y/%m/%d')
|
|
is_followup_view = view == ENTEZAR_FOLLOWUP_VIEW
|
|
|
|
visits = last_visit_map(now=now)
|
|
sources = appointment_sources_map(now=now)
|
|
|
|
# جستوجو: نام، آدرس، تلفن (همه شمارهها) و کد شناسایی.
|
|
def _apply_search(queryset):
|
|
"""فیلتر جستوجو؛ فیلد نامعتبر ⇒ بدون فیلتر (سازگاری با رفتار قبلی)."""
|
|
if not searchVal:
|
|
return queryset
|
|
if searchOn == 'address':
|
|
return queryset.filter(address__contains=searchVal)
|
|
if searchOn == 'name':
|
|
return queryset.filter(name_family__contains=searchVal)
|
|
if searchOn == 'phone':
|
|
return queryset.filter(
|
|
Q(phone1__contains=searchVal) | Q(phone2__contains=searchVal)
|
|
| Q(phone3__contains=searchVal)
|
|
)
|
|
if searchOn == 'code':
|
|
return queryset.filter(idCode__contains=searchVal)
|
|
return queryset
|
|
|
|
searchStatus = bool(searchVal) and searchOn in (
|
|
'address', 'name', 'phone', 'code')
|
|
|
|
# پایهٔ فهرست انتظار: فقط مشتریانی که دورهشان گذشته (کارتهای آمار).
|
|
customers_objj = Customer.objects.filter(
|
|
laghv_bazdid=False, name_family__isnull=False,
|
|
installDate__lte=due_cut_date,
|
|
)
|
|
customers_objj = _apply_search(customers_objj)
|
|
|
|
# مشتریان عقبافتاده: بدون بازدید معتبر، یا بازدید معتبرشان پیش از مقطع دوره.
|
|
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
|
|
]
|
|
|
|
# تفکیک بر اساس **منبعِ** قرار: هر مشتری میتواند صفر تا سه منبع داشته باشد
|
|
# (قرار دستی، بازدید برنامهریزیشده، هماهنگی). قبلاً «هماهنگی» با «بازدید»
|
|
# در یک گروه جمع میشد و در نمای «بازدید برنامهریزیشده» دیده میشد؛ حالا
|
|
# هر منبع گروه نمایشیِ خودش را دارد.
|
|
with_manual, with_scheduled, with_coordination = set(), set(), set()
|
|
for customer_id in stale_ids:
|
|
customer_sources = sources.get(customer_id) or {}
|
|
if SOURCE_MANUAL in customer_sources:
|
|
with_manual.add(customer_id)
|
|
if SOURCE_SCHEDULED in customer_sources:
|
|
with_scheduled.add(customer_id)
|
|
if SOURCE_COORDINATION in customer_sources:
|
|
with_coordination.add(customer_id)
|
|
|
|
coordinated_ids = [cid for cid in stale_ids if cid in (
|
|
with_manual | with_scheduled | with_coordination)]
|
|
selected_map = {
|
|
# نداشتن هیچ قرار
|
|
'uncoordinated': [cid for cid in stale_ids if cid not in (
|
|
with_manual | with_scheduled | with_coordination)],
|
|
# هر مشتری که این منبع را دارد (حتی اگر منبع دیگری هم داشته باشد)
|
|
'manual': [cid for cid in stale_ids if cid in with_manual],
|
|
'scheduled': [cid for cid in stale_ids if cid in with_scheduled],
|
|
'coordination': [cid for cid in stale_ids if cid in with_coordination],
|
|
# اجتماع هر سه منبع
|
|
'coordinated': coordinated_ids,
|
|
'all': stale_ids,
|
|
}
|
|
|
|
# ---------- نمای «نیازمند پیگیری» (یک تاریخ مشخص) ----------
|
|
# این نما **مستقل** از «عقبافتاده» است: مشتری سالمی که با او قرار تماس
|
|
# گذاشته شده هم باید دیده شود. قاعدهٔ حذفِ «بازدید/نصب» در خود
|
|
# ``followups_on_date`` اعمال میشود (مشتریِ دارای قرار بازدید یا نصب نمیآید).
|
|
today_followups = followups_on_date(today_date, now=now)
|
|
|
|
# پیگیریهای عقبافتاده: موعدشان گذشته و هنوز تعیین تکلیف نشدهاند.
|
|
# فقط در نمای پیگیری محاسبه میشود (کوئری اضافه برای بقیهٔ نماها لازم نیست).
|
|
overdue_map = overdue_followups(now=now, target_date=today_date) \
|
|
if is_followup_view else {}
|
|
overdue_oldest = min(overdue_map.values()) if overdue_map else None
|
|
|
|
# فیلتر «فقط عقبافتادهها» (?missed=1) — همان نما با منبعِ دیگر.
|
|
show_missed = is_followup_view and \
|
|
(request.GET.get('missed') or '').strip().lower() in ('1', 'true', 'on')
|
|
|
|
if is_followup_view:
|
|
if show_missed:
|
|
# تاریخِ موعدِ ازدسترفته بهصورت datetime تا با بقیهٔ مسیرها یکسان باشد.
|
|
followups = {
|
|
cid: {SOURCE_COORDINATION: as_aware(day)}
|
|
for cid, day in overdue_map.items()
|
|
}
|
|
elif followup_date == today_date:
|
|
followups = today_followups
|
|
else:
|
|
followups = followups_on_date(followup_date, now=now)
|
|
else:
|
|
followups = {}
|
|
|
|
def _followup_ids(mapping):
|
|
"""شناسههای واجد شرایط (فعال/لغونشده/دارای نام + جستوجو) با ترتیب زمانی."""
|
|
if not mapping:
|
|
return []
|
|
eligible = _apply_search(
|
|
Customer.objects.filter(
|
|
id__in=list(mapping),
|
|
is_active=True,
|
|
laghv_bazdid=False,
|
|
name_family__isnull=False,
|
|
)
|
|
)
|
|
allowed = set(eligible.values_list('id', flat=True))
|
|
return sorted(
|
|
(cid for cid in mapping if cid in allowed),
|
|
key=lambda cid: (min(mapping[cid].values()), cid),
|
|
)
|
|
|
|
followup_ids = _followup_ids(followups) if is_followup_view else []
|
|
# شمارندهها از همان مسیرِ فهرست میآیند تا «عدد هشدار = تعداد ردیفها» بماند
|
|
# (مشتری غیرفعال/لغوشده/بینام یا فیلترشده شمرده نمیشود).
|
|
today_followup_count = len(_followup_ids(today_followups))
|
|
overdue_followup_count = len(_followup_ids(
|
|
{cid: {SOURCE_COORDINATION: as_aware(day)} for cid, day in overdue_map.items()}
|
|
))
|
|
selected_map[ENTEZAR_FOLLOWUP_VIEW] = followup_ids
|
|
|
|
selected = selected_map[view]
|
|
|
|
# برای نمای پیگیری، فهرست مشتریان از همان شناسههای تأییدشده میآید
|
|
# (ممکن است در «عقبافتاده» نباشند).
|
|
if is_followup_view:
|
|
customers_objj = _apply_search(
|
|
Customer.objects.filter(id__in=followup_ids)
|
|
)
|
|
|
|
# «مدت تأخیر» = از چه زمانی بازدید عقب افتاده است (به روز).
|
|
# مبنای محاسبه «موعد بازدید» است، نه خود بازدید: موعد = بازدید + دوره.
|
|
# اینطور مشتریِ تازهمعوق هم در بازهٔ کوتاه قرار میگیرد و عدد معنا پیدا میکند.
|
|
# محاسبه روی کلِ نمای جاری انجام میشود تا شمارندههای بازهها با فیلتر
|
|
# «میزان تأخیر» همخوان باشند (اول نمای جاری، بعد فیلتر تأخیر).
|
|
today = today_date
|
|
overdue_days = {}
|
|
for customer_id in selected:
|
|
anchor = visits.get(customer_id)
|
|
if anchor is not None:
|
|
due = visit_due_date(anchor, months=period_months)
|
|
base = due.astimezone(timezone.get_current_timezone()).date()
|
|
else:
|
|
# بدون بازدید: مقطع دوره (تاریخ نصب خیلی قدیمی، عدد را بیمعنا میکند).
|
|
base = due_cut_date
|
|
overdue_days[customer_id] = max((today - base).days, 0)
|
|
|
|
# آمار «تأخیر» روی نمای جاری (قبل از اعمال فیلتر تأخیر).
|
|
buckets = {'lt30': 0, '30to90': 0, '90to180': 0, 'gt180': 0}
|
|
for days in overdue_days.values():
|
|
if days < 30:
|
|
buckets['lt30'] += 1
|
|
elif days < 90:
|
|
buckets['30to90'] += 1
|
|
elif days < 180:
|
|
buckets['90to180'] += 1
|
|
else:
|
|
buckets['gt180'] += 1
|
|
|
|
# اعمال فیلتر «میزان تأخیر».
|
|
delay_ranges = {
|
|
'lt30': (0, 30),
|
|
'30to90': (30, 90),
|
|
'90to180': (90, 180),
|
|
'gt180': (180, None),
|
|
}
|
|
# فیلتر «میزان تأخیر» فقط برای نمای «عقبافتاده» معنا دارد؛ در نمای
|
|
# «پیگیری امروز» مشتری ممکن است اصلاً تأخیری نداشته باشد.
|
|
if delay and not is_followup_view:
|
|
low, high = delay_ranges[delay]
|
|
selected = [
|
|
cid for cid in selected
|
|
if overdue_days.get(cid, 0) >= low
|
|
and (high is None or overdue_days.get(cid, 0) < high)
|
|
]
|
|
|
|
# ---------- مرتبسازی (روی همهٔ ستونها) ----------
|
|
# یک کوئری سبک برای فیلدهای Customer که برای مرتبسازی لازم است.
|
|
info = {
|
|
row['id']: row
|
|
for row in Customer.objects.filter(id__in=selected).values(
|
|
'id', 'name_family', 'phone1', 'address')
|
|
} if selected else {}
|
|
|
|
def _source_date(cid, key):
|
|
return (sources.get(cid) or {}).get(key)
|
|
|
|
def _sort_value(cid):
|
|
"""مقدار مرتبسازی برای هر ستون (تاریخها aware یا None)."""
|
|
if sort_field == 'name':
|
|
return (info.get(cid, {}).get('name_family') or '')
|
|
if sort_field == 'phone':
|
|
return (info.get(cid, {}).get('phone1') or '')
|
|
if sort_field == 'address':
|
|
return (info.get(cid, {}).get('address') or '')
|
|
if sort_field == 'status':
|
|
# ترتیب منطقی: بدون قرار ← قرار دستی ← بازدید ← هماهنگی ← چندقراره
|
|
entry = sources.get(cid) or {}
|
|
if not entry:
|
|
return 0
|
|
days = {value.astimezone(current_tz).date() for value in entry.values()}
|
|
if len(days) > 1:
|
|
return 4
|
|
if SOURCE_MANUAL in entry:
|
|
return 1
|
|
if SOURCE_SCHEDULED in entry:
|
|
return 2
|
|
return 3
|
|
if sort_field == 'manual':
|
|
return _source_date(cid, SOURCE_MANUAL)
|
|
if sort_field == 'scheduled':
|
|
return _source_date(cid, SOURCE_SCHEDULED)
|
|
if sort_field == 'coordination':
|
|
return _source_date(cid, SOURCE_COORDINATION)
|
|
if sort_field == 'overdue':
|
|
return overdue_days.get(cid, 0)
|
|
# last_visit: مشتریِ بدون بازدید، قدیمیترین حساب میشود.
|
|
return visits.get(cid) or due_cut
|
|
|
|
if selected:
|
|
if is_followup_view and not raw_sort:
|
|
# در نمای پیگیری، ترتیب طبیعی «زودترین موعد پیگیری» است
|
|
# (ترتیب از ``_followup_ids`` آمده) — نه قدیمیترین بازدید.
|
|
ordered_ids = list(selected)
|
|
else:
|
|
values = {cid: _sort_value(cid) for cid in selected}
|
|
with_value = [cid for cid in selected if values[cid] is not None]
|
|
without_value = [cid for cid in selected if values[cid] is None]
|
|
with_value.sort(key=lambda cid: (values[cid], cid),
|
|
reverse=(sort_dir == 'desc'))
|
|
# مقادیر خالی همیشه آخر میآیند (مستقل از جهت مرتبسازی) تا فهرست
|
|
# با ستونهای «قرار دستی/بازدید/هماهنگی» بیمعنا نشود.
|
|
ordered_ids = with_value + without_value
|
|
else:
|
|
ordered_ids = []
|
|
|
|
customers_objj = customers_objj.filter(id__in=ordered_ids)
|
|
if ordered_ids:
|
|
ordering = Case(
|
|
*[When(id=cid, then=Value(index))
|
|
for index, cid in enumerate(ordered_ids)],
|
|
output_field=models.IntegerField(),
|
|
)
|
|
customers_objj = customers_objj.order_by(ordering)
|
|
else:
|
|
customers_objj = customers_objj.order_by('id')
|
|
|
|
counts = {
|
|
'total': len(stale_ids),
|
|
'uncoordinated': len(selected_map['uncoordinated']),
|
|
# عددِ هر کارت دقیقاً همان چیزی است که با کلیک روی آن دیده میشود.
|
|
# یک مشتری میتواند چند منبع داشته باشد، پس جمع سه منبع میتواند از
|
|
# «تعیین تکلیف شده» بیشتر شود؛ جزئیات در دراپداون «نوع قرار» است.
|
|
'manual': len(selected_map['manual']),
|
|
'scheduled': len(selected_map['scheduled']),
|
|
'coordination': len(selected_map['coordination']),
|
|
'coordinated': len(selected_map['coordinated']),
|
|
# کارت «نیازمند پیگیری امروز» — همیشه عدد امروز را نشان میدهد،
|
|
# مستقل از تاریخی که کاربر در تقویم انتخاب کرده است.
|
|
'followup_today': today_followup_count,
|
|
# پیگیریهای موعدگذشته که هنوز تعیین تکلیف نشدهاند (هشدار قرمز).
|
|
'followup_overdue': overdue_followup_count,
|
|
'selected': len(selected),
|
|
'view_total': len(selected_map[view]),
|
|
'buckets': buckets,
|
|
# برای متن راهنمای بالای صفحه («بیش از N ماه از بازدید گذشته»).
|
|
'period_months': period_months,
|
|
}
|
|
|
|
try:
|
|
entezar_bazdid = CacheTable.objects.get(key='9mahbazdid')
|
|
# کارت داشبورد = «کارِ فوری» = تعیین تکلیف نشده.
|
|
# فقط در صورت تغییر مینویسیم تا یک 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
|
|
|
|
paginator = Paginator(customers_objj, ENTEZAR_PAGE_SIZE)
|
|
page_number = request.GET.get('page', None)
|
|
customerHa = paginator.get_page(page_number)
|
|
|
|
# لینک صفحهبندی با حفظ همهٔ فیلترهای فعلی.
|
|
def _page_link(number):
|
|
params = request.GET.copy()
|
|
params['page'] = number
|
|
return '?' + params.urlencode()
|
|
|
|
page_prev = _page_link(customerHa.previous_page_number()) if customerHa.has_previous() else ''
|
|
page_next = _page_link(customerHa.next_page_number()) if customerHa.has_next() else ''
|
|
|
|
# ---------- لینکهای حفظکنندهٔ فیلترها ----------
|
|
def _qs(**overrides):
|
|
"""پارامترهای فعلی + تغییرهای دادهشده (بدون page). None ⇒ حذف."""
|
|
params = request.GET.copy()
|
|
params.pop('page', None)
|
|
for key, value in overrides.items():
|
|
if value is None:
|
|
params.pop(key, None)
|
|
else:
|
|
params[key] = value
|
|
query = params.urlencode()
|
|
return '?' + query if query else '?'
|
|
|
|
view_links = {name: _qs(view=name) for name in ENTEZAR_VIEWS}
|
|
# کارت «نیازمند پیگیری امروز» همیشه به امروز میرود (تاریخ انتخابی پاک میشود).
|
|
view_links[ENTEZAR_FOLLOWUP_VIEW] = _qs(view=ENTEZAR_FOLLOWUP_VIEW, fdate=None)
|
|
|
|
# ---------- ناوبری تقویم در نمای پیگیری ----------
|
|
def _followup_link(target_date):
|
|
return _qs(view=ENTEZAR_FOLLOWUP_VIEW,
|
|
fdate=JalaliDate(target_date).strftime('%Y/%m/%d'))
|
|
|
|
followup_nav = {
|
|
'prev': _followup_link(followup_date - datetime.timedelta(days=1)),
|
|
'next': _followup_link(followup_date + datetime.timedelta(days=1)),
|
|
'today': _followup_link(today_date),
|
|
'is_today': followup_date == today_date,
|
|
}
|
|
# لینک هشدار «عقبافتادهها» و لینک بازگشت از آن حالت.
|
|
followup_overdue_link = _qs(view=ENTEZAR_FOLLOWUP_VIEW, fdate=None, missed=1)
|
|
followup_ontime_link = _qs(view=ENTEZAR_FOLLOWUP_VIEW, fdate=None, missed=None)
|
|
|
|
delay_links = {'ez_all': _qs(delay=None)}
|
|
delay_links.update({name: _qs(delay=name) for name in ENTEZAR_DELAYS})
|
|
sort_links = {}
|
|
for column, default_dir in ENTEZAR_SORT_COLUMNS.items():
|
|
# کلیک دوباره روی همان ستون، جهت را برمیگرداند.
|
|
next_dir = ('desc' if sort_dir == 'asc' else 'asc') \
|
|
if sort_field == column else default_dir
|
|
sort_links[column] = _qs(sort=column, dir=next_dir)
|
|
|
|
# سرستونهای مرتبشدنی (به همان ترتیب ستونهای جدول).
|
|
# در نمای «نیازمند پیگیری امروز» ستون «تأخیر» نمایش داده **نمیشود**:
|
|
# این فهرست دربارهٔ «موعد امروز» است، نه عقبافتادگی بازدید.
|
|
columns = [
|
|
('name', 'نام و نام خانوادگی', 21),
|
|
('status', 'وضعیت قرار', 12),
|
|
('manual', 'قرار دستی', 10),
|
|
('scheduled', 'بازدید برنامهریزیشده', 10),
|
|
('coordination', 'هماهنگی', 10),
|
|
('last_visit', 'آخرین بازدید', 9),
|
|
('overdue', 'تأخیر', 8),
|
|
('phone', 'تلفن', 9),
|
|
('address', 'آدرس', 11),
|
|
]
|
|
if is_followup_view:
|
|
hidden_width = next(width for key, _, width in columns if key == 'overdue')
|
|
columns = [col for col in columns if col[0] != 'overdue']
|
|
# عرض ستون حذفشده به نام اضافه میشود تا مجموع همچنان ۱۰۰٪ بماند.
|
|
columns[0] = (columns[0][0], columns[0][1], columns[0][2] + hidden_width)
|
|
|
|
table_columns = [{'key': key, 'label': label, 'width': width}
|
|
for key, label, width in columns]
|
|
hidden_columns = ['overdue'] if is_followup_view else []
|
|
|
|
sort_header = [
|
|
{
|
|
'key': column,
|
|
'label': label,
|
|
'link': sort_links[column],
|
|
'active': sort_field == column,
|
|
'dir': sort_dir if sort_field == column else '',
|
|
}
|
|
for column, label in ((key, label) for key, label, _ in columns)
|
|
]
|
|
# لینک «حذف همهٔ فیلترها»: نمای پیشفرض (امروز)، بدون تأخیر/جستوجو/مرتبسازی.
|
|
clear_link = '?view=' + ENTEZAR_DEFAULT_VIEW
|
|
has_filters = bool(
|
|
delay or searchVal or sort_field != 'last_visit'
|
|
or sort_dir != 'asc'
|
|
# تاریخِ انتخابشده یا حالت «فقط عقبافتادهها» هم فیلتر است.
|
|
or (is_followup_view and followup_date != today_date)
|
|
or show_missed
|
|
)
|
|
|
|
# اطلاعات هر ردیف برای قالب: قرارها + مدت تأخیر + آخرین بازدید مؤثر.
|
|
appointment_rows = {}
|
|
for customer in customerHa.object_list:
|
|
entry = dict(sources.get(customer.id) or {})
|
|
# در نمای پیگیری، تاریخ همان روز ملاک نمایش است (حتی اگر گذشته باشد و
|
|
# در ``appointment_sources_map`` که فقط آینده را میبیند، نباشد).
|
|
followup_entry = followups.get(customer.id) or {}
|
|
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(appointment_days),
|
|
# تاریخ پیگیری این نمای خاص (برای ستون اختصاصی نمای «امروز»).
|
|
'followup_at': (min(followup_entry.values())
|
|
if followup_entry else None),
|
|
}
|
|
|
|
return render(request, 'admin_panel/entezar.html', {
|
|
'customers': customerHa, 'searchStatus': searchStatus, 'full_path': full_path,
|
|
'searchVal': searchVal, 'searchOn': searchOn,
|
|
'view': view, 'counts': counts, 'appointment_rows': appointment_rows,
|
|
'view_links': view_links, 'delay_links': delay_links,
|
|
'sort_links': sort_links, 'sort_header': sort_header,
|
|
'clear_link': clear_link,
|
|
'has_filters': has_filters,
|
|
'delay': delay, 'delays': ENTEZAR_DELAYS,
|
|
'sort': sort_field, 'sort_dir': sort_dir,
|
|
'sort_columns': ENTEZAR_SORT_COLUMNS,
|
|
'table_columns': table_columns,
|
|
'hidden_columns': hidden_columns,
|
|
'column_count': len(table_columns),
|
|
'page_prev': page_prev, 'page_next': page_next,
|
|
'views': ENTEZAR_VIEWS,
|
|
# نمای «نیازمند پیگیری»
|
|
'is_followup_view': is_followup_view,
|
|
'followup_view': ENTEZAR_FOLLOWUP_VIEW,
|
|
'followup_date': followup_date,
|
|
'followup_date_jalali': followup_date_jalali,
|
|
'followup_nav': followup_nav,
|
|
'show_missed': show_missed,
|
|
'followup_overdue_link': followup_overdue_link,
|
|
'followup_ontime_link': followup_ontime_link,
|
|
})
|
|
|
|
|
|
@login_required(login_url='/panel/p_login/')
|
|
@permission_required('admin_panel.DelCustomer', raise_exception=True)
|
|
def customerDel(request, c_id):
|
|
customer_obj = get_object_or_404(Customer, id=c_id)
|
|
|
|
try:
|
|
# ۱. بررسی وجود هرگونه سابقه مالی/حسابداری (حتی با مانده صفر)
|
|
has_articles = Article.objects.filter(customer=customer_obj).exists()
|
|
|
|
# ۲. بررسی وجود هرگونه سابقه چک یا نسیه (حتی در صورت پاس شدن و تسویه کامل)
|
|
has_sarresids = Sarresid.objects.filter(customer=customer_obj).exists()
|
|
|
|
# ۳. بررسی وجود هرگونه فاکتور ثبت شده برای این مشتری
|
|
has_factors = Factor.objects.filter(customer=customer_obj).exists()
|
|
|
|
# ۴. بررسی وجود هرگونه سابقه اقدام، بازدید یا نظرسنجی
|
|
has_actions = Action.objects.filter(customer=customer_obj).exists()
|
|
|
|
# ۵. اگر هر کدام از این سوابق وجود داشته باشد، به هیچ عنوان اجازه غیرفعالسازی/حذف نمیدهیم
|
|
if has_articles or has_sarresids or has_factors or has_actions:
|
|
error_msg = f"عملیات ناموفق: مشتری '{customer_obj.name_family}' به دلیل داشتن سابقه در سیستم قابل حذف نیست! دلایل: "
|
|
reasons = []
|
|
|
|
if has_articles:
|
|
reasons.append("داشتن تراکنش مالی یا گردش حسابداری")
|
|
if has_sarresids:
|
|
reasons.append("داشتن سابقه چک یا نسیه (حتی تسویه شده)")
|
|
if has_factors:
|
|
reasons.append("داشتن فاکتور ثبتشده")
|
|
if has_actions:
|
|
reasons.append("داشتن سابقه اقدامات، بازدید یا نظرسنجی")
|
|
|
|
error_msg += "، ".join(reasons) + "."
|
|
|
|
# ارسال پیام خطا به تمپلیت و بازگرداندن کاربر به صفحه قبلی
|
|
messages.error(request, error_msg)
|
|
return redirect(request.META.get('HTTP_REFERER', '/panel/customers/'))
|
|
|
|
# ۶. در صورتی که مشتری کاملاً خام بود و هیچ سابقهای در هیچ بخشی نداشت، حذف میشود
|
|
customer_obj.delete()
|
|
messages.success(request, f"مشتری '{customer_obj.name_family}' که فاقد هرگونه سابقهای بود، با موفقیت از سیستم حذف شد.")
|
|
return redirect('/panel/customers/')
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in deleting customer {c_id}: {str(e)}", exc_info=True)
|
|
messages.error(request, "خطای سیستمی در بررسی سوابق و حذف مشتری رخ داد.")
|
|
return redirect(request.META.get('HTTP_REFERER', '/panel/customers/'))
|
|
|
|
|
|
@login_required(login_url='/panel/p_login/')
|
|
def customerHesabdari(request ,c_id):
|
|
customer = Customer.objects.get(id=c_id)
|
|
# مانده حساب از همان تابع مشترکی میآید که قالبهای پیامک استفاده میکنند تا
|
|
# عددِ این صفحه و پیامک هرگز واگرا نشوند (منبع حقیقت: sms/balance.py).
|
|
mandehesabCs = customer_balance(customer)
|
|
try:
|
|
articls = Article.objects.filter(customer=customer).order_by('-createdDT')
|
|
except:
|
|
articls = None
|
|
|
|
dateNow = datetime.datetime.now().date
|
|
data = {'customer': customer, 'mandehesabCs': mandehesabCs, 'articls': articls}
|
|
return render(request, 'admin_panel/customerHesabDari.html', data)
|
|
|
|
@login_required(login_url='/panel/p_login/')
|
|
def sarresidh(request ,c_id):
|
|
customer = Customer.objects.get(id=c_id)
|
|
try:
|
|
all_sarresid = Sarresid.objects.filter(customer=customer).order_by('-sarresidDT')
|
|
except:
|
|
all_sarresid = None
|
|
|
|
data = {'customer': customer, 'all_sarresid': all_sarresid}
|
|
return render(request, 'admin_panel/customerSarresid.html', data)
|
|
|
|
@login_required(login_url='/panel/p_login/')
|
|
def customerHesabdariAdd(request ,c_id):
|
|
user_op = request.user
|
|
price = request.POST.get('price', None)
|
|
type = request.POST.get('type', None)
|
|
typePy = request.POST.get('typePy', None)
|
|
desc = request.POST.get('desc', None)
|
|
nahvePardakhat = request.POST.get('nahvePardakhat', None)
|
|
|
|
newArticl = Article()
|
|
newArticl.customer = Customer.objects.get(id=c_id)
|
|
newArticl.desc = desc
|
|
newArticl.typePy = typePy
|
|
if type == 'credit':
|
|
newArticl.credit = price
|
|
newArticl.debit = 0
|
|
elif type == 'debit':
|
|
newArticl.debit = price
|
|
newArticl.credit = 0
|
|
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')
|