refactor
This commit is contained in:
1 parent
6f36db63a1
commit
250cdbab7f
42 files changed
+96363
-11699
No files matched your search
@@ -0,0 +1,458 @@
|
||||
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))
|
||||
Reference in new issue
Block a user