from django.db import transaction, connection from django.db.models import Sum, Q from django.shortcuts import redirect, render, get_object_or_404 from django.http import HttpResponse, HttpResponseBadRequest from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage from django.core.exceptions import PermissionDenied, ValidationError from django.views.decorators.http import require_POST from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required from ..models import StoreRoom, ProductCategory, Product, ProductInStore, StoreTransferRequest from ..services import (create_transfer_request, approve_transfer_request, reject_transfer_request, cancel_transfer_request) from ._helpers import logger, _safe_int @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) @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 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))