From bd775d6249007e1fd8fbb298b6cab73b577be174 Mon Sep 17 00:00:00 2001 From: javad Date: Tue, 30 Jun 2026 20:25:39 +0330 Subject: [PATCH] hesabdari va dashboard --- admin_panel/templates/admin_panel/base.html | 4 + .../admin_panel/financial_dashboard.html | 372 ++++++++++++++ .../templates/admin_panel/hesabdari.html | 469 +++++++++--------- admin_panel/urls.py | 1 + admin_panel/views.py | 206 +++++++- 5 files changed, 808 insertions(+), 244 deletions(-) create mode 100644 admin_panel/templates/admin_panel/financial_dashboard.html diff --git a/admin_panel/templates/admin_panel/base.html b/admin_panel/templates/admin_panel/base.html index aef92a6..1e8ee02 100644 --- a/admin_panel/templates/admin_panel/base.html +++ b/admin_panel/templates/admin_panel/base.html @@ -99,6 +99,10 @@ گزارش جامع انبار + + + داشبورد مدیریتی + diff --git a/admin_panel/templates/admin_panel/financial_dashboard.html b/admin_panel/templates/admin_panel/financial_dashboard.html new file mode 100644 index 0000000..71c5393 --- /dev/null +++ b/admin_panel/templates/admin_panel/financial_dashboard.html @@ -0,0 +1,372 @@ +{% extends "./base.html" %} +{% load custom_tags %} + +{% block bodyPage %} +
+ +
+
+

اتاق فرمان مالی و داشبورد تجاری

+

رصد زنده شریان نقدینگی، سن مطالبات و مانیتورینگ پویای انبار | سال مالی

+
+ +
+
+
+
+ + + +
+
+ + + +
+
+ +
+ {% if filters.azTarikh or filters.taTarikh %} + + {% endif %} +
+
+
+
+ +
+
+
+
+
+
+

کل مطالبات معوق بازار (سرمایه قفل شده)

+

{{ kpi_total_unpaid }}

+
+
+
+
+
+
+
+
+
+
+

حجم ناخالص فروش در بازه انتخابی

+

{{ kpi_total_sales }}

+
+
+
+
+
+
+
+
+
+
+

تعداد چک‌های سررسیدگذشته و برگشتی

+

{{ kpi_overdue_checks }} فقره

+
+
+
+
+
+
+ +
+
+
+
+
+

📈 روند جمع کل بدهی مشتریان در ۱۲ ماه گذشته

+
+
+ محاسبه بر اساس ماهیانه +
+
+
+ +
+
+
+
+ +
+ +
+
+

+ 📊 ماتریس ریسک و تحلیل سنی مطالبات +

+
+ +
+
+ 📌 سیگنال مدیریتی: مبالغ انباشته شده در بخش بحرانی (قرمز) پتانسیل بالایی برای تبدیل شدن به سوخت سرمایه دارند. +
+
+
+ +
+
+

+ 💳 آنالیز کانال‌های مالی ورودی نقدینگی +

+
+ +
+
+
+ +
+
+

+ 📦 رادار هزینه‌های جانبی کالا (مقایسه آماری خروج گارانتی در برابر اشانتیون) +

+
+
+
+ +
+
+
+
+

🔍 خلاصه وضعیت خروجی‌های غیردرآمدی انبار:

+
+ حجم کالای مصرفی در تعهدات گارانتی + {{ warranty_qty }} عدد +
+
+ حجم کالای مصرفی در جوایز و اشانتیون + {{ promotion_qty }} عدد +
+

+ 💡 راهبرد نظارتی انبار: نوسان شدید در این دو نمودار به مدیر کمک می‌کند تا متوجه افت کیفیت در محصولات تامین‌شده (جهش گارانتی) یا توزیع بی‌ضابطه و خارج از فاکتور توسط اپراتورها (جهش اشانتیون) شود. +

+
+
+
+
+
+ +
+
+ + +{% endblock %} + +{% block jsEnd %} + + + +{% endblock %} \ No newline at end of file diff --git a/admin_panel/templates/admin_panel/hesabdari.html b/admin_panel/templates/admin_panel/hesabdari.html index c7cd2ed..340982b 100644 --- a/admin_panel/templates/admin_panel/hesabdari.html +++ b/admin_panel/templates/admin_panel/hesabdari.html @@ -3,236 +3,248 @@ {% if perms.admin_panel.ViewReportMali %} {% block bodyPage %} -
-
-
-
+ -
- -

- {% if page_name == 'sarresidShodeha' %} -

- سررسید شده ها - {{ sum_sarresid }} -

- {% endif %} - {% if page_name == 'sarresidNaShodeha' %} -

- سررسید نشده ها - {{ sarresidNashode }} -

- {% endif %} - {% if page_name == 'checkPass' %} - چک های پاس شده - {% endif %} - {% if page_name == 'checkPassFaild' %} - -

- چک های پاس نشده - {{ count_check_passfail }} - عدد | جمع مبلغ: - {{ sum_check_passfail }} -

- {% endif %} - - {{ mandehesabKP }} -

