From 16eb747b5cf63173f0f14b25df20682c507ab4bb Mon Sep 17 00:00:00 2001 From: javad Date: Sun, 16 Aug 2026 15:42:48 +0330 Subject: [PATCH] hesabresi and desc transfer --- admin_panel/services.py | 4 +- .../admin_panel/financial_dashboard.html | 361 +++++++++++++++--- admin_panel/views.py | 100 +++-- check_finance.py | 72 ++++ financial_audit_20260816_151225.csv | 18 + ghost_hunter.py | 91 +++++ reconciliation_tool.py | 110 ++++++ 7 files changed, 678 insertions(+), 78 deletions(-) create mode 100644 check_finance.py create mode 100644 financial_audit_20260816_151225.csv create mode 100644 ghost_hunter.py create mode 100644 reconciliation_tool.py diff --git a/admin_panel/services.py b/admin_panel/services.py index 0c177bb..6f56034 100644 --- a/admin_panel/services.py +++ b/admin_panel/services.py @@ -127,7 +127,7 @@ def approve_transfer_request(*, request_id: int, approver: AbstractBaseUser) -> s_in=0, s_out=transfer.quantity, movement_type=ProductInStore.MovementType.TRANSFER, - desc=f"انتقال به {transfer.target_store.store_name} | درخواست #{transfer.id} | تایید توسط {approver.username}" + desc=f"انتقال به {transfer.target_store.store_name} | درخواست #{transfer.id} | تایید توسط {approver.username} | {transfer.desc}" ) # ۵. صدور سند ورود قطعی به انبار مقصد @@ -137,7 +137,7 @@ def approve_transfer_request(*, request_id: int, approver: AbstractBaseUser) -> s_in=transfer.quantity, s_out=0, movement_type=ProductInStore.MovementType.TRANSFER, - desc=f"انتقال از {transfer.source_store.store_name} | درخواست #{transfer.id} | تایید توسط {approver.username}" + desc=f"انتقال از {transfer.source_store.store_name} | درخواست #{transfer.id} | تایید توسط {approver.username} | {transfer.desc}" ) # ۶. به‌روزرسانی نهایی درخواست diff --git a/admin_panel/templates/admin_panel/financial_dashboard.html b/admin_panel/templates/admin_panel/financial_dashboard.html index 71c5393..b625062 100644 --- a/admin_panel/templates/admin_panel/financial_dashboard.html +++ b/admin_panel/templates/admin_panel/financial_dashboard.html @@ -38,38 +38,51 @@ -
-
-
-
-
-
-

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

-

{{ kpi_total_unpaid }}

-
-
-
-
+ + {% if group1_count > 0 or group2_count > 0 or group4_count > 0 %} +
+
+ + حسابرسی خودکار سیستم: + تعداد {{ group1_count }} مورد بدهی پرخطر، {{ group2_count }} مورد خطای ثبتی چک و {{ group4_count }} مورد رکورد نامتعارف یافت شد!
+ مشاهده گزارش حسابرسی (پایین صفحه) +
+ {% endif %} + + +
-

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

-

{{ kpi_total_sales }}

+

مطالبات دارای سند (چک/سررسید)

+

{{ kpi_total_unpaid }}

-
+
-

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

-

{{ kpi_overdue_checks }} فقره

+

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

+

{{ kpi_total_sales }}

+
+
+
+
+
+
+
+
+
+
+

مجموع مطالبات پرخطر (بدون سند)

+

{{ group1_total }}

@@ -77,6 +90,7 @@
+
@@ -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 %} + + + + + + + + + {% endfor %} + +
ردیفنام مشتریتراز دفتر کل (بدهی)اسناد موجودمقدار کسر چک (بدهی روی هوا)عملیات
{{ forloop.counter }}{{ c.name_family }}{{ c.net_debt }}{{ c.t_sarresid }}{{ c.diff }} + ثبت چک/پیگیری +
+
+ {% if group1_count > 20 %} +

