Files
khatibi/admin_panel/views/customers.py
T
2026-10-21 11:28:01 +03:30

654 lines
30 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 ..sms.rules import customer_toggle_map
from ..visit_policy import (SOURCE_COORDINATION, SOURCE_MANUAL, SOURCE_SCHEDULED,
appointment_sources_map, last_visit_map, visit_due_at,
visit_due_date, visit_period_months)
# نماهای صفحهٔ «انتظار» (فیلتر ?view=). پیش‌فرض = فقط «بدون قرار» تا رفتار
# قبلی صفحه برای کاربران حفظ شود.
ENTEZAR_VIEWS = ('uncoordinated', 'manual', 'scheduled', 'coordinated', 'all')
ENTEZAR_DEFAULT_VIEW = 'uncoordinated'
# حالت‌های مرتب‌سازی (?sort=):
# oldest ⇒ قدیمی‌ترین بازدید اول (پیش‌فرض/اولویت کارشناس)
# newest ⇒ جدیدترین بازدید اول
# overdue ⇒ بیشترین تأخیر اول
ENTEZAR_SORTS = ('oldest', 'newest', 'overdue')
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
sms_toggles = customer_toggle_map(customer)
data = {
'customer': customer, 'categorys': categorys, 'dateNow': dateNow,
'factorha': factorha, 'mandehesabCs': mandehesabCs,
'sms_toggles': sms_toggles,
}
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`` (سه منبع: قرار دستی، بازدید برنامه‌ریزی‌شده،
هماهنگیِ در انتظار). هر مشتری دقیقاً در یکی از چهار گروه زیر است:
* ``A`` — معوق بدون هیچ قرار (پیش‌فرض نمایش؛ رفتار قبلی صفحه)
* ``B`` — معوق با قرار دستی (تعویق از فرم هماهنگی)
* ``C`` — معوق با بازدید برنامه‌ریزی‌شده یا هماهنگی در انتظار
* ``D`` — معوق با بیش از یک منبعِ قرار
فیلتر با ``?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 # مقدار نامعتبر ⇒ پیش‌فرض، بدون خطا
sort = (request.GET.get('sort') or 'oldest').strip().lower()
if sort not in ENTEZAR_SORTS:
sort = '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) تا صفحه و اسکنر پیامک یک جواب بدهند.
due_cut_date = due_cut.astimezone(timezone.get_current_timezone()).date()
visits = last_visit_map(now=now)
sources = appointment_sources_map(now=now)
customers_objj = Customer.objects.filter(
laghv_bazdid=False, name_family__isnull=False,
installDate__lte=due_cut_date,
)
# جست‌وجو: نام، آدرس، تلفن (همه شماره‌ها) و کد شناسایی.
if searchVal:
if searchOn == 'address':
customers_objj = customers_objj.filter(address__contains=searchVal)
elif searchOn == 'name':
customers_objj = customers_objj.filter(name_family__contains=searchVal)
elif searchOn == 'phone':
customers_objj = customers_objj.filter(
Q(phone1__contains=searchVal) | Q(phone2__contains=searchVal)
| Q(phone3__contains=searchVal)
)
elif searchOn == 'code':
customers_objj = customers_objj.filter(idCode__contains=searchVal)
else:
searchStatus = False
else:
searchStatus = False
# مشتریان عقب‌افتاده: بدون بازدید معتبر، یا بازدید معتبرشان پیش از مقطع دوره.
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
]
# تفکیک چهارگروهی (MECE): هر مشتری فقط در یک گروه.
groups = {'A': [], 'B': [], 'C': [], 'D': []}
for customer_id in stale_ids:
customer_sources = sources.get(customer_id) or {}
manual = SOURCE_MANUAL in customer_sources
scheduled = (SOURCE_SCHEDULED in customer_sources
or SOURCE_COORDINATION in customer_sources)
if manual and scheduled:
groups['D'].append(customer_id)
elif manual:
groups['B'].append(customer_id)
elif scheduled:
groups['C'].append(customer_id)
else:
groups['A'].append(customer_id)
selected = {
'uncoordinated': groups['A'],
'manual': groups['B'] + groups['D'],
'scheduled': groups['C'] + groups['D'],
'coordinated': groups['B'] + groups['C'] + groups['D'],
'all': stale_ids,
}[view]
# «مدت تأخیر» = از چه زمانی بازدید عقب افتاده است (به روز).
# مبنای محاسبه «موعد بازدید» است، نه خود بازدید: موعد = بازدید + دوره.
# این‌طور مشتریِ تازه‌معوق هم در بازهٔ کوتاه قرار می‌گیرد و عدد معنا پیدا می‌کند.
today = now.astimezone(timezone.get_current_timezone()).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)
# مرتب‌سازی:
# oldest ⇒ قدیمی‌ترین بازدید اول (اولویت کارشناس)
# newest ⇒ جدیدترین بازدید اول
# overdue ⇒ بیشترین تأخیر اول (چه کسی بیشتر از همه عقب افتاده)
if sort == 'overdue':
ordered_ids = sorted(selected, key=lambda cid: (-overdue_days.get(cid, 0), cid))
customers_objj = customers_objj.filter(id__in=ordered_ids)
if ordered_ids:
# ترتیب در لایهٔ دیتابیس با CASE/WHEN (بدون بار حافظه‌ای).
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')
else:
# مرتب‌سازی بر مبنای **تاریخ مؤثر بازدید** (از Action)، نه ستون ممکن‌القدمت.
ordered_ids = sorted(
selected,
key=lambda cid: (
(visits.get(cid) or due_cut),
cid,
),
reverse=(sort == 'newest'),
)
customers_objj = customers_objj.filter(id__in=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)
# آمار «تأخیر» (روی کل گروه انتخابی، نه فقط صفحهٔ جاری).
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
counts = {
'total': len(stale_ids),
'uncoordinated': len(groups['A']),
# توجه: عددِ هر کارت باید **همان چیزی** باشد که با کلیک روی آن کارت
# دیده می‌شود. فیلتر ``view=manual`` گروه‌های B و D را نشان می‌دهد و
# ``view=scheduled`` گروه‌های C و D را؛ پس اینجا هم همان اجتماع
# شمرده می‌شود (قبلاً فقط B/C شمرده می‌شد و عدد با فهرست نمی‌خواند).
'manual': len(groups['B']) + len(groups['D']),
'scheduled': len(groups['C']) + len(groups['D']),
'manual_only': len(groups['B']),
'scheduled_only': len(groups['C']),
'both': len(groups['D']),
'coordinated': len(groups['B']) + len(groups['C']) + len(groups['D']),
'selected': len(selected),
'buckets': buckets,
# برای متن راهنمای بالای صفحه («بیش از N ماه از بازدید گذشته»).
'period_months': period_months,
}
try:
entezar_bazdid = CacheTable.objects.get(key='9mahbazdid')
# کارت داشبورد = «کارِ فوری» = بدون قرار.
entezar_bazdid.value = counts['uncoordinated']
entezar_bazdid.save()
except CacheTable.DoesNotExist:
pass
paginator = Paginator(customers_objj, 20)
page_number = request.GET.get('page', None)
customerHa = paginator.get_page(page_number)
# اطلاعات هر ردیف برای قالب: قرارها + مدت تأخیر + آخرین بازدید مؤثر.
appointment_rows = {}
for customer in customerHa.object_list:
entry = sources.get(customer.id) or {}
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),
'group': (
'D' if (SOURCE_MANUAL in entry and (
SOURCE_SCHEDULED in entry or SOURCE_COORDINATION in entry))
else 'B' if SOURCE_MANUAL in entry
else 'C' if (SOURCE_SCHEDULED in entry or SOURCE_COORDINATION in entry)
else 'A'
),
}
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,
'sort': sort, 'sorts': ENTEZAR_SORTS,
'views': ENTEZAR_VIEWS,
})
@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')