hesabdari va dashboard

This commit is contained in:
javad committed 2026-06-30 20:25:39 +03:30
1 parent 767e84cbcf
commit bd775d6249
5 files changed
+808 -244

No files matched your search

@@ -99,6 +99,10 @@
<a href="{% url 'admin_panel:inventory_movements_report' %}" style="padding-right: 10px;" class="navbar-item">
گزارش جامع انبار
</a>
<a href="{% url 'admin_panel:financial_dashboard' %}" style="padding-right: 10px;" class="navbar-item">
داشبورد مدیریتی
</a>
</div>
</div>
@@ -0,0 +1,372 @@
{% extends "./base.html" %}
{% load custom_tags %}
{% block bodyPage %}
<div class="container is-fluid mt-4 mb-6">
<div class="columns is-vcentered pb-4 mb-5" style="border-bottom: 2px solid #f5f5f5;">
<div class="column is-12-mobile is-half-tablet has-text-centered-mobile">
<h1 class="title is-4 is-size-5-mobile has-text-dark">اتاق فرمان مالی و داشبورد تجاری</h1>
<p class="subtitle is-7 has-text-grey mt-1">رصد زنده شریان نقدینگی، سن مطالبات و مانیتورینگ پویای انبار | <span class="tag is-dark is-light">سال مالی</span></p>
</div>
<div class="column is-12-mobile is-half-tablet">
<form method="GET" class="box shadow-sm bg-light-filter p-3 m-0">
<div class="columns is-mobile is-multiline is-vcentered m-0">
<div class="column is-6-mobile is-narrow-tablet p-1">
<label class="label is-size-7 pr-1 mb-1">از تاریخ</label>
<input type="text" class="input is-small date-picker-az pwt-datepicker-input-element w-100" placeholder="آغاز دوره">
<input type="hidden" name="azTarikh" id="azTarikhAlt" value="{{ filters.azTarikh }}">
</div>
<div class="column is-6-mobile is-narrow-tablet p-1">
<label class="label is-size-7 pr-1 mb-1">تا تاریخ</label>
<input type="text" class="input is-small date-picker-ta pwt-datepicker-input-element w-100" placeholder="پایان دوره">
<input type="hidden" name="taTarikh" id="taTarikhAlt" value="{{ filters.taTarikh }}">
</div>
<div class="column is-12-mobile is-narrow-tablet p-1 mt-auto has-text-centered-mobile">
<button type="submit" class="button is-danger is-small is-fullwidth-mobile px-4">
<span>اعمال گزارش پویا</span>
</button>
</div>
{% if filters.azTarikh or filters.taTarikh %}
<div class="column is-12-mobile is-narrow-tablet p-1 mt-auto has-text-centered-mobile">
<a href="{% url 'admin_panel:financial_dashboard' %}" class="button is-light is-small is-fullwidth-mobile has-text-grey">حذف فیلتر</a>
</div>
{% endif %}
</div>
</form>
</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>
</div>
<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>
</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="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>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="columns is-multiline mt-1">
<div class="column is-12">
<div class="box p-4 shadow-card">
<div class="level is-mobile is-flex-wrap-wrap mb-4 pb-2" style="border-bottom: 1px solid #f0f0f0;">
<div class="level-left mb-2-mobile">
<p class="title is-6 is-size-7-mobile mb-0">📈 روند جمع کل بدهی مشتریان در ۱۲ ماه گذشته</p>
</div>
<div class="level-right">
<span class="tag is-info is-light is-rounded is-small-mobile">محاسبه بر اساس ماهیانه</span>
</div>
</div>
<div class="chart-wrapper-medium">
<canvas id="debtTrendChart"></canvas>
</div>
</div>
</div>
</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>
</div>
<div class="notification is-light is-danger is-paddingless p-3 mt-4 is-size-7" style="line-height: 1.6;">
📌 <strong>سیگنال مدیریتی:</strong> مبالغ انباشته شده در بخش <strong>بحرانی (قرمز)</strong> پتانسیل بالایی برای تبدیل شدن به سوخت سرمایه دارند.
</div>
</div>
</div>
<div class="column is-12-mobile is-5-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="paymentMethodsChart"></canvas>
</div>
</div>
</div>
<div class="column is-12">
<div class="box p-4 shadow-card">
<p class="title is-6 is-size-7-mobile pb-2 mb-4" style="border-bottom: 1px solid #f0f0f0;">
📦 رادار هزینه‌های جانبی کالا (مقایسه آماری خروج گارانتی در برابر اشانتیون)
</p>
<div class="columns is-vcentered is-multiline">
<div class="column is-12-mobile is-7-desktop">
<div class="chart-wrapper-short">
<canvas id="inventoryLeakChart"></canvas>
</div>
</div>
<div class="column is-12-mobile is-5-desktop responsive-border-between">
<div class="px-3">
<h4 class="title is-7 mb-3 has-text-grey-dark">🔍 خلاصه وضعیت خروجی‌های غیردرآمدی انبار:</h4>
<div class="tags has-addons mb-2 is-justify-content-space-between-mobile">
<span class="tag is-warning is-light is-flex-grow-1-mobile">حجم کالای مصرفی در تعهدات گارانتی</span>
<span class="tag is-warning">{{ warranty_qty }} عدد</span>
</div>
<div class="tags has-addons mb-4 is-justify-content-space-between-mobile">
<span class="tag is-info is-light is-flex-grow-1-mobile">حجم کالای مصرفی در جوایز و اشانتیون</span>
<span class="tag is-info">{{ promotion_qty }} عدد</span>
</div>
<p class="is-size-7 has-text-grey" style="text-align: justify; line-height: 1.8;">
💡 <strong>راهبرد نظارتی انبار:</strong> نوسان شدید در این دو نمودار به مدیر کمک می‌کند تا متوجه افت کیفیت در محصولات تامین‌شده (جهش گارانتی) یا توزیع بی‌ضابطه و خارج از فاکتور توسط اپراتورها (جهش اشانتیون) شود.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
/* استایل‌های پایه کارت‌ها */
.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-warning { border-right: 4px solid #ffdd57 !important; }
.bg-light-filter { background-color: #fafafa; border-radius: 6px; }
.h-100 { height: 100%; }
.w-100 { width: 100%; }
/* کانتینرهای ریسپانسیو برای نمودارها */
.chart-wrapper-tall { position: relative; height: 320px; width: 100%; }
.chart-wrapper-medium { position: relative; height: 280px; width: 100%; }
.chart-wrapper-short { position: relative; height: 220px; width: 100%; }
/* کلاس سفارشی برای جداکننده خطی بین متن و نمودار انبار */
.responsive-border-between {
border-right: 1px solid #f0f0f0;
}
/* مدیا کوئری اختصاصی برای موبایل و تبلت‌های کوچک */
@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>
{% endblock %}
{% block jsEnd %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
$(document).ready(function() {
// ۱. پیاده‌سازی موتور تقویم جلالی
var pickerAz = $(".date-picker-az").pDatepicker({
altField: '#azTarikhAlt',
initialValue: false,
autoClose: true,
format: 'YYYY/MM/DD',
calendar: { persian: { locale: 'fa' } },
responsive: true // قابلیت تقویم برای موبایل
});
var pickerTa = $(".date-picker-ta").pDatepicker({
altField: '#taTarikhAlt',
initialValue: false,
autoClose: true,
format: 'YYYY/MM/DD',
calendar: { persian: { locale: 'fa' } },
responsive: true
});
{% 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)');
new Chart(ctxTrend, {
type: 'line',
data: {
labels: {{ trend_labels|safe }},
datasets: [{
label: 'جمع کل بدهی (ریال)',
data: {{ trend_data|safe }},
backgroundColor: gradientTrend,
borderColor: '#3e8ed0',
borderWidth: 2.5,
pointBackgroundColor: '#ffffff',
pointBorderColor: '#3e8ed0',
pointBorderWidth: 2,
pointRadius: 4,
pointHoverRadius: 6,
fill: true,
tension: 0.35
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
titleFont: { family: 'Tahoma, Vazir', size: 12 },
bodyFont: { family: 'Tahoma, Vazir', size: 12 },
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) { label += ': '; }
if (context.parsed.y !== null) {
label += new Intl.NumberFormat('fa-IR').format(context.parsed.y);
}
return label;
}
}
}
},
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',
},
}
});
// ۲. نمودار سنی مطالبات
var ctxAging = document.getElementById('agingChart').getContext('2d');
new Chart(ctxAging, {
type: 'bar',
data: {
labels: ['سررسید آینده', 'تاخیر ناچیز', 'تاخیر پرخطر', 'بحرانی'],
datasets: [{
data: {{ aging_data|safe }},
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 // ضخامت میله‌ها برای موبایل بهتر است
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
y: { beginAtZero: true, ticks: { font: { family: 'Tahoma, Vazir', size: 10 } } },
x: { ticks: { font: { family: 'Tahoma, Vazir', size: 10 } } }
}
}
});
// ۳. نمودار دایره‌ای تسویه مبالغ
var ctxPay = document.getElementById('paymentMethodsChart').getContext('2d');
new Chart(ctxPay, {
type: 'doughnut',
data: {
labels: {{ pay_labels|safe }},
datasets: [{
data: {{ pay_data|safe }},
backgroundColor: ['#485fc7', '#48c78e', '#ffdd57', '#f14668', '#3e8ed0', '#7a7a7a'],
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
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',
data: {
labels: ['گارانتی', 'اشانتیون'],
datasets: [{
data: [{{ warranty_qty }}, {{ promotion_qty }}],
backgroundColor: ['rgba(255, 221, 87, 0.8)', 'rgba(62, 142, 208, 0.8)'],
borderColor: ['#ffdd57', '#3e8ed0'],
borderWidth: 1.2,
barThickness: 25
}]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
x: { beginAtZero: true, ticks: { font: { family: 'Tahoma, Vazir', size: 10 } } },
y: { ticks: { font: { family: 'Tahoma, Vazir', size: 10 } } }
}
}
});
});
</script>
{% endblock %}
+235 -234
View File
@@ -3,236 +3,248 @@
{% if perms.admin_panel.ViewReportMali %}
{% block bodyPage %}
<div class="">
<div class="columns m-0">
<div class="column is-12">
<div class="buttons">
<style>
.risk-card { transition: transform 0.2s, box-shadow 0.2s; border-radius: 8px; border: 1px solid #eee; }
.risk-card:hover { transform: translateY(-3px); box-shadow: 0 4px 15px rgba(0,0,0,0.05); }
.border-danger { border-bottom: 4px solid #f14668 !important; }
.border-warning { border-bottom: 4px solid #ffdd57 !important; }
.border-info { border-bottom: 4px solid #3e8ed0 !important; }
.border-dark { border-bottom: 4px solid #363636 !important; }
.filter-tabs { margin-bottom: 1.5rem; }
</style>
</div>
<p class="has-text-centered">
{% if page_name == 'sarresidShodeha' %}
<p class="toCurrency has-text-centered">
سررسید شده ها
{{ sum_sarresid }}
</p>
{% endif %}
{% if page_name == 'sarresidNaShodeha' %}
<p class="toCurrency has-text-centered">
سررسید نشده ها
{{ sarresidNashode }}
</p>
{% endif %}
{% if page_name == 'checkPass' %}
چک های پاس شده
{% endif %}
{% if page_name == 'checkPassFaild' %}
<p class="toCurrency has-text-centered">
چک های پاس نشده
{{ count_check_passfail }}
عدد | جمع مبلغ:
{{ sum_check_passfail }}
</p>
{% endif %}
<span class="toCurrency">{{ mandehesabKP }}</span>
</p>
<div class="table-container column is-12-desktop is-12-mobile m-0">
<table class="table m-auto has-text-centered table is-bordered is-striped is-narrow is-fullwidth mojodiTB ">
<thead>
<tr>
<th style="min-width: 150px;">مبلغ</th>
<th style="min-width: 150px;">نام</th>
<th style="min-width: 100px;">نوع</th>
<th style="min-width: 100px;">سررسید</th>
<th style="min-width: 100px;">ثبت</th>
<th style="min-width: 400px;">توضیحات</th>
<th style="min-width: 100;">وضعیت</th>
<th style="min-width: 100;">وصول شد</th>
<div class="container is-fluid mt-4 mb-6">
<div class="columns m-0 is-multiline">
<div class="column is-12">
</tr>
</thead>
<tfoot>
<tr>
<th class="toCurrency" style="min-width: 150px;">مبلغ</th>
<th style="min-width: 150px;">نام</th>
<th style="min-width: 100px;">نوع</th>
<th style="min-width: 100px;">سررسید</th>
<th style="min-width: 100px;">ثبت</th>
<th style="min-width: 400px;">توضیحات</th>
<th style="min-width: 100;">وضعیت</th>
<th style="min-width: 100;">وصول شد</th>
</tr>
</tfoot>
<tbody id="listPrds">
{% for sarresid in all_sarresid %}
<tr class="IteamPrd">
<td>
<span class="bedehkar toCurrency">{{ sarresid.price }}</span>
<span class=" is-hidden"></span>
</td>
<td class="">
<a target="_blank" href="{% url 'admin_panel:customerNew' sarresid.customer.id %}">
{{ sarresid.customer.name_family }}
</a>
</td>
<td class="">
{% if sarresid.type == 'check' %}چک{% endif %}
{% if sarresid.type == 'nesiye' %}نسیه{% endif %}
</td>
<td class="">{{ sarresid.sarresidDT | toJalali }}</td>
<td class="">{{ sarresid.createdDT | toJalali }}</td>
<td class="desc">
{% if sarresid.desc %}
{{ sarresid.desc }}
{% endif %}
{% if sarresid.factor %}
فاکتور شماره {{ sarresid.factor.id }}
{% endif %}
</td>
<td class="">
{% if sarresid.payStatus == "true" %}پرداخت شده{% endif %}
{% if sarresid.payStatus == "false" %}پرداخت نشده{% endif %}
</td>
<td class="">
{% if sarresid.payStatus == "false" %}
<span class="button is-danger VosolButton" csid= {{ sarresid.customer.id }} price={{ sarresid.price }} type={{ sarresid.type }} articlID={{ sarresid.id }}>وصول شد</span>
{% else %}
<span>بله</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if page_name == 'sarresidShodeha' %}
<div class="mb-5">
<h2 class="title is-5 mb-4 has-text-grey-dark">📊 حسابداری | سررسید شده ها</h2>
<div class="columns is-multiline is-mobile">
<div class="column is-6-mobile is-3-desktop">
<div class="box risk-card border-dark has-text-centered p-4 h-100">
<p class="heading has-text-grey">کل مطالبات سررسید شده</p>
<p class="title is-size-4 is-size-5-mobile has-text-dark toCurrency">{{ sum_sarresid }}</p>
</div>
</div>
{% if all_sarresid.has_previous %}
<a href="?page={{ all_sarresid.previous_page_number }}" class="button is-danger">صفحه قبل</a>
{% endif %}
{% if all_sarresid.has_next %}
<a href="?page={{ all_sarresid.next_page_number }}" class="button is-danger">صفحه بعد</a>
{% endif %}
<div class="column is-6-mobile is-3-desktop">
<div class="box risk-card border-info has-text-centered p-4 h-100">
<p class="heading has-text-grey">تأخیر ناچیز (۱ تا ۳۰ روز)</p>
<p class="title is-size-4 is-size-5-mobile has-text-info toCurrency">{{ minor_sum }}</p>
</div>
</div>
</div>
<div class="column is-6-mobile is-3-desktop">
<div class="box risk-card border-warning has-text-centered p-4 h-100">
<p class="heading has-text-grey">پرخطر (۳۱ تا ۹۰ روز)</p>
<p class="title is-size-4 is-size-5-mobile has-text-warning-dark toCurrency">{{ high_sum }}</p>
</div>
</div>
</div>
</div>
</div>
<div class="modal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Modal title</p>
<button class="delete" aria-label="close"></button>
</header>
<section class="modal-card-body">
<form>
<input type="text" class="is-hidden" name="articlID">
</form>
</section>
<footer class="modal-card-foot">
<button class="button is-success">Save changes</button>
<button class="button">Cancel</button>
</footer>
</div>
</div>
<form class="modal nesiyeModal" method="post" action="{% url 'admin_panel:pardakhtNesiye' 9101010101 %}">
{% csrf_token %}
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">نسیه</p>
<button class="delete" aria-label="close"></button>
</header>
<section class="modal-card-body">
<div class="field">
<label class="label">مانده از این نسیه</label>
<div class="control">
<input id="priceBedehi" disabled class="input reset_input" type="text" placeholder="واحد تومان">
</div>
</div>
<input name="sr_articlID" id="sr_articlID" class="input reset_input is-hidden" type="text">
<div class="field">
<label class="label">مبلغ پرداختی فعلی</label>
<div class="control">
<input name="next_sarresidPrice" required class="input reset_input" type="text" placeholder="واحد تومان">
</div>
</div>
<div class="field">
<label class="label">نحوه پرداخت</label>
<div class="control">
<div class="select is-fullwidth">
<select required class="reset_input" name="nahvePardakht">
<option></option>
<option value="check">چک</option>
<option value="naghd">نقد</option>
<option value="kartbekart">کارت به کارت</option>
<option value="kartkhan">کارت خوان</option>
<option value="takhfif">تخفیف</option>
</select>
<div class="column is-6-mobile is-3-desktop">
<div class="box risk-card border-danger has-text-centered p-4 h-100">
<p class="heading has-text-grey">بحرانی (بیش از ۹۰ روز)</p>
<p class="title is-size-4 is-size-5-mobile has-text-danger toCurrency">{{ critical_sum }}</p>
</div>
</div>
</div>
</div>
<div class="tabs is-toggle is-fullwidth filter-tabs is-small-mobile">
<ul>
<li class="{% if current_filter == 'all' or not current_filter %}is-active{% endif %}">
<a href="?risk_level=all">همه معوقات</a>
</li>
<li class="{% if current_filter == 'minor' %}is-active{% endif %}">
<a href="?risk_level=minor" class="has-text-info">تأخیر ناچیز</a>
</li>
<li class="{% if current_filter == 'high' %}is-active{% endif %}">
<a href="?risk_level=high" class="has-text-warning-dark">پرخطر</a>
</li>
<li class="{% if current_filter == 'critical' %}is-active{% endif %}">
<a href="?risk_level=critical" class="has-text-danger">بحرانی</a>
</li>
</ul>
</div>
{% endif %}
{% if page_name == 'sarresidNaShodeha' %}
<p class="toCurrency has-text-centered is-size-5 mb-4">سررسید نشده ها: {{ sarresidNashode }}</p>
{% endif %}
{% if page_name == 'checkPass' %}
<p class="has-text-centered is-size-5 mb-4">چک های پاس شده</p>
{% endif %}
{% if page_name == 'checkPassFaild' %}
<p class="has-text-centered is-size-5 mb-4">
چک های برگشتی: {{ count_check_passfail }} فقره | جمع مبلغ: <span class="toCurrency">{{ sum_check_passfail }}</span>
</p>
{% endif %}
<div class="table-container column is-12-desktop is-12-mobile m-0 shadow-sm" style="border-radius: 8px; border: 1px solid #eaeaea;">
<table class="table m-auto has-text-centered is-bordered is-striped is-narrow is-fullwidth is-hoverable mojodiTB">
<thead class="has-background-light">
<tr>
<th style="min-width: 150px;">مبلغ (تومان)</th>
<th style="min-width: 150px;">نام مشتری</th>
<th style="min-width: 100px;">نوع سند</th>
<th style="min-width: 120px;">سررسید</th>
<th style="min-width: 120px;">تاریخ ثبت</th>
<th style="min-width: 300px;">توضیحات</th>
<th style="min-width: 100px;">وضعیت</th>
<th style="min-width: 100px;">عملیات</th>
</tr>
</thead>
<tbody id="listPrds">
{% for sarresid in all_sarresid %}
<tr class="IteamPrd">
<td>
<span class="bedehkar toCurrency has-text-weight-bold">{{ sarresid.price }}</span>
</td>
<td>
<a target="_blank" href="{% url 'admin_panel:customerNew' sarresid.customer.id %}" class="has-text-link">
{{ sarresid.customer.name_family }}
</a>
</td>
<td>
{% if sarresid.type == 'check' %}<span class="tag is-light is-rounded">چک</span>{% endif %}
{% if sarresid.type == 'nesiye' %}<span class="tag is-white is-rounded border">نسیه</span>{% endif %}
</td>
<td class="has-text-danger-dark">{{ sarresid.sarresidDT | toJalali }}</td>
<td class="has-text-grey is-size-7">{{ sarresid.createdDT | toJalali }}</td>
<td class="desc is-size-7" style="text-align: right;">
{% if sarresid.desc %}{{ sarresid.desc }}<br>{% endif %}
{% if sarresid.factor %}
<span class="tag is-light is-info">فاکتور شماره {{ sarresid.factor.id }}</span>
{% endif %}
</td>
<td>
{% if sarresid.payStatus == "true" %}<span class="has-text-success">پرداخت شده</span>{% endif %}
{% if sarresid.payStatus == "false" %}<span class="has-text-danger">پرداخت نشده</span>{% endif %}
</td>
<td>
{% if sarresid.payStatus == "false" %}
<button class="button is-danger is-small is-outlined VosolButton" csid="{{ sarresid.customer.id }}" price="{{ sarresid.price }}" type="{{ sarresid.type }}" articlID="{{ sarresid.id }}">وصول شد</button>
{% else %}
<span class="icon has-text-success"><i class="fas fa-check-circle"></i> بله</span>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="8" class="has-text-grey p-5">هیچ سندی در این محدوده یافت نشد.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="field">
<label class="label">سررسید بعدی</label>
<div class="control">
<input name="xxxxxaaaa" required class="input reset_input" id="NextSarResid" type="text">
<div class="mt-4 is-flex is-justify-content-center">
<div class="buttons">
{% if all_sarresid.has_previous %}
<a href="?page={{ all_sarresid.previous_page_number }}&risk_level={{ current_filter|default:'all' }}" class="button is-light">صفحه قبل</a>
{% endif %}
{% if all_sarresid.has_next %}
<a href="?page={{ all_sarresid.next_page_number }}&risk_level={{ current_filter|default:'all' }}" class="button is-light">صفحه بعد</a>
{% endif %}
</div>
</div>
<div class="field">
<label class="label">توضیحات</label>
<div class="control">
<textarea name="desc_sar" class="input"></textarea>
</div>
</div>
</div>
</div>
<div class="modal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Modal title</p>
<button class="delete" aria-label="close"></button>
</header>
<section class="modal-card-body">
<form>
<input type="text" class="is-hidden" name="articlID">
</form>
</section>
<footer class="modal-card-foot">
<button class="button is-success">Save changes</button>
<button class="button">Cancel</button>
</footer>
</div>
</div>
<form class="modal nesiyeModal" method="post" action="{% url 'admin_panel:pardakhtNesiye' 9101010101 %}">
{% csrf_token %}
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">نسیه</p>
<button class="delete" aria-label="close"></button>
</header>
<section class="modal-card-body">
<div class="field">
<label class="label">مانده از این نسیه</label>
<div class="control">
<input id="priceBedehi" disabled class="input reset_input" type="text" placeholder="واحد تومان">
</div>
</div>
<input name="sr_articlID" id="sr_articlID" class="input reset_input is-hidden" type="text">
<div class="field">
<label class="label">مبلغ پرداختی فعلی</label>
<div class="control">
<input name="next_sarresidPrice" required class="input reset_input" type="text" placeholder="واحد تومان">
</div>
</div>
<div class="field">
<label class="label">نحوه پرداخت</label>
<div class="control">
<div class="select is-fullwidth">
<select required class="reset_input" name="nahvePardakht">
<option></option>
<option value="check">چک</option>
<option value="naghd">نقد</option>
<option value="kartbekart">کارت به کارت</option>
<option value="kartkhan">کارت خوان</option>
<option value="takhfif">تخفیف</option>
</select>
</div>
</div>
</div>
<div class="field">
<label class="label">سررسید بعدی</label>
<div class="control">
<input name="xxxxxaaaa" required class="input reset_input" id="NextSarResid" type="text">
</div>
</div>
<div class="field">
<label class="label">توضیحات</label>
<div class="control">
<textarea name="desc_sar" class="input"></textarea>
</div>
</div>
<input required type="text" class="is-hidden reset_input" name="next_sarresidDateTime" id="NextSarResidVal">
</section>
<footer class="modal-card-foot">
<input type="submit" class="button is-success" value="ثبت">
<span class="button cancellBTNSR">لغو</span>
</footer>
</div>
</form>
<input required type="text" class="is-hidden reset_input" name="next_sarresidDateTime" id="NextSarResidVal">
</section>
<footer class="modal-card-foot">
<input type="submit" class="button is-success" value="ثبت">
<span class="button cancellBTNSR">لغو</span>
</footer>
</div>
</form>
{% csrf_token %}
{% endblock %}
{% block jsEnd %}
<script>
<script>
$(document).ready(function() {
var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
$(".modal-background").click(function(){
$(".modal").removeClass("is-active");
$('.reset_input').val('');
})
$(".cancellBTNSR").click(function(){
$(".modal").removeClass("is-active");
$('.reset_input').val('');
@@ -244,9 +256,7 @@ $(document).ready(function() {
format: 'YYYY/MM/DD',
autoClose: true,
calendar:{
persian: {
locale: 'fa'
}
persian: { locale: 'fa' }
},
onSelect: (unix)=>{
$("#NextSarResidVal").val(unix)
@@ -255,7 +265,6 @@ $(document).ready(function() {
var csid = null
$(".VosolButton").click(function(){
var articlID = $(this).attr("articlID")
var type = $(this).attr("type")
var price = $(this).attr("price")
@@ -265,26 +274,20 @@ $(document).ready(function() {
if( confirm('چک وصول شده؟') ){
var prompt_desc = prompt('لطفا توضیحات را وارد کنید و تایید کنید')
var url_check = '/panel/passCheck/'+articlID;
if( prompt_desc ){
$.ajax({
url: url_check,
type: 'post',
data: {
message: prompt_desc
},
headers: {'X-CSRFToken': csrftoken },
success: function (data) {
console.log("ok ok");
window.location.reload();
}
});
}
if( prompt_desc ){
$.ajax({
url: url_check,
type: 'post',
data: { message: prompt_desc },
headers: {'X-CSRFToken': csrftoken },
success: function (data) {
window.location.reload();
}
});
}
}
} else {
var actionUrl = $(".nesiyeModal").attr('action')
//alert(actionUrl.replace("9101010101", csid ))
//$(".nesiyeModal").attr('action', actionUrl.replace("9101010101", csid ) )
$(".nesiyeModal").addClass("is-active");
$("#priceBedehi").val(price);
$("#sr_articlID").val(articlID)
@@ -294,10 +297,8 @@ $(document).ready(function() {
$("form").submit(function(e){
var actionUrl = $(".nesiyeModal").attr('action')
$(".nesiyeModal").attr('action', actionUrl.replace("9101010101", csid ) )
});
});
</script>
{% endblock %}
{% endif %}
</script>
{% endblock %}
{% endif %}
+1
View File
@@ -12,6 +12,7 @@ urlpatterns = [
path('bedehi/', views.bedehi, name='bedehi'),
path('report/', views.report, name='report'),
path('report/inventory-movements/', views.inventory_movements_report, name='inventory_movements_report'),
path('report/dashboard/', views.financial_dashboard, name='financial_dashboard'),
path('hesabdari/', views.hesabdari, name='hesabdari'),
path('hesabdari/sarresid_nashode/', views.hesabdari_residnashone, name='hesabdari_residnashone'),
path('hesabdari/check/pass/', views.hesabdari_checkPass, name='hesabdari_check_pass'),
+196 -10
View File
@@ -266,6 +266,159 @@ def inventory_movements_report(request):
return render(request, 'admin_panel/report_inventory_movements.html', context)
@login_required(login_url='/panel/p_login/')
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
def financial_dashboard(request):
"""
داشبورد هوش مالی و مدیریت نقدینگی سطح Enterprise (BI Dashboard)
همراه با روند ۱۲ ماهه جمع کل بدهی مشتریان (تفاضل debit و credit).
"""
# دریافت تایم‌استمپ‌های ارسالی از تقویم شمسی فرانت‌اند
az_tarikh = request.GET.get('azTarikh', '')
ta_tarikh = request.GET.get('taTarikh', '')
az_dt = None
ta_dt = None
if az_tarikh:
try:
az_dt = datetime.datetime.fromtimestamp(int(az_tarikh) / 1000.0)
az_dt = az_dt.replace(hour=0, minute=0, second=0)
except (ValueError, TypeError):
pass
if ta_tarikh:
try:
ta_dt = datetime.datetime.fromtimestamp(int(ta_tarikh) / 1000.0)
ta_dt = ta_dt.replace(hour=23, minute=59, second=59)
except (ValueError, TypeError):
pass
# نقطه اتکا (لنگر) محاسبات: زمان انتخابی کاربر یا زمان حال
anchor_dt = ta_dt if ta_dt else datetime.datetime.now()
# =========================================================================
# محاسبه روند ۱۲ ماهه غلتان (TTM) - نسخه اصلاح‌شده و امن
# =========================================================================
jalali_months = ['فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور', 'مهر', 'آبان', 'آذر', 'دی', 'بهمن', 'اسفند']
trend_labels = []
trend_data = []
# روش استاندارد و بدون باگ برای تبدیل تاریخ میلادی به شمسی در persiantools
# با این کار مطمئن می‌شویم مقادیر حتماً عدد صحیح و مجاز هستند
anchor_jd = JalaliDate(anchor_dt.date())
curr_y = int(anchor_jd.year)
curr_m = int(anchor_jd.month)
# حلقه استخراج داده برای ۱۲ ماه گذشته
for _ in range(12):
if curr_m < 1:
curr_m = 12
curr_y -= 1
label = f"{jalali_months[curr_m - 1]} {curr_y}"
trend_labels.insert(0, label)
# ❗️ اصلاح اساسی: ترتیب آرگومان‌ها در این تابع باید (month, year) باشد
last_day = JalaliDate.days_in_month(curr_m, curr_y)
# اما ترتیب سازنده کلاس اصلی همچنان (year, month, day) است
end_of_month_jd = JalaliDate(curr_y, curr_m, last_day)
end_of_month_gd = end_of_month_jd.to_gregorian()
# ساخت رشته زمان برای پایان آن ماه (تا ساعت 23:59:59)
end_dt_str = end_of_month_gd.strftime('%Y-%m-%d 23:59:59')
# محاسبه مانده حساب کل تا پایان این ماه
totals = Article.objects.filter(createdDT__lte=end_dt_str).aggregate(
t_deb=Sum('debit'),
t_cred=Sum('credit')
)
t_deb = totals['t_deb'] or 0
t_cred = totals['t_cred'] or 0
month_total_debt = t_deb - t_cred
trend_data.insert(0, month_total_debt)
# شیفت به ماه قبل برای دور بعدی حلقه
curr_m -= 1
# =========================================================================
# قرنطینه داده‌های سایر بخش‌های داشبورد بر اساس سال مالی ۱۴۰۰
# =========================================================================
sarresid_qs = Sarresid.objects.filter(factor__productinstore__year=1400).distinct()
factor_qs = Factor.objects.filter(productinstore__year=1400, is_active=True).distinct()
article_qs = Article.objects.filter(factor__productinstore__year=1400).distinct()
store_qs = ProductInStore.objects.filter(year=1400)
if az_dt:
sarresid_qs = sarresid_qs.filter(createdDT__gte=az_dt)
factor_qs = factor_qs.filter(create_date__gte=az_dt)
article_qs = article_qs.filter(createdDT__gte=az_dt)
store_qs = store_qs.filter(createdDT__gte=az_dt)
if ta_dt:
sarresid_qs = sarresid_qs.filter(createdDT__lte=ta_dt)
factor_qs = factor_qs.filter(create_date__lte=ta_dt)
article_qs = article_qs.filter(createdDT__lte=ta_dt)
store_qs = store_qs.filter(createdDT__lte=ta_dt)
# ۱. محاسبات کارت‌های شاخص عملکرد (KPI Cards)
total_unpaid_debts = sarresid_qs.filter(payStatus="false").aggregate(Sum('price'))['price__sum'] or 0
total_sales_period = factor_qs.aggregate(Sum('amount'))['amount__sum'] or 0
overdue_checks_count = sarresid_qs.filter(payStatus="false", type="check", sarresidDT__lte=anchor_dt).count()
# ۲. نمودار اول: تحلیل پویای سن مطالبات
debt_future = sarresid_qs.filter(payStatus="false", sarresidDT__gt=anchor_dt).aggregate(Sum('price'))['price__sum'] or 0
dt_30_days_ago = anchor_dt - datetime.timedelta(days=30)
debt_0_30 = sarresid_qs.filter(payStatus="false", sarresidDT__lte=anchor_dt, sarresidDT__gte=dt_30_days_ago).aggregate(Sum('price'))['price__sum'] or 0
dt_90_days_ago = anchor_dt - datetime.timedelta(days=90)
debt_30_90 = sarresid_qs.filter(payStatus="false", sarresidDT__lt=dt_30_days_ago, sarresidDT__gte=dt_90_days_ago).aggregate(Sum('price'))['price__sum'] or 0
debt_above_90 = sarresid_qs.filter(payStatus="false", sarresidDT__lt=dt_90_days_ago).aggregate(Sum('price'))['price__sum'] or 0
# ۳. نمودار دوم: ماتریس کیفیت ورودی پول
payment_articles = article_qs.filter(credit__gt=0).values('nahvePardakht').annotate(total_amount=Sum('credit')).order_by('-total_amount')
pay_labels = []
pay_data = []
for item in payment_articles:
label = item['nahvePardakht'] or 'سایر/نامشخص'
pay_labels.append(label)
pay_data.append(item['total_amount'])
# ۴. نمودار سوم: مانیتورینگ نشت انبار
store_leaks = store_qs.filter(
movement_type__in=[ProductInStore.MovementType.WARRANTY, ProductInStore.MovementType.PROMOTION]
).values('movement_type').annotate(total_qty=Sum('s_out'))
leak_data = {'WARRANTY': 0, 'PROMOTION': 0}
for leak in store_leaks:
leak_data[leak['movement_type']] = leak['total_qty']
# ارسال ساختار داده به تمپلیت
context = {
'kpi_total_unpaid': total_unpaid_debts,
'kpi_total_sales': total_sales_period,
'kpi_overdue_checks': overdue_checks_count,
'trend_labels': trend_labels, # نام ماه‌ها برای محور X
'trend_data': trend_data, # جمع کل بدهی‌ها برای محور Y
'aging_data': [debt_future, debt_0_30, debt_30_90, debt_above_90],
'pay_labels': pay_labels,
'pay_data': pay_data,
'warranty_qty': leak_data['WARRANTY'],
'promotion_qty': leak_data['PROMOTION'],
'filters': {
'azTarikh': az_tarikh,
'taTarikh': ta_tarikh,
}
}
return render(request, 'admin_panel/financial_dashboard.html', context)
@login_required(login_url='/panel/p_login/')
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
def bedehi(request):
@@ -288,25 +441,58 @@ def bedehi(request):
@login_required(login_url='/panel/p_login/')
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
def hesabdari(request):
page_name = 'sarresidShodeha'
anchor_dt = datetime.datetime.now()
dateNow = datetime.datetime.today().strftime('%Y-%m-%d 23:59:59')
all_sarresid = Sarresid.objects.filter(payStatus="false", sarresidDT__lte=dateNow).order_by('-sarresidDT')
# تعریف محدوده‌های زمانی ماتریس ریسک (سن مطالبات)
dt_30_days_ago = anchor_dt - datetime.timedelta(days=30)
dt_90_days_ago = anchor_dt - datetime.timedelta(days=90)
sum_sarresid = all_sarresid.aggregate(Sum('price'))['price__sum']
# کوئری پایه: تمام سررسیدهای پرداخت‌نشده که از موعد آن‌ها گذشته است
base_qs = Sarresid.objects.filter(payStatus="false", sarresidDT__lte=anchor_dt)
page_number = request.GET.get('page', None)
paginator = Paginator(all_sarresid, per_page=50)
# محاسبه پرسرعت مجموع مبالغ در هر دسته (توسط موتور دیتابیس)
matrix_totals = base_qs.aggregate(
minor_delay=Sum('price', filter=Q(sarresidDT__gte=dt_30_days_ago)),
high_risk=Sum('price', filter=Q(sarresidDT__lt=dt_30_days_ago, sarresidDT__gte=dt_90_days_ago)),
critical=Sum('price', filter=Q(sarresidDT__lt=dt_90_days_ago)),
total_sum=Sum('price')
)
# دریافت فیلتر انتخابی کاربر از URL
risk_filter = request.GET.get('risk_level', 'all')
# اعمال فیلتر روی لیست نمایش داده شده
if risk_filter == 'minor':
filtered_qs = base_qs.filter(sarresidDT__gte=dt_30_days_ago)
elif risk_filter == 'high':
filtered_qs = base_qs.filter(sarresidDT__lt=dt_30_days_ago, sarresidDT__gte=dt_90_days_ago)
elif risk_filter == 'critical':
filtered_qs = base_qs.filter(sarresidDT__lt=dt_90_days_ago)
else:
filtered_qs = base_qs
# مرتب‌سازی: قدیمی‌ترین‌ها (خطرناک‌ترین‌ها) در صدر جدول قرار می‌گیرند
filtered_qs = filtered_qs.order_by('sarresidDT')
page_number = request.GET.get('page', 1)
paginator = Paginator(filtered_qs, per_page=50)
all_sarresid_pgi = paginator.get_page(page_number)
#paginator = Paginator(all_sarresid, per_page=200)
data = {'sum_sarresid': sum_sarresid,'all_sarresid': all_sarresid_pgi, "dateNow":dateNow, "page_name": page_name}
data = {
'page_name': page_name,
'dateNow': anchor_dt.strftime('%Y-%m-%d %H:%M:%S'),
'all_sarresid': all_sarresid_pgi,
'current_filter': risk_filter,
# متغیرهای ماتریس ریسک
'sum_sarresid': matrix_totals['total_sum'] or 0,
'minor_sum': matrix_totals['minor_delay'] or 0,
'high_sum': matrix_totals['high_risk'] or 0,
'critical_sum': matrix_totals['critical'] or 0,
}
return render(request, 'admin_panel/hesabdari.html', data)
@login_required(login_url='/panel/p_login/')
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
def hesabdari_checkPass(request):