Files
2026-10-09 20:02:36 +03:30

459 lines
21 KiB
Python

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))