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)