# services/transfer_services.py import logging from typing import Optional from django.db import transaction from django.core.exceptions import ValidationError, PermissionDenied from django.utils import timezone from django.utils.translation import gettext_lazy as _ from django.db.models import Sum, QuerySet from django.contrib.auth import get_user_model from django.contrib.auth.models import AbstractBaseUser from .models import StoreTransferRequest, ProductInStore, StoreRoom, Product # تنظیم لابر استاندارد برای مانیتورینگ سازمانی logger = logging.getLogger(__name__) User = get_user_model() def _check_target_store_permission(store: StoreRoom, user: AbstractBaseUser) -> None: """بررسی دسترسی کاربر به انبار مقصد""" if not (user.is_superuser or store.user.filter(id=user.id).exists()): logger.warning( "Unauthorized transfer approval attempt", extra={"user_id": user.id, "store_id": store.id} ) raise PermissionDenied(_("شما دسترسی و مالکیت لازم برای مدیریت ورود کالا به این انبار را ندارید.")) def _check_source_store_permission(store: StoreRoom, user: AbstractBaseUser) -> None: """بررسی دسترسی کاربر به انبار مبدا""" if not (user.is_superuser or store.user.filter(id=user.id).exists()): logger.warning( "Unauthorized transfer request attempt", extra={"user_id": user.id, "store_id": store.id} ) raise PermissionDenied(_("شما دسترسی لازم برای مدیریت خروج کالا از این انبار را ندارید.")) def get_product_current_stock(store: StoreRoom, product: Product, lock_for_update: bool = False) -> int: """ محاسبه موجودی لحظه‌ای یک کالا در انبار مشخص. پارامتر lock_for_update در زمان تایید تراکنش فعال می‌شود تا از Race Condition در موجودی جلوگیری کند. """ qs = ProductInStore.objects.filter(StoreRoom=store, Product=product) # قفل کردن رکوردهای موجودی انبار برای جلوگیری از کسر همزمان توسط چند تراکنش if lock_for_update: qs = qs.select_for_update() totals = qs.aggregate(stock=Sum('s_in') - Sum('s_out')) return totals['stock'] or 0 @transaction.atomic def create_transfer_request( *, source_store: StoreRoom, target_store: StoreRoom, product: Product, quantity: int, requester: AbstractBaseUser, desc: Optional[str] = None ) -> StoreTransferRequest: """ سرویس ثبت درخواست انتقال جدید """ _check_source_store_permission(source_store, requester) if source_store.id == target_store.id: raise ValidationError(_("انبار مبدا و مقصد نمی‌توانند یکسان باشند.")) if quantity <= 0: raise ValidationError(_("تعداد انتقال باید بیشتر از صفر باشد.")) # در زمان ثبت درخواست، نیازی به قفل دیتابیس نیست (فقط چک اولیه) current_stock = get_product_current_stock(source_store, product, lock_for_update=False) if current_stock < quantity: raise ValidationError(_("موجودی انبار مبدا کافی نیست. موجودی فعلی: %(stock)s") % {'stock': current_stock}) transfer_request = StoreTransferRequest.objects.create( source_store=source_store, target_store=target_store, product=product, quantity=quantity, requester=requester, desc=desc, status=StoreTransferRequest.Status.PENDING ) logger.info( f"Transfer request #{transfer_request.id} created by user #{requester.id}", extra={"transfer_id": transfer_request.id, "quantity": quantity} ) return transfer_request @transaction.atomic def approve_transfer_request(*, request_id: int, approver: AbstractBaseUser) -> StoreTransferRequest: """ سرویس تایید انتقال: قفل رکورد درخواست و رکوردهای انبار، کسر از مبدا، ورود به مقصد و تغییر وضعیت """ try: # ۱. قفل کردن رکورد درخواست برای جلوگیری از Double Approval transfer = StoreTransferRequest.objects.select_for_update().get(pk=request_id) except StoreTransferRequest.DoesNotExist: raise ValidationError(_("درخواست انتقال مورد نظر یافت نشد.")) if transfer.status != StoreTransferRequest.Status.PENDING: raise ValidationError(_("این درخواست قبلاً تعیین تکلیف شده است و قابل تایید نیست.")) # ۲. بررسی سطح دسترسی تاییدکننده _check_target_store_permission(transfer.target_store, approver) # ۳. بررسی موجودی با فعال‌سازی قفل (lock_for_update=True) برای جلوگیری از Race Condition موجودی انبار current_stock = get_product_current_stock(transfer.source_store, transfer.product, lock_for_update=True) if current_stock < transfer.quantity: raise ValidationError( _("موجودی انبار مبدا برای تایید این انتقال کافی نیست. موجودی فعلی: %(stock)s") % {'stock': current_stock} ) now = timezone.now() # ۴. صدور سند خروج قطعی از انبار مبدا out_tx = ProductInStore.objects.create( StoreRoom=transfer.source_store, Product=transfer.product, s_in=0, s_out=transfer.quantity, movement_type=ProductInStore.MovementType.TRANSFER, desc=f"انتقال به {transfer.target_store.store_name} | درخواست #{transfer.id} | تایید توسط {approver.username}" ) # ۵. صدور سند ورود قطعی به انبار مقصد in_tx = ProductInStore.objects.create( StoreRoom=transfer.target_store, Product=transfer.product, s_in=transfer.quantity, s_out=0, movement_type=ProductInStore.MovementType.TRANSFER, desc=f"انتقال از {transfer.source_store.store_name} | درخواست #{transfer.id} | تایید توسط {approver.username}" ) # ۶. به‌روزرسانی نهایی درخواست transfer.status = StoreTransferRequest.Status.APPROVED transfer.approver = approver transfer.action_date = now transfer.out_transaction = out_tx transfer.in_transaction = in_tx transfer.save(update_fields=['status', 'approver', 'action_date', 'out_transaction', 'in_transaction', 'updated_at']) logger.info( f"Transfer request #{transfer.id} approved successfully by user #{approver.id}", extra={"transfer_id": transfer.id, "out_tx": out_tx.id, "in_tx": in_tx.id} ) return transfer @transaction.atomic def reject_transfer_request(*, request_id: int, rejector: AbstractBaseUser, reason: str = "") -> StoreTransferRequest: """ سرویس رد درخواست توسط انبار مقصد """ try: transfer = StoreTransferRequest.objects.select_for_update().get(pk=request_id) except StoreTransferRequest.DoesNotExist: raise ValidationError(_("درخواست انتقال مورد نظر یافت نشد.")) if transfer.status != StoreTransferRequest.Status.PENDING: raise ValidationError(_("این درخواست قبلاً تعیین تکلیف شده است.")) _check_target_store_permission(transfer.target_store, rejector) transfer.status = StoreTransferRequest.Status.REJECTED transfer.approver = rejector transfer.action_date = timezone.now() if reason: transfer.rejection_reason = reason transfer.save(update_fields=['status', 'approver', 'action_date', 'rejection_reason', 'updated_at']) logger.info(f"Transfer request #{transfer.id} rejected by user #{rejector.id}. Reason: {reason}") return transfer @transaction.atomic def cancel_transfer_request(*, request_id: int, user: AbstractBaseUser) -> StoreTransferRequest: """ سرویس انصراف از انتقال توسط ایجادکننده یا مسئول انبار مبدا """ try: transfer = StoreTransferRequest.objects.select_for_update().get(pk=request_id) except StoreTransferRequest.DoesNotExist: raise ValidationError(_("درخواست انتقال مورد نظر یافت نشد.")) if transfer.status != StoreTransferRequest.Status.PENDING: raise ValidationError(_("فقط درخواست‌های در انتظار تایید قابل انصراف هستند.")) if not (user.is_superuser or transfer.requester_id == user.id or transfer.source_store.user.filter(id=user.id).exists()): raise PermissionDenied(_("شما اجازه انصراف از این درخواست را ندارید.")) transfer.status = StoreTransferRequest.Status.CANCELED transfer.approver = user transfer.action_date = timezone.now() transfer.save(update_fields=['status', 'approver', 'action_date', 'updated_at']) logger.info(f"Transfer request #{transfer.id} canceled by user #{user.id}") return transfer