@@ -96,11 +110,10 @@
-
- 📊 ماتریس ریسک و تحلیل سنی مطالبات
+ 📊 ماتریس ریسک و تحلیل سنی اسناد (چک/نسیه)
@@ -152,8 +165,266 @@
+
+
+
+
+
+
+
+
+
سیستم حسابرسی و مغایرتگیری
+
+
+
+
+
+ مجموع تراز دفتر کل:
+ {{ global_mande }}
+
+
+ مجموع چکها/اسناد در سیستم:
+ {{ g_sar }}
+
+
+ اختلاف ریاضی سیستم:
+ {{ global_diff }}
+
+
+
+
+
+
+
+
+
+
+
+
+ گروه ۱: مطالبات فاقد پوشش (کسری چک / بدهی خطرناک)
+
+
+
+ مجموع کسر سند: {{ group1_total }} تومان
+ تعداد: {{ group1_count }} نفر
+
+
+
+ {% if group1_sample %}
+
+
+
+
+ | ردیف |
+ نام مشتری |
+ تراز دفتر کل (بدهی) |
+ اسناد موجود |
+ مقدار کسر چک (بدهی روی هوا) |
+ عملیات |
+
+
+
+ {% for c in group1_sample %}
+
+ | {{ forloop.counter }} |
+ {{ c.name_family }} |
+ {{ c.net_debt }} |
+ {{ c.t_sarresid }} |
+ {{ c.diff }} |
+
+ ثبت چک/پیگیری
+ |
+
+ {% endfor %}
+
+
+
+ {% if group1_count > 20 %}
+
+ * جهت جلوگیری از کندی سیستم، صرفاً ۲۰ مشتری با بیشترین بدهی رو هوا نمایش داده شدهاند.
+
+ {% endif %}
+ {% else %}
+
+ 🎉 عالی! هیچ مشتری بدهکاری فاقد ثبت پوشش سند (کسری چک) در سیستم وجود ندارد.
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+ گروه ۲: خطای ثبتی اپراتورها (چک پاسنشده اضافی در سیستم)
+
+
+
+ جمع خطای مالی: {{ group2_total }} تومان
+ تعداد: {{ group2_count }} فقره
+
+
+
+
+ 💡 راهنما: این افراد چک خود را نقدی یا کارتبهکارت تسویه کردهاند، اما اپراتور فراموش کرده وضعیت چک را از "پرداختنشده" به "پاسشده" تغییر دهد.
+
+
+ {% if group2_sample %}
+
+
+
+
+ | ردیف |
+ نام مشتری |
+ تراز واقعی مشتری |
+ چکهای ثبتشده (غلط) |
+ مبلغ چک اضافه در سیستم |
+ عملیات |
+
+
+
+ {% for c in group2_sample %}
+
+ | {{ forloop.counter }} |
+ {{ c.name_family }} |
+ {{ c.net_debt }} |
+ {{ c.t_sarresid }} |
+ {{ c.fake_amount }} |
+
+ اصلاح چکها
+ |
+
+ {% endfor %}
+
+
+
+ {% if group2_count > 20 %}
+
+ * جهت جلوگیری از کندی سیستم، صرفاً ۲۰ مشتری با بیشترین میزان خطای ثبتی نمایش داده شدهاند.
+
+ {% endif %}
+ {% else %}
+
+ 🎉 عالی! هیچ چک اضافی یا پاسنشدهی اشتباهی در سیستم یافت نشد.
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+ گروه ۳: پیشدریافتها و بستانکاریهای خالص (بدون چک)
+
+
+
+ مجموع تعهدات: {{ group3_total }} تومان
+ تعداد: {{ group3_count }} نفر
+
+
+
+ {% if group3_sample %}
+
+
+
+
+ | ردیف |
+ نام مشتری |
+ شماره تماس |
+ مبلغ بستانکاری (تراز منفی) |
+ وضعیت چک |
+ عملیات |
+
+
+
+ {% for c in group3_sample %}
+
+ | {{ forloop.counter }} |
+ {{ c.name_family }} |
+ {{ c.phone1 }} |
+ {{ c.net_debt }} |
+ ندارد |
+
+ بررسی پرونده
+ |
+
+ {% endfor %}
+
+
+
+ {% if group3_count > 20 %}
+
+ * جهت جلوگیری از کندی سیستم، صرفاً ۲۰ مشتری بستانکار نمایش داده شدهاند.
+
+ {% endif %}
+ {% else %}
+
+ هیچ پیشدریافت یا بستانکاری خالصی در سیستم یافت نشد.
+
+ {% endif %}
+
+
+
+
+ {% if group4_count > 0 %}
+
+
+
+
+
+
+ گروه ۴: رکوردهای نامتعارف (بستانکارانی که چک پرداختنشده دارند!)
+
+
+
+ مجموع اختلاف: {{ group4_total }}
+ تعداد: {{ group4_count }} مورد
+
+
+
+
+ 💡 توجه: این مشتریان در دفتر کل طلبکارند، اما اپراتور چک پرداختنشدهای هم از آنها نگه داشته است! (نیاز به بررسی حسابداری دارد).
+
+
+
+
+
+
+ | ردیف |
+ نام مشتری |
+ تراز واقعی (بستانکار) |
+ چک ثبتشده |
+ عملیات |
+
+
+
+ {% for c in group4_sample %}
+
+ | {{ forloop.counter }} |
+ {{ c.name_family }} |
+ {{ c.net_debt }} |
+ {{ c.t_sarresid }} |
+
+ بررسی فنی
+ |
+
+ {% endfor %}
+
+
+
+
+
+ {% endif %}
+
@@ -211,7 +477,7 @@
autoClose: true,
format: 'YYYY/MM/DD',
calendar: { persian: { locale: 'fa' } },
- responsive: true // قابلیت تقویم برای موبایل
+ responsive: true
});
var pickerTa = $(".date-picker-ta").pDatepicker({
@@ -226,9 +492,8 @@
{% if filters.azTarikh %} pickerAz.setDate({{ filters.azTarikh }}); {% endif %}
{% if filters.taTarikh %} pickerTa.setDate({{ filters.taTarikh }}); {% endif %}
- // 🌟 نمودار روند خطی مانده حساب کل مشتریان در ۱۲ ماه
+ // نمودار روند خطی
var ctxTrend = document.getElementById('debtTrendChart').getContext('2d');
-
var gradientTrend = ctxTrend.createLinearGradient(0, 0, 0, 300);
gradientTrend.addColorStop(0, 'rgba(62, 142, 208, 0.45)');
gradientTrend.addColorStop(1, 'rgba(62, 142, 208, 0.0)');
@@ -273,27 +538,14 @@
}
},
scales: {
- y: {
- beginAtZero: true,
- ticks: { font: { family: 'Tahoma, Vazir', size: 10 } }
- },
- x: {
- ticks: {
- font: { family: 'Tahoma, Vazir', size: 10 },
- maxRotation: 45, // چرخش لیبلها برای جا شدن در موبایل
- minRotation: 0
- },
- grid: { display: false }
- }
- },
- interaction: {
- intersect: false,
- mode: 'index',
+ y: { beginAtZero: true, ticks: { font: { family: 'Tahoma, Vazir', size: 10 } } },
+ x: { ticks: { font: { family: 'Tahoma, Vazir', size: 10 }, maxRotation: 45, minRotation: 0 }, grid: { display: false } }
},
+ interaction: { intersect: false, mode: 'index' },
}
});
- // ۲. نمودار سنی مطالبات
+ // نمودار سنی مطالبات
var ctxAging = document.getElementById('agingChart').getContext('2d');
new Chart(ctxAging, {
type: 'bar',
@@ -304,7 +556,7 @@
backgroundColor: ['rgba(72, 199, 142, 0.85)', 'rgba(255, 221, 87, 0.85)', 'rgba(241, 70, 104, 0.5)', 'rgba(241, 70, 104, 0.95)'],
borderColor: ['#48c78e', '#ffdd57', '#f14668', '#f14668'],
borderWidth: 1.5,
- barThickness: 30 // ضخامت میلهها برای موبایل بهتر است
+ barThickness: 30
}]
},
options: {
@@ -318,7 +570,7 @@
}
});
- // ۳. نمودار دایرهای تسویه مبالغ
+ // نمودار دایرهای تسویه مبالغ
var ctxPay = document.getElementById('paymentMethodsChart').getContext('2d');
new Chart(ctxPay, {
type: 'doughnut',
@@ -335,14 +587,14 @@
maintainAspectRatio: false,
plugins: {
legend: {
- position: window.innerWidth < 768 ? 'bottom' : 'right', // در موبایل لجند پایین میرود
+ position: window.innerWidth < 768 ? 'bottom' : 'right',
labels: { font: { family: 'Tahoma, Vazir', size: 11 }, boxWidth: 12 }
}
}
}
});
- // ۴. نمودار افقی انبار
+ // نمودار افقی انبار
var ctxLeak = document.getElementById('inventoryLeakChart').getContext('2d');
new Chart(ctxLeak, {
type: 'bar',
@@ -367,6 +619,17 @@
}
}
});
+
+ // اسکرول نرم برای لینکهای درون صفحه
+ $('a[href^="#"]').on('click', function(event) {
+ var target = $(this.getAttribute('href'));
+ if( target.length ) {
+ event.preventDefault();
+ $('html, body').stop().animate({
+ scrollTop: target.offset().top - 20
+ }, 800);
+ }
+ });
});
{% endblock %}
\ No newline at end of file
diff --git a/admin_panel/views.py b/admin_panel/views.py
index 621fb7f..98674ea 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
+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):
diff --git a/check_finance.py b/check_finance.py
new file mode 100644
index 0000000..7a81037
--- /dev/null
+++ b/check_finance.py
@@ -0,0 +1,72 @@
+import os
+import django
+from django.db.models import Sum
+
+# تنظیمات محیطی جنگو برای اجرای اسکریپت خارج از وبسرور
+# ⚠️ مهم: کلمه 'myproject' را با نام پوشه اصلی پروژه خود (که فایل settings.py در آن است) جایگزین کنید.
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'khatibi.settings')
+django.setup()
+
+# ایمپورت مدلها بعد از راهاندازی محیط جنگو
+from admin_panel.models import Article, Sarresid, Customer
+
+def check_financial_discrepancy():
+ print("\n" + "="*50)
+ print(" 🔍 در حال محاسبه و مقایسه اطلاعات مالی سیستم...")
+ print("="*50 + "\n")
+
+ # ۱. محاسبه تراز کل دفتر (mandehesab) بر اساس ستونهای debit و credit[cite: 3]
+ articles_agg = Article.objects.aggregate(total_debit=Sum('debit'), total_credit=Sum('credit'))
+ total_debit = articles_agg['total_debit'] or 0
+ total_credit = articles_agg['total_credit'] or 0
+ total_mandehesab = total_debit - total_credit
+
+ # ۲. محاسبه مجموع اسناد پرداختنشده (چکها و نسیهها)[cite: 3]
+ sarresid_agg = Sarresid.objects.filter(payStatus="false").aggregate(total_price=Sum('price'))
+ total_sarresid = sarresid_agg['total_price'] or 0
+
+ # ۳. محاسبه اختلاف بین دفتر کل و اسناد در دست
+ discrepancy = total_mandehesab - total_sarresid
+
+ print("📊 [ خلاصه وضعیت کل سیستم ]")
+ print("-" * 40)
+ print(f"💰 تراز کل دفتر کل (mandehesab): {total_mandehesab:,} ریال")
+ print(f"📑 مجموع اسناد و چکهای در دست: {total_sarresid:,} ریال")
+ print(f"⚠️ کل اختلاف (بدهیهای بدون سند): {discrepancy:,} ریال")
+ print("-" * 40 + "\n")
+
+ # ۴. تحلیل به تفکیک مشتریان برای پیدا کردن منشا اختلاف
+ print("🔎 [ لیست مشتریان دارای بدهی باز (منشأ اختلاف) ]")
+ print("-" * 40)
+
+ customers = Customer.objects.filter(is_active=True)
+ count = 0
+
+ for customer in customers:
+ # الف: محاسبه مانده حساب این مشتری
+ c_agg = Article.objects.filter(customer=customer).aggregate(t_d=Sum('debit'), t_c=Sum('credit'))
+ c_debit = c_agg['t_d'] or 0
+ c_credit = c_agg['t_c'] or 0
+ c_mandehesab = c_debit - c_credit
+
+ # ب: محاسبه سررسیدهای پرداخت نشده این مشتری
+ s_agg = Sarresid.objects.filter(customer=customer, payStatus="false").aggregate(t_p=Sum('price'))
+ c_sarresid = s_agg['t_p'] or 0
+
+ # ج: بررسی اختلاف
+ c_diff = c_mandehesab - c_sarresid
+
+ # اگر اختلاف بیشتر از صفر بود، یعنی مشتری بدهی رو هوا (بدون سند) دارد
+ if c_diff > 0:
+ count += 1
+ print(f"👤 {customer.name_family} (کد: {customer.id})")
+ print(f" ↳ تراز کل: {c_mandehesab:,} | اسناد: {c_sarresid:,} | مبلغ اختلاف: {c_diff:,}")
+ print("-" * 20)
+
+ if count == 0:
+ print("✅ هیچ مشتری با بدهی دفتری (بدون سند) یافت نشد. حسابها تراز است.")
+ else:
+ print(f"\nتعداد کل مشتریان دارای اختلاف حساب: {count} نفر")
+
+if __name__ == '__main__':
+ check_financial_discrepancy()
\ No newline at end of file
diff --git a/financial_audit_20260816_151225.csv b/financial_audit_20260816_151225.csv
new file mode 100644
index 0000000..808472c
--- /dev/null
+++ b/financial_audit_20260816_151225.csv
@@ -0,0 +1,18 @@
+شناسه مشتری,نام و نام خانوادگی,شماره تماس,گروه خطا,شرح خطا,تراز واقعی (Article),جمع اسناد در دست (Sarresid),مبلغ مغایرت
+5337,لوله گستر(ستون شنی),57999,گروه 1 (خطرناک),بدهی دفتری باز - بدون چک یا موعد نسیه,700000,0,700000
+4223,توکلی,9124967375,گروه 1 (خطرناک),بدهی دفتری باز - بدون چک یا موعد نسیه,560000,0,560000
+1819,محسن رهبر,9134712017,گروه 1 (خطرناک),بدهی دفتری باز - بدون چک یا موعد نسیه,390000,0,390000
+2118,وفادار,9139711662,گروه 1 (خطرناک),بدهی دفتری باز - بدون چک یا موعد نسیه,366000,0,366000
+4065,تقی معانی,9131724408,گروه 1 (خطرناک),بدهی دفتری باز - بدون چک یا موعد نسیه,295000,0,295000
+2616,رئوفی,9121279418,گروه 1 (خطرناک),بدهی دفتری باز - بدون چک یا موعد نسیه,220000,0,220000
+2928,مشایخی,9136588376,گروه 1 (خطرناک),بدهی دفتری باز - بدون چک یا موعد نسیه,140000,0,140000
+4919,رضا شهاب,9197948545,گروه 1 (خطرناک),بدهی دفتری باز - بدون چک یا موعد نسیه,80000,0,80000
+3943,زهرا معصومی,9132710355,گروه 1 (خطرناک),بدهی دفتری باز - بدون چک یا موعد نسیه,50000,0,50000
+3740,نصرالله محمدیان,9133729061,گروه 1 (خطرناک),بدهی دفتری باز - بدون چک یا موعد نسیه,35000,0,35000
+4098,خانم نخعی,9128178421,گروه 1 (خطرناک),بدهی دفتری باز - بدون چک یا موعد نسیه,20000,0,20000
+1543,گلشن زندی (مادر خانم فرخی),9133710770,گروه 1 (خطرناک),بدهی دفتری باز - بدون چک یا موعد نسیه,15000,0,15000
+11417,بابک صحرایی,9123009013,گروه 2 (خطای ثبتی),چک پاس نشده در سیستم بیشتر از بدهی واقعی است,1470020000,1500000000,29980000
+9699,آقا صمد (شعبه آباده),9133713616,گروه 2 (خطای ثبتی),چک پاس نشده در سیستم بیشتر از بدهی واقعی است,258100000,271000000,12900000
+5889,خیری,9134711283,گروه 2 (خطای ثبتی),چک پاس نشده در سیستم بیشتر از بدهی واقعی است,0,2098000,2098000
+8038, رضا اشراقی ,9133712633,گروه 2 (خطای ثبتی),چک پاس نشده در سیستم بیشتر از بدهی واقعی است,4595000,4985000,390000
+7327,محمد یاسینی ***,9133717483,گروه 2 (خطای ثبتی),چک پاس نشده در سیستم بیشتر از بدهی واقعی است,550000,620000,70000
diff --git a/ghost_hunter.py b/ghost_hunter.py
new file mode 100644
index 0000000..ae93531
--- /dev/null
+++ b/ghost_hunter.py
@@ -0,0 +1,91 @@
+import os
+import csv
+import django
+from django.db.models import Sum, Subquery, OuterRef, F, IntegerField
+from django.db.models.functions import Coalesce
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'khatibi.settings') # myproject را تغییر دهید
+django.setup()
+
+from admin_panel.models import Article, Sarresid, Customer
+
+def run_360_audit():
+ print("\n" + "="*70)
+ print(" 🔎 کالبدشکافی ۳۶۰ درجه سیستم مالی (ترازنامه دقیق)")
+ print("="*70 + "\n")
+
+ # ---------------------------------------------------------
+ # ۱. محاسبه تراز کل سیستم (پایه ریاضی)
+ # ---------------------------------------------------------
+ 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
+
+ print(f"💰 تراز کل دفتر کل (mandehesab): {global_mande:15,} ریال")
+ print(f"📑 مجموع چکهای دست شرکت: {g_sar:15,} ریال")
+ print(f"⚠️ کل اختلاف سیستم: {global_diff:15,} ریال\n")
+
+ # ---------------------------------------------------------
+ # ۲. اسکن پرسرعت تمام مشتریان
+ # ---------------------------------------------------------
+ sq_deb = Article.objects.filter(customer=OuterRef('pk')).values('customer').annotate(s=Sum('debit')).values('s')
+ sq_cre = Article.objects.filter(customer=OuterRef('pk')).values('customer').annotate(s=Sum('credit')).values('s')
+ sq_sar = Sarresid.objects.filter(customer=OuterRef('pk'), payStatus="false").values('customer').annotate(s=Sum('price')).values('s')
+
+ customers = Customer.objects.annotate(
+ net_debt=Coalesce(Subquery(sq_deb, output_field=IntegerField()), 0) - Coalesce(Subquery(sq_cre, output_field=IntegerField()), 0),
+ sarresid_total=Coalesce(Subquery(sq_sar, output_field=IntegerField()), 0),
+ ).annotate(
+ diff=F('net_debt') - F('sarresid_total')
+ )
+
+ # ---------------------------------------------------------
+ # ۳. دستهبندی ریاضیاتی (Bucketing)
+ # ---------------------------------------------------------
+ cat1_risk = 0 # 🔴 گروه 1: بدهکارانی که کسر سند/چک دارند (بدهی رو هوا)
+ cat2_fake_check = 0 # 🟡 گروه 2: چک پاس نشده در سیستم بیشتر از بدهی است (خطای اپراتور)
+ cat3_advance = 0 # 🟢 گروه 3: پیشدریافت (مشتری بستانکار است و چکی ندارد)
+ cat4_advance_check = 0 # 🟣 گروه 4: عجیبترین! مشتری هم بستانکار است، هم چک پاسنشده دارد!
+
+ print("📊 [ تفکیک دقیق اختلاف به ۴ گروه حسابداری ]")
+ print("-" * 70)
+
+ for c in customers:
+ d = c.diff
+ if d > 0:
+ cat1_risk += d
+ elif d < 0:
+ if c.net_debt >= 0:
+ cat2_fake_check += d
+ elif c.net_debt < 0 and c.sarresid_total == 0:
+ cat3_advance += d
+ elif c.net_debt < 0 and c.sarresid_total > 0:
+ cat4_advance_check += d
+
+ print(f"🔴 گروه 1 (کسری چک / بدهی خطرناک): {cat1_risk:+15,} ریال [باعث مثبت شدن اختلاف]")
+ print(f"🟡 گروه 2 (خطای ثبتی / چک اضافی): {cat2_fake_check:+15,} ریال [باعث منفی شدن اختلاف]")
+ print(f"🟢 گروه 3 (پیشدریافت / بستانکاری خالص): {cat3_advance:+15,} ریال [باعث منفی شدن اختلاف]")
+ print(f"🟣 گروه 4 (بستانکارانی که چک هم دادهاند!): {cat4_advance_check:+15,} ریال [باعث منفی شدن اختلاف]")
+
+ print("-" * 70)
+ calculated_diff = cat1_risk + cat2_fake_check + cat3_advance + cat4_advance_check
+ print(f"🧮 جمع ریاضی هر 4 گروه بالا: {calculated_diff:15,} ریال")
+
+ if calculated_diff == global_diff:
+ print("✅ تطابق 100%: هیچ پولی گم نشده و محاسبات از نظر حسابداری کاملاً تراز است!")
+ else:
+ print(f"❌ هنوز {global_diff - calculated_diff:,} ریال اختلاف وجود دارد!")
+
+ # ---------------------------------------------------------
+ # ۴. استخراج اسامی عاملین اختلاف منفی ۱۱۱ میلیونی (گروههای ۳ و ۴)
+ # ---------------------------------------------------------
+ print("\n🔎 [ چه کسانی باعث آن 111 میلیون اختلاف منفی شدهاند؟ (پیشدریافتها) ]")
+ bostankar_qs = customers.filter(net_debt__lt=0).order_by('net_debt')[:10]
+
+ for c in bostankar_qs:
+ print(f" 👤 {c.name_family} | تراز: {c.net_debt:,} | چک در دست: {c.sarresid_total:,} | تاثیر در اختلاف: {c.diff:,}")
+
+if __name__ == '__main__':
+ run_360_audit()
\ No newline at end of file
diff --git a/reconciliation_tool.py b/reconciliation_tool.py
new file mode 100644
index 0000000..0e517e5
--- /dev/null
+++ b/reconciliation_tool.py
@@ -0,0 +1,110 @@
+import os
+import csv
+import datetime
+
+# ============================================================================
+# ۱. تنظیمات محیطی جنگو (بسیار مهم)
+# ============================================================================
+# نام 'myproject' را با نام پوشه اصلی پروژه خود (که فایل settings.py در آنجاست) عوض کنید
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'khatibi.settings')
+
+import django
+django.setup()
+
+from django.db.models import Sum, Subquery, OuterRef, F, IntegerField
+from django.db.models.functions import Coalesce
+from admin_panel.models import Article, Sarresid, Customer
+
+# ============================================================================
+# ۲. هسته اصلی محاسبات (الگوریتم پرسرعت با Subquery)
+# ============================================================================
+def run_audit():
+ print("\n" + "="*65)
+ print(" 🚀 در حال اسکن دیتابیس و اجرای عملیات مغایرتگیری هوشمند...")
+ print("="*65 + "\n")
+
+ # واکشی سریع مجموع بدهکاری مشتریان
+ 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')
+
+ # پردازش نهایی در لایه دیتابیس (بدون نیاز به لود کردن دیتا در RAM سرور)
+ 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') # اختلاف تراز و اسناد
+ )
+
+ # استخراج گروه اول: بدهکاران مطلقاً بدون چک
+ group1 = base_customers.filter(net_debt__gt=0, t_sarresid=0).order_by('-net_debt')
+
+ # استخراج گروه دوم: دارای خطای چک اضافی در سیستم
+ group2 = base_customers.filter(diff__lt=0, t_sarresid__gt=0).annotate(
+ fake_amount=F('t_sarresid') - F('net_debt')
+ ).order_by('-fake_amount')
+
+ # ============================================================================
+ # ۳. نمایش گزارش در کنسول
+ # ============================================================================
+ print(f"🔴 گروه اول: بدهکاران فاقد سند/چک (تعداد: {group1.count()})")
+ print("-" * 65)
+ for c in group1[:10]: # نمایش ۱۰ نفر اول در کنسول
+ print(f"👤 {c.name_family} | موبایل: {c.phone1} | بدهی روی هوا: {c.net_debt:,} ریال")
+ if group1.count() > 10:
+ print(f" ... و {group1.count() - 10} نفر دیگر (لیست کامل در فایل خروجی)")
+
+ print("\n" + f"🟡 گروه دوم: دارای خطای ثبت چک اضافه (تعداد: {group2.count()})")
+ print("-" * 65)
+ for c in group2[:10]:
+ print(f"👤 {c.name_family} | تراز واقعی: {c.net_debt:,} | مبلغ چک تو سیستم: {c.t_sarresid:,} | ❌ چک اضافی: {c.fake_amount:,}")
+ if group2.count() > 10:
+ print(f" ... و {group2.count() - 10} نفر دیگر (لیست کامل در فایل خروجی)")
+
+ # ============================================================================
+ # ۴. تولید فایل خروجی CSV برای حسابدار
+ # ============================================================================
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
+ filename = f"financial_audit_{timestamp}.csv"
+
+ with open(filename, mode='w', newline='', encoding='utf-8-sig') as file:
+ writer = csv.writer(file)
+
+ # هدر فایل
+ writer.writerow(["شناسه مشتری", "نام و نام خانوادگی", "شماره تماس", "گروه خطا", "شرح خطا", "تراز واقعی (Article)", "جمع اسناد در دست (Sarresid)", "مبلغ مغایرت"])
+
+ # نوشتن دیتای گروه اول
+ for c in group1:
+ writer.writerow([
+ c.id, c.name_family, c.phone1,
+ "گروه 1 (خطرناک)", "بدهی دفتری باز - بدون چک یا موعد نسیه",
+ c.net_debt, c.t_sarresid, c.diff
+ ])
+
+ # نوشتن دیتای گروه دوم
+ for c in group2:
+ writer.writerow([
+ c.id, c.name_family, c.phone1,
+ "گروه 2 (خطای ثبتی)", "چک پاس نشده در سیستم بیشتر از بدهی واقعی است",
+ c.net_debt, c.t_sarresid, c.fake_amount
+ ])
+
+ print("\n" + "="*65)
+ print(f"✅ عملیات با موفقیت انجام شد. فایل گزارش تولید شد: {filename}")
+ print("="*65 + "\n")
+
+if __name__ == '__main__':
+ run_audit()
\ No newline at end of file