+ * جهت جلوگیری از کندی سیستم، صرفاً ۲۰ مشتری با بیشترین بدهی رو هوا نمایش داده شده‌اند. +

+ {% endif %} + {% else %} +
+ 🎉 عالی! هیچ مشتری بدهکاری فاقد ثبت پوشش سند (کسری چک) در سیستم وجود ندارد. +
+ {% endif %} +
+
+ + +
+
+
+
+

+ + گروه ۲: خطای ثبتی اپراتورها (چک پاس‌نشده اضافی در سیستم) +

+
+
+ جمع خطای مالی: {{ group2_total }} تومان + تعداد: {{ group2_count }} فقره +
+
+ +

+ 💡 راهنما: این افراد چک خود را نقدی یا کارت‌به‌کارت تسویه کرده‌اند، اما اپراتور فراموش کرده وضعیت چک را از "پرداخت‌نشده" به "پاس‌شده" تغییر دهد. +

+ + {% if group2_sample %} +
+ + + + + + + + + + + + + {% for c in group2_sample %} + + + + + + + + + {% endfor %} + +
ردیفنام مشتریتراز واقعی مشتریچک‌های ثبت‌شده (غلط)مبلغ چک اضافه در سیستمعملیات
{{ forloop.counter }}{{ c.name_family }}{{ c.net_debt }}{{ c.t_sarresid }}{{ c.fake_amount }} + اصلاح چک‌ها +
+
+ {% if group2_count > 20 %} +

+ * جهت جلوگیری از کندی سیستم، صرفاً ۲۰ مشتری با بیشترین میزان خطای ثبتی نمایش داده شده‌اند. +

+ {% endif %} + {% else %} +
+ 🎉 عالی! هیچ چک اضافی یا پاس‌نشده‌ی اشتباهی در سیستم یافت نشد. +
+ {% endif %} +
+
+ + +
+
+
+
+

+ + گروه ۳: پیش‌دریافت‌ها و بستانکاری‌های خالص (بدون چک) +

+
+
+ مجموع تعهدات: {{ group3_total }} تومان + تعداد: {{ group3_count }} نفر +
+
+ + {% if group3_sample %} +
+ + + + + + + + + + + + + {% for c in group3_sample %} + + + + + + + + + {% endfor %} + +
ردیفنام مشتریشماره تماسمبلغ بستانکاری (تراز منفی)وضعیت چکعملیات
{{ forloop.counter }}{{ c.name_family }}{{ c.phone1 }}{{ c.net_debt }}ندارد + بررسی پرونده +
+
+ {% if group3_count > 20 %} +

+ * جهت جلوگیری از کندی سیستم، صرفاً ۲۰ مشتری بستانکار نمایش داده شده‌اند. +

+ {% endif %} + {% else %} +
+ هیچ پیش‌دریافت یا بستانکاری خالصی در سیستم یافت نشد. +
+ {% endif %} +
+
+ + + {% if group4_count > 0 %} +
+
+
+
+

+ + گروه ۴: رکوردهای نامتعارف (بستانکارانی که چک پرداخت‌نشده دارند!) +

+
+
+ مجموع اختلاف: {{ group4_total }} + تعداد: {{ group4_count }} مورد +
+
+ +

+ 💡 توجه: این مشتریان در دفتر کل طلبکارند، اما اپراتور چک پرداخت‌نشده‌ای هم از آن‌ها نگه داشته است! (نیاز به بررسی حسابداری دارد). +

+ +
+ + + + + + + + + + + + {% for c in group4_sample %} + + + + + + + + {% endfor %} + +
ردیفنام مشتریتراز واقعی (بستانکار)چک ثبت‌شدهعملیات
{{ forloop.counter }}{{ c.name_family }}{{ c.net_debt }}{{ c.t_sarresid }} + بررسی فنی +
+
+
+
+ {% 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