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

+2 -2
View File
@@ -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}"
)
# ۶. به‌روزرسانی نهایی درخواست
@@ -38,38 +38,51 @@
</div>
</div>
<div class="columns is-multiline">
<div class="column is-12-mobile is-4-tablet">
<div class="box p-4 has-background-white shadow-card border-left-danger h-100">
<div class="level is-mobile m-0">
<div class="level-left">
<div>
<p class="heading has-text-grey-dark is-size-7 mb-1">کل مطالبات معوق بازار (سرمایه قفل شده)</p>
<p class="title is-size-4 is-size-5-mobile toCurrency has-text-danger-dark">{{ kpi_total_unpaid }}</p>
</div>
</div>
</div>
</div>
<!-- هشدار هوشمند خطاهای سیستمی -->
{% if group1_count > 0 or group2_count > 0 or group4_count > 0 %}
<div class="notification is-danger is-light mb-4 py-3 shadow-sm is-flex is-align-items-center is-justify-content-space-between is-flex-wrap-wrap">
<div class="mb-2-mobile">
<span class="icon has-text-danger"><i class="fas fa-exclamation-circle"></i></span>
<strong class="has-text-danger-dark">حسابرسی خودکار سیستم:</strong>
تعداد <b>{{ group1_count }} مورد</b> بدهی پرخطر، <b>{{ group2_count }} مورد</b> خطای ثبتی چک و <b>{{ group4_count }} مورد</b> رکورد نامتعارف یافت شد!
</div>
<a href="#audit-section" class="button is-danger is-small is-rounded is-outlined px-4">مشاهده گزارش حسابرسی (پایین صفحه)</a>
</div>
{% endif %}
<!-- بخش اول: کارت‌های شاخص (KPI) -->
<div class="columns is-multiline">
<div class="column is-12-mobile is-4-tablet">
<div class="box p-4 has-background-white shadow-card border-left-success h-100">
<div class="level is-mobile m-0">
<div class="level-left">
<div>
<p class="heading has-text-grey-dark is-size-7 mb-1">حجم ناخالص فروش در بازه انتخابی</p>
<p class="title is-size-4 is-size-5-mobile toCurrency has-text-success-dark">{{ kpi_total_sales }}</p>
<p class="heading has-text-grey-dark is-size-7 mb-1" title="مجموع مبلغ اسناد در دست (چک‌ها و سررسیدهای پرداخت نشده)">مطالبات دارای سند (چک/سررسید)</p>
<p class="title is-size-4 is-size-5-mobile toCurrency has-text-success-dark">{{ kpi_total_unpaid }}</p>
</div>
</div>
</div>
</div>
</div>
<div class="column is-12-mobile is-4-tablet">
<div class="box p-4 has-background-white shadow-card border-left-warning h-100">
<div class="box p-4 has-background-white shadow-card border-left-info h-100">
<div class="level is-mobile m-0">
<div class="level-left">
<div>
<p class="heading has-text-grey-dark is-size-7 mb-1">تعداد چک‌های سررسیدگذشته و برگشتی</p>
<p class="title is-size-4 is-size-5-mobile has-text-warning-dark">{{ kpi_overdue_checks }} <span class="is-size-6">فقره</span></p>
<p class="heading has-text-grey-dark is-size-7 mb-1">حجم ناخالص فروش در بازه انتخابی</p>
<p class="title is-size-4 is-size-5-mobile toCurrency has-text-info-dark">{{ kpi_total_sales }}</p>
</div>
</div>
</div>
</div>
</div>
<div class="column is-12-mobile is-4-tablet">
<div class="box p-4 has-background-danger-light shadow-card border-left-danger h-100" title="بدهی مشتریانی که هیچ‌گونه چک یا سررسیدی برای بدهی خود ارائه نکرده‌اند">
<div class="level is-mobile m-0">
<div class="level-left">
<div>
<p class="heading has-text-danger-dark is-size-7 mb-1 font-weight-bold">مجموع مطالبات پرخطر (بدون سند)</p>
<p class="title is-size-4 is-size-5-mobile toCurrency has-text-danger-dark">{{ group1_total }}</p>
</div>
</div>
</div>
@@ -77,6 +90,7 @@
</div>
</div>
<!-- بخش دوم: نمودارها -->
<div class="columns is-multiline mt-1">
<div class="column is-12">
<div class="box p-4 shadow-card">
@@ -96,11 +110,10 @@
</div>
<div class="columns is-multiline mt-1">
<div class="column is-12-mobile is-7-desktop">
<div class="box p-4 shadow-card h-100">
<p class="title is-6 is-size-7-mobile pb-2 mb-4" style="border-bottom: 1px solid #f0f0f0;">
📊 ماتریس ریسک و تحلیل سنی مطالبات
📊 ماتریس ریسک و تحلیل سنی اسناد (چک/نسیه)
</p>
<div class="chart-wrapper-tall">
<canvas id="agingChart"></canvas>
@@ -152,8 +165,266 @@
</div>
</div>
</div>
</div>
<!-- ========================================================= -->
<!-- بخش سوم: کالبدشکافی ۳۶۰ درجه و سیستم جامع حسابرسی -->
<!-- ========================================================= -->
<div id="audit-section" class="columns is-multiline mt-5 mb-4">
<div class="column is-12">
<h3 class="title is-5 mb-2"><i class="fas fa-microscope has-text-primary mr-2"></i> سیستم حسابرسی و مغایرت‌گیری </h3>
<!-- خلاصه تراز کل -->
<div class="box p-3 has-background-light shadow-sm" style="border: 1px solid #e0e0e0;">
<div class="columns is-mobile is-multiline has-text-centered is-size-7">
<div class="column is-4-tablet is-12-mobile">
<span class="has-text-grey">مجموع تراز دفتر کل:</span> <br>
<strong class="is-size-5 toCurrency">{{ global_mande }}</strong>
</div>
<div class="column is-4-tablet is-12-mobile" style="border-right: 1px solid #ddd; border-left: 1px solid #ddd;">
<span class="has-text-grey">مجموع چک‌ها/اسناد در سیستم:</span> <br>
<strong class="is-size-5 toCurrency has-text-success-dark">{{ g_sar }}</strong>
</div>
<div class="column is-4-tablet is-12-mobile">
<span class="has-text-grey">اختلاف ریاضی سیستم:</span> <br>
<strong class="is-size-5 toCurrency has-text-danger-dark" dir="ltr">{{ global_diff }}</strong>
</div>
</div>
</div>
</div>
<!-- 🔴 گروه اول: بدهکاران پرخطر -->
<div class="column is-12">
<div class="box p-4 shadow-card border-left-danger">
<div class="level is-mobile is-flex-wrap-wrap mb-4 pb-2" style="border-bottom: 1px solid #fee2e2;">
<div class="level-left mb-2-mobile">
<p class="title is-6 is-size-7-mobile mb-0 has-text-danger-dark">
<span class="icon is-small ml-1"><i class="fas fa-exclamation-triangle"></i></span>
گروه ۱: مطالبات فاقد پوشش (کسری چک / بدهی خطرناک)
</p>
</div>
<div class="level-right">
<span class="tag is-danger is-light ml-2">مجموع کسر سند: <span class="toCurrency mr-1">{{ group1_total }}</span> تومان</span>
<span class="tag is-danger">تعداد: {{ group1_count }} نفر</span>
</div>
</div>
{% if group1_sample %}
<div class="table-container">
<table class="table is-fullwidth is-hoverable is-striped is-bordered is-size-7">
<thead>
<tr class="has-background-danger-light">
<th class="has-text-centered">ردیف</th>
<th>نام مشتری</th>
<th>تراز دفتر کل (بدهی)</th>
<th>اسناد موجود</th>
<th class="has-text-danger-dark">مقدار کسر چک (بدهی روی هوا)</th>
<th class="has-text-centered">عملیات</th>
</tr>
</thead>
<tbody>
{% for c in group1_sample %}
<tr>
<td class="has-text-centered">{{ forloop.counter }}</td>
<td><strong>{{ c.name_family }}</strong></td>
<td class="toCurrency has-text-grey">{{ c.net_debt }}</td>
<td class="toCurrency has-text-success">{{ c.t_sarresid }}</td>
<td class="toCurrency has-text-danger has-text-weight-bold">{{ c.diff }}</td>
<td class="has-text-centered">
<a href="{% url 'admin_panel:customer' c.id %}" target="_blank" class="button is-small is-light is-danger is-outlined rounded">ثبت چک/پیگیری</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if group1_count > 20 %}
<p class="is-size-7 has-text-grey mt-2 has-text-centered">
* جهت جلوگیری از کندی سیستم، صرفاً ۲۰ مشتری با <strong>بیشترین بدهی رو هوا</strong> نمایش داده شده‌اند.
</p>
{% endif %}
{% else %}
<div class="notification is-success is-light has-text-centered py-4">
🎉 عالی! هیچ مشتری بدهکاری فاقد ثبت پوشش سند (کسری چک) در سیستم وجود ندارد.
</div>
{% endif %}
</div>
</div>
<!-- 🟡 گروه دوم: چک‌های اضافی و خطای اپراتور -->
<div class="column is-12">
<div class="box p-4 shadow-card border-left-warning">
<div class="level is-mobile is-flex-wrap-wrap mb-4 pb-2" style="border-bottom: 1px solid #fff3c4;">
<div class="level-left mb-2-mobile">
<p class="title is-6 is-size-7-mobile mb-0 has-text-warning-dark">
<span class="icon is-small ml-1"><i class="fas fa-search-dollar"></i></span>
گروه ۲: خطای ثبتی اپراتورها (چک پاس‌نشده اضافی در سیستم)
</p>
</div>
<div class="level-right">
<span class="tag is-warning is-light ml-2">جمع خطای مالی: <span class="toCurrency mr-1">{{ group2_total }}</span> تومان</span>
<span class="tag is-warning">تعداد: {{ group2_count }} فقره</span>
</div>
</div>
<p class="is-size-7 has-text-grey-dark mb-3">
💡 <b>راهنما:</b> این افراد چک خود را نقدی یا کارت‌به‌کارت تسویه کرده‌اند، اما اپراتور فراموش کرده وضعیت چک را از "پرداخت‌نشده" به "پاس‌شده" تغییر دهد.
</p>
{% if group2_sample %}
<div class="table-container">
<table class="table is-fullwidth is-hoverable is-striped is-bordered is-size-7">
<thead>
<tr class="has-background-warning-light">
<th class="has-text-centered">ردیف</th>
<th>نام مشتری</th>
<th>تراز واقعی مشتری</th>
<th>چک‌های ثبت‌شده (غلط)</th>
<th class="has-text-danger-dark">مبلغ چک اضافه در سیستم</th>
<th class="has-text-centered">عملیات</th>
</tr>
</thead>
<tbody>
{% for c in group2_sample %}
<tr>
<td class="has-text-centered">{{ forloop.counter }}</td>
<td><strong>{{ c.name_family }}</strong></td>
<td class="toCurrency has-text-info">{{ c.net_debt }}</td>
<td class="toCurrency has-text-grey">{{ c.t_sarresid }}</td>
<td class="toCurrency has-text-danger has-text-weight-bold">{{ c.fake_amount }}</td>
<td class="has-text-centered">
<a href="{% url 'admin_panel:customer' c.id %}" target="_blank" class="button is-small is-light is-warning is-outlined rounded">اصلاح چک‌ها</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if group2_count > 20 %}
<p class="is-size-7 has-text-grey mt-2 has-text-centered">
* جهت جلوگیری از کندی سیستم، صرفاً ۲۰ مشتری با <strong>بیشترین میزان خطای ثبتی</strong> نمایش داده شده‌اند.
</p>
{% endif %}
{% else %}
<div class="notification is-success is-light has-text-centered py-4">
🎉 عالی! هیچ چک اضافی یا پاس‌نشده‌ی اشتباهی در سیستم یافت نشد.
</div>
{% endif %}
</div>
</div>
<!-- 🟢 گروه سوم: پیش‌دریافت‌ها -->
<div class="column is-12">
<div class="box p-4 shadow-card border-left-success">
<div class="level is-mobile is-flex-wrap-wrap mb-4 pb-2" style="border-bottom: 1px solid #d4edda;">
<div class="level-left mb-2-mobile">
<p class="title is-6 is-size-7-mobile mb-0 has-text-success-dark">
<span class="icon is-small ml-1"><i class="fas fa-hand-holding-usd"></i></span>
گروه ۳: پیش‌دریافت‌ها و بستانکاری‌های خالص (بدون چک)
</p>
</div>
<div class="level-right">
<span class="tag is-success is-light ml-2">مجموع تعهدات: <span class="toCurrency mr-1" dir="ltr">{{ group3_total }}</span> تومان</span>
<span class="tag is-success">تعداد: {{ group3_count }} نفر</span>
</div>
</div>
{% if group3_sample %}
<div class="table-container">
<table class="table is-fullwidth is-hoverable is-striped is-bordered is-size-7">
<thead>
<tr class="has-background-success-light">
<th class="has-text-centered">ردیف</th>
<th>نام مشتری</th>
<th>شماره تماس</th>
<th class="has-text-success-dark">مبلغ بستانکاری (تراز منفی)</th>
<th class="has-text-centered">وضعیت چک</th>
<th class="has-text-centered">عملیات</th>
</tr>
</thead>
<tbody>
{% for c in group3_sample %}
<tr>
<td class="has-text-centered">{{ forloop.counter }}</td>
<td><strong>{{ c.name_family }}</strong></td>
<td><span dir="ltr">{{ c.phone1 }}</span></td>
<td class="toCurrency has-text-success-dark has-text-weight-bold" dir="ltr">{{ c.net_debt }}</td>
<td class="has-text-centered has-text-grey">ندارد</td>
<td class="has-text-centered">
<a href="{% url 'admin_panel:customer' c.id %}" target="_blank" class="button is-small is-light is-success is-outlined rounded">بررسی پرونده</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if group3_count > 20 %}
<p class="is-size-7 has-text-grey mt-2 has-text-centered">
* جهت جلوگیری از کندی سیستم، صرفاً ۲۰ مشتری بستانکار نمایش داده شده‌اند.
</p>
{% endif %}
{% else %}
<div class="notification is-light has-text-centered py-4">
هیچ پیش‌دریافت یا بستانکاری خالصی در سیستم یافت نشد.
</div>
{% endif %}
</div>
</div>
<!-- 🟣 گروه چهارم: رکوردهای نامتعارف -->
{% if group4_count > 0 %}
<div class="column is-12">
<div class="box p-4 shadow-card" style="border-right: 4px solid #b86bff !important;">
<div class="level is-mobile is-flex-wrap-wrap mb-4 pb-2" style="border-bottom: 1px solid #f3e8ff;">
<div class="level-left mb-2-mobile">
<p class="title is-6 is-size-7-mobile mb-0" style="color: #6b21a8;">
<span class="icon is-small ml-1"><i class="fas fa-bug"></i></span>
گروه ۴: رکوردهای نامتعارف (بستانکارانی که چک پرداخت‌نشده دارند!)
</p>
</div>
<div class="level-right">
<span class="tag is-light ml-2" style="background-color: #f3e8ff; color: #6b21a8;">مجموع اختلاف: <span class="toCurrency mr-1" dir="ltr">{{ group4_total }}</span></span>
<span class="tag" style="background-color: #b86bff; color: white;">تعداد: {{ group4_count }} مورد</span>
</div>
</div>
<p class="is-size-7 has-text-grey-dark mb-3">
💡 <b>توجه:</b> این مشتریان در دفتر کل طلبکارند، اما اپراتور چک پرداخت‌نشده‌ای هم از آن‌ها نگه داشته است! (نیاز به بررسی حسابداری دارد).
</p>
<div class="table-container">
<table class="table is-fullwidth is-hoverable is-striped is-bordered is-size-7">
<thead>
<tr style="background-color: #faf5ff;">
<th class="has-text-centered">ردیف</th>
<th>نام مشتری</th>
<th>تراز واقعی (بستانکار)</th>
<th>چک ثبت‌شده</th>
<th class="has-text-centered">عملیات</th>
</tr>
</thead>
<tbody>
{% for c in group4_sample %}
<tr>
<td class="has-text-centered">{{ forloop.counter }}</td>
<td><strong>{{ c.name_family }}</strong></td>
<td class="toCurrency has-text-success-dark" dir="ltr">{{ c.net_debt }}</td>
<td class="toCurrency has-text-danger">{{ c.t_sarresid }}</td>
<td class="has-text-centered">
<a href="{% url 'admin_panel:customer' c.id %}" target="_blank" class="button is-small is-light is-outlined rounded" style="border-color: #b86bff; color: #b86bff;">بررسی فنی</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
</div>
</div>
<style>
@@ -161,6 +432,7 @@
.shadow-card { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.04); border: 1px solid #f0f0f0; border-radius: 8px; }
.border-left-danger { border-right: 4px solid #f14668 !important; }
.border-left-success { border-right: 4px solid #48c78e !important; }
.border-left-info { border-right: 4px solid #3e8ed0 !important; }
.border-left-warning { border-right: 4px solid #ffdd57 !important; }
.bg-light-filter { background-color: #fafafa; border-radius: 6px; }
.h-100 { height: 100%; }
@@ -178,22 +450,16 @@
/* مدیا کوئری اختصاصی برای موبایل و تبلت‌های کوچک */
@media screen and (max-width: 1023px) {
/* در موبایل خط عمودی به خط افقی بالا تغییر می‌کند */
.responsive-border-between {
border-right: none;
border-top: 1px solid #f0f0f0;
padding-top: 1.5rem !important;
margin-top: 1rem;
}
/* کاهش ارتفاع نمودارها در موبایل برای جلوگیری از اسکرول طولانی */
.chart-wrapper-tall { height: 260px; }
.chart-wrapper-medium { height: 240px; }
.chart-wrapper-short { height: 200px; }
.mb-2-mobile { margin-bottom: 0.5rem !important; }
/* برای بهتر دیده شدن تگ‌ها در صفحه کوچک */
.is-flex-grow-1-mobile { flex-grow: 1; text-align: right; }
}
</style>
@@ -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);
}
});
});
</script>
{% endblock %}
+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):