hesabresi and desc transfer

This commit is contained in:
javad committed 2026-08-16 15:42:48 +03:30
1 parent e353ed6fc2
commit 16eb747b5c
7 files changed
+678 -78

No files matched your search

+73 -27
View File
@@ -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
from django.db.models import Count, Sum, Q, Avg, F, Subquery, OuterRef, IntegerField
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
@@ -282,10 +282,9 @@ def inventory_movements_report(request):
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
def financial_dashboard(request):
"""
داشبورد هوش مالی و مدیریت نقدینگی سطح Enterprise (BI Dashboard)
همراه با روند ۱۲ ماهه جمع کل بدهی مشتریان (تفاضل debit و credit).
داشبورد هوش مالی و مدیریت نقدینگی (BI Dashboard)
مجهز به هسته حسابرسی ۳۶۰ درجه برای کشف تمامی مغایرت‌ها (بدهی رو هوا، چک‌های الکی، پیش‌دریافت‌ها)
"""
# دریافت تایم‌استمپ‌های ارسالی از تقویم شمسی فرانت‌اند
az_tarikh = request.GET.get('azTarikh', '')
ta_tarikh = request.GET.get('taTarikh', '')
@@ -306,23 +305,19 @@ def financial_dashboard(request):
except (ValueError, TypeError):
pass
# نقطه اتکا (لنگر) محاسبات: زمان انتخابی کاربر یا زمان حال
anchor_dt = ta_dt if ta_dt else datetime.datetime.now()
# =========================================================================
# محاسبه روند ۱۲ ماهه غلتان (TTM) - نسخه اصلاح‌شده و امن
# محاسبه روند ۱۲ ماهه غلتان (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
@@ -331,17 +326,11 @@ def financial_dashboard(request):
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')
@@ -351,14 +340,11 @@ def financial_dashboard(request):
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()
@@ -376,12 +362,65 @@ def financial_dashboard(request):
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()
# ۲. نمودار اول: تحلیل پویای سن مطالبات
# =========================================================================
# 👑 هسته حسابرسی ۳۶۰ درجه (برگرفته از 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
@@ -389,7 +428,6 @@ def financial_dashboard(request):
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 = []
@@ -398,7 +436,6 @@ def financial_dashboard(request):
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'))
@@ -407,14 +444,24 @@ def financial_dashboard(request):
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
# آمار کلان ریاضیاتی
'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,
@@ -430,7 +477,6 @@ def financial_dashboard(request):
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):