- -
- - - - - - - - - - - +
+
+
- -
- - - - - - - - - - - - - - - - - {% for sarresid in all_sarresid %} - - - - - - - - - - - - - - - - {% endfor %} - -
مبلغنامنوعسررسیدثبتتوضیحاتوضعیتوصول شد
مبلغنامنوعسررسیدثبتتوضیحاتوضعیتوصول شد
- {{ sarresid.price }} - - - - {{ sarresid.customer.name_family }} - - - {% if sarresid.type == 'check' %}چک{% endif %} - {% if sarresid.type == 'nesiye' %}نسیه{% endif %} - - {{ sarresid.sarresidDT | toJalali }}{{ sarresid.createdDT | toJalali }} - {% if sarresid.desc %} - {{ sarresid.desc }} - {% endif %} - {% if sarresid.factor %} - فاکتور شماره {{ sarresid.factor.id }} - {% endif %} - - {% if sarresid.payStatus == "true" %}پرداخت شده{% endif %} - {% if sarresid.payStatus == "false" %}پرداخت نشده{% endif %} - - {% if sarresid.payStatus == "false" %} - وصول شد - {% else %} - بله - {% endif %} -
+ {% if page_name == 'sarresidShodeha' %} +
+

📊 حسابداری | سررسید شده ها

+ +
+ +
+
+

کل مطالبات سررسید شده

+

{{ sum_sarresid }}

+
+
- {% if all_sarresid.has_previous %} - صفحه قبل - {% endif %} - {% if all_sarresid.has_next %} - صفحه بعد - {% endif %} +
+
+

تأخیر ناچیز (۱ تا ۳۰ روز)

+

{{ minor_sum }}

+
+
-
+
+
+

پرخطر (۳۱ تا ۹۰ روز)

+

{{ high_sum }}

+
+
-
-
-
- - - - - -{% csrf_token %} {% endblock %} {% block jsEnd %} - - {% endblock %} -{% endif %} + +{% endblock %} +{% endif %} \ No newline at end of file diff --git a/admin_panel/urls.py b/admin_panel/urls.py index 1625511..7aa5f04 100644 --- a/admin_panel/urls.py +++ b/admin_panel/urls.py @@ -12,6 +12,7 @@ urlpatterns = [ path('bedehi/', views.bedehi, name='bedehi'), 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('hesabdari/', views.hesabdari, name='hesabdari'), path('hesabdari/sarresid_nashode/', views.hesabdari_residnashone, name='hesabdari_residnashone'), path('hesabdari/check/pass/', views.hesabdari_checkPass, name='hesabdari_check_pass'), diff --git a/admin_panel/views.py b/admin_panel/views.py index eb96fae..bbc4a72 100644 --- a/admin_panel/views.py +++ b/admin_panel/views.py @@ -266,6 +266,159 @@ def inventory_movements_report(request): 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): + """ + داشبورد هوش مالی و مدیریت نقدینگی سطح Enterprise (BI Dashboard) + همراه با روند ۱۲ ماهه جمع کل بدهی مشتریان (تفاضل debit و credit). + """ + # دریافت تایم‌استمپ‌های ارسالی از تقویم شمسی فرانت‌اند + 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 = [] + + # روش استاندارد و بدون باگ برای تبدیل تاریخ میلادی به شمسی در persiantools + # با این کار مطمئن می‌شویم مقادیر حتماً عدد صحیح و مجاز هستند + 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) + + # ❗️ اصلاح اساسی: ترتیب آرگومان‌ها در این تابع باید (month, year) باشد + last_day = JalaliDate.days_in_month(curr_m, curr_y) + + # اما ترتیب سازنده کلاس اصلی همچنان (year, month, day) است + end_of_month_jd = JalaliDate(curr_y, curr_m, last_day) + end_of_month_gd = end_of_month_jd.to_gregorian() + + # ساخت رشته زمان برای پایان آن ماه (تا ساعت 23:59:59) + 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) + + # ۱. محاسبات کارت‌های شاخص عملکرد (KPI Cards) + 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() + + # ۲. نمودار اول: تحلیل پویای سن مطالبات + 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, + + 'trend_labels': trend_labels, # نام ماه‌ها برای محور X + 'trend_data': trend_data, # جمع کل بدهی‌ها برای محور Y + + '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): @@ -288,25 +441,58 @@ def bedehi(request): @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() - dateNow = datetime.datetime.today().strftime('%Y-%m-%d 23:59:59') - all_sarresid = Sarresid.objects.filter(payStatus="false", sarresidDT__lte=dateNow).order_by('-sarresidDT') + # تعریف محدوده‌های زمانی ماتریس ریسک (سن مطالبات) + dt_30_days_ago = anchor_dt - datetime.timedelta(days=30) + dt_90_days_ago = anchor_dt - datetime.timedelta(days=90) - sum_sarresid = all_sarresid.aggregate(Sum('price'))['price__sum'] + # کوئری پایه: تمام سررسیدهای پرداخت‌نشده که از موعد آن‌ها گذشته است + base_qs = Sarresid.objects.filter(payStatus="false", sarresidDT__lte=anchor_dt) - page_number = request.GET.get('page', None) - paginator = Paginator(all_sarresid, per_page=50) + # محاسبه پرسرعت مجموع مبالغ در هر دسته (توسط موتور دیتابیس) + 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) - #paginator = Paginator(all_sarresid, per_page=200) - - data = {'sum_sarresid': sum_sarresid,'all_sarresid': all_sarresid_pgi, "dateNow":dateNow, "page_name": page_name} + 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):