207 lines
9.2 KiB
Python
207 lines
9.2 KiB
Python
# 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 |