diff --git a/admin_panel/templates/admin_panel/base.html b/admin_panel/templates/admin_panel/base.html index 0f6f888..54d1185 100644 --- a/admin_panel/templates/admin_panel/base.html +++ b/admin_panel/templates/admin_panel/base.html @@ -94,6 +94,9 @@ گزارش + + گزارش وصول مطالبات + گزارش جامع انبار diff --git a/admin_panel/templates/admin_panel/report_debtors_no_vosol.html b/admin_panel/templates/admin_panel/report_debtors_no_vosol.html new file mode 100644 index 0000000..0b50158 --- /dev/null +++ b/admin_panel/templates/admin_panel/report_debtors_no_vosol.html @@ -0,0 +1,288 @@ +{% extends "./base.html" %} +{% load custom_tags %} + +{% if perms.admin_panel.ViewReportMali %} +{% block bodyPage %} + + +
+
+
+ +
+

+ + داشبورد هوشمند وصول مطالبات (رها شده) +

+ + +
+
+
+

مشتریان هرگز پیگیری نشده

+

{{ stats.never_count }} نفر

+
+
+
+
+

مبلغ (هرگز پیگیری نشده)

+

{{ stats.never_amount }}

+
+
+
+
+

رها شده (بیش از {{ current_days }} روز)

+

{{ stats.expired_count }} نفر

+
+
+
+
+

مبلغ (پیگیری رها شده)

+

{{ stats.expired_amount }}

+
+
+
+
+ + +
+
+ + + +
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+ +
+
+
+
+
+ + +
+ +
+
+
+
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + {% for customer in page_obj %} + + + + + + + + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
#نام مشتریشماره تماسمانده کل حساب + + مبلغ معوقه + + + + + تاریخ قدیمی‌ترین تاخیر + + + + + آخرین سابقه وصول + + + عملیات
{{ forloop.counter0|add:page_obj.start_index }} + + {{ customer.name_family }} + + +
{{ customer.phone1|default:"-" }}
+ {% if customer.phone2 %}
{{ customer.phone2 }}
{% endif %} +
+ {{ customer.net_debt }} + + + {{ customer.overdue_amount }} + +
+ {{ customer.overdue_count }} فقره +
+ {{ customer.oldest_overdue_date | toJalali }} + + {% if customer.last_vosol_date %} +
+ {{ customer.last_vosol_date | toJalali }} +
+
+ {{ customer.last_vosol_desc|default:"(بدون توضیحات ثبت شده)" }} +
+ {% else %} + هرگز پیگیری نشده + {% endif %} +
+ + + + ثبت تماس + +
+
+ با این تنظیمات و فیلترها، هیچ مشتری بدهکار رها شده‌ای یافت نشد. +
+
+ + +
+
+ {% if page_obj.has_previous %} + صفحه قبل + {% endif %} + {% if page_obj.has_next %} + صفحه بعد + {% endif %} +
+
+ +
+
+
+{% endblock %} + +{% block jsEnd %} + +{% endblock %} +{% endif %} \ No newline at end of file diff --git a/admin_panel/urls.py b/admin_panel/urls.py index 0df434d..b55d4ec 100644 --- a/admin_panel/urls.py +++ b/admin_panel/urls.py @@ -13,6 +13,7 @@ urlpatterns = [ path('report/', views.report, name='report'), path('report/inventory-movements/', views.inventory_movements_report, name='inventory_movements_report'), path('report/dashboard/', views.financial_dashboard, name='financial_dashboard'), + path('report/debtors-no-vosol/', views.report_debtors_no_vosol, name='report_debtors_no_vosol'), path('report/rezayat_dashboard/', views.rezayat_dashboard, name='rezayat_dashboard'), path('hesabdari/', views.hesabdari, name='hesabdari'), path('hesabdari/sarresid_nashode/', views.hesabdari_residnashone, name='hesabdari_residnashone'), diff --git a/admin_panel/views.py b/admin_panel/views.py index 9b1a3dd..17f413b 100644 --- a/admin_panel/views.py +++ b/admin_panel/views.py @@ -22,7 +22,7 @@ from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest # بخشBadRequest جدید from django.db import transaction, connection -from django.db.models import Count, Sum, Q, Avg, F, Subquery, OuterRef, IntegerField +from django.db.models import Count, Sum, Q, Avg, F, Subquery, OuterRef, IntegerField, Exists, Min, Max, Case, When from django.contrib.auth.models import Permission, User from django.contrib.auth import authenticate, login, logout from django.contrib.auth.decorators import login_required, permission_required @@ -196,6 +196,117 @@ def report(request): 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):