from django.shortcuts import redirect, render, get_object_or_404 from django.urls.base import reverse from django.urls import reverse # تکراری در کد اصلی شما بود، اما جهت عدم تغییر حفظ شد from persiantools.jdatetime import JalaliDate from persiantools import characters, digits import datetime from django.http import JsonResponse from asgiref.sync import async_to_sync import asyncio import cv2 import numpy as np import ddddocr import re import base64 from playwright.async_api import async_playwright from django.contrib import messages from django.views.decorators.http import require_POST from django.utils import timezone # جدید from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage from django.core.exceptions import PermissionDenied, ValidationError # جدید 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, F, Subquery, OuterRef, IntegerField, Exists, Min, Max, Case, When 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 import os import json # جدید import logging # جدید from .models import * from .services import create_transfer_request, approve_transfer_request, reject_transfer_request, cancel_transfer_request logger = logging.getLogger(__name__) class InventoryException(Exception): """خطای اختصاصی برای کنترل موجودی انبار""" pass def _safe_int(value, default=0): """تابع امن برای تبدیل مقادیر دریافتی از جاوااسکریپت به عدد""" if value is None: return default try: clean_val = str(value).strip() return int(clean_val) if clean_val else default except (ValueError, TypeError): return default def _parse_jalali_date(date_str): """تبدیل امن تاریخ ارسالی از pDatepicker به میلادی""" if not date_str or not str(date_str).strip(): return None try: clean_date = digits.fa_to_en(digits.ar_to_fa(str(date_str).strip())) y, m, d = map(int, clean_date.split("/")) return JalaliDate(y, m, d).to_gregorian() except Exception as e: logger.warning(f"Error parsing Jalali date '{date_str}': {e}") return None def _parse_timestamp(ts_value): """تبدیل یونیکس تایم‌استمپ‌های ارسالی از جاوااسکریپت""" if not ts_value: return None try: # تبدیل میلی‌ثانیه جاوااسکریپت به ثانیه پایتون return datetime.datetime.fromtimestamp(int(ts_value) / 1000.0, tz=timezone.get_current_timezone()) except (ValueError, TypeError): return None def p_login(request): username = request.POST.get('username', None ) password = request.POST.get('password', None ) if username != None and password != None: if not request.user.is_authenticated: user = authenticate(request, username=username, password=password) if user is not None: login(request, user) return redirect("/panel/ActionList/"+str(request.user.id)+"/today") else: data = {'message': 'یوزنیم یا پسورد اشتباه است'} return render(request, 'admin_panel/login.html', data) else: return redirect("/panel/") else: if request.user.is_authenticated: return redirect("/panel/") else: return render(request, 'admin_panel/login.html') #return logout(request) def p_logout(request): logout(request) return redirect("/panel/p_login/") @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) @csrf_exempt @login_required(login_url='/panel/p_login/') def customers(request): searchVal = request.GET.get('searchVal', '') searchOn = request.GET.get('searchOn', '') searchStatus = True full_path = request.get_full_path() if searchOn == 'code': all_customers = Customer.objects.filter(idCode__contains=searchVal).order_by('-create_date') elif searchOn == 'phone': all_customers = Customer.objects.filter(Q(phone1__contains=searchVal) | Q(phone2__contains=searchVal) | Q(phone3__contains=searchVal) ).order_by('-create_date') elif searchOn == 'name': all_customers = Customer.objects.filter(name_family__contains=searchVal).order_by('-create_date') elif searchOn == 'address': all_customers = Customer.objects.filter(address__contains=searchVal).order_by('-create_date') elif searchOn == 'laghv': all_customers = Customer.objects.filter(laghv_bazdid=True).order_by('-create_date') else: searchStatus = False all_customers = Customer.objects.filter(is_active=True).order_by('-create_date') paginator = Paginator(all_customers, 25) # Show 25 contacts per page. page_number = request.GET.get('page', None) all_customers = paginator.get_page(page_number) data = {'customersD': all_customers, 'searchStatus': searchStatus, 'full_path': full_path, 'searchVal': searchVal, 'searchOn':searchOn} return render(request, 'admin_panel/customers.html', data) @login_required(login_url='/panel/p_login/') def customersSearch(request, searchVal,searchOn): searchStatus = True if searchOn == 'code': all_customers = Customer.objects.filter(idCode__contains=searchVal).order_by('-create_date') elif searchOn == 'phone': all_customers = Customer.objects.filter(phone1__contains=searchVal).order_by('-create_date') elif searchOn == 'name': all_customers = Customer.objects.filter(name_family__contains=searchVal).order_by('-create_date') elif searchOn == 'address': all_customers = Customer.objects.filter(address__contains=searchVal).order_by('-create_date') else: searchStatus = False all_customers = Customer.objects.all().order_by('-create_date') paginator = Paginator(all_customers, 25) # Show 25 contacts per page. page_number = request.GET.get('page', None) all_customers = paginator.get_page(page_number) data = {'customersD': all_customers, 'searchStatus': searchStatus} return render(request, 'admin_panel/customers.html', data) @login_required(login_url='/panel/p_login/') def customer(request ,c_id): customer = Customer.objects.get(id=c_id) categorys = ProductCategory.objects.all() factorha = Factor.objects.filter(customer__id = c_id) #all_prd = serializers.serialize("json", all_prda) #return JsonResponse(all_prd,safe = False) dateNow = datetime.datetime.now().date data = {'customer': customer, 'categorys': categorys, 'dateNow': dateNow, 'factorha': factorha} return render(request, 'admin_panel/customerID.html', data) @login_required(login_url='/panel/p_login/') def customerNew(request ,c_id): customer = Customer.objects.get(id=c_id) categorys = ProductCategory.objects.all() factorha = Factor.objects.filter(customer__id = c_id) #all_prd = serializers.serialize("json", all_prda) #return JsonResponse(all_prd,safe = False) dateNow = datetime.datetime.now().date data = {'customer': customer, 'categorys': categorys, 'dateNow': dateNow, 'factorha': factorha} return render(request, 'admin_panel/customerNew.html', data) @login_required(login_url='/panel/p_login/') def customerNew2(request ,c_id): customer = get_object_or_404(Customer, id=c_id, is_active=True) categorys = ProductCategory.objects.all() factorha = Factor.objects.filter(customer__id = c_id) debit = Article.objects.filter(customer=customer).aggregate(Sum('debit'))['debit__sum'] credit = Article.objects.filter(customer=customer).aggregate(Sum('credit'))['credit__sum'] mandehesabCs = 0 try: if (debit - credit) < 0: mandehesabCs = abs(debit - credit) except: pass #all_prd = serializers.serialize("json", all_prda) #return JsonResponse(all_prd,safe = False) dateNow = datetime.datetime.now().date data = {'customer': customer, 'categorys': categorys, 'dateNow': dateNow, 'factorha': factorha, 'mandehesabCs':mandehesabCs} return render(request, 'admin_panel/customerNew2.html', data) @login_required(login_url='/panel/p_login/') def customerFactors(request, c_id): customer = get_object_or_404(Customer, id=c_id) categorys = ProductCategory.objects.all() # کوئری پایه برای دریافت همه فاکتورهای این مشتری factor_qs = Factor.objects.filter(customer=customer) # دریافت پارامترهای فیلتر و جستجو از آدرس URL search_query = request.GET.get('search_factor', '').strip() filter_status = request.GET.get('status', 'all') filter_type = request.GET.get('type', 'all') az_tarikh = request.GET.get('az_tarikh', '').strip() ta_tarikh = request.GET.get('ta_tarikh', '').strip() # ۱. اعمال فیلتر جستجو (شناسه فاکتور یا متن توضیحات) if search_query: if search_query.isdigit(): factor_qs = factor_qs.filter(models.Q(id=int(search_query)) | models.Q(desc__icontains=search_query)) else: factor_qs = factor_qs.filter(models.Q(desc__icontains=search_query)) # ۲. اعمال فیلتر وضعیت پرداخت و حذف نرم if filter_status != 'all': if filter_status == 'deleted': factor_qs = factor_qs.filter(is_active=False) else: factor_qs = factor_qs.filter(pay_status=filter_status, is_active=True) # ۳. اعمال فیلتر نوع فاکتور (نصب، خدمات، گارانتی) if filter_type != 'all': factor_qs = factor_qs.filter(FactorType=filter_type) # ۴. اعمال فیلتر تاریخ شروع (از تاریخ) if az_tarikh: az_gregorian = _parse_jalali_date(az_tarikh) if az_gregorian: az_datetime = datetime.datetime.combine(az_gregorian, datetime.time.min) factor_qs = factor_qs.filter(create_date__gte=az_datetime) # ۵. اعمال فیلتر تاریخ پایان (تا تاریخ) if ta_tarikh: ta_gregorian = _parse_jalali_date(ta_tarikh) if ta_gregorian: ta_datetime = datetime.datetime.combine(ta_gregorian, datetime.time.max) factor_qs = factor_qs.filter(create_date__lte=ta_datetime) # مرتب‌سازی بر اساس آخرین تاریخ ایجاد factorha = factor_qs.order_by('-create_date') dateNow = datetime.datetime.now().date data = { 'customer': customer, 'categorys': categorys, 'dateNow': dateNow, 'factorha': factorha, 'search_query': search_query, 'filter_status': filter_status, 'filter_type': filter_type, 'az_tarikh': az_tarikh, 'ta_tarikh': ta_tarikh } return render(request, 'admin_panel/customerIDFactor.html', data) @login_required(login_url='/panel/p_login/') def customerFactor(request ,c_id, f_id): customer = Customer.objects.get(id=c_id) categorys = ProductCategory.objects.all() factorO = Factor.objects.get(customer=customer, id=f_id) fiteam = FactorIteam.objects.filter(factor=factorO) #all_prd = serializers.serialize("json", all_prda) #return JsonResponse(all_prd,safe = False) dateNow = datetime.datetime.now().date data = {'customer': customer, 'categorys': categorys, 'dateNow': dateNow, 'factorO': factorO, 'fiteam': fiteam} return render(request, 'admin_panel/customerFactor.html', data) def productsJson(request): from django.core import serializers from django.http import JsonResponse all_prd = Product.objects.all() all_prd = serializers.serialize("json", all_prd) return JsonResponse(all_prd,safe = False) def ActionJson(request, action_id): from django.core import serializers from django.http import JsonResponse AcJson = Action.objects.filter(id=action_id) AcJson = serializers.serialize("json", AcJson) return JsonResponse(AcJson,safe = False) def CustomerJson(request, c_id): from django.core import serializers from django.http import JsonResponse customerJson = Customer.objects.filter(id=c_id) customerJson = serializers.serialize("json", customerJson) return JsonResponse(customerJson,safe = False) @login_required(login_url='/panel/p_login/') def productMojodi(request, p_id): # پیدا کردن انبار کاربر با متد filter به جای get برای جلوگیری از کرش سیستم store_user = StoreRoom.objects.filter(owners=request.user).first() # اگر کاربر هنوز مالک هیچ انباری نشده است if not store_user: return HttpResponse("کاربر مالکیت انبار ندارد") # یا هر مقداری که فرانت‌اند انتظار دارد # محاسبه موجودی برای انبار پیدا شده totals = ProductInStore.objects.filter( Product_id=p_id, StoreRoom=store_user, year=1400 ).aggregate( total_in=Sum('s_in'), total_out=Sum('s_out') ) total_in = totals['total_in'] or 0 total_out = totals['total_out'] or 0 mojodi_prd = total_in - total_out return HttpResponse(str(mojodi_prd)) @login_required(login_url='/panel/p_login/') def customersAdd(request): an_name = request.POST.get('an_name', '') an_address = request.POST.get('an_address', '') installDate = request.POST.get('installDate', '') idnCode = request.POST.get('idnCode', '') # دریافت آرایه‌های عنوان و شماره تلفن titles_list = request.POST.getlist('phone_titles') phones_list = request.POST.getlist('phones') # ترکیب عنوان‌ها و شماره‌ها و حذف ردیف‌های خالی valid_phones = [] for title, phone in zip(titles_list, phones_list): if phone.strip(): valid_phones.append({ 'title': title.strip() if title.strip() else 'تلفن', 'phone': phone.strip() }) year, month, day = map(int, installDate.split('/')) installDate_gregorian = JalaliDate(year, month, day).to_gregorian() newCs = Customer() newCs.name_family = an_name newCs.address = an_address newCs.installDate = installDate_gregorian newCs.idCode = idnCode # حفظ سازگاری با کدهای قدیمی (ریختن ۳ شماره اول در فیلدهای قدیمی) newCs.phone1 = valid_phones[0]['phone'] if len(valid_phones) > 0 else '' newCs.phone2 = valid_phones[1]['phone'] if len(valid_phones) > 1 else '' newCs.phone3 = valid_phones[2]['phone'] if len(valid_phones) > 2 else '' newCs.save() # ذخیره شماره‌ها به صورت نامحدود در جدول PhoneNumber for item in valid_phones: PhoneNumber.objects.create(customer=newCs, title=item['title'], phone_number=item['phone']) return redirect('/panel/customers/') @login_required(login_url='/panel/p_login/') def customerEdit(request, c_id): an_name = request.POST.get('an_name', '') an_address = request.POST.get('an_address', '') installDate = request.POST.get('installDate', '') idnCode = request.POST.get('idnCode', '') laghv_bazdid = request.POST.get('laghv_bazdid', False) # دریافت آرایه‌های عنوان و شماره تلفن titles_list = request.POST.getlist('phone_titles') phones_list = request.POST.getlist('phones') valid_phones = [] for title, phone in zip(titles_list, phones_list): if phone.strip(): valid_phones.append({ 'title': title.strip() if title.strip() else 'تلفن', 'phone': phone.strip() }) year, month, day = map(int, installDate.split('/')) installDate_gregorian = JalaliDate(year, month, day).to_gregorian() newCs = get_object_or_404(Customer, id=c_id) newCs.name_family = an_name newCs.address = an_address newCs.installDate = installDate_gregorian newCs.idCode = idnCode newCs.laghv_bazdid = True if laghv_bazdid == 'true' else False # آپدیت فیلدهای قدیمی جهت حفظ سازگاری newCs.phone1 = valid_phones[0]['phone'] if len(valid_phones) > 0 else '' newCs.phone2 = valid_phones[1]['phone'] if len(valid_phones) > 1 else '' newCs.phone3 = valid_phones[2]['phone'] if len(valid_phones) > 2 else '' newCs.save() # بروزرسانی جدول بی‌نهایت (حذف قبلی‌ها و درج مجدد) PhoneNumber.objects.filter(customer=newCs).delete() for item in valid_phones: PhoneNumber.objects.create(customer=newCs, title=item['title'], phone_number=item['phone']) return redirect('/panel/customer/'+str(c_id)) @login_required(login_url='/panel/p_login/') def entezar(request): a = datetime.datetime.now() searchVal = request.GET.get('searchVal', '') searchOn = request.GET.get('searchOn', '') searchStatus = True full_path = request.get_full_path() from django.db.models import Q bazdid = os.environ.get('bazdid', 9) bazdid = int ( int(bazdid) * 4.3 ) dateTime6MonthAgo = a - datetime.timedelta(weeks=bazdid) ''' customers = Customer.objects.filter(name_family__isnull=False) m6mbazdid = [] for customerObj in customers: try: actionAa = Action.objects.filter(customer=customerObj, actionType='bazdid', status='finish').last() if actionAa.status == 'finish': customerObj.last_bazdid_dt = actionAa.actionDateTime customerObj.save() if actionAa.actionDateTime.date() < dateTime6MonthAgo.date(): m6mbazdid.append(actionAa) except: pass ''' if searchOn == 'address': print("addressaddressaddressaddressaddress") customers_objj = Customer.objects.filter(Q(last_bazdid_dt__lte=dateTime6MonthAgo) | Q(last_bazdid_dt__isnull=True), installDate__lte=dateTime6MonthAgo, laghv_bazdid=False, name_family__isnull=False, address__contains=searchVal).order_by('last_bazdid_dt') else: searchStatus = False customers_objj = Customer.objects.filter(Q(last_bazdid_dt__lte=dateTime6MonthAgo) | Q(last_bazdid_dt__isnull=True), installDate__lte=dateTime6MonthAgo, laghv_bazdid=False, name_family__isnull=False).order_by('last_bazdid_dt') #customers_objj = Customer.objects.filter(name_family__isnull=False, last_bazdid_dt__lte=dateTime6MonthAgo) try: entezar_bazdid = CacheTable.objects.get(key='9mahbazdid') entezar_bazdid.value = Customer.objects.filter( Q(last_bazdid_dt__lte=dateTime6MonthAgo) | Q(last_bazdid_dt__isnull=True), installDate__lte=dateTime6MonthAgo, laghv_bazdid=False, name_family__isnull=False).count() entezar_bazdid.save() except: pass paginator = Paginator(customers_objj, 20) # Show 25 contacts per page. page_number = request.GET.get('page', None) customerHa = paginator.get_page(page_number) return render(request, 'admin_panel/entezar.html', {'customers': customerHa, 'searchStatus': searchStatus, 'full_path': full_path, 'searchVal': searchVal, 'searchOn':searchOn}) @login_required(login_url='/panel/p_login/') @permission_required('admin_panel.OnlyAnbar', raise_exception=True) def stores(request): if request.user.username not in ('admin', 'office1', 'office2', 'office3', 'khatibi'): all_store = StoreRoom.objects.filter(user__id=request.user.id).order_by('id')[:20] else: all_store = StoreRoom.objects.all().order_by('id')[:20] data = {'all_store': all_store} return render(request, 'admin_panel/stores.html', data) from django.db.models import Sum, Subquery, OuterRef, IntegerField, Value from django.db.models.functions import Coalesce from django.shortcuts import render, get_object_or_404 from django.contrib.auth.decorators import login_required, permission_required from .models import StoreRoom, ProductCategory, Product, ProductInStore, StoreTransferRequest @login_required(login_url='/panel/p_login/') @permission_required('admin_panel.OnlyAnbar', raise_exception=True) def store(request, s_id): productCategory = ProductCategory.objects.all() storeO = get_object_or_404(StoreRoom, id=s_id) searchStatus = False searchQS = request.POST.get('searchQS', '').strip() mojodi = request.POST.get('mojodi', '') # ۱. محاسبه موجودی فیزیکی کل کالاها با یک کوئری تک‌ضرب و سریع GROUP BY stock_balances = ProductInStore.objects.filter( StoreRoom=storeO, year=1400 ).values('Product_id').annotate( total=Sum('s_in') - Sum('s_out') ).order_by() stock_dict = {item['Product_id']: item['total'] for item in stock_balances} # ۲. محاسبه کل کالاهای در انتظار خروج pending_out_balances = StoreTransferRequest.objects.filter( source_store=storeO, status=StoreTransferRequest.Status.PENDING ).values('product_id').annotate( total=Sum('quantity') ).order_by() pending_out_dict = {item['product_id']: item['total'] for item in pending_out_balances} # ۳. محاسبه کل کالاهای در انتظار ورود pending_in_balances = StoreTransferRequest.objects.filter( target_store=storeO, status=StoreTransferRequest.Status.PENDING ).values('product_id').annotate( total=Sum('quantity') ).order_by() pending_in_dict = {item['product_id']: item['total'] for item in pending_in_balances} # ۴. استخراج سریع تمام آیدی‌های منحصربه‌فرد مرتبط با این انبار در سطح حافظه all_pids = set(stock_dict.keys()) | set(pending_out_dict.keys()) | set(pending_in_dict.keys()) # ۵. کوئری نهایی فوق‌بهینه برای خواندن کالاها (حل باگ N+1 با دستور select_related) products_qs = Product.objects.select_related('ProductCategory').filter(id__in=all_pids) # اعمال فیلتر متنی جستجو در سطح دیتابیس if searchQS: products_qs = products_qs.filter(product_name__icontains=searchQS) searchStatus = True # 🛠️ اصلاح فرآیند ترتیب: استفاده از 'id' جهت انطباق ۱۰۰٪ با ترتیب پیش‌فرض و سنتی دیتابیس شما # نکته: اگر مایلید بر اساس زمان ایجاد مرتب شود، می‌توانید آن را به 'createdDT' یا '-createdDT' تغییر دهید. products_list = list(products_qs.order_by('id')) # ۶. معماری زمان ثابت O(1): چسباندن پرسرعت دیتاها در حافظه رم سرور for prd in products_list: prd.mojodi = stock_dict.get(prd.id, 0) prd.pending_out = pending_out_dict.get(prd.id, 0) prd.pending_in = pending_in_dict.get(prd.id, 0) # ۷. اعمال فیلترهای منطقی موجودی در لایه پایتون if mojodi == "0": products_list = [p for p in products_list if p.mojodi <= 0] searchStatus = True elif mojodi == "1": products_list = [p for p in products_list if p.mojodi > 0] searchStatus = True data = { 'storeO': storeO, 'productSum': products_list, 's_id': s_id, 'searchStatus': searchStatus, "mojodi": mojodi, 'djsearchQS': searchQS, 'productCategory': productCategory } return render(request, 'admin_panel/store.html', data) @login_required(login_url='/panel/p_login/') @permission_required('admin_panel.OnlyAnbar', raise_exception=True) def operations(request, s_id): page = request.GET.get('page', 1) productCategory = ProductCategory.objects.all() storeO = get_object_or_404(StoreRoom, id=s_id) # بهینه‌سازی کلیدی: استفاده از select_related برای جلوگیری از کندی و پر شدن RAM دیتابیس operationsList = ProductInStore.objects.filter( StoreRoom=storeO, year=1400 ).select_related('Product').order_by('-createdDT') paginator = Paginator(operationsList, 25) try: data_list = paginator.page(page) except PageNotAnInteger: data_list = paginator.page(1) except EmptyPage: data_list = paginator.page(paginator.num_pages) data = { 'storeO': storeO, 'operationsList': data_list, 's_id': s_id, 'productCategory': productCategory } return render(request, 'admin_panel/operations.html', data) @login_required(login_url='/panel/p_login/') @permission_required('admin_panel.OnlyAnbar', raise_exception=True) def operationProduct(request, s_id, p_id): productCategory = ProductCategory.objects.all() storeO = get_object_or_404(StoreRoom, id=s_id) # بهینه‌سازی کلیدی در فیلتر اختصاصی محصول operationsList = ProductInStore.objects.filter( StoreRoom=storeO, year=1400, Product__id=p_id ).select_related('Product').order_by('-createdDT') data = { 'storeO': storeO, 'operationsList': operationsList, 's_id': s_id, 'productCategory': productCategory } return render(request, 'admin_panel/operations.html', data) @login_required(login_url='/panel/p_login/') @permission_required('admin_panel.OnlyAnbar', raise_exception=True) def newOperation(request, s_id, p_id): productCategory = ProductCategory.objects.all() storeO = StoreRoom.objects.get(id=s_id) allStores = StoreRoom.objects.exclude(id=s_id) product = Product.objects.get(id=p_id) data = {'s_id': s_id,'p_id': p_id, 'allStores': allStores, 'storeO': storeO, 'product':product, 'productCategory': productCategory} return render(request, 'admin_panel/newOperation.html', data ) @login_required(login_url='/panel/p_login/') @permission_required('admin_panel.OnlyAnbar', raise_exception=True) def store_transfers(request, s_id): """ داشبورد و کارتابل جامع مدیریت انتقال‌های انبار """ storeO = get_object_or_404(StoreRoom, id=s_id) # ۱. کارتابل ورودی (بارهایی که به سمت این انبار آمده و منتظر تایید ما هستند) # استفاده از select_related برای جلوگیری از صدها کوئری اضافی به دیتابیس incoming_pending = StoreTransferRequest.objects.filter( target_store=storeO, status=StoreTransferRequest.Status.PENDING ).select_related('product', 'source_store', 'requester').order_by('-created_at') # ۲. کارتابل خروجی (بارهایی که ما فرستادیم و منتظر تایید انبار مقصد هستیم) outgoing_pending = StoreTransferRequest.objects.filter( source_store=storeO, status=StoreTransferRequest.Status.PENDING ).select_related('product', 'target_store', 'requester').order_by('-created_at') # ۳. تاریخچه انتقالات اخیر (۵۰ مورد آخر تعیین تکلیف شده برای این انبار) history_list = StoreTransferRequest.objects.filter( Q(source_store=storeO) | Q(target_store=storeO) ).exclude( status=StoreTransferRequest.Status.PENDING ).select_related('product', 'source_store', 'target_store', 'approver').order_by('-action_date')[:50] data = { 'storeO': storeO, 's_id': s_id, 'incoming_pending': incoming_pending, 'outgoing_pending': outgoing_pending, 'history_list': history_list, } return render(request, 'admin_panel/store_transfers.html', data) @login_required(login_url='/panel/p_login/') @permission_required('admin_panel.OnlyAnbar', raise_exception=True) @require_POST def process_transfer_action(request, transfer_id): """ پردازش امن اقدامات انباردار (تایید، رد، انصراف) با تراکنش‌های اتمیک """ action = request.POST.get('action') reason = request.POST.get('reason', '').strip() # دریافت درخواست انتقال transfer = get_object_or_404(StoreTransferRequest, id=transfer_id) # تعیین انباری که کاربر از آنجا اقدام کرده جهت ریدایرکت صحیح redirect_store_id = transfer.target_store_id if action in ['approve', 'reject'] else transfer.source_store_id try: if action == 'approve': approve_transfer_request(request_id=transfer.id, approver=request.user) messages.success(request, f"انتقال {transfer.quantity} عدد {transfer.product.product_name} با موفقیت تایید و به موجودی انبار اضافه شد.") elif action == 'reject': if not reason: messages.error(request, "برای رد کردن بار، وارد کردن علت رد الزامی است.") return redirect('admin_panel:store_transfers', s_id=redirect_store_id) reject_transfer_request(request_id=transfer.id, rejector=request.user, reason=reason) messages.warning(request, f"درخواست انتقال بار رد شد و به انبار مبدا اطلاع داده شد.") elif action == 'cancel': cancel_transfer_request(request_id=transfer.id, user=request.user) messages.info(request, "درخواست انتقال توسط شما لغو گردید.") else: messages.error(request, "دستور ارسالی نامعتبر است.") except ValidationError as e: # نمایش خطاهای منطقی (مثلاً کمبود موجودی انبار مبدا در لحظه تایید) error_text = ' '.join(e.messages) if hasattr(e, 'messages') else str(e) messages.error(request, f"خطا در انجام عملیات: {error_text}") except PermissionError as e: messages.error(request, str(e)) except Exception as e: logger.error(f"Error processing transfer action #{transfer_id}: {str(e)}", exc_info=True) messages.error(request, "خطای سیستمی در پردازش تراکنش رخ داد.") return redirect('admin_panel:store_transfers', s_id=redirect_store_id) @login_required(login_url='/panel/p_login/') @permission_required('admin_panel.ViewStore', raise_exception=True) def newStore(request): storeName = request.POST.get('storeName', '') newStore = StoreRoom() newStore.store_name = storeName newStore.save() return redirect('/panel/store/') @login_required(login_url='/panel/p_login/') @permission_required('admin_panel.OnlyAnbar', raise_exception=True) def productAdd(request): namePrd = request.POST.get('namePrd', '') category = request.POST.get('category', '') price = request.POST.get('price', '') mojodiAval = request.POST.get('mojodiAval', '') storeroom = request.POST.get('storeroom', '') newProduct = Product() newProduct.product_name = namePrd newProduct.ProductCategory = ProductCategory.objects.get(id=category) newProduct.product_price = price newProduct.save() prd_in_store = ProductInStore() prd_in_store.Product = newProduct prd_in_store.StoreRoom = StoreRoom.objects.get(id=storeroom) prd_in_store.s_in = mojodiAval prd_in_store.desc = "موجودی اولیه: "+str(mojodiAval) prd_in_store.movement_type = ProductInStore.MovementType.INITIAL prd_in_store.save() #return HttpResponse(installDate) return redirect('/panel/store/1') @login_required(login_url='/panel/p_login/') @permission_required('admin_panel.ViewStore', raise_exception=True) def productEdit(request): prd_code = request.POST.get('prd_code', '') namePrd = request.POST.get('namePrd', '') category = request.POST.get('category', '') price = request.POST.get('price', '') #return HttpResponse(prd_code) editProduct = Product.objects.get(id=prd_code) editProduct.product_name = namePrd editProduct.ProductCategory = ProductCategory.objects.get(id=category) editProduct.product_price = price editProduct.save() return redirect('/panel/store/1') @login_required(login_url='/panel/p_login/') @permission_required('admin_panel.ViewStore', raise_exception=True) def productDel(request, p_id, s_id): Product.objects.get(id=p_id).delete() return redirect('/panel/store/'+str(s_id)) @login_required(login_url='/panel/p_login/') @permission_required('admin_panel.OnlyAnbar', raise_exception=True) def operation(request): if request.method != 'POST': return HttpResponseBadRequest("متد ارسالی نامعتبر است.") user_op = request.user user_store_room = get_object_or_404(StoreRoom, owners=user_op) # استفاده از توابع امن اختصاصی پروژه برای جداسازی مقادیر عددی prd_id = _safe_int(request.POST.get('prd_id')) action_type = request.POST.get('actionType', '') tedad = _safe_int(request.POST.get('tedad')) azanbar = _safe_int(request.POST.get('azanbar')) beanbar = _safe_int(request.POST.get('beanbar')) desc = request.POST.get('desc', '').strip() if tedad <= 0: return HttpResponseBadRequest("تعداد وارد شده باید بیشتر از صفر باشد.") s_room = get_object_or_404(StoreRoom, id=azanbar) product_obj = get_object_or_404(Product, id=prd_id) try: # ===================================================================== # بخش اول: انتقال بین انبارها (واگذاری کامل به لایه سرویس) # ===================================================================== if action_type == 'transfer': if not beanbar: return HttpResponse("انبار مقصد مشخص نشده است.") target_store = get_object_or_404(StoreRoom, id=_safe_int(beanbar)) # فراخوانی لایه سرویس برای ثبت درخواست انتقال (بدون تغییر مستقیم موجودی) create_transfer_request( source_store=s_room, target_store=target_store, product=product_obj, quantity=tedad, requester=request.user, desc=desc or f"درخواست انتقال از انبار {s_room.store_name} به {target_store.store_name}" ) logger.info( f"Transfer request created: {tedad}x {product_obj.product_name} from Store #{s_room.id} to Store #{target_store.id} by User #{request.user.id}" ) return redirect('/panel/store/' + str(s_room.id)) # ===================================================================== # بخش دوم: ورود و خروج دستی (کنترل مستقیم با Transaction و Lock) # ===================================================================== elif action_type in ['vorod', 'khoroj']: user_s_room = get_object_or_404(StoreRoom, owners=user_op) if not user_op.is_superuser and s_room != user_s_room: return HttpResponse("شما دسترسی ورود / خروج به این انبار ندارید") with transaction.atomic(): # ساخت کوئری پایه برای دریافت تراکنش‌های سال جاری کالا qs = ProductInStore.objects.filter( Product=product_obj, StoreRoom=s_room, year=1400 ) # قفل‌گذاری ردیف‌ها برای جلوگیری از Race Condition (در دیتابیس‌های پشتیبان) if connection.features.has_select_for_update: qs = qs.select_for_update() locked_records = list(qs) # --- الف) عملیات ورود دستی --- if action_type == 'vorod': ProductInStore.objects.create( StoreRoom=s_room, Product=product_obj, s_in=tedad, s_out=0, year=1400, desc=f"ورود: {tedad} به انبار {s_room.store_name}#{desc}", movement_type=ProductInStore.MovementType.DIRECT_IN ) # --- ب) عملیات خروج دستی --- elif action_type == 'khoroj': # محاسبه سریع موجودی در رم بدون فشار مجدد به دیتابیس total_in = sum(item.s_in for item in locked_records if item.s_in) total_out = sum(item.s_out for item in locked_records if item.s_out) mojodi_prd = total_in - total_out if mojodi_prd < tedad: # تطابق ۱۰۰ درصدی با پاسخ متنی مورد انتظار فرانت‌اند return HttpResponse("موجودی کافی نیست، موجودی فعلی: " + str(mojodi_prd)) ProductInStore.objects.create( StoreRoom=s_room, Product=product_obj, s_in=0, s_out=tedad, year=1400, desc=f"خروج: {tedad} از انبار {s_room.store_name}#{desc}", movement_type=ProductInStore.MovementType.DIRECT_OUT ) else: return HttpResponseBadRequest("نوع عملیات نامعتبر است.") except ValidationError as e: # مدیریت خطاهای اعتبارسنجی (مثل کمبود موجودی در لایه سرویس یا یکسان بودن انبارها) error_msg = ' '.join(e.messages) if hasattr(e, 'messages') else str(e) logger.warning(f"Validation error in store operation: {error_msg}") return HttpResponse(error_msg) except PermissionDenied as e: # مدیریت خطای دسترسی کاربر به انبار logger.warning(f"Permission denied in store operation for user {request.user.id}: {str(e)}") return HttpResponse("شما دسترسی لازم برای انجام این عملیات در این انبار را ندارید.", status=403) except Exception as e: logger.error(f"Critical error in store operation view: {str(e)}", exc_info=True) return HttpResponse("خطای سیستمی رخ داد. تراکنش لغو شد.", status=500) return redirect('/panel/store/' + str(s_room.id)) @login_required(login_url='/panel/p_login/') def newFactor(request, c_id): prdsList = request.POST.get('prdsList', '') sarResidDate = request.POST.get('sarResidDate', None) pardakhti = request.POST.get('pardakhti', None) #return HttpResponse(pardakhti) if pardakhti: pardakhti=int(pardakhti) if pardakhti>=1: pass else: pardakhti = None #return HttpResponse(sarResidDate) try: sarResidDate = digits.fa_to_en(digits.ar_to_fa(sarResidDate)) #return HttpResponse(sarResidDate) sarResidDate = sarResidDate.split("/") #return HttpResponse(sarResidDate[2]) sarResidDate = JalaliDate(int(sarResidDate[0]), int(sarResidDate[1]), int(sarResidDate[2])).to_gregorian() #return HttpResponse(sarResidDate) except: sarResidDate = None #return HttpResponse(sarResidDate[1]) #sarResidDate = JalaliDate(sarResidDate[0], sarResidDate[1], sarResidDate[2]).to_gregorian() payType = request.POST.get('payType', '') FactorDate = request.POST.get('FactorDate', '') FactorType = request.POST.get('FactorType', '') FactorDesc = request.POST.get('FactorDesc', '') Factorfail = False customerObj=Customer.objects.get(id=c_id) storeUser = StoreRoom.objects.get(user__id=request.user.id) import json prdsList = json.loads(prdsList) sum_price = 0 for prd in prdsList: if prd['garanty_Esh'] == ' ' or prd['garanty_Esh'] == '' or prd['garanty_Esh'] == None: sum_price += prd['prd_count'] * prd['price'] #return HttpResponse(prd_code) try: newFactor = Factor() newFactor.customer= customerObj newFactor.amount = sum_price if payType in ('naghd', 'kartKhan', 'cardToCard'): newFactor.pay_status = 'paid' else: newFactor.pay_status = 'unpaid' newFactor.FactorType = FactorType newFactor.PayType = payType newFactor.desc = FactorDesc if sarResidDate != None: newFactor.deadline = sarResidDate newFactor.user = User.objects.get(id=request.user.id) newFactor.save() except: try: Factorfail=True newFactor.delete() except: pass if Factorfail != True: #accFactor To article is_naghd = False articleHesabDari = Article() articleHesabDari.debit = newFactor.amount articleHesabDari.credit = 0 articleHesabDari.customer = customerObj articleHesabDari.factor = newFactor if payType in ('naghd', 'kartKhan', 'cardToCard'): is_naghd = True articleHesabDari.type = payType articleHesabDari.desc = str(FactorType) + " " + str(customerObj.name_family) + " طی فاکتور شماره: " + str(newFactor.id) articleHesabDari.save() if is_naghd: articleHesabDari2 = Article() articleHesabDari2.debit = 0 articleHesabDari2.credit = newFactor.amount articleHesabDari2.customer = customerObj articleHesabDari2.type = payType articleHesabDari2.factor = newFactor articleHesabDari2.desc = str(FactorType) + " " + str(customerObj.name_family) + " تسویه نقدی فاکتور شماره: " + str(newFactor.id) articleHesabDari2.save() elif pardakhti != None and pardakhti!='' and pardakhti != ' ': articleHesabDari2 = Article() articleHesabDari2.debit = 0 articleHesabDari2.credit = pardakhti articleHesabDari2.customer = customerObj articleHesabDari2.type = payType articleHesabDari2.factor = newFactor articleHesabDari2.desc = str(customerObj.name_family) + " پرداخت نقد در محل برا فاکتور " + str(newFactor.id) articleHesabDari2.save() for prd in prdsList: try: fIteams = FactorIteam() fIteams.factor = newFactor if prd['prd_id'] == 9999: Gproduct = None fIteams.product = Gproduct fIteams.product_name = prd['product_name'] fIteams.num = prd['prd_count'] fIteams.amount = int(prd['priceKol']) else: Gproduct = Product.objects.get(id=prd['prd_id']) fIteams.product = Gproduct fIteams.product_name = Gproduct.product_name fIteams.num = prd['prd_count'] fIteams.amount = prd['prd_count'] * Gproduct.product_price fIteams.garantyEsh = prd['garanty_Esh'] fIteams.save() except: Factorfail = True newFactor.delete() break if Factorfail != True: for prd in prdsList: if prd['prd_id'] != 9999: pInStore = ProductInStore() sRoom = storeUser productO = Product.objects.get(id=prd['prd_id']) pInStore.StoreRoom = sRoom pInStore.Product = productO pInStore.factor = newFactor productS_in = ProductInStore.objects.filter(Product=productO, StoreRoom=sRoom, year=1400).aggregate(Sum('s_in'))['s_in__sum'] productS_out = ProductInStore.objects.filter(Product=productO, StoreRoom=sRoom, year=1400).aggregate(Sum('s_out'))['s_out__sum'] mojodi_prd = productS_in - productS_out if mojodi_prd >= prd['prd_count']: pInStore.s_out = prd['prd_count'] if prd['garanty_Esh'] != ' ' and prd['garanty_Esh'] != '' and prd['garanty_Esh'] != None: pInStore.desc = "خروج: " + str(prd['prd_count']) + "#"+str(prd['garanty_Esh'])+ "#" + str(customerObj.name_family) + " * " + str(request.user.first_name) + " " + str(request.user.last_name) else: pInStore.desc = "خروج: " + str(prd['prd_count']) + "#فروش" + "#" + str(customerObj.name_family) + " * " + str(request.user.first_name) + " " + str(request.user.last_name) pInStore.save() else: return HttpResponse("موجودی کافی نیست، موجودی فعلی: "+ str(mojodi_prd)) return redirect('/panel/customer/'+str(c_id)+'/factors/') @login_required(login_url='/panel/p_login/') def newFactor2(request, c_id): if request.method != 'POST': return HttpResponseBadRequest("متد ارسالی باید POST باشد.") user_op = request.user try: prds_list = json.loads(request.POST.get('prdsList') or '[]') json_pardakht_ha = json.loads(request.POST.get('PardakhtHa') or '{}') except json.JSONDecodeError: return HttpResponseBadRequest("فرمت داده‌های ارسالی JSON نامعتبر است.") factor_type = request.POST.get('FactorType', '') factor_desc = request.POST.get('FactorDesc', '') sar_resid_date = _parse_jalali_date(request.POST.get('sarResidDate')) customer_obj = get_object_or_404(Customer, id=c_id) store_room = get_object_or_404(StoreRoom, owners=user_op) # رفع مشکل متنی/عددی در استخراج شناسه کالاها product_ids = [] for item in prds_list: p_id = item.get('prd_id') if p_id and _safe_int(p_id) != 9999: product_ids.append(_safe_int(p_id)) products_by_id = {p.id: p for p in Product.objects.filter(id__in=product_ids)} sum_price = sum( _safe_int(item.get('prd_count')) * _safe_int(item.get('price')) for item in prds_list if not str(item.get('garanty_Esh', '')).strip() ) try: with transaction.atomic(): new_factor = Factor.objects.create( customer=customer_obj, amount=sum_price, FactorType=factor_type, desc=factor_desc, deadline=sar_resid_date, user=user_op ) Article.objects.create( debit=sum_price, credit=0, customer=customer_obj, factor=new_factor, desc=f"{customer_obj.name_family} فاکتور شماره: {new_factor.id}", user=user_op ) payment_map = { 'l_naghd': 'نقد', 'l_kartbekart': 'کارت به کارت', 'l_kartKhan': 'کارت خوان', 'l_takhfif': 'تخفیف' } for key, amount_raw in json_pardakht_ha.items(): amount = _safe_int(amount_raw) if amount <= 0: continue if key in payment_map: nahve_pardakht = payment_map[key] Article.objects.create( debit=0, credit=amount, customer=customer_obj, factor=new_factor, desc=f"{nahve_pardakht} {customer_obj.name_family} فاکتور شماره: {new_factor.id}", nahvePardakht=nahve_pardakht, payStatus="true", user=user_op ) elif key in ['l_nesiye', 'l_checkPrice']: is_nesiye = (key == 'l_nesiye') type_str = 'nesiye' if is_nesiye else 'check' ts_key = 'l_nesiyeSarResid' if is_nesiye else 'l_checkPriceSarResid' sar_resid_dt = _parse_timestamp(json_pardakht_ha.get(ts_key)) Sarresid.objects.create( customer=customer_obj, price=amount, payStatus="false", type=type_str, user=user_op, factor=new_factor, sarresidDT=sar_resid_dt ) factor_items_pool = [] user_fullname = f"{user_op.first_name} {user_op.last_name}".strip() or user_op.username for item in prds_list: # تبدیل امن شناسه به عدد prd_id = _safe_int(item.get('prd_id')) prd_count = _safe_int(item.get('prd_count')) garanty_esh = item.get('garanty_Esh', '') if prd_id == 9999: factor_items_pool.append(FactorIteam( factor=new_factor, product=None, product_name=item.get('product_name', ''), num=prd_count, amount=_safe_int(item.get('priceKol')), garantyEsh=garanty_esh )) else: product_obj = products_by_id.get(prd_id) if not product_obj: raise InventoryException(f"خطا در دریافت محصول: محصولی با شناسه {prd_id} در سیستم وجود ندارد.") factor_items_pool.append(FactorIteam( factor=new_factor, product=product_obj, product_name=product_obj.product_name, num=prd_count, amount=prd_count * _safe_int(product_obj.product_price), garantyEsh=garanty_esh )) FactorIteam.objects.bulk_create(factor_items_pool, batch_size=500) # فرآیند کسر از انبار (Enterprise Level) for item in prds_list: prd_id = _safe_int(item.get('prd_id')) if prd_id == 9999: continue prd_count = _safe_int(item.get('prd_count')) garanty_esh = item.get('garanty_Esh', '') product_obj = products_by_id.get(prd_id) if not product_obj: continue # ۱. کوئری پایه qs = ProductInStore.objects.filter( Product=product_obj, StoreRoom=store_room, year=1400 ) # ۲. قفل دیتابیس هوشمند و سازگار با تمام پایگاه‌های داده if connection.features.has_select_for_update: qs = qs.select_for_update() # ۳. بارگذاری امن در رم برای جلوگیری از خطای Aggregate در دیتابیس قفل‌شده locked_records = list(qs) # ۴. محاسبه دقیق و پرسرعت در پایتون total_in = sum(rec.s_in for rec in locked_records if rec.s_in) total_out = sum(rec.s_out for rec in locked_records if rec.s_out) mojodi_prd = total_in - total_out if mojodi_prd < prd_count: # استفاده از متن دقیق خطای قبلی جهت حفظ سازگاری با فرانت‌اند raise InventoryException(f"موجودی کافی نیست، موجودی فعلی {product_obj.product_name}: {mojodi_prd}") # ۵. تشخیص نوع تراکنش برای سیستم گزارش‌گیری جدید انبار if garanty_esh and str(garanty_esh).strip(): desc_text = f"خروج: {prd_count}#{garanty_esh}#{customer_obj.name_family} * {user_fullname}" if "اشانتیون" in str(garanty_esh): m_type = ProductInStore.MovementType.PROMOTION else: m_type = ProductInStore.MovementType.WARRANTY else: desc_text = f"خروج: {prd_count}#فروش#{customer_obj.name_family} * {user_fullname}" m_type = ProductInStore.MovementType.SALE # ۶. ثبت تراکنش خروجی انبار ProductInStore.objects.create( StoreRoom=store_room, Product=product_obj, factor=new_factor, s_out=prd_count, desc=desc_text, movement_type=m_type, # ثبت ساختاریافته نوع خروج برای سرعت در گزارش‌گیری year=1400 ) except InventoryException as e: return HttpResponse(str(e), status=400) except Exception as e: logger.error(f"Error creating factor for customer {c_id}: {str(e)}", exc_info=True) return HttpResponse("خطایی غیرمنتظره در سرور رخ داد. اطلاعات ذخیره نشد.", status=500) return redirect(f'/panel/customer/{c_id}/factors/') @login_required(login_url='/panel/p_login/') @permission_required('admin_panel.DelFactor', raise_exception=True) @require_POST def FactorDel(request, c_id, f_id): customer_obj = get_object_or_404(Customer, id=c_id) factor_initial = get_object_or_404(Factor, id=f_id, customer=customer_obj) if factor_initial.pay_status == 'cancell' or not factor_initial.is_active: return HttpResponseBadRequest("این فاکتور قبلاً باطل یا برگشت داده شده است.") user_fullname = f"{request.user.first_name} {request.user.last_name}".strip() or request.user.username cancel_reason = request.POST.get('cancel_reason', 'error') payment_action = request.POST.get('payment_action', 'keep') if cancel_reason == 'return': reason_text = "برگشت از فروش (مرجوعی مشتری)" inv_movement = ProductInStore.MovementType.RETURN else: reason_text = "اصلاح خطای کاربری" inv_movement = ProductInStore.MovementType.ERROR_CORRECTION try: with transaction.atomic(): if connection.features.has_select_for_update: factor = Factor.objects.select_for_update().get(pk=factor_initial.pk) else: factor = Factor.objects.get(pk=factor_initial.pk) if factor.pay_status == 'cancell' or not factor.is_active: return HttpResponseBadRequest("خطای همزمانی: این فاکتور در لحظه فرستادن درخواست باطل شده بود.") factor.is_active = False factor.pay_status = 'cancell' factor.save(update_fields=['is_active', 'pay_status']) # واکشی اسناد پرداخت قبل از ایجاد سند جدید (جلوگیری از باگ تکرار سند) payments_to_reverse = [] if payment_action == 'reverse' or cancel_reason == 'error': payments_to_reverse = list(Article.objects.filter( Q(debit=0) | Q(debit__isnull=True), factor=factor, credit__gt=0 )) # ۱. صدور سند معکوس مالی برای کل مبلغ فاکتور factor_amount = int(factor.amount or 0) if factor_amount > 0: Article.objects.create( debit=0, credit=factor_amount, customer=customer_obj, factor=factor, type=factor.PayType, desc=f"برگشت از فروش و ابطال فاکتور شماره {factor.id} (مرجوعی کالا توسط مشتری) - کاربر: {user_fullname}" if cancel_reason == 'return' else f"اصلاح خطای سیستم: ابطال فاکتور اشتباه شماره {factor.id} (بدون جابجایی واقعی کالا) - کاربر: {user_fullname}", user=request.user, payStatus="true" ) # ۲. خنثی کردن پرداخت‌های اولیه فاکتور for p in payments_to_reverse: Article.objects.create( debit=p.credit, credit=0, customer=customer_obj, factor=factor, type=p.type, nahvePardakht=p.nahvePardakht, desc=f"عودت وجه پرداخت شده / ابطال رسید فاکتور {factor.id} (به دلیل مرجوعی کالا) - کاربر: {user_fullname}" if cancel_reason == 'return' else f"اصلاح خطای سیستم: ابطال رسید پرداخت فاکتور اشتباه شماره {factor.id} - کاربر: {user_fullname}", user=request.user, payStatus="true" ) # ۳. 🛠️ اصلاح انبار (حل باگ عدم تغییر موجودی سیستم) stock_movements = list(ProductInStore.objects.filter(factor=factor, s_out__gt=0)) for movement in stock_movements: pInStore = ProductInStore( StoreRoom=movement.StoreRoom, Product=movement.Product, factor=factor, s_in=movement.s_out, # برگرداندن تعداد کسر شده به ستون ورودی s_out=0, year=movement.year, # 💡 فیکس قطعی: استفاده دقیق از سال مالی تراکنش اصلی (۱۴۰۰) جهت همخوانی با توابع سیستم desc=f"ورود به انبار بابت برگشت از فروش کالا (فاکتور {factor.id}) - کاربر: {user_fullname}" if cancel_reason == 'return' else f"ورود به انبار بابت اصلاح خطای ثبت فاکتور {factor.id} - کاربر: {user_fullname}", movement_type=inv_movement ) pInStore.save() # فراخوانی متد save مدل شما که محاسبات تاریخچه و موجودی لحظه‌ای را انجام می‌دهد # ۴. ابطال اقساط و سررسیدهای پاس نشده Sarresid.objects.filter(factor=factor, payStatus="false").update( payStatus="cancell", desc=f"ابطال بابت {reason_text} فاکتور {factor.id}" ) logger.info(f"Factor {factor.id} successfully cancelled. Reason: {cancel_reason}, Payment: {payment_action}") except Exception as e: logger.error(f"Critical error in FactorDel for factor {f_id}: {str(e)}", exc_info=True) return HttpResponse("خطای سیستمی در فرآیند ابطال و صدور اسناد اصلاحی رخ داد.", status=500) return redirect('admin_panel:customerFactors', c_id=c_id) from django.contrib import messages from django.shortcuts import get_object_or_404, redirect import logging logger = logging.getLogger(__name__) @login_required(login_url='/panel/p_login/') @permission_required('admin_panel.DelCustomer', raise_exception=True) def customerDel(request, c_id): customer_obj = get_object_or_404(Customer, id=c_id) try: # ۱. بررسی وجود هرگونه سابقه مالی/حسابداری (حتی با مانده صفر) has_articles = Article.objects.filter(customer=customer_obj).exists() # ۲. بررسی وجود هرگونه سابقه چک یا نسیه (حتی در صورت پاس شدن و تسویه کامل) has_sarresids = Sarresid.objects.filter(customer=customer_obj).exists() # ۳. بررسی وجود هرگونه فاکتور ثبت شده برای این مشتری has_factors = Factor.objects.filter(customer=customer_obj).exists() # ۴. بررسی وجود هرگونه سابقه اقدام، بازدید یا نظرسنجی has_actions = Action.objects.filter(customer=customer_obj).exists() # ۵. اگر هر کدام از این سوابق وجود داشته باشد، به هیچ عنوان اجازه غیرفعال‌سازی/حذف نمی‌دهیم if has_articles or has_sarresids or has_factors or has_actions: error_msg = f"عملیات ناموفق: مشتری '{customer_obj.name_family}' به دلیل داشتن سابقه در سیستم قابل حذف نیست! دلایل: " reasons = [] if has_articles: reasons.append("داشتن تراکنش مالی یا گردش حسابداری") if has_sarresids: reasons.append("داشتن سابقه چک یا نسیه (حتی تسویه شده)") if has_factors: reasons.append("داشتن فاکتور ثبت‌شده") if has_actions: reasons.append("داشتن سابقه اقدامات، بازدید یا نظرسنجی") error_msg += "، ".join(reasons) + "." # ارسال پیام خطا به تمپلیت و بازگرداندن کاربر به صفحه قبلی messages.error(request, error_msg) return redirect(request.META.get('HTTP_REFERER', '/panel/customers/')) # ۶. در صورتی که مشتری کاملاً خام بود و هیچ سابقه‌ای در هیچ بخشی نداشت، حذف می‌شود customer_obj.delete() messages.success(request, f"مشتری '{customer_obj.name_family}' که فاقد هرگونه سابقه‌ای بود، با موفقیت از سیستم حذف شد.") return redirect('/panel/customers/') except Exception as e: logger.error(f"Error in deleting customer {c_id}: {str(e)}", exc_info=True) messages.error(request, "خطای سیستمی در بررسی سوابق و حذف مشتری رخ داد.") return redirect(request.META.get('HTTP_REFERER', '/panel/customers/')) @login_required(login_url='/panel/p_login/') def customerHesabdari(request ,c_id): customer = Customer.objects.get(id=c_id) try: debit = Article.objects.filter(customer=customer).aggregate(Sum('debit'))['debit__sum'] credit = Article.objects.filter(customer=customer).aggregate(Sum('credit'))['credit__sum'] mandehesabCs = debit - credit articls = Article.objects.filter(customer=customer).order_by('-createdDT') except: articls =None mandehesabCs = 0 dateNow = datetime.datetime.now().date data = {'customer': customer, 'mandehesabCs': mandehesabCs, 'articls': articls} return render(request, 'admin_panel/customerHesabDari.html', data) @login_required(login_url='/panel/p_login/') def sarresidh(request ,c_id): customer = Customer.objects.get(id=c_id) try: all_sarresid = Sarresid.objects.filter(customer=customer).order_by('-sarresidDT') except: all_sarresid = None data = {'customer': customer, 'all_sarresid': all_sarresid} return render(request, 'admin_panel/customerSarresid.html', data) @login_required(login_url='/panel/p_login/') def customerHesabdariAdd(request ,c_id): user_op = request.user price = request.POST.get('price', None) type = request.POST.get('type', None) typePy = request.POST.get('typePy', None) desc = request.POST.get('desc', None) nahvePardakhat = request.POST.get('nahvePardakhat', None) newArticl = Article() newArticl.customer = Customer.objects.get(id=c_id) newArticl.desc = desc newArticl.typePy = typePy if type == 'credit': newArticl.credit = price newArticl.debit = 0 elif type == 'debit': newArticl.debit = price newArticl.credit = 0 newArticl.user = user_op newArticl.nahvePardakht = nahvePardakhat newArticl.save() return redirect('/panel/customer/'+str(c_id)+'/hesabdari') @login_required(login_url='/panel/p_login/') def customerAction(request, c_id): # استفاده از get_object_or_404 برای امنیت بیشتر customer = get_object_or_404(Customer, id=c_id) # دریافت پارامترهای فیلتر از URL (پیش‌فرض: all) filter_type = request.GET.get('actionType', 'all') filter_status = request.GET.get('status', 'all') # کوئری پایه برای اقدامات این مشتری actions = Action.objects.filter(customer=customer) # اعمال فیلتر بر اساس نوع اقدام (بازدید، نصب و ...) if filter_type != 'all': actions = actions.filter(actionType=filter_type) # اعمال فیلتر بر اساس وضعیت (انجام شده، در انتظار و ...) if filter_status != 'all': actions = actions.filter(status=filter_status) # مرتب‌سازی نزولی بر اساس تاریخ ایجاد actions = actions.order_by("-createdDT") allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat') data = { 'customer': customer, 'actions': actions, 'allUsers': allUsers, # ارسال مقادیر فیلتر به قالب برای حفظ وضعیت انتخاب‌شده در دراپ‌داون‌ها 'filter_type': filter_type, 'filter_status': filter_status } return render(request, 'admin_panel/customerAction.html', data) @login_required(login_url='/panel/p_login/') def customerNazar(request ,c_id): customer = Customer.objects.get(id=c_id) actions = Action.objects.filter(customer=customer, nazarDT__isnull=False).order_by("-createdDT") allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat') data = {'customer': customer, 'actions': actions, 'allUsers': allUsers} return render(request, 'admin_panel/customer/nazar.html', data) @login_required(login_url='/panel/p_login/') def customerNewAction(request ,c_id): actionType = request.POST.get('actionType', None) dateTimeAnjam = request.POST.get('dateTimeAnjam', None) #return HttpResponse(float(dateTimeAnjam+".0")) try: dateTimeAnjam = datetime.datetime.fromtimestamp(int(dateTimeAnjam)/1e3) except: pass desc = request.POST.get('desc', None) userID = request.POST.get('userID', None) is_done = request.POST.get('is_done', None) result = request.POST.get('result', None) FinishDateTime = request.POST.get('FinishDateTime', None) try: FinishDateTime = datetime.datetime.fromtimestamp(int(FinishDateTime)/1e3) except: pass customer_obj = Customer.objects.get(id=int(c_id)) newAction = Action() newAction.actionType = actionType newAction.actionDateTime = dateTimeAnjam newAction.desc = desc newAction.forUser = User.objects.get(id=int(userID)) newAction.customer = customer_obj if is_done: newAction.status = 'finish' newAction.finishDateTime = FinishDateTime newAction.result = result else: newAction.status = 'waite' newAction.save() if newAction.status == 'finish' and newAction.actionType == 'bazdid': customer_obj.last_bazdid_dt = newAction.finishDateTime customer_obj.save() return redirect('/panel/customer/'+str(c_id)+'/action') @login_required(login_url='/panel/p_login/') def ActionList(request): actions = Action.objects.filter(forUser__id=request.user.id, status='waite').order_by("-createdDT") allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat') data = {'actions': actions, 'allUsers': allUsers} return render(request, 'admin_panel/ActionList.html', data) @login_required(login_url='/panel/p_login/') def ActionListUser(request, u_id): actions = Action.objects.filter(forUser__id=u_id, status='waite').order_by("-createdDT") allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat') data = {'actions': actions, 'allUsers': allUsers} return render(request, 'admin_panel/ActionList.html', data) @login_required(login_url='/panel/p_login/') def customerEditAction(request ,c_id, action_id): actionType = request.POST.get('actionType', None) dateTimeAnjam = request.POST.get('dateTimeAnjam', None) print("ok") #return HttpResponse(float(dateTimeAnjam+".0")) try: dateTimeAnjam = datetime.datetime.fromtimestamp(int(dateTimeAnjam)/1e3) except: pass desc = request.POST.get('desc', None) userID = request.POST.get('userID', None) is_done = request.POST.get('is_done', None) result = request.POST.get('result', None) FinishDateTime = request.POST.get('FinishDateTime', None) try: FinishDateTime = datetime.datetime.fromtimestamp(int(FinishDateTime)/1e3) except: pass customer_obj = Customer.objects.get(id=int(c_id)) newAction = Action.objects.get(id=action_id) newAction.actionType = actionType newAction.actionDateTime = dateTimeAnjam newAction.desc = desc newAction.forUser = User.objects.get(id=int(userID)) newAction.customer = customer_obj if is_done: newAction.status = 'finish' newAction.finishDateTime = FinishDateTime newAction.result = result else: newAction.status = 'waite' newAction.save() if newAction.status == 'finish' and newAction.actionType == 'bazdid': customer_obj.last_bazdid_dt = newAction.finishDateTime customer_obj.save() #return redirect('/panel/customer/'+str(c_id)+'/action') return redirect(request.META.get('HTTP_REFERER', 'redirect_if_referer_not_found')) @login_required(login_url='/panel/p_login/') def ActionListAll(request): actions = Action.objects.filter().order_by("-id") allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat') data = {'actions': actions, 'allUsers': allUsers} return render(request, 'admin_panel/ActionList.html', data) @login_required(login_url='/panel/p_login/') def ActionListToday(request): startTodayDT= datetime.datetime.today().strftime('%Y-%m-%d 00:00:00') endTodayDT= datetime.datetime.today().strftime('%Y-%m-%d 23:59:59') #startTodayDT = datetime.datetime.strptime(startTodayDT, '%Y-%m-%d-00:00:00') #endTodayDT = datetime.datetime.strptime(endTodayDT, '%Y-%m-%d-23:59:59') #return HttpResponse(todayDT) actions = Action.objects.filter(actionDateTime__gte=startTodayDT,actionDateTime__lte=endTodayDT, forUser__id=request.user.id, status='waite').order_by("-actionDateTime")[:50] allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat') data = {'actions': actions, 'allUsers': allUsers} return render(request, 'admin_panel/ActionList.html', data) @login_required(login_url='/panel/p_login/') def ActionListUserToday(request, u_id): dateTimeAction = request.GET.get('dateTime', None) status = request.GET.get('status', None) unixADT = dateTimeAction if dateTimeAction: dateTimeAction = datetime.datetime.fromtimestamp(int(dateTimeAction)/1e3) startTodayDT = dateTimeAction.strftime('%Y-%m-%d 00:00:00') endTodayDT= dateTimeAction.strftime('%Y-%m-%d 23:59:59') else: startTodayDT= datetime.datetime.today().strftime('%Y-%m-%d 00:00:00') endTodayDT= datetime.datetime.today().strftime('%Y-%m-%d 23:59:59') #startTodayDT = datetime.datetime.strptime(startTodayDT, '%Y-%m-%d-00:00:00') #endTodayDT = datetime.datetime.strptime(endTodayDT, '%Y-%m-%d-23:59:59') #return HttpResponse(todayDT) if status == 'waite' or status == None or status == '' or status == ' ': status = 'waite' actions = Action.objects.filter(actionDateTime__gte=startTodayDT,actionDateTime__lte=endTodayDT, forUser__id=u_id, status=status).order_by("-actionDateTime")[:50] actionPr = Action.objects.filter(actionDateTime__lt=startTodayDT, forUser__id=u_id, status=status).order_by("-actionDateTime")[:200] elif status == 'finish': actionPr = None actions = Action.objects.filter(actionDateTime__gte=startTodayDT,actionDateTime__lte=endTodayDT, forUser__id=u_id, status=status).order_by("-actionDateTime")[:50] allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat') data = {'actions': actions, 'allUsers': allUsers, 'u_id': u_id, "unixADT": unixADT, "status": status, 'actionPr': actionPr} return render(request, 'admin_panel/ActionList.html', data) import datetime from django.db.models import Q from django.shortcuts import render from django.contrib.auth.decorators import login_required from .models import Action @login_required(login_url='/panel/p_login/') def rezayat(request): statusRezayat = request.GET.get('status', 'waite') now = datetime.datetime.now() # محاسبه بازه‌های زمانی ۳۰ روز و ۶۰ روز قبل dt_30_days_ago = now - datetime.timedelta(days=30) dt_60_days_ago = now - datetime.timedelta(days=60) if statusRezayat == 'finish': # نمایش نظرسنجی‌های تعیین‌تکلیف شده (ثبت شده یا کنسل شده) actions = Action.objects.exclude(actionType='vosol').filter( status='finish', nazarDT__isnull=False ).order_by("-nazarDT")[:500] else: statusRezayat = 'waite' # نمایش نظرسنجی‌های در انتظار با اعمال شرط عدم انقضای زمان actions = Action.objects.exclude(actionType='vosol').filter( status='finish', nazarDT__isnull=True, # نظرسنجی هنوز نهایی نشده createdDT__gte=dt_60_days_ago # حداکثر ۶۰ روز از ایجاد گذشته باشد ).filter( Q(finishDateTime__isnull=True) | Q(finishDateTime__gte=dt_30_days_ago) # حداکثر ۳۰ روز از اتمام گذشته باشد ).filter( Q(nextNazarDT__isnull=True) | Q(nextNazarDT__lte=now) # موعد پیگیری رسیده یا تعویقی نداشته باشد ).order_by("-id")[:500] data = {'actions': actions, "status": statusRezayat} return render(request, 'admin_panel/customerRezayat.html', data) @login_required(login_url='/panel/p_login/') def rezayatEdit(request, ac_id): csGrade = request.POST.get('csGrade', None) csDesc = request.POST.get('csDesc', None) if csDesc and csGrade: action = get_object_or_404(Action, id=ac_id) if csGrade == '-1': # عدم موفقیت: تنظیم nextNazarDT برای ۳ روز بعد (nazarDT پر نمی‌شود تا نهایی نشود) action.nazar = csDesc action.grade = int(csGrade) action.nextNazarDT = datetime.datetime.now() + datetime.timedelta(days=3) elif csGrade == '-2': # کنسل کردن: پر کردن nazarDT برای خروج کامل از لیست کارهای در انتظار action.nazar = csDesc action.grade = int(csGrade) action.nazarDT = datetime.datetime.now() action.nextNazarDT = None else: # ثبت امتیاز عادی action.nazar = csDesc action.grade = int(csGrade) action.nazarDT = datetime.datetime.now() action.nextNazarDT = None action.save() return redirect('/panel/rezayat/') else: 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) @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("خطا در ثبت ") # ===================================================================== # توابع کمکی و منطق استعلام چک (برگرفته از اسکریپت‌های شما) # ===================================================================== def solve_base64_captcha(base64_string: str) -> str: try: if ',' in base64_string: base64_string = base64_string.split(',')[1] image_bytes = base64.b64decode(base64_string) np_arr = np.frombuffer(image_bytes, np.uint8) img = cv2.imdecode(np_arr, cv2.IMREAD_COLOR) if img is None: return "" gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) blur = cv2.medianBlur(gray, 3) _, thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) kernel = np.ones((2, 2), np.uint8) cleaned_img = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=1) final_img = cv2.bitwise_not(cleaned_img) success, encoded_image = cv2.imencode('.png', final_img) if not success: return "" processed_bytes = encoded_image.tobytes() ocr = ddddocr.DdddOcr(show_ad=False) extracted_text = ocr.classification(processed_bytes) numbers = re.findall(r'\d+', extracted_text) if len(numbers) >= 2: num1, num2 = int(numbers[0]), int(numbers[1]) if '+' in extracted_text: ans = num1 + num2 elif '-' in extracted_text: ans = num1 - num2 elif '*' in extracted_text or 'x' in extracted_text.lower(): ans = num1 * num2 else: ans = num1 + num2 return str(ans) return "" except Exception: return "" def parse_cheque_result_html(html_content: str) -> dict: sayad_id = re.search(r'شناسه\s*?\s*(\d+)\s*?', html_content) owner_name = re.search(r'متعلق به\s*?\s*(.*?)\s*?\s*می باشد', html_content) validity_date = re.search(r'تا تاریخ\s*?\s*([\d/]+)\s*?', html_content) status = re.search(r'در وضعیت\s*?\s*(.*?)\s*?\s*در پایگاه', html_content) clean_full_text = re.sub(r'<[^>]+>', '', html_content).strip() return { "success": True, "data": { "sayad_id": sayad_id.group(1) if sayad_id else "", "owner_name": owner_name.group(1).strip() if owner_name else "", "validity_date": validity_date.group(1) if validity_date else "", "status": status.group(1).strip() if status else "", "raw_message": clean_full_text }, "error": None } async def run_cbi_sayad_inquiry_logic(sayad_id: str, max_retries: int = 7) -> str: lightpanda_url = os.environ.get("LIGHTPANDA_URL") or "ws://127.0.0.1:9222" async with async_playwright() as p: try: browser = await p.chromium.connect_over_cdp(lightpanda_url) except Exception as e: return json.dumps({"success": False, "data": None, "error": f"عدم اتصال به Lightpanda: {str(e)}"}) for attempt in range(1, max_retries + 1): context = await browser.new_context(ignore_https_errors=True, bypass_csp=True) page = await context.new_page() try: url = "https://cbi.ir/EstelamSayad/19689.aspx" await page.goto(url, wait_until="domcontentloaded", timeout=20000) captcha_selector = "#ctl00_ucBody_ucContent_ctl00_imgcpatcha" await page.wait_for_selector(captcha_selector, state="visible", timeout=15000) await asyncio.sleep(1) base64_src = await page.locator(captcha_selector).get_attribute("src") viewstate = await page.locator("#__VIEWSTATE").get_attribute("value") or "" viewstate_gen = await page.locator("#__VIEWSTATEGENERATOR").get_attribute("value") or "453B48D8" if not base64_src or not viewstate: await context.close() continue captcha_answer = solve_base64_captcha(base64_src) if not captcha_answer: await context.close() continue ajax_raw_response = await page.evaluate(""" async ({ sayadId, captchaAns, viewState, viewStateGen }) => { try { const formData = new URLSearchParams(); formData.append('ctl00$ucBody$ucContent$ctl00$ScriptManager1', 'ctl00$ucBody$ucContent$ctl00$UpdatePanel1|ctl00$ucBody$ucContent$ctl00$btnSearch'); formData.append('__EVENTTARGET', ''); formData.append('__EVENTARGUMENT', ''); formData.append('__VIEWSTATE', viewState); formData.append('__VIEWSTATEGENERATOR', viewStateGen); formData.append('__VIEWSTATEENCRYPTED', ''); formData.append('ctl00$ucHeader$txtQuickSearch', ''); formData.append('ctl00$ucBody$ucContent$ctl00$txtChequeNumber', sayadId); formData.append('ctl00$ucBody$ucContent$ctl00$txtCaptchaInput', captchaAns); formData.append('__ASYNCPOST', 'true'); formData.append('ctl00$ucBody$ucContent$ctl00$btnSearch', 'استعلام چک صیادی'); const response = await fetch(window.location.href, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest', 'Cache-Control': 'no-cache' }, body: formData.toString() }); return await response.text(); } catch (err) { return 'FETCH_ERROR: ' + err.message; } } """, {'sayadId': sayad_id, 'captchaAns': captcha_answer, 'viewState': viewstate, 'viewStateGen': viewstate_gen}) if "FETCH_ERROR:" in ajax_raw_response: await context.close() await asyncio.sleep(1) continue error_match = re.search(r'id="ctl00_ucBody_ucContent_ctl00_lblError"[^>]*>(.*?)', ajax_raw_response, re.DOTALL) if error_match and error_match.group(1).strip(): clean_error = re.sub(r'<[^>]+>', '', error_match.group(1)).strip() if "عبارت محاسباتی" in clean_error: await context.close() continue else: await context.close() await browser.close() return json.dumps({"success": False, "data": None, "error": clean_error}) result_match = re.search(r'id="ctl00_ucBody_ucContent_ctl00_lblChequeNumberResult"[^>]*>(.*?)', ajax_raw_response, re.DOTALL) if result_match: raw_html = result_match.group(1) parsed_result = parse_cheque_result_html(raw_html) await context.close() await browser.close() return json.dumps(parsed_result) await context.close() except Exception: await context.close() await asyncio.sleep(1) await browser.close() return json.dumps({"success": False, "data": None, "error": "استعلام ناموفق بود (خطای شبکه یا تشخیص کپچا)"}) async def run_bpi_cheque_inquiry_logic(national_id: str, sayad_id: str, id_type: int = 1): lightpanda_url = os.environ.get("LIGHTPANDA_URL") or "ws://127.0.0.1:9222" async with async_playwright() as p: try: browser = await p.chromium.connect_over_cdp(lightpanda_url) except Exception as e: return {"success": False, "error": f"عدم اتصال به مرورگر Lightpanda: {str(e)}"} context = await browser.new_context() page = await context.new_page() try: await page.goto("https://vbank.bpi.ir/public/inquiries/inquiry-bounced-cheques", wait_until="domcontentloaded", timeout=20000) api_url = f"https://sec.bpi.ir/prls/api/v1/inquiry/chequeStatus?IdCode={national_id}&IdType={id_type}&SayadId={sayad_id}" response = await context.request.get( api_url, headers={ "Referer": "https://vbank.bpi.ir/public/inquiries/inquiry-bounced-cheques", "Accept": "application/json, text/plain, */*" } ) if response.ok: data = await response.json() return {"success": True, "data": data} else: error_text = await response.text() return {"success": False, "error": f"سرور خطا برگرداند (کد {response.status}): {error_text}"} except Exception as e: return {"success": False, "error": f"خطایی در فرآیند استعلام رخ داد: {str(e)}"} finally: await context.close() await browser.close() # ===================================================================== # View های مربوط به استعلام (API) # ===================================================================== @login_required(login_url='/panel/p_login/') @permission_required('admin_panel.ViewStore', raise_exception=True) def api_check_status_inquiry(request): """ استعلام وضعیت چک از بانک مرکزی پارامتر ورودی: sayad_id (GET) """ sayad_id = request.GET.get('sayad_id', '').strip() if not sayad_id or not sayad_id.isdigit() or len(sayad_id) != 16: return JsonResponse({"success": False, "error": "شناسه صیادی باید دقیقاً ۱۶ رقم باشد."}) try: # اجرای تابع Async به صورت Sync result_json_str = async_to_sync(run_cbi_sayad_inquiry_logic)(sayad_id) # تبدیل رشته جیسون به دیکشنری برای ارسال مجدد با JsonResponse result_data = json.loads(result_json_str) return JsonResponse(result_data) except Exception as e: logger.error(f"Error in CBI Check Status Inquiry: {str(e)}", exc_info=True) return JsonResponse({"success": False, "error": "خطای سرور در برقراری ارتباط با بانک مرکزی."}) @login_required(login_url='/panel/p_login/') @permission_required('admin_panel.ViewStore', raise_exception=True) def api_check_sabt_inquiry(request): """ استعلام ثبت چک از بانک پاسارگاد پارامترهای ورودی: sayad_id, national_id, id_type (GET) """ sayad_id = request.GET.get('sayad_id', '').strip() national_id = request.GET.get('national_id', '').strip() id_type = _safe_int(request.GET.get('id_type', 1)) # عدد 1 حقیقی، 2 حقوقی # اگر کدملی از فرانت‌اند خالی ارسال شد، از یک کدملی پیش‌فرض استفاده می‌کنیم if not national_id: national_id = "1219968218" if not sayad_id: return JsonResponse({"success": False, "error": "شناسه صیادی برای استعلام ثبت الزامی است."}) try: # اجرای تابع Async به صورت Sync result_data = async_to_sync(run_bpi_cheque_inquiry_logic)(national_id, sayad_id, id_type) return JsonResponse(result_data) except Exception as e: logger.error(f"Error in BPI Check Registration Inquiry: {str(e)}", exc_info=True) return JsonResponse({"success": False, "error": "خطای سرور در برقراری ارتباط با سامانه بانک."})