Files
khatibi/admin_panel/views/finance.py
T
2026-10-09 20:02:36 +03:30

975 lines
43 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import datetime
import json
from django.db import transaction
from django.db.models import (Sum, Q, Count, Avg, Min, F, Subquery, OuterRef,
IntegerField, Value, Case, When)
from django.db.models.functions import Coalesce
from django.shortcuts import redirect, render, get_object_or_404
from django.http import HttpResponse, HttpResponseBadRequest
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
from django.views.decorators.http import require_POST
from django.contrib import messages
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required, permission_required
from persiantools.jdatetime import JalaliDate
from ..models import (Article, Sarresid, Customer, Factor, ProductInStore,
StoreRoom, Action)
from ._helpers import logger, _safe_int
@login_required(login_url='/panel/p_login/')
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
def report(request):
nahvePardakhat = [
{'fa': 'تخفیف', 'en': 'تخفیف'},
{'fa': 'نسیه', 'en': 'نسیه'},
{'fa': 'چک', 'en': 'چک'},
{'fa': 'کارتخوان', 'en': 'کارت خوان'},
{'fa': 'کارت به کارت', 'en': 'کارت به کارت'},
{'fa': 'نقدی', 'en': 'نقد'},
]
userHa = User.objects.filter()
if request.method == "GET":
nahvePardakht = request.GET.get("nahvepardakht", None)
user = request.GET.get("user", None)
azTarikh = request.GET.get("azTarikh", None)
taTarikh = request.GET.get("taTarikh", None)
pagenumber = request.GET.get("page", None)
if azTarikh and nahvePardakht:
query_data = {
'nahvePardakht': nahvePardakht,
'user': user,
'azTarikh': azTarikh,
'taTarikh': taTarikh,
}
azDateTime = datetime.datetime.fromtimestamp(int(azTarikh) / 1e3)
azDateTime = azDateTime.strftime('%Y-%m-%d 00:00:00')
taDateTime = datetime.datetime.fromtimestamp(int(taTarikh) / 1e3)
taDateTime = taDateTime.strftime('%Y-%m-%d 23:59:59')
print("b")
user_a = User.objects.get(username=user)
print(user_a)
if nahvePardakht == "نسیه" or nahvePardakht == "چک":
if nahvePardakht == 'نسیه':
typeSarresid = 'nesiye'
elif nahvePardakht == 'چک':
typeSarresid = 'check'
sumSarresidha = Sarresid.objects.filter(
type=typeSarresid,
user=user_a,
createdDT__lte=taDateTime,
createdDT__gte=azDateTime,
payStatus='false'
).aggregate(Sum('price'))
sarresidha = Sarresid.objects.filter(
type=typeSarresid,
user=user_a,
createdDT__lte=taDateTime,
createdDT__gte=azDateTime,
payStatus='false'
)
paginator = Paginator(sarresidha, 25) # Show 25 contacts per page.
page_number = request.GET.get('page', None)
all_sarresidha = paginator.get_page(pagenumber)
data = {'sarresidha': all_sarresidha ,'sumSarresidha': sumSarresidha, 'nahvePardakhat': nahvePardakhat, 'userHa':userHa, 'query_data': query_data}
return render(request, 'admin_panel/reportCheckNesiye.html', data)
else:
sumAricles = Article.objects.filter(
nahvePardakht=nahvePardakht,
user=user_a,
createdDT__lte=taDateTime,
createdDT__gte=azDateTime,
).aggregate(Sum('credit'))
aricles1 = Article.objects.filter(
nahvePardakht=nahvePardakht,
user=user_a,
createdDT__lte=taDateTime,
createdDT__gte=azDateTime,
)
paginator = Paginator(aricles1, 25) # Show 25 contacts per page.
page_number = request.GET.get('page', None)
all_aricles = paginator.get_page(page_number)
data = {'aricles': all_aricles,'sum_credit': sumAricles, 'nahvePardakhat': nahvePardakhat, 'userHa':userHa, 'query_data': query_data}
return render(request, 'admin_panel/report.html', data)
else:
data = {'nahvePardakhat': nahvePardakhat, 'userHa':userHa}
return render(request, 'admin_panel/report.html', data)
data = {'nahvePardakhat': nahvePardakhat, 'userHa':userHa}
return render(request, 'admin_panel/report.html', data)
@login_required(login_url='/panel/p_login/')
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
def report_debtors_no_vosol(request):
now = datetime.datetime.now()
# --- ۱. دریافت و اعتبارسنجی پارامترهای فیلتر از فرانت‌اند ---
try:
days_threshold = int(request.GET.get('days', 20))
if days_threshold < 0:
days_threshold = 20
except ValueError:
days_threshold = 20
filter_type = request.GET.get('filter_type', 'all') # all, never, expired
sort_field = request.GET.get('sort', 'amount') # amount, oldest_date, last_action
sort_dir = request.GET.get('dir', 'desc') # asc, desc
threshold_date = now - datetime.timedelta(days=days_threshold)
# --- ۲. ساخت 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_overdue_amt = Sarresid.objects.filter(
customer=OuterRef('pk'), payStatus='false', sarresidDT__lte=now
).values('customer').annotate(s=Sum('price')).values('s')
sq_oldest_date = Sarresid.objects.filter(
customer=OuterRef('pk'), payStatus='false', sarresidDT__lte=now
).values('customer').annotate(min_dt=Min('sarresidDT')).values('min_dt')
sq_overdue_count = Sarresid.objects.filter(
customer=OuterRef('pk'), payStatus='false', sarresidDT__lte=now
).values('customer').annotate(c=Count('id')).values('c')
# استخراج آخرین سابقه اکشن وصول
latest_vosol_qs = Action.objects.filter(
customer=OuterRef('pk'), actionType='vosol'
).order_by('-createdDT')
# --- ۳. ساخت کوئری پایه (فقط مشتریان فعال و دارای معوقه) ---
base_qs = Customer.objects.filter(is_active=True).annotate(
t_debit=Coalesce(Subquery(sq_debit, output_field=IntegerField()), Value(0)),
t_credit=Coalesce(Subquery(sq_credit, output_field=IntegerField()), Value(0)),
overdue_amount=Coalesce(Subquery(sq_overdue_amt, output_field=IntegerField()), Value(0)),
overdue_count=Coalesce(Subquery(sq_overdue_count, output_field=IntegerField()), Value(0)),
oldest_overdue_date=Subquery(sq_oldest_date),
last_vosol_date=Subquery(latest_vosol_qs.values('createdDT')[:1]),
last_vosol_desc=Subquery(latest_vosol_qs.values('desc')[:1])
).annotate(
net_debt=F('t_debit') - F('t_credit')
).filter(
overdue_amount__gt=0 # شرط اساسی: فقط کسانی که معوقه سررسید گذشته دارند
)
# استخراج مشتریانی که نیازمند پیگیری هستند (هرگز پیگیری نشده‌اند یا موعد پیگیری‌شان گذشته است)
action_needed_qs = base_qs.filter(
Q(last_vosol_date__isnull=True) | Q(last_vosol_date__lt=threshold_date)
)
# --- ۴. محاسبه آمار دقیق (بدون خطا) برای داشبورد ---
# استفاده از Case/When برای جلوگیری کامل از خطای None در Aggregate
stats = action_needed_qs.aggregate(
never_count=Coalesce(Sum(Case(When(last_vosol_date__isnull=True, then=1), default=Value(0), output_field=IntegerField())), Value(0)),
never_amount=Coalesce(Sum(Case(When(last_vosol_date__isnull=True, then=F('overdue_amount')), default=Value(0), output_field=IntegerField())), Value(0)),
expired_count=Coalesce(Sum(Case(When(last_vosol_date__isnull=False, then=1), default=Value(0), output_field=IntegerField())), Value(0)),
expired_amount=Coalesce(Sum(Case(When(last_vosol_date__isnull=False, then=F('overdue_amount')), default=Value(0), output_field=IntegerField())), Value(0)),
)
# --- ۵. اعمال فیلتر کشویی (Dropdown) ---
if filter_type == 'never':
final_qs = action_needed_qs.filter(last_vosol_date__isnull=True)
elif filter_type == 'expired':
final_qs = action_needed_qs.filter(last_vosol_date__isnull=False)
else: # all
final_qs = action_needed_qs
# --- ۶. اعمال مرتب‌سازی (Sorting) پویا ---
if sort_field == 'amount':
order_expr = F('overdue_amount').desc() if sort_dir == 'desc' else F('overdue_amount').asc()
elif sort_field == 'oldest_date':
order_expr = F('oldest_overdue_date').desc(nulls_last=True) if sort_dir == 'desc' else F('oldest_overdue_date').asc(nulls_last=True)
elif sort_field == 'last_action':
order_expr = F('last_vosol_date').desc(nulls_last=True) if sort_dir == 'desc' else F('last_vosol_date').asc(nulls_first=True)
else:
order_expr = F('overdue_amount').desc()
final_qs = final_qs.order_by(order_expr)
# --- ۷. صفحه‌بندی ایمن ---
paginator = Paginator(final_qs, 50)
page_number = request.GET.get('page', 1)
try:
page_obj = paginator.page(page_number)
except (PageNotAnInteger, EmptyPage):
page_obj = paginator.page(1)
context = {
'page_obj': page_obj,
'page_name': 'debtors_no_vosol',
'stats': stats,
# پارامترهای کنترل پنل
'current_days': days_threshold,
'current_filter': filter_type,
'current_sort': sort_field,
'current_dir': sort_dir,
}
return render(request, 'admin_panel/report_debtors_no_vosol.html', context)
@login_required(login_url='/panel/p_login/')
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
def inventory_movements_report(request):
"""
گزارش جامع گردش و خروج کالا از انبار به همراه فیلتر متنی نام کالا،
نوع تراکنش، کاربر، انبار و بازه زمانی شمسی.
"""
all_users = User.objects.all().order_by('first_name')
all_stores = StoreRoom.objects.all()
movement_choices = ProductInStore.MovementType.choices
# دریافت پارامترهای فیلتر از فرانت‌اند
selected_type = request.GET.get('movement_type', '')
selected_user = request.GET.get('user_id', '')
selected_store = request.GET.get('store_id', '')
selected_product_name = request.GET.get('product_name', '').strip() # فیلد جدید جستجوی نام کالا
az_tarikh = request.GET.get('azTarikh', '')
ta_tarikh = request.GET.get('taTarikh', '')
# ۱. کوئری پایه بهینه
movements_list = ProductInStore.objects.select_related(
'Product', 'StoreRoom', 'factor__customer', 'factor__user'
).filter(year=1400).order_by('-createdDT')
# ۲. اعمال فیلترهای پویا
if selected_type:
movements_list = movements_list.filter(movement_type=selected_type)
if selected_user:
movements_list = movements_list.filter(factor__user__id=_safe_int(selected_user))
if selected_store:
movements_list = movements_list.filter(StoreRoom__id=_safe_int(selected_store))
if selected_product_name:
# فیلتر متنی حساس نبودن به حروف بزرگ و کوچک (Case-Insensitive Contains)
movements_list = movements_list.filter(Product__product_name__icontains=selected_product_name)
# ۳. پردازش بازه زمانی
if az_tarikh:
try:
az_dt = datetime.datetime.fromtimestamp(int(az_tarikh) / 1000.0)
movements_list = movements_list.filter(createdDT__gte=az_dt.strftime('%Y-%m-%d 00:00:00'))
except (ValueError, TypeError):
pass
if ta_tarikh:
try:
ta_dt = datetime.datetime.fromtimestamp(int(ta_tarikh) / 1000.0)
movements_list = movements_list.filter(createdDT__lte=ta_dt.strftime('%Y-%m-%d 23:59:59'))
except (ValueError, TypeError):
pass
# ۴. محاسبه آماری سریع دیتابیس روی نتایج فیلتر شده
summary_stats = movements_list.aggregate(
total_out_qty=Sum('s_out'),
total_in_qty=Sum('s_in')
)
# ۵. صفحه‌بندی نتایج
paginator = Paginator(movements_list, 50)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
context = {
'page_obj': page_obj,
'all_users': all_users,
'all_stores': all_stores,
'movement_choices': movement_choices,
'summary_stats': summary_stats,
'filters': {
'movement_type': selected_type,
'user_id': _safe_int(selected_user) if selected_user else '',
'store_id': _safe_int(selected_store) if selected_store else '',
'product_name': selected_product_name, # ارسال فیلد جدید به قالب برای حفظ مقدار ورودی
'azTarikh': az_tarikh,
'taTarikh': ta_tarikh,
}
}
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):
"""
داشبورد هوش مالی و مدیریت نقدینگی (BI Dashboard)
مجهز به هسته حسابرسی ۳۶۰ درجه برای کشف تمامی مغایرت‌ها (بدهی رو هوا، چک‌های الکی، پیش‌دریافت‌ها)
"""
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 = []
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)
last_day = JalaliDate.days_in_month(curr_m, curr_y)
end_of_month_jd = JalaliDate(curr_y, curr_m, last_day)
end_of_month_gd = end_of_month_jd.to_gregorian()
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)
# کارت‌های شاخص عملکرد عمومی
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
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,
# آمار کلان ریاضیاتی
'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,
'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):
try:
debit = Article.objects.filter().aggregate(Sum('debit'))['debit__sum']
credit = Article.objects.filter().aggregate(Sum('credit'))['credit__sum']
mandehesabCs = debit - credit
articls = Article.objects.filter().order_by('-createdDT')
articlsPaginator = Paginator(articls, 10)
pageNumber = request.GET.get("page", 1)
articl_objs = articlsPaginator.get_page(pageNumber)
except:
articls =None
mandehesabCs = 0
data = {'articls': articl_objs, 'mandehesabKP': mandehesabCs}
return render(request, 'admin_panel/bedehi.html', data)
@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()
# تعریف محدوده‌های زمانی ماتریس ریسک (سن مطالبات)
dt_30_days_ago = anchor_dt - datetime.timedelta(days=30)
dt_90_days_ago = anchor_dt - datetime.timedelta(days=90)
# کوئری پایه: تمام سررسیدهای پرداخت‌نشده که از موعد آن‌ها گذشته است
base_qs = Sarresid.objects.filter(payStatus="false", sarresidDT__lte=anchor_dt)
# محاسبه پرسرعت مجموع مبالغ در هر دسته (توسط موتور دیتابیس)
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)
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):
page_name = 'checkPass'
dateNow = datetime.datetime.today().strftime('%Y-%m-%d 23:59:59')
all_sarresid = Sarresid.objects.filter(payStatus="true", type='check').order_by('-sarresidDT')
page_number = request.GET.get('page', None)
paginator = Paginator(all_sarresid, per_page=50)
all_sarresid_pgi = paginator.get_page(page_number)
#paginator = Paginator(all_sarresid, per_page=200)
data = {'all_sarresid': all_sarresid_pgi, "dateNow":dateNow, "page_name":page_name}
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_checkPassFail(request):
page_name = 'checkPassFaild'
dateNow = datetime.datetime.today().strftime('%Y-%m-%d 23:59:59')
all_sarresid = Sarresid.objects.filter(payStatus="false", type='check').order_by('sarresidDT')
count_check_passfail = all_sarresid.count()
sum_check_passfail = all_sarresid.aggregate(Sum('price'))['price__sum']
page_number = request.GET.get('page', None)
paginator = Paginator(all_sarresid, per_page=50)
all_sarresid_pgi = paginator.get_page(page_number)
#paginator = Paginator(all_sarresid, per_page=200)
data = {'all_sarresid': all_sarresid_pgi, 'sum_check_passfail':sum_check_passfail, 'count_check_passfail': count_check_passfail, "dateNow":dateNow, "page_name":page_name}
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_residnashone(request):
page_name = 'sarresidNaShodeha'
dateNow = datetime.datetime.today().strftime('%Y-%m-%d 23:59:59')
all_sarresid = Sarresid.objects.filter(payStatus="false", sarresidDT__gte=dateNow).order_by('sarresidDT')
sarresidNashode = all_sarresid.aggregate(Sum('price'))['price__sum']
page_number = request.GET.get('page', None)
paginator = Paginator(all_sarresid, per_page=50)
all_sarresid_pgi = paginator.get_page(page_number)
#paginator = Paginator(all_sarresid, per_page=200)
data = {'sarresidNashode': sarresidNashode,'all_sarresid': all_sarresid_pgi, "dateNow":dateNow, "page_name":page_name}
return render(request, 'admin_panel/hesabdari.html', data)
@login_required(login_url='/panel/p_login/')
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
def bedehiKhadamat(request):
#bedehiKhadamat
try:
debit = Article.objects.filter(typePy='khadamat').aggregate(Sum('debit'))['debit__sum']
credit = Article.objects.filter(typePy='khadamat').aggregate(Sum('credit'))['credit__sum']
mandehesabCs = debit - credit
articls = Article.objects.filter(typePy='khadamat').order_by('-createdDT')[:100]
except:
articls =None
mandehesabCs = 0
data = {'articls': articls, 'mandehesabKP': mandehesabCs}
return render(request, 'admin_panel/bedehi.html', data)
@login_required(login_url='/panel/p_login/')
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
def bedehiNasb(request):
try:
debit = Article.objects.filter(typePy='nasb').aggregate(Sum('debit'))['debit__sum']
credit = Article.objects.filter(typePy='nasb').aggregate(Sum('credit'))['credit__sum']
mandehesabCs = debit - credit
articls = Article.objects.filter(typePy='nasb').order_by('-createdDT')[:100]
except:
articls =None
mandehesabCs = 0
data = {'articls': articls, 'mandehesabKP': mandehesabCs}
return render(request, 'admin_panel/bedehi.html', data)
@login_required(login_url='/panel/p_login/')
@permission_required('admin_panel.ViewStore', raise_exception=True)
def passCheck(request, articl_id):
sarresidINC = Sarresid.objects.get(id=articl_id)
user_op = request.user
desc = request.POST.get('message')
articleHesabDari2 = Article()
articleHesabDari2.debit = 0
articleHesabDari2.credit = sarresidINC.price
articleHesabDari2.customer = sarresidINC.customer
articleHesabDari2.factor = sarresidINC.factor
articleHesabDari2.desc = 'وصول چک' + " " + str(sarresidINC.customer.name_family)
if sarresidINC.factor:
articleHesabDari2.desc += " فاکتور شماره: " + str(sarresidINC.factor.id)
articleHesabDari2.nahvePardakht = 'وصول چک'
articleHesabDari2.payStatus = "true"
articleHesabDari2.user = user_op
articleHesabDari2.save()
sarresidINC.payStatus = "true"
sarresidINC.desc = desc
sarresidINC.save()
return HttpResponse("ok")
#return redirect(request.META.get('HTTP_REFERER'))
@login_required(login_url='/panel/p_login/')
@permission_required('admin_panel.ViewStore', raise_exception=True)
@require_POST
def update_check_info(request, sarresid_id):
# دریافت رکورد مورد نظر
sarresid = get_object_or_404(Sarresid, id=sarresid_id)
# اطمینان از اینکه آیتم حتما چک است
if sarresid.type != 'check':
messages.error(request, "عملیات نامعتبر: این سند چک نیست.")
return redirect(request.META.get('HTTP_REFERER', '/'))
# دریافت مقادیر فرم با حذف فاصله‌های اضافی
check_row_number = request.POST.get('check_row_number', '').strip()
check_sayyad_id = request.POST.get('check_sayyad_id', '').strip()
check_owner_name = request.POST.get('check_owner_name', '').strip()
check_owner_phone = request.POST.get('check_owner_phone', '').strip()
# اعتبارسنجی فیلدهای اجباری
if not check_row_number or not check_sayyad_id or not check_owner_name:
messages.error(request, "شماره ردیف، شناسه صیادی و نام مالک اجباری است.")
return redirect(request.META.get('HTTP_REFERER', '/'))
# اعتبارسنجی شناسه صیادی (باید دقیقا ۱۶ رقم عدد باشد)
if not check_sayyad_id.isdigit() or len(check_sayyad_id) != 16:
messages.error(request, "شناسه صیادی باید دقیقاً ۱۶ رقم عدد باشد.")
return redirect(request.META.get('HTTP_REFERER', '/'))
# اعتبارسنجی شماره تلفن (در صورت وارد شدن باید فقط عدد باشد)
if check_owner_phone and not check_owner_phone.isdigit():
messages.error(request, "شماره تلفن مالک چک باید فقط شامل اعداد باشد.")
return redirect(request.META.get('HTTP_REFERER', '/'))
try:
# بروزرسانی اطلاعات
sarresid.check_row_number = check_row_number
sarresid.check_sayyad_id = check_sayyad_id
sarresid.check_owner_name = check_owner_name
sarresid.check_owner_phone = check_owner_phone
# آپدیت فیلد توضیحات با اطلاعات جدید
sarresid.desc = f"صیادی: {check_sayyad_id} | مالک: {check_owner_name} | تلفن: {check_owner_phone or 'ندارد'}"
sarresid.save()
messages.success(request, "اطلاعات چک با موفقیت ثبت/بروزرسانی شد.")
except Exception as e:
messages.error(request, "خطای سیستمی در بروزرسانی اطلاعات رخ داد.")
return redirect(request.META.get('HTTP_REFERER', '/'))
@login_required(login_url='/panel/p_login/')
@permission_required('admin_panel.ViewStore', raise_exception=True)
def addSarresid(request, cs_id):
if request.method != 'POST':
return HttpResponseBadRequest("متد ارسالی نامعتبر است.")
try:
customerObj = get_object_or_404(Customer, id=cs_id)
# دریافت فیلدهای اصلی
sarrresidUNIX = request.POST.get('n_sarresidDateTime')
sarresidPrice = request.POST.get('n_sarresidPrice')
sarresidType = request.POST.get('n_sarresidType')
# دریافت و پاک‌سازی فیلدهای چک (جلوگیری از باگ فاصله‌های خالی)
check_row_number = request.POST.get('check_row_number', '').strip()
check_sayyad_id = request.POST.get('check_sayyad_id', '').strip()
check_owner_name = request.POST.get('check_owner_name', '').strip()
check_owner_phone = request.POST.get('check_owner_phone', '').strip()
# --- لایه اول: اعتبارسنجی فیلدهای پایه ---
if not sarrresidUNIX or not sarresidPrice or not sarresidType:
messages.error(request, "لطفا فیلدهای اصلی (مبلغ، نوع و تاریخ) را پر کنید.")
return redirect(request.META.get('HTTP_REFERER', '/'))
# --- لایه دوم: اعتبارسنجی دقیق و امنیتی فیلدهای چک ---
if sarresidType == "check":
if not check_row_number or not check_sayyad_id or not check_owner_name:
messages.error(request, "برای ثبت چک، وارد کردن شماره ردیف، شناسه صیادی و نام مالک اجباری است.")
return redirect(request.META.get('HTTP_REFERER', '/'))
# بررسی ۱۶ رقمی و عددی بودن شناسه صیادی
if not check_sayyad_id.isdigit() or len(check_sayyad_id) != 16:
messages.error(request, "شناسه صیادی باید دقیقاً ۱۶ رقم عدد باشد.")
return redirect(request.META.get('HTTP_REFERER', '/'))
# بررسی عددی بودن شماره تلفن (در صورت وارد شدن)
if check_owner_phone and not check_owner_phone.isdigit():
messages.error(request, "شماره تلفن مالک چک باید فقط شامل اعداد باشد.")
return redirect(request.META.get('HTTP_REFERER', '/'))
# تبدیل نوع داده‌ها با مدیریت خطا (جلوگیری از کرش سرور 500)
try:
sarResidDateTime01 = datetime.datetime.fromtimestamp(int(sarrresidUNIX) / 1000.0)
price_int = int(sarresidPrice.replace(',', ''))
except ValueError:
messages.error(request, "مقادیر تاریخ یا مبلغ نامعتبر است.")
return redirect(request.META.get('HTTP_REFERER', '/'))
# --- لایه سوم: ذخیره‌سازی اتمیک (تضمین جامعیت داده‌ها) ---
with transaction.atomic():
# ۱. ثبت سند حسابداری
articleHesabDariObj = Article()
articleHesabDariObj.debit = price_int
articleHesabDariObj.credit = 0
articleHesabDariObj.customer = customerObj
articleHesabDariObj.user = request.user
if sarresidType == "check":
articleHesabDariObj.desc = f"بدهی بابت ثبت چک دستی (صیادی: {check_sayyad_id})"
elif sarresidType == "nesiye":
articleHesabDariObj.desc = "بدهی بابت ثبت نسیه دستی"
articleHesabDariObj.save()
# ۲. ثبت سررسید / چک
sarresidINC = Sarresid()
sarresidINC.customer = customerObj
sarresidINC.price = price_int
sarresidINC.payStatus = "false"
sarresidINC.type = sarresidType
sarresidINC.user = request.user
sarresidINC.sarresidDT = sarResidDateTime01
if sarresidType == "check":
sarresidINC.check_row_number = check_row_number
sarresidINC.check_sayyad_id = check_sayyad_id
sarresidINC.check_owner_name = check_owner_name
sarresidINC.check_owner_phone = check_owner_phone
sarresidINC.desc = f"صیادی: {check_sayyad_id} | مالک: {check_owner_name} | تلفن: {check_owner_phone or 'ندارد'}"
sarresidINC.save()
messages.success(request, "اطلاعات با موفقیت ثبت شد.")
return redirect(request.META.get('HTTP_REFERER', '/'))
except Exception as e:
logger.error(f"Error in addSarresid: {str(e)}", exc_info=True)
messages.error(request, "خطای سیستمی رخ داد. لطفا مجددا تلاش کنید.")
return redirect(request.META.get('HTTP_REFERER', '/'))
@login_required(login_url='/panel/p_login/')
@permission_required('admin_panel.ViewStore', raise_exception=True)
def pardakhtNesiye(request, cs_id):
try:
customerObj = Customer.objects.get(id=cs_id)
user_op = request.user
sarrresidUNIX = request.POST.get('next_sarresidDateTime', None)
sarresidPrice = int(request.POST.get('next_sarresidPrice', None))
sr_articlID = request.POST.get('sr_articlID', None)
nahvePardakht = request.POST.get('nahvePardakht', None)
desc_sar = request.POST.get('desc_sar', None)
sarResidDateTime01 = datetime.datetime.fromtimestamp(int(sarrresidUNIX)/1000.0)
SarresidObj = Sarresid.objects.get(id=sr_articlID)
#return HttpResponse(sarresidPrice)
debitNow = SarresidObj.price - sarresidPrice
#return HttpResponse(debitNow)
if debitNow==0:
#توسیه بدهی بخورد
SarresidObj.price = 0
SarresidObj.payStatus = "true"
elif debitNow >= 1:
SarresidObj.price = debitNow
SarresidObj.sarresidDT = sarResidDateTime01
else:
return HttpResponse("خطا در مورد مبلغ")
SarresidObj.save()
if str(nahvePardakht) == 'naghd':
nahvePardakht = 'نقد'
elif str(nahvePardakht) == 'kartbekart':
nahvePardakht = 'کارت به کارت'
elif str(nahvePardakht) == 'kartkhan':
nahvePardakht = 'کارت خوان'
elif str(nahvePardakht) == 'takhfif':
nahvePardakht = 'تخفیف'
elif str(nahvePardakht) == 'check':
nahvePardakht = 'چک'
articleHesabDariObj = Article()
articleHesabDariObj.debit = 0
articleHesabDariObj.credit = int(sarresidPrice)
if desc_sar:
articleHesabDariObj.desc = "پرداختی بابت نسیه" + " | " + desc_sar
else:
articleHesabDariObj.desc = "پرداختی بابت نسیه"
articleHesabDariObj.customer = customerObj
articleHesabDariObj.nahvePardakht = nahvePardakht
articleHesabDariObj.user = user_op
articleHesabDariObj.save()
return redirect(request.META.get('HTTP_REFERER'))
except:
return HttpResponse("خطا در ثبت ")
@login_required(login_url='/panel/p_login/')
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
def rezayat_dashboard(request):
# دریافت تمامی نظرسنجی‌های ثبت شده معتبر (که تاریخ نظرسنجی دارند)
valid_surveys = Action.objects.filter(
nazarDT__isnull=False,
grade__in=[5, 10, 15, 20]
)
# ۱. محاسبات شاخص‌های کلیدی (KPIs)
total_surveys = valid_surveys.count()
avg_grade = valid_surveys.aggregate(Avg('grade'))['grade__avg'] or 0
# ۲. استخراج داده‌ها برای نمودار توزیع رضایت
grade_distribution = valid_surveys.values('grade').annotate(count=Count('id'))
chart_dict = {20: 0, 15: 0, 10: 0, 5: 0}
for item in grade_distribution:
g = item['grade']
if g in chart_dict:
chart_dict[g] = item['count']
chart_labels = ['عالی (۲۰)', 'متوسط (۱۵)', 'ضعیف (۱۰)', 'بد (۵)']
chart_data_list = [chart_dict[20], chart_dict[15], chart_dict[10], chart_dict[5]]
# ۳. تبدیل امن آرایه‌ها به JSON برای Chart.js
chart_labels_json = json.dumps(chart_labels, ensure_ascii=False)
chart_data_json = json.dumps(chart_data_list)
has_chart_data = sum(chart_data_list) > 0
# ۴. واکشی لیست مشتریان نیازمند پیگیری (شامل متوسط ۱۵، ضعیف ۱۰ و بد ۵)
low_score_actions = Action.objects.filter(
nazarDT__isnull=False,
grade__in=[5, 10, 15] # <-- امتیاز ۱۵ (متوسط) نیز اضافه شد
).order_by('-nazarDT')
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
context = {
'total_surveys': total_surveys,
'avg_grade': round(avg_grade, 1),
'chart_labels_json': chart_labels_json,
'chart_data_json': chart_data_json,
'has_chart_data': has_chart_data,
'low_score_actions': low_score_actions,
# تعداد کل موارد نیازمند پیگیری (متوسط + ضعیف + بد)
'unhappy_count': chart_dict[15] + chart_dict[10] + chart_dict[5],
'allUsers': allUsers,
}
return render(request, 'admin_panel/report_rezayat.html', context)