986 lines
44 KiB
Python
986 lines
44 KiB
Python
import datetime
|
||
import json
|
||
|
||
from django.db import transaction
|
||
from django.db.models import (Sum, Q, Count, Avg, Min, F, Subquery, OuterRef,
|
||
IntegerField, Value, Case, When)
|
||
from django.db.models.functions import Coalesce
|
||
from django.shortcuts import redirect, render, get_object_or_404
|
||
from django.http import HttpResponse, HttpResponseBadRequest
|
||
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
|
||
from django.views.decorators.http import require_POST
|
||
from django.contrib import messages
|
||
from django.contrib.auth.models import User
|
||
from django.contrib.auth.decorators import login_required, permission_required
|
||
from persiantools.jdatetime import JalaliDate
|
||
|
||
from ..models import (Article, Sarresid, Customer, Factor, ProductInStore,
|
||
StoreRoom, Action)
|
||
from ..sms.triggers import notify_payment_thanks
|
||
from ._helpers import logger, _safe_int
|
||
|
||
|
||
@login_required(login_url='/panel/p_login/')
|
||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||
def report(request):
|
||
|
||
nahvePardakhat = [
|
||
{'fa': 'تخفیف', 'en': 'تخفیف'},
|
||
{'fa': 'نسیه', 'en': 'نسیه'},
|
||
{'fa': 'چک', 'en': 'چک'},
|
||
{'fa': 'کارتخوان', 'en': 'کارت خوان'},
|
||
{'fa': 'کارت به کارت', 'en': 'کارت به کارت'},
|
||
{'fa': 'نقدی', 'en': 'نقد'},
|
||
]
|
||
|
||
userHa = User.objects.filter()
|
||
|
||
if request.method == "GET":
|
||
nahvePardakht = request.GET.get("nahvepardakht", None)
|
||
user = request.GET.get("user", None)
|
||
azTarikh = request.GET.get("azTarikh", None)
|
||
taTarikh = request.GET.get("taTarikh", None)
|
||
pagenumber = request.GET.get("page", None)
|
||
|
||
|
||
if azTarikh and nahvePardakht:
|
||
|
||
query_data = {
|
||
'nahvePardakht': nahvePardakht,
|
||
'user': user,
|
||
'azTarikh': azTarikh,
|
||
'taTarikh': taTarikh,
|
||
}
|
||
|
||
azDateTime = datetime.datetime.fromtimestamp(int(azTarikh) / 1e3)
|
||
azDateTime = azDateTime.strftime('%Y-%m-%d 00:00:00')
|
||
|
||
taDateTime = datetime.datetime.fromtimestamp(int(taTarikh) / 1e3)
|
||
taDateTime = taDateTime.strftime('%Y-%m-%d 23:59:59')
|
||
print("b")
|
||
user_a = User.objects.get(username=user)
|
||
print(user_a)
|
||
|
||
if nahvePardakht == "نسیه" or nahvePardakht == "چک":
|
||
if nahvePardakht == 'نسیه':
|
||
typeSarresid = 'nesiye'
|
||
elif nahvePardakht == 'چک':
|
||
typeSarresid = 'check'
|
||
|
||
|
||
sumSarresidha = Sarresid.objects.filter(
|
||
type=typeSarresid,
|
||
user=user_a,
|
||
createdDT__lte=taDateTime,
|
||
createdDT__gte=azDateTime,
|
||
payStatus='false'
|
||
).aggregate(Sum('price'))
|
||
|
||
sarresidha = Sarresid.objects.filter(
|
||
type=typeSarresid,
|
||
user=user_a,
|
||
createdDT__lte=taDateTime,
|
||
createdDT__gte=azDateTime,
|
||
payStatus='false'
|
||
)
|
||
|
||
paginator = Paginator(sarresidha, 25) # Show 25 contacts per page.
|
||
page_number = request.GET.get('page', None)
|
||
all_sarresidha = paginator.get_page(pagenumber)
|
||
data = {'sarresidha': all_sarresidha ,'sumSarresidha': sumSarresidha, 'nahvePardakhat': nahvePardakhat, 'userHa':userHa, 'query_data': query_data}
|
||
return render(request, 'admin_panel/reportCheckNesiye.html', data)
|
||
|
||
else:
|
||
|
||
sumAricles = Article.objects.filter(
|
||
nahvePardakht=nahvePardakht,
|
||
user=user_a,
|
||
createdDT__lte=taDateTime,
|
||
createdDT__gte=azDateTime,
|
||
).aggregate(Sum('credit'))
|
||
|
||
aricles1 = Article.objects.filter(
|
||
nahvePardakht=nahvePardakht,
|
||
user=user_a,
|
||
createdDT__lte=taDateTime,
|
||
createdDT__gte=azDateTime,
|
||
)
|
||
|
||
paginator = Paginator(aricles1, 25) # Show 25 contacts per page.
|
||
page_number = request.GET.get('page', None)
|
||
all_aricles = paginator.get_page(page_number)
|
||
data = {'aricles': all_aricles,'sum_credit': sumAricles, 'nahvePardakhat': nahvePardakhat, 'userHa':userHa, 'query_data': query_data}
|
||
return render(request, 'admin_panel/report.html', data)
|
||
else:
|
||
data = {'nahvePardakhat': nahvePardakhat, 'userHa':userHa}
|
||
return render(request, 'admin_panel/report.html', data)
|
||
|
||
|
||
data = {'nahvePardakhat': nahvePardakhat, 'userHa':userHa}
|
||
return render(request, 'admin_panel/report.html', data)
|
||
|
||
|
||
@login_required(login_url='/panel/p_login/')
|
||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||
def report_debtors_no_vosol(request):
|
||
now = datetime.datetime.now()
|
||
|
||
# --- ۱. دریافت و اعتبارسنجی پارامترهای فیلتر از فرانتاند ---
|
||
try:
|
||
days_threshold = int(request.GET.get('days', 20))
|
||
if days_threshold < 0:
|
||
days_threshold = 20
|
||
except ValueError:
|
||
days_threshold = 20
|
||
|
||
filter_type = request.GET.get('filter_type', 'all') # all, never, expired
|
||
sort_field = request.GET.get('sort', 'amount') # amount, oldest_date, last_action
|
||
sort_dir = request.GET.get('dir', 'desc') # asc, desc
|
||
|
||
threshold_date = now - datetime.timedelta(days=days_threshold)
|
||
|
||
# --- ۲. ساخت Subquery های فوقسریع و ایمن ---
|
||
sq_debit = Article.objects.filter(customer=OuterRef('pk')).values('customer').annotate(s=Sum('debit')).values('s')
|
||
sq_credit = Article.objects.filter(customer=OuterRef('pk')).values('customer').annotate(s=Sum('credit')).values('s')
|
||
|
||
sq_overdue_amt = Sarresid.objects.filter(
|
||
customer=OuterRef('pk'), payStatus='false', sarresidDT__lte=now
|
||
).values('customer').annotate(s=Sum('price')).values('s')
|
||
|
||
sq_oldest_date = Sarresid.objects.filter(
|
||
customer=OuterRef('pk'), payStatus='false', sarresidDT__lte=now
|
||
).values('customer').annotate(min_dt=Min('sarresidDT')).values('min_dt')
|
||
|
||
sq_overdue_count = Sarresid.objects.filter(
|
||
customer=OuterRef('pk'), payStatus='false', sarresidDT__lte=now
|
||
).values('customer').annotate(c=Count('id')).values('c')
|
||
|
||
# استخراج آخرین سابقه اکشن وصول
|
||
latest_vosol_qs = Action.objects.filter(
|
||
customer=OuterRef('pk'), actionType='vosol'
|
||
).order_by('-createdDT')
|
||
|
||
# --- ۳. ساخت کوئری پایه (فقط مشتریان فعال و دارای معوقه) ---
|
||
base_qs = Customer.objects.filter(is_active=True).annotate(
|
||
t_debit=Coalesce(Subquery(sq_debit, output_field=IntegerField()), Value(0)),
|
||
t_credit=Coalesce(Subquery(sq_credit, output_field=IntegerField()), Value(0)),
|
||
overdue_amount=Coalesce(Subquery(sq_overdue_amt, output_field=IntegerField()), Value(0)),
|
||
overdue_count=Coalesce(Subquery(sq_overdue_count, output_field=IntegerField()), Value(0)),
|
||
oldest_overdue_date=Subquery(sq_oldest_date),
|
||
last_vosol_date=Subquery(latest_vosol_qs.values('createdDT')[:1]),
|
||
last_vosol_desc=Subquery(latest_vosol_qs.values('desc')[:1])
|
||
).annotate(
|
||
net_debt=F('t_debit') - F('t_credit')
|
||
).filter(
|
||
overdue_amount__gt=0 # شرط اساسی: فقط کسانی که معوقه سررسید گذشته دارند
|
||
)
|
||
|
||
# استخراج مشتریانی که نیازمند پیگیری هستند (هرگز پیگیری نشدهاند یا موعد پیگیریشان گذشته است)
|
||
action_needed_qs = base_qs.filter(
|
||
Q(last_vosol_date__isnull=True) | Q(last_vosol_date__lt=threshold_date)
|
||
)
|
||
|
||
# --- ۴. محاسبه آمار دقیق (بدون خطا) برای داشبورد ---
|
||
# استفاده از Case/When برای جلوگیری کامل از خطای None در Aggregate
|
||
stats = action_needed_qs.aggregate(
|
||
never_count=Coalesce(Sum(Case(When(last_vosol_date__isnull=True, then=1), default=Value(0), output_field=IntegerField())), Value(0)),
|
||
never_amount=Coalesce(Sum(Case(When(last_vosol_date__isnull=True, then=F('overdue_amount')), default=Value(0), output_field=IntegerField())), Value(0)),
|
||
expired_count=Coalesce(Sum(Case(When(last_vosol_date__isnull=False, then=1), default=Value(0), output_field=IntegerField())), Value(0)),
|
||
expired_amount=Coalesce(Sum(Case(When(last_vosol_date__isnull=False, then=F('overdue_amount')), default=Value(0), output_field=IntegerField())), Value(0)),
|
||
)
|
||
|
||
# --- ۵. اعمال فیلتر کشویی (Dropdown) ---
|
||
if filter_type == 'never':
|
||
final_qs = action_needed_qs.filter(last_vosol_date__isnull=True)
|
||
elif filter_type == 'expired':
|
||
final_qs = action_needed_qs.filter(last_vosol_date__isnull=False)
|
||
else: # all
|
||
final_qs = action_needed_qs
|
||
|
||
# --- ۶. اعمال مرتبسازی (Sorting) پویا ---
|
||
if sort_field == 'amount':
|
||
order_expr = F('overdue_amount').desc() if sort_dir == 'desc' else F('overdue_amount').asc()
|
||
elif sort_field == 'oldest_date':
|
||
order_expr = F('oldest_overdue_date').desc(nulls_last=True) if sort_dir == 'desc' else F('oldest_overdue_date').asc(nulls_last=True)
|
||
elif sort_field == 'last_action':
|
||
order_expr = F('last_vosol_date').desc(nulls_last=True) if sort_dir == 'desc' else F('last_vosol_date').asc(nulls_first=True)
|
||
else:
|
||
order_expr = F('overdue_amount').desc()
|
||
|
||
final_qs = final_qs.order_by(order_expr)
|
||
|
||
# --- ۷. صفحهبندی ایمن ---
|
||
paginator = Paginator(final_qs, 50)
|
||
page_number = request.GET.get('page', 1)
|
||
try:
|
||
page_obj = paginator.page(page_number)
|
||
except (PageNotAnInteger, EmptyPage):
|
||
page_obj = paginator.page(1)
|
||
|
||
context = {
|
||
'page_obj': page_obj,
|
||
'page_name': 'debtors_no_vosol',
|
||
'stats': stats,
|
||
# پارامترهای کنترل پنل
|
||
'current_days': days_threshold,
|
||
'current_filter': filter_type,
|
||
'current_sort': sort_field,
|
||
'current_dir': sort_dir,
|
||
}
|
||
|
||
return render(request, 'admin_panel/report_debtors_no_vosol.html', context)
|
||
|
||
|
||
@login_required(login_url='/panel/p_login/')
|
||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||
def inventory_movements_report(request):
|
||
"""
|
||
گزارش جامع گردش و خروج کالا از انبار به همراه فیلتر متنی نام کالا،
|
||
نوع تراکنش، کاربر، انبار و بازه زمانی شمسی.
|
||
"""
|
||
all_users = User.objects.all().order_by('first_name')
|
||
all_stores = StoreRoom.objects.all()
|
||
movement_choices = ProductInStore.MovementType.choices
|
||
|
||
# دریافت پارامترهای فیلتر از فرانتاند
|
||
selected_type = request.GET.get('movement_type', '')
|
||
selected_user = request.GET.get('user_id', '')
|
||
selected_store = request.GET.get('store_id', '')
|
||
selected_product_name = request.GET.get('product_name', '').strip() # فیلد جدید جستجوی نام کالا
|
||
az_tarikh = request.GET.get('azTarikh', '')
|
||
ta_tarikh = request.GET.get('taTarikh', '')
|
||
|
||
# ۱. کوئری پایه بهینه
|
||
movements_list = ProductInStore.objects.select_related(
|
||
'Product', 'StoreRoom', 'factor__customer', 'factor__user'
|
||
).filter(year=1400).order_by('-createdDT')
|
||
|
||
# ۲. اعمال فیلترهای پویا
|
||
if selected_type:
|
||
movements_list = movements_list.filter(movement_type=selected_type)
|
||
|
||
if selected_user:
|
||
movements_list = movements_list.filter(factor__user__id=_safe_int(selected_user))
|
||
|
||
if selected_store:
|
||
movements_list = movements_list.filter(StoreRoom__id=_safe_int(selected_store))
|
||
|
||
if selected_product_name:
|
||
# فیلتر متنی حساس نبودن به حروف بزرگ و کوچک (Case-Insensitive Contains)
|
||
movements_list = movements_list.filter(Product__product_name__icontains=selected_product_name)
|
||
|
||
# ۳. پردازش بازه زمانی
|
||
if az_tarikh:
|
||
try:
|
||
az_dt = datetime.datetime.fromtimestamp(int(az_tarikh) / 1000.0)
|
||
movements_list = movements_list.filter(createdDT__gte=az_dt.strftime('%Y-%m-%d 00:00:00'))
|
||
except (ValueError, TypeError):
|
||
pass
|
||
|
||
if ta_tarikh:
|
||
try:
|
||
ta_dt = datetime.datetime.fromtimestamp(int(ta_tarikh) / 1000.0)
|
||
movements_list = movements_list.filter(createdDT__lte=ta_dt.strftime('%Y-%m-%d 23:59:59'))
|
||
except (ValueError, TypeError):
|
||
pass
|
||
|
||
# ۴. محاسبه آماری سریع دیتابیس روی نتایج فیلتر شده
|
||
summary_stats = movements_list.aggregate(
|
||
total_out_qty=Sum('s_out'),
|
||
total_in_qty=Sum('s_in')
|
||
)
|
||
|
||
# ۵. صفحهبندی نتایج
|
||
paginator = Paginator(movements_list, 50)
|
||
page_number = request.GET.get('page')
|
||
page_obj = paginator.get_page(page_number)
|
||
|
||
context = {
|
||
'page_obj': page_obj,
|
||
'all_users': all_users,
|
||
'all_stores': all_stores,
|
||
'movement_choices': movement_choices,
|
||
'summary_stats': summary_stats,
|
||
'filters': {
|
||
'movement_type': selected_type,
|
||
'user_id': _safe_int(selected_user) if selected_user else '',
|
||
'store_id': _safe_int(selected_store) if selected_store else '',
|
||
'product_name': selected_product_name, # ارسال فیلد جدید به قالب برای حفظ مقدار ورودی
|
||
'azTarikh': az_tarikh,
|
||
'taTarikh': ta_tarikh,
|
||
}
|
||
}
|
||
|
||
return render(request, 'admin_panel/report_inventory_movements.html', context)
|
||
|
||
|
||
@login_required(login_url='/panel/p_login/')
|
||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||
def financial_dashboard(request):
|
||
"""
|
||
داشبورد هوش مالی و مدیریت نقدینگی (BI Dashboard)
|
||
مجهز به هسته حسابرسی ۳۶۰ درجه برای کشف تمامی مغایرتها (بدهی رو هوا، چکهای الکی، پیشدریافتها)
|
||
"""
|
||
az_tarikh = request.GET.get('azTarikh', '')
|
||
ta_tarikh = request.GET.get('taTarikh', '')
|
||
|
||
az_dt = None
|
||
ta_dt = None
|
||
|
||
if az_tarikh:
|
||
try:
|
||
az_dt = datetime.datetime.fromtimestamp(int(az_tarikh) / 1000.0)
|
||
az_dt = az_dt.replace(hour=0, minute=0, second=0)
|
||
except (ValueError, TypeError):
|
||
pass
|
||
|
||
if ta_tarikh:
|
||
try:
|
||
ta_dt = datetime.datetime.fromtimestamp(int(ta_tarikh) / 1000.0)
|
||
ta_dt = ta_dt.replace(hour=23, minute=59, second=59)
|
||
except (ValueError, TypeError):
|
||
pass
|
||
|
||
anchor_dt = ta_dt if ta_dt else datetime.datetime.now()
|
||
|
||
# =========================================================================
|
||
# محاسبه روند ۱۲ ماهه غلتان (TTM)
|
||
# =========================================================================
|
||
jalali_months = ['فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور', 'مهر', 'آبان', 'آذر', 'دی', 'بهمن', 'اسفند']
|
||
trend_labels = []
|
||
trend_data = []
|
||
|
||
anchor_jd = JalaliDate(anchor_dt.date())
|
||
curr_y = int(anchor_jd.year)
|
||
curr_m = int(anchor_jd.month)
|
||
|
||
for _ in range(12):
|
||
if curr_m < 1:
|
||
curr_m = 12
|
||
curr_y -= 1
|
||
|
||
label = f"{jalali_months[curr_m - 1]} {curr_y}"
|
||
trend_labels.insert(0, label)
|
||
|
||
last_day = JalaliDate.days_in_month(curr_m, curr_y)
|
||
end_of_month_jd = JalaliDate(curr_y, curr_m, last_day)
|
||
end_of_month_gd = end_of_month_jd.to_gregorian()
|
||
end_dt_str = end_of_month_gd.strftime('%Y-%m-%d 23:59:59')
|
||
|
||
totals = Article.objects.filter(createdDT__lte=end_dt_str).aggregate(
|
||
t_deb=Sum('debit'),
|
||
t_cred=Sum('credit')
|
||
)
|
||
t_deb = totals['t_deb'] or 0
|
||
t_cred = totals['t_cred'] or 0
|
||
month_total_debt = t_deb - t_cred
|
||
|
||
trend_data.insert(0, month_total_debt)
|
||
curr_m -= 1
|
||
|
||
# =========================================================================
|
||
# فیلترهای پایه (دادههای زماندار و داشبورد)
|
||
# =========================================================================
|
||
sarresid_qs = Sarresid.objects.filter(factor__productinstore__year=1400).distinct()
|
||
factor_qs = Factor.objects.filter(productinstore__year=1400, is_active=True).distinct()
|
||
article_qs = Article.objects.filter(factor__productinstore__year=1400).distinct()
|
||
store_qs = ProductInStore.objects.filter(year=1400)
|
||
|
||
if az_dt:
|
||
sarresid_qs = sarresid_qs.filter(createdDT__gte=az_dt)
|
||
factor_qs = factor_qs.filter(create_date__gte=az_dt)
|
||
article_qs = article_qs.filter(createdDT__gte=az_dt)
|
||
store_qs = store_qs.filter(createdDT__gte=az_dt)
|
||
|
||
if ta_dt:
|
||
sarresid_qs = sarresid_qs.filter(createdDT__lte=ta_dt)
|
||
factor_qs = factor_qs.filter(create_date__lte=ta_dt)
|
||
article_qs = article_qs.filter(createdDT__lte=ta_dt)
|
||
store_qs = store_qs.filter(createdDT__lte=ta_dt)
|
||
|
||
# کارتهای شاخص عملکرد عمومی
|
||
total_unpaid_debts = sarresid_qs.filter(payStatus="false").aggregate(Sum('price'))['price__sum'] or 0
|
||
total_sales_period = factor_qs.aggregate(Sum('amount'))['amount__sum'] or 0
|
||
overdue_checks_count = sarresid_qs.filter(payStatus="false", type="check", sarresidDT__lte=anchor_dt).count()
|
||
|
||
# =========================================================================
|
||
# 👑 هسته حسابرسی ۳۶۰ درجه (برگرفته از Ghost Hunter / Deep Audit)
|
||
# =========================================================================
|
||
|
||
# الف) تراز کل ریاضی سیستم
|
||
g_art = Article.objects.aggregate(d=Sum('debit'), c=Sum('credit'))
|
||
global_mande = (g_art['d'] or 0) - (g_art['c'] or 0)
|
||
g_sar = Sarresid.objects.filter(payStatus="false").aggregate(s=Sum('price'))['s'] or 0
|
||
global_diff = global_mande - g_sar
|
||
|
||
# ب) محاسبات Subquery فوقسریع برای تمام مشتریان
|
||
sq_debit = Article.objects.filter(customer=OuterRef('pk')).values('customer').annotate(s=Sum('debit')).values('s')
|
||
sq_credit = Article.objects.filter(customer=OuterRef('pk')).values('customer').annotate(s=Sum('credit')).values('s')
|
||
sq_sarresid = Sarresid.objects.filter(customer=OuterRef('pk'), payStatus="false").values('customer').annotate(s=Sum('price')).values('s')
|
||
|
||
base_customers = Customer.objects.filter(is_active=True).annotate(
|
||
t_debit=Coalesce(Subquery(sq_debit, output_field=IntegerField()), 0),
|
||
t_credit=Coalesce(Subquery(sq_credit, output_field=IntegerField()), 0),
|
||
t_sarresid=Coalesce(Subquery(sq_sarresid, output_field=IntegerField()), 0),
|
||
).annotate(
|
||
net_debt=F('t_debit') - F('t_credit') # تراز نهایی
|
||
).annotate(
|
||
diff=F('net_debt') - F('t_sarresid') # میزان کسر/اضافه سند
|
||
)
|
||
|
||
# 🔴 گروه 1: بدهکارانی که کسر سند/چک دارند (بدهی خطرناک روی هوا) [diff > 0]
|
||
group1_qs = base_customers.filter(diff__gt=0).order_by('-diff')
|
||
group1_count = group1_qs.count()
|
||
group1_total = sum(group1_qs.values_list('diff', flat=True)) or 0
|
||
group1_sample = group1_qs[:20]
|
||
|
||
# 🟡 گروه 2: چک پاسنشده در سیستم بیشتر از بدهی است (خطای ثبتی اپراتور) [diff < 0 AND net_debt >= 0]
|
||
group2_qs = base_customers.filter(diff__lt=0, net_debt__gte=0).annotate(
|
||
fake_amount=F('t_sarresid') - F('net_debt')
|
||
).order_by('-fake_amount')
|
||
group2_count = group2_qs.count()
|
||
group2_total = sum(group2_qs.values_list('fake_amount', flat=True)) or 0
|
||
group2_sample = group2_qs[:20]
|
||
|
||
# 🟢 گروه 3: پیشدریافت خالص (بستانکار است و هیچ چکی ندارد) [diff < 0 AND net_debt < 0 AND t_sarresid = 0]
|
||
group3_qs = base_customers.filter(diff__lt=0, net_debt__lt=0, t_sarresid=0).order_by('net_debt') # net_debt منفی است
|
||
group3_count = group3_qs.count()
|
||
group3_total = sum(group3_qs.values_list('net_debt', flat=True)) or 0
|
||
group3_sample = group3_qs[:20]
|
||
|
||
# 🟣 گروه 4: عجیبترین حالت! مشتری بستانکار است، اما چک پاسنشده هم در سیستم دارد! [diff < 0 AND net_debt < 0 AND t_sarresid > 0]
|
||
group4_qs = base_customers.filter(diff__lt=0, net_debt__lt=0, t_sarresid__gt=0).order_by('net_debt')
|
||
group4_count = group4_qs.count()
|
||
group4_total = sum(group4_qs.values_list('diff', flat=True)) or 0
|
||
group4_sample = group4_qs[:20]
|
||
|
||
# =========================================================================
|
||
# نمودارها و آمار تکمیلی داشبورد
|
||
# =========================================================================
|
||
debt_future = sarresid_qs.filter(payStatus="false", sarresidDT__gt=anchor_dt).aggregate(Sum('price'))['price__sum'] or 0
|
||
dt_30_days_ago = anchor_dt - datetime.timedelta(days=30)
|
||
debt_0_30 = sarresid_qs.filter(payStatus="false", sarresidDT__lte=anchor_dt, sarresidDT__gte=dt_30_days_ago).aggregate(Sum('price'))['price__sum'] or 0
|
||
dt_90_days_ago = anchor_dt - datetime.timedelta(days=90)
|
||
debt_30_90 = sarresid_qs.filter(payStatus="false", sarresidDT__lt=dt_30_days_ago, sarresidDT__gte=dt_90_days_ago).aggregate(Sum('price'))['price__sum'] or 0
|
||
debt_above_90 = sarresid_qs.filter(payStatus="false", sarresidDT__lt=dt_90_days_ago).aggregate(Sum('price'))['price__sum'] or 0
|
||
|
||
payment_articles = article_qs.filter(credit__gt=0).values('nahvePardakht').annotate(total_amount=Sum('credit')).order_by('-total_amount')
|
||
pay_labels = []
|
||
pay_data = []
|
||
for item in payment_articles:
|
||
label = item['nahvePardakht'] or 'سایر/نامشخص'
|
||
pay_labels.append(label)
|
||
pay_data.append(item['total_amount'])
|
||
|
||
store_leaks = store_qs.filter(
|
||
movement_type__in=[ProductInStore.MovementType.WARRANTY, ProductInStore.MovementType.PROMOTION]
|
||
).values('movement_type').annotate(total_qty=Sum('s_out'))
|
||
|
||
leak_data = {'WARRANTY': 0, 'PROMOTION': 0}
|
||
for leak in store_leaks:
|
||
leak_data[leak['movement_type']] = leak['total_qty']
|
||
|
||
context = {
|
||
'kpi_total_unpaid': total_unpaid_debts,
|
||
'kpi_total_sales': total_sales_period,
|
||
'kpi_overdue_checks': overdue_checks_count,
|
||
|
||
# آمار کلان ریاضیاتی
|
||
'global_mande': global_mande,
|
||
'g_sar': g_sar,
|
||
'global_diff': global_diff,
|
||
|
||
# متغیرهای ۴ گروه حسابرسی
|
||
'group1_count': group1_count, 'group1_total': group1_total, 'group1_sample': group1_sample,
|
||
'group2_count': group2_count, 'group2_total': group2_total, 'group2_sample': group2_sample,
|
||
'group3_count': group3_count, 'group3_total': group3_total, 'group3_sample': group3_sample,
|
||
'group4_count': group4_count, 'group4_total': group4_total, 'group4_sample': group4_sample,
|
||
|
||
'trend_labels': trend_labels,
|
||
'trend_data': trend_data,
|
||
|
||
'aging_data': [debt_future, debt_0_30, debt_30_90, debt_above_90],
|
||
'pay_labels': pay_labels,
|
||
'pay_data': pay_data,
|
||
'warranty_qty': leak_data['WARRANTY'],
|
||
'promotion_qty': leak_data['PROMOTION'],
|
||
|
||
'filters': {
|
||
'azTarikh': az_tarikh,
|
||
'taTarikh': ta_tarikh,
|
||
}
|
||
}
|
||
|
||
return render(request, 'admin_panel/financial_dashboard.html', context)
|
||
|
||
|
||
@login_required(login_url='/panel/p_login/')
|
||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||
def bedehi(request):
|
||
try:
|
||
debit = Article.objects.filter().aggregate(Sum('debit'))['debit__sum']
|
||
credit = Article.objects.filter().aggregate(Sum('credit'))['credit__sum']
|
||
mandehesabCs = debit - credit
|
||
articls = Article.objects.filter().order_by('-createdDT')
|
||
articlsPaginator = Paginator(articls, 10)
|
||
pageNumber = request.GET.get("page", 1)
|
||
articl_objs = articlsPaginator.get_page(pageNumber)
|
||
|
||
except:
|
||
articls =None
|
||
mandehesabCs = 0
|
||
|
||
data = {'articls': articl_objs, 'mandehesabKP': mandehesabCs}
|
||
return render(request, 'admin_panel/bedehi.html', data)
|
||
|
||
|
||
@login_required(login_url='/panel/p_login/')
|
||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||
def hesabdari(request):
|
||
page_name = 'sarresidShodeha'
|
||
anchor_dt = datetime.datetime.now()
|
||
|
||
# تعریف محدودههای زمانی ماتریس ریسک (سن مطالبات)
|
||
dt_30_days_ago = anchor_dt - datetime.timedelta(days=30)
|
||
dt_90_days_ago = anchor_dt - datetime.timedelta(days=90)
|
||
|
||
# کوئری پایه: تمام سررسیدهای پرداختنشده که از موعد آنها گذشته است
|
||
base_qs = Sarresid.objects.filter(payStatus="false", sarresidDT__lte=anchor_dt)
|
||
|
||
# محاسبه پرسرعت مجموع مبالغ در هر دسته (توسط موتور دیتابیس)
|
||
matrix_totals = base_qs.aggregate(
|
||
minor_delay=Sum('price', filter=Q(sarresidDT__gte=dt_30_days_ago)),
|
||
high_risk=Sum('price', filter=Q(sarresidDT__lt=dt_30_days_ago, sarresidDT__gte=dt_90_days_ago)),
|
||
critical=Sum('price', filter=Q(sarresidDT__lt=dt_90_days_ago)),
|
||
total_sum=Sum('price')
|
||
)
|
||
|
||
# دریافت فیلتر انتخابی کاربر از URL
|
||
risk_filter = request.GET.get('risk_level', 'all')
|
||
|
||
# اعمال فیلتر روی لیست نمایش داده شده
|
||
if risk_filter == 'minor':
|
||
filtered_qs = base_qs.filter(sarresidDT__gte=dt_30_days_ago)
|
||
elif risk_filter == 'high':
|
||
filtered_qs = base_qs.filter(sarresidDT__lt=dt_30_days_ago, sarresidDT__gte=dt_90_days_ago)
|
||
elif risk_filter == 'critical':
|
||
filtered_qs = base_qs.filter(sarresidDT__lt=dt_90_days_ago)
|
||
else:
|
||
filtered_qs = base_qs
|
||
|
||
# مرتبسازی: قدیمیترینها (خطرناکترینها) در صدر جدول قرار میگیرند
|
||
filtered_qs = filtered_qs.order_by('-sarresidDT')
|
||
|
||
page_number = request.GET.get('page', 1)
|
||
paginator = Paginator(filtered_qs, per_page=50)
|
||
all_sarresid_pgi = paginator.get_page(page_number)
|
||
|
||
data = {
|
||
'page_name': page_name,
|
||
'dateNow': anchor_dt.strftime('%Y-%m-%d %H:%M:%S'),
|
||
'all_sarresid': all_sarresid_pgi,
|
||
'current_filter': risk_filter,
|
||
# متغیرهای ماتریس ریسک
|
||
'sum_sarresid': matrix_totals['total_sum'] or 0,
|
||
'minor_sum': matrix_totals['minor_delay'] or 0,
|
||
'high_sum': matrix_totals['high_risk'] or 0,
|
||
'critical_sum': matrix_totals['critical'] or 0,
|
||
}
|
||
|
||
return render(request, 'admin_panel/hesabdari.html', data)
|
||
|
||
|
||
@login_required(login_url='/panel/p_login/')
|
||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||
def hesabdari_checkPass(request):
|
||
|
||
page_name = 'checkPass'
|
||
|
||
dateNow = datetime.datetime.today().strftime('%Y-%m-%d 23:59:59')
|
||
all_sarresid = Sarresid.objects.filter(payStatus="true", type='check').order_by('-sarresidDT')
|
||
|
||
page_number = request.GET.get('page', None)
|
||
paginator = Paginator(all_sarresid, per_page=50)
|
||
|
||
all_sarresid_pgi = paginator.get_page(page_number)
|
||
|
||
#paginator = Paginator(all_sarresid, per_page=200)
|
||
|
||
data = {'all_sarresid': all_sarresid_pgi, "dateNow":dateNow, "page_name":page_name}
|
||
return render(request, 'admin_panel/hesabdari.html', data)
|
||
|
||
|
||
@login_required(login_url='/panel/p_login/')
|
||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||
def hesabdari_checkPassFail(request):
|
||
|
||
page_name = 'checkPassFaild'
|
||
|
||
dateNow = datetime.datetime.today().strftime('%Y-%m-%d 23:59:59')
|
||
all_sarresid = Sarresid.objects.filter(payStatus="false", type='check').order_by('sarresidDT')
|
||
|
||
count_check_passfail = all_sarresid.count()
|
||
|
||
sum_check_passfail = all_sarresid.aggregate(Sum('price'))['price__sum']
|
||
|
||
page_number = request.GET.get('page', None)
|
||
paginator = Paginator(all_sarresid, per_page=50)
|
||
|
||
all_sarresid_pgi = paginator.get_page(page_number)
|
||
|
||
#paginator = Paginator(all_sarresid, per_page=200)
|
||
|
||
data = {'all_sarresid': all_sarresid_pgi, 'sum_check_passfail':sum_check_passfail, 'count_check_passfail': count_check_passfail, "dateNow":dateNow, "page_name":page_name}
|
||
return render(request, 'admin_panel/hesabdari.html', data)
|
||
|
||
|
||
@login_required(login_url='/panel/p_login/')
|
||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||
def hesabdari_residnashone(request):
|
||
|
||
page_name = 'sarresidNaShodeha'
|
||
|
||
dateNow = datetime.datetime.today().strftime('%Y-%m-%d 23:59:59')
|
||
all_sarresid = Sarresid.objects.filter(payStatus="false", sarresidDT__gte=dateNow).order_by('sarresidDT')
|
||
|
||
sarresidNashode = all_sarresid.aggregate(Sum('price'))['price__sum']
|
||
page_number = request.GET.get('page', None)
|
||
paginator = Paginator(all_sarresid, per_page=50)
|
||
|
||
all_sarresid_pgi = paginator.get_page(page_number)
|
||
|
||
#paginator = Paginator(all_sarresid, per_page=200)
|
||
|
||
data = {'sarresidNashode': sarresidNashode,'all_sarresid': all_sarresid_pgi, "dateNow":dateNow, "page_name":page_name}
|
||
return render(request, 'admin_panel/hesabdari.html', data)
|
||
|
||
|
||
@login_required(login_url='/panel/p_login/')
|
||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||
def bedehiKhadamat(request):
|
||
#bedehiKhadamat
|
||
try:
|
||
debit = Article.objects.filter(typePy='khadamat').aggregate(Sum('debit'))['debit__sum']
|
||
credit = Article.objects.filter(typePy='khadamat').aggregate(Sum('credit'))['credit__sum']
|
||
mandehesabCs = debit - credit
|
||
articls = Article.objects.filter(typePy='khadamat').order_by('-createdDT')[:100]
|
||
except:
|
||
articls =None
|
||
mandehesabCs = 0
|
||
|
||
data = {'articls': articls, 'mandehesabKP': mandehesabCs}
|
||
return render(request, 'admin_panel/bedehi.html', data)
|
||
|
||
|
||
@login_required(login_url='/panel/p_login/')
|
||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||
def bedehiNasb(request):
|
||
try:
|
||
debit = Article.objects.filter(typePy='nasb').aggregate(Sum('debit'))['debit__sum']
|
||
credit = Article.objects.filter(typePy='nasb').aggregate(Sum('credit'))['credit__sum']
|
||
mandehesabCs = debit - credit
|
||
articls = Article.objects.filter(typePy='nasb').order_by('-createdDT')[:100]
|
||
except:
|
||
articls =None
|
||
mandehesabCs = 0
|
||
|
||
data = {'articls': articls, 'mandehesabKP': mandehesabCs}
|
||
return render(request, 'admin_panel/bedehi.html', data)
|
||
|
||
|
||
@login_required(login_url='/panel/p_login/')
|
||
@permission_required('admin_panel.ViewStore', raise_exception=True)
|
||
def passCheck(request, articl_id):
|
||
|
||
sarresidINC = Sarresid.objects.get(id=articl_id)
|
||
user_op = request.user
|
||
|
||
desc = request.POST.get('message')
|
||
|
||
articleHesabDari2 = Article()
|
||
articleHesabDari2.debit = 0
|
||
articleHesabDari2.credit = sarresidINC.price
|
||
articleHesabDari2.customer = sarresidINC.customer
|
||
articleHesabDari2.factor = sarresidINC.factor
|
||
articleHesabDari2.desc = 'وصول چک' + " " + str(sarresidINC.customer.name_family)
|
||
if sarresidINC.factor:
|
||
articleHesabDari2.desc += " فاکتور شماره: " + str(sarresidINC.factor.id)
|
||
articleHesabDari2.nahvePardakht = 'وصول چک'
|
||
articleHesabDari2.payStatus = "true"
|
||
articleHesabDari2.user = user_op
|
||
articleHesabDari2.save()
|
||
|
||
sarresidINC.payStatus = "true"
|
||
sarresidINC.desc = desc
|
||
sarresidINC.save()
|
||
|
||
# سند وصول (credit>0 و debit=0) یعنی مشتری پول داده ⇒ پیامک تشکر از پرداخت.
|
||
notify_payment_thanks(articleHesabDari2.customer, articleHesabDari2.credit,
|
||
article_id=articleHesabDari2.id,
|
||
method=articleHesabDari2.nahvePardakht, created_by=user_op)
|
||
|
||
return HttpResponse("ok")
|
||
#return redirect(request.META.get('HTTP_REFERER'))
|
||
|
||
|
||
@login_required(login_url='/panel/p_login/')
|
||
@permission_required('admin_panel.ViewStore', raise_exception=True)
|
||
@require_POST
|
||
def update_check_info(request, sarresid_id):
|
||
# دریافت رکورد مورد نظر
|
||
sarresid = get_object_or_404(Sarresid, id=sarresid_id)
|
||
|
||
# اطمینان از اینکه آیتم حتما چک است
|
||
if sarresid.type != 'check':
|
||
messages.error(request, "عملیات نامعتبر: این سند چک نیست.")
|
||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||
|
||
# دریافت مقادیر فرم با حذف فاصلههای اضافی
|
||
check_row_number = request.POST.get('check_row_number', '').strip()
|
||
check_sayyad_id = request.POST.get('check_sayyad_id', '').strip()
|
||
check_owner_name = request.POST.get('check_owner_name', '').strip()
|
||
check_owner_phone = request.POST.get('check_owner_phone', '').strip()
|
||
|
||
# اعتبارسنجی فیلدهای اجباری
|
||
if not check_row_number or not check_sayyad_id or not check_owner_name:
|
||
messages.error(request, "شماره ردیف، شناسه صیادی و نام مالک اجباری است.")
|
||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||
|
||
# اعتبارسنجی شناسه صیادی (باید دقیقا ۱۶ رقم عدد باشد)
|
||
if not check_sayyad_id.isdigit() or len(check_sayyad_id) != 16:
|
||
messages.error(request, "شناسه صیادی باید دقیقاً ۱۶ رقم عدد باشد.")
|
||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||
|
||
# اعتبارسنجی شماره تلفن (در صورت وارد شدن باید فقط عدد باشد)
|
||
if check_owner_phone and not check_owner_phone.isdigit():
|
||
messages.error(request, "شماره تلفن مالک چک باید فقط شامل اعداد باشد.")
|
||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||
|
||
try:
|
||
# بروزرسانی اطلاعات
|
||
sarresid.check_row_number = check_row_number
|
||
sarresid.check_sayyad_id = check_sayyad_id
|
||
sarresid.check_owner_name = check_owner_name
|
||
sarresid.check_owner_phone = check_owner_phone
|
||
# آپدیت فیلد توضیحات با اطلاعات جدید
|
||
sarresid.desc = f"صیادی: {check_sayyad_id} | مالک: {check_owner_name} | تلفن: {check_owner_phone or 'ندارد'}"
|
||
|
||
sarresid.save()
|
||
messages.success(request, "اطلاعات چک با موفقیت ثبت/بروزرسانی شد.")
|
||
except Exception as e:
|
||
messages.error(request, "خطای سیستمی در بروزرسانی اطلاعات رخ داد.")
|
||
|
||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||
|
||
|
||
@login_required(login_url='/panel/p_login/')
|
||
@permission_required('admin_panel.ViewStore', raise_exception=True)
|
||
def addSarresid(request, cs_id):
|
||
if request.method != 'POST':
|
||
return HttpResponseBadRequest("متد ارسالی نامعتبر است.")
|
||
|
||
try:
|
||
customerObj = get_object_or_404(Customer, id=cs_id)
|
||
|
||
# دریافت فیلدهای اصلی
|
||
sarrresidUNIX = request.POST.get('n_sarresidDateTime')
|
||
sarresidPrice = request.POST.get('n_sarresidPrice')
|
||
sarresidType = request.POST.get('n_sarresidType')
|
||
|
||
# دریافت و پاکسازی فیلدهای چک (جلوگیری از باگ فاصلههای خالی)
|
||
check_row_number = request.POST.get('check_row_number', '').strip()
|
||
check_sayyad_id = request.POST.get('check_sayyad_id', '').strip()
|
||
check_owner_name = request.POST.get('check_owner_name', '').strip()
|
||
check_owner_phone = request.POST.get('check_owner_phone', '').strip()
|
||
|
||
# --- لایه اول: اعتبارسنجی فیلدهای پایه ---
|
||
if not sarrresidUNIX or not sarresidPrice or not sarresidType:
|
||
messages.error(request, "لطفا فیلدهای اصلی (مبلغ، نوع و تاریخ) را پر کنید.")
|
||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||
|
||
# --- لایه دوم: اعتبارسنجی دقیق و امنیتی فیلدهای چک ---
|
||
if sarresidType == "check":
|
||
if not check_row_number or not check_sayyad_id or not check_owner_name:
|
||
messages.error(request, "برای ثبت چک، وارد کردن شماره ردیف، شناسه صیادی و نام مالک اجباری است.")
|
||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||
|
||
# بررسی ۱۶ رقمی و عددی بودن شناسه صیادی
|
||
if not check_sayyad_id.isdigit() or len(check_sayyad_id) != 16:
|
||
messages.error(request, "شناسه صیادی باید دقیقاً ۱۶ رقم عدد باشد.")
|
||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||
|
||
# بررسی عددی بودن شماره تلفن (در صورت وارد شدن)
|
||
if check_owner_phone and not check_owner_phone.isdigit():
|
||
messages.error(request, "شماره تلفن مالک چک باید فقط شامل اعداد باشد.")
|
||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||
|
||
# تبدیل نوع دادهها با مدیریت خطا (جلوگیری از کرش سرور 500)
|
||
try:
|
||
sarResidDateTime01 = datetime.datetime.fromtimestamp(int(sarrresidUNIX) / 1000.0)
|
||
price_int = int(sarresidPrice.replace(',', ''))
|
||
except ValueError:
|
||
messages.error(request, "مقادیر تاریخ یا مبلغ نامعتبر است.")
|
||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||
|
||
# --- لایه سوم: ذخیرهسازی اتمیک (تضمین جامعیت دادهها) ---
|
||
with transaction.atomic():
|
||
# ۱. ثبت سند حسابداری
|
||
articleHesabDariObj = Article()
|
||
articleHesabDariObj.debit = price_int
|
||
articleHesabDariObj.credit = 0
|
||
articleHesabDariObj.customer = customerObj
|
||
articleHesabDariObj.user = request.user
|
||
|
||
if sarresidType == "check":
|
||
articleHesabDariObj.desc = f"بدهی بابت ثبت چک دستی (صیادی: {check_sayyad_id})"
|
||
elif sarresidType == "nesiye":
|
||
articleHesabDariObj.desc = "بدهی بابت ثبت نسیه دستی"
|
||
|
||
articleHesabDariObj.save()
|
||
|
||
# ۲. ثبت سررسید / چک
|
||
sarresidINC = Sarresid()
|
||
sarresidINC.customer = customerObj
|
||
sarresidINC.price = price_int
|
||
sarresidINC.payStatus = "false"
|
||
sarresidINC.type = sarresidType
|
||
sarresidINC.user = request.user
|
||
sarresidINC.sarresidDT = sarResidDateTime01
|
||
|
||
if sarresidType == "check":
|
||
sarresidINC.check_row_number = check_row_number
|
||
sarresidINC.check_sayyad_id = check_sayyad_id
|
||
sarresidINC.check_owner_name = check_owner_name
|
||
sarresidINC.check_owner_phone = check_owner_phone
|
||
sarresidINC.desc = f"صیادی: {check_sayyad_id} | مالک: {check_owner_name} | تلفن: {check_owner_phone or 'ندارد'}"
|
||
|
||
sarresidINC.save()
|
||
|
||
messages.success(request, "اطلاعات با موفقیت ثبت شد.")
|
||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in addSarresid: {str(e)}", exc_info=True)
|
||
messages.error(request, "خطای سیستمی رخ داد. لطفا مجددا تلاش کنید.")
|
||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||
|
||
|
||
@login_required(login_url='/panel/p_login/')
|
||
@permission_required('admin_panel.ViewStore', raise_exception=True)
|
||
def pardakhtNesiye(request, cs_id):
|
||
try:
|
||
customerObj = Customer.objects.get(id=cs_id)
|
||
user_op = request.user
|
||
|
||
sarrresidUNIX = request.POST.get('next_sarresidDateTime', None)
|
||
sarresidPrice = int(request.POST.get('next_sarresidPrice', None))
|
||
sr_articlID = request.POST.get('sr_articlID', None)
|
||
nahvePardakht = request.POST.get('nahvePardakht', None)
|
||
desc_sar = request.POST.get('desc_sar', None)
|
||
|
||
sarResidDateTime01 = datetime.datetime.fromtimestamp(int(sarrresidUNIX)/1000.0)
|
||
|
||
SarresidObj = Sarresid.objects.get(id=sr_articlID)
|
||
#return HttpResponse(sarresidPrice)
|
||
debitNow = SarresidObj.price - sarresidPrice
|
||
#return HttpResponse(debitNow)
|
||
|
||
if debitNow==0:
|
||
#توسیه بدهی بخورد
|
||
SarresidObj.price = 0
|
||
SarresidObj.payStatus = "true"
|
||
elif debitNow >= 1:
|
||
SarresidObj.price = debitNow
|
||
SarresidObj.sarresidDT = sarResidDateTime01
|
||
else:
|
||
return HttpResponse("خطا در مورد مبلغ")
|
||
SarresidObj.save()
|
||
|
||
|
||
if str(nahvePardakht) == 'naghd':
|
||
nahvePardakht = 'نقد'
|
||
elif str(nahvePardakht) == 'kartbekart':
|
||
nahvePardakht = 'کارت به کارت'
|
||
elif str(nahvePardakht) == 'kartkhan':
|
||
nahvePardakht = 'کارت خوان'
|
||
elif str(nahvePardakht) == 'takhfif':
|
||
nahvePardakht = 'تخفیف'
|
||
elif str(nahvePardakht) == 'check':
|
||
nahvePardakht = 'چک'
|
||
|
||
articleHesabDariObj = Article()
|
||
articleHesabDariObj.debit = 0
|
||
articleHesabDariObj.credit = int(sarresidPrice)
|
||
|
||
if desc_sar:
|
||
articleHesabDariObj.desc = "پرداختی بابت نسیه" + " | " + desc_sar
|
||
else:
|
||
articleHesabDariObj.desc = "پرداختی بابت نسیه"
|
||
|
||
articleHesabDariObj.customer = customerObj
|
||
articleHesabDariObj.nahvePardakht = nahvePardakht
|
||
articleHesabDariObj.user = user_op
|
||
articleHesabDariObj.save()
|
||
|
||
# پرداخت نسیه (بستانکار) ⇒ پیامک تشکر از پرداخت (تخفیف مستثناست).
|
||
notify_payment_thanks(customerObj, articleHesabDariObj.credit,
|
||
article_id=articleHesabDariObj.id,
|
||
method=nahvePardakht, created_by=user_op)
|
||
|
||
return redirect(request.META.get('HTTP_REFERER'))
|
||
|
||
except:
|
||
return HttpResponse("خطا در ثبت ")
|
||
|
||
|
||
@login_required(login_url='/panel/p_login/')
|
||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||
def rezayat_dashboard(request):
|
||
# دریافت تمامی نظرسنجیهای ثبت شده معتبر (که تاریخ نظرسنجی دارند)
|
||
valid_surveys = Action.objects.filter(
|
||
nazarDT__isnull=False,
|
||
grade__in=[5, 10, 15, 20]
|
||
)
|
||
|
||
# ۱. محاسبات شاخصهای کلیدی (KPIs)
|
||
total_surveys = valid_surveys.count()
|
||
avg_grade = valid_surveys.aggregate(Avg('grade'))['grade__avg'] or 0
|
||
|
||
# ۲. استخراج دادهها برای نمودار توزیع رضایت
|
||
grade_distribution = valid_surveys.values('grade').annotate(count=Count('id'))
|
||
chart_dict = {20: 0, 15: 0, 10: 0, 5: 0}
|
||
for item in grade_distribution:
|
||
g = item['grade']
|
||
if g in chart_dict:
|
||
chart_dict[g] = item['count']
|
||
|
||
chart_labels = ['عالی (۲۰)', 'متوسط (۱۵)', 'ضعیف (۱۰)', 'بد (۵)']
|
||
chart_data_list = [chart_dict[20], chart_dict[15], chart_dict[10], chart_dict[5]]
|
||
|
||
# ۳. تبدیل امن آرایهها به JSON برای Chart.js
|
||
chart_labels_json = json.dumps(chart_labels, ensure_ascii=False)
|
||
chart_data_json = json.dumps(chart_data_list)
|
||
has_chart_data = sum(chart_data_list) > 0
|
||
|
||
# ۴. واکشی لیست مشتریان نیازمند پیگیری (شامل متوسط ۱۵، ضعیف ۱۰ و بد ۵)
|
||
low_score_actions = Action.objects.filter(
|
||
nazarDT__isnull=False,
|
||
grade__in=[5, 10, 15] # <-- امتیاز ۱۵ (متوسط) نیز اضافه شد
|
||
).order_by('-nazarDT')
|
||
|
||
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
|
||
|
||
context = {
|
||
'total_surveys': total_surveys,
|
||
'avg_grade': round(avg_grade, 1),
|
||
'chart_labels_json': chart_labels_json,
|
||
'chart_data_json': chart_data_json,
|
||
'has_chart_data': has_chart_data,
|
||
'low_score_actions': low_score_actions,
|
||
# تعداد کل موارد نیازمند پیگیری (متوسط + ضعیف + بد)
|
||
'unhappy_count': chart_dict[15] + chart_dict[10] + chart_dict[5],
|
||
'allUsers': allUsers,
|
||
}
|
||
|
||
return render(request, 'admin_panel/report_rezayat.html', context)
|