store transfer and panel anbar
This commit is contained in:
1 parent
bd775d6249
commit
c5e9b1d18a
13 files changed
+2968
-190
No files matched your search
@@ -0,0 +1,207 @@
|
||||
# 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
|
||||
Reference in new issue
Block a user