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
+32
-1
@@ -18,4 +18,35 @@ admin.site.register(Article)
|
||||
admin.site.register(Sarresid)
|
||||
admin.site.register(Action)
|
||||
admin.site.register(CacheTable)
|
||||
admin.site.register(ProductStockHistory)
|
||||
admin.site.register(ProductStockHistory)
|
||||
|
||||
|
||||
@admin.register(StoreTransferRequest)
|
||||
class StoreTransferRequestAdmin(admin.ModelAdmin):
|
||||
# ۱. حل مشکل لودینگ و هنگ کردن فرم (تبدیل منوی کشویی غولآسا به باکس شناسه عددی)
|
||||
# این دستور فیلدهای پرجمعیت را به شدت بهینه و سبک میکند
|
||||
raw_id_fields = ['out_transaction', 'in_transaction', 'product', 'requester', 'approver']
|
||||
|
||||
# ۲. ستونهایی که میخواهید در صفحه جدول (Changelist) نمایش داده شوند
|
||||
list_display = [
|
||||
'id',
|
||||
'source_store',
|
||||
'target_store',
|
||||
'product',
|
||||
'quantity',
|
||||
'status',
|
||||
'created_at',
|
||||
'action_date'
|
||||
]
|
||||
|
||||
# ۳. فیکس قطعی باگ N+1 (لود کردن روابط در یک کوئری واحد به جای صدها کوئری تودرتو برای __str__)
|
||||
list_select_related = ['source_store', 'target_store', 'product', 'requester', 'approver']
|
||||
|
||||
# ۴. فیلترهای سمت راست صفحه (فقط روی فیلدهای کمجمعیت مثل وضعیت و انبار فیلتر بگذارید)
|
||||
list_filter = ['status', 'source_store', 'target_store', 'created_at']
|
||||
|
||||
# ۵. باکس جستجوی بهینه شده
|
||||
search_fields = ['id', 'desc', 'product__product_name', 'requester__username']
|
||||
|
||||
# ۶. غیرقابل ادیت کردن فیلدهای حساس مالی در پنل ادمین (اختیاری اما برای Enterprise الزامی است)
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.2.25 on 2026-07-03 10:14
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('admin_panel', '0047_alter_productinstore_movement_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='productinstore',
|
||||
options={'permissions': [('ViewProductInStore', 'ViewProductInStore'), ('EditProductInStore', 'EditProductInStore')]},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.2.25 on 2026-07-03 10:38
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('admin_panel', '0048_alter_productinstore_options'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='productinstore',
|
||||
options={'permissions': [('ViewProductInStore', 'ViewProductInStore'), ('EditProductInStore', 'EditProductInStore'), ('OnlyAnbar', 'OnlyAnbar')]},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,41 @@
|
||||
# Generated by Django 3.2.25 on 2026-07-03 12:24
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('admin_panel', '0049_alter_productinstore_options'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='StoreTransferRequest',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('quantity', models.PositiveIntegerField(verbose_name='تعداد انتقال')),
|
||||
('status', models.CharField(choices=[('PENDING', 'در انتظار تایید انبار مقصد'), ('APPROVED', 'تایید شده و منتقل شده'), ('REJECTED', 'رد شده توسط انبار مقصد'), ('CANCELED', 'انصراف توسط مبدا')], db_index=True, default='PENDING', max_length=15, verbose_name='وضعیت')),
|
||||
('desc', models.CharField(blank=True, max_length=500, null=True, verbose_name='توضیحات درخواست')),
|
||||
('rejection_reason', models.CharField(blank=True, max_length=500, null=True, verbose_name='دلیل رد درخواست')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='زمان ثبت درخواست')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='زمان آخرین به\u200cروزرسانی')),
|
||||
('action_date', models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='زمان تعیین تکلیف')),
|
||||
('approver', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='processed_transfers', to=settings.AUTH_USER_MODEL, verbose_name='کاربر تعیین\u200cتکلیف\u200cکننده')),
|
||||
('in_transaction', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transfer_in_ref', to='admin_panel.productinstore', verbose_name='سند ورود انبار مقصد')),
|
||||
('out_transaction', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transfer_out_ref', to='admin_panel.productinstore', verbose_name='سند خروج انبار مبدا')),
|
||||
('product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='admin_panel.product', verbose_name='کالا')),
|
||||
('requester', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='requested_transfers', to=settings.AUTH_USER_MODEL, verbose_name='کاربر درخواست\u200cدهنده')),
|
||||
('source_store', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='outgoing_transfers', to='admin_panel.storeroom', verbose_name='انبار مبدا')),
|
||||
('target_store', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='incoming_transfers', to='admin_panel.storeroom', verbose_name='انبار مقصد')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'درخواست انتقال بین انبار',
|
||||
'verbose_name_plural': 'درخواست\u200cهای انتقال بین انبار',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
]
|
||||
+93
-1
@@ -1,3 +1,4 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import Sum
|
||||
# from django.db.models.signals import post_save, post_delete
|
||||
@@ -146,7 +147,12 @@ class Factor(models.Model):
|
||||
self.save()
|
||||
|
||||
class ProductInStore(models.Model):
|
||||
|
||||
class Meta:
|
||||
permissions = [
|
||||
("ViewProductInStore", "ViewProductInStore"),
|
||||
("EditProductInStore", "EditProductInStore"),
|
||||
("OnlyAnbar", "OnlyAnbar"),
|
||||
]
|
||||
class MovementType(models.TextChoices):
|
||||
SALE = 'SALE', 'فروش عادی'
|
||||
WARRANTY = 'WARRANTY', 'گارانتی'
|
||||
@@ -273,6 +279,92 @@ class ProductStockHistory(models.Model):
|
||||
def __str__(self):
|
||||
return f"{self.product.product_name} in {self.store_room.store_name} - Stock: {self.current_stock}"
|
||||
|
||||
|
||||
class StoreTransferRequest(models.Model):
|
||||
class Status(models.TextChoices):
|
||||
PENDING = 'PENDING', 'در انتظار تایید انبار مقصد'
|
||||
APPROVED = 'APPROVED', 'تایید شده و منتقل شده'
|
||||
REJECTED = 'REJECTED', 'رد شده توسط انبار مقصد'
|
||||
CANCELED = 'CANCELED', 'انصراف توسط مبدا'
|
||||
|
||||
source_store = models.ForeignKey(
|
||||
'StoreRoom',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='outgoing_transfers',
|
||||
verbose_name="انبار مبدا"
|
||||
)
|
||||
target_store = models.ForeignKey(
|
||||
'StoreRoom',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='incoming_transfers',
|
||||
verbose_name="انبار مقصد"
|
||||
)
|
||||
product = models.ForeignKey(
|
||||
'Product',
|
||||
on_delete=models.PROTECT,
|
||||
verbose_name="کالا"
|
||||
)
|
||||
quantity = models.PositiveIntegerField(verbose_name="تعداد انتقال")
|
||||
|
||||
status = models.CharField(
|
||||
max_length=15,
|
||||
choices=Status.choices,
|
||||
default=Status.PENDING,
|
||||
db_index=True,
|
||||
verbose_name="وضعیت"
|
||||
)
|
||||
desc = models.CharField(max_length=500, blank=True, null=True, verbose_name="توضیحات درخواست")
|
||||
rejection_reason = models.CharField(max_length=500, blank=True, null=True, verbose_name="دلیل رد درخواست")
|
||||
|
||||
requester = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='requested_transfers',
|
||||
verbose_name="کاربر درخواستدهنده"
|
||||
)
|
||||
approver = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='processed_transfers',
|
||||
verbose_name="کاربر تعیینتکلیفکننده"
|
||||
)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True, db_index=True, verbose_name="زمان ثبت درخواست")
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="زمان آخرین بهروزرسانی")
|
||||
action_date = models.DateTimeField(null=True, blank=True, db_index=True, verbose_name="زمان تعیین تکلیف")
|
||||
|
||||
out_transaction = models.ForeignKey(
|
||||
'ProductInStore',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='transfer_out_ref',
|
||||
verbose_name="سند خروج انبار مبدا"
|
||||
)
|
||||
in_transaction = models.ForeignKey(
|
||||
'ProductInStore',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='transfer_in_ref',
|
||||
verbose_name="سند ورود انبار مقصد"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"[{self.get_status_display()}] {self.quantity} عدد {self.product.product_name} از {self.source_store.store_name} به {self.target_store.store_name}"
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if self.source_store_id == self.target_store_id:
|
||||
raise ValidationError("انبار مبدا و مقصد نمیتوانند یکسان باشند.")
|
||||
if self.quantity and self.quantity <= 0:
|
||||
raise ValidationError("تعداد انتقال باید بیشتر از صفر باشد.")
|
||||
|
||||
class FactorIteam(models.Model): # پیشنهاد: در آینده املای نام این کلاس را به FactorItem اصلاح کنید
|
||||
factor = models.ForeignKey(Factor, on_delete=models.CASCADE)
|
||||
product = models.ForeignKey(Product, on_delete=models.CASCADE, null=True, blank=True)
|
||||
|
||||
@@ -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
|
||||
@@ -47,7 +47,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if not perms.admin_panel.ViewStore %}
|
||||
{% if not perms.admin_panel.ViewStore %}
|
||||
<a href="{% url 'admin_panel:ActionListUserToday' request.user.id %}" class="navbar-item">
|
||||
اصلی
|
||||
</a>
|
||||
@@ -63,7 +63,6 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<a href="{% url 'admin_panel:stores' %}" class="navbar-item">
|
||||
انبار
|
||||
</a>
|
||||
|
||||
@@ -96,16 +96,28 @@
|
||||
|
||||
|
||||
<div id="storeData" class="column is-vcentered is-centered pt-0 is-12-mobile is-12-desktop">
|
||||
<div class="tabs is-centered m-0">
|
||||
<ul>
|
||||
{% with url_name=request.resolver_match.url_name %}
|
||||
<li class="{% if url_name == 'store' %}is-active{% endif %}"><a href="{% url 'admin_panel:store' s_id %}">موجودی</a></li>
|
||||
|
||||
<li class="{% if url_name == 'operations' %}is-active{% endif %}"><a href="{% url 'admin_panel:operations' s_id %}">دفترعملیات</a></li>
|
||||
|
||||
{% endwith %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tabs is-centered m-0">
|
||||
<ul>
|
||||
{% with url_name=request.resolver_match.url_name %}
|
||||
<li class="{% if url_name == 'store' %}is-active{% endif %}">
|
||||
<a href="{% url 'admin_panel:store' s_id %}">موجودی</a>
|
||||
</li>
|
||||
|
||||
<li class="{% if url_name == 'operations' %}is-active{% endif %}">
|
||||
<a href="{% url 'admin_panel:operations' s_id %}">دفترعملیات</a>
|
||||
</li>
|
||||
|
||||
<li class="{% if url_name == 'store_transfers' %}is-active{% endif %}">
|
||||
<a href="{% url 'admin_panel:store_transfers' s_id %}">
|
||||
انتقالهای انبار
|
||||
{% if storeO.incoming_transfers.all|length > 0 %}
|
||||
<span class="tag is-danger is-rounded is-small ml-1">!</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endwith %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% block storeBody %}
|
||||
|
||||
|
||||
@@ -3,86 +3,103 @@
|
||||
{% load static %}
|
||||
|
||||
{% block storeBody %}
|
||||
<p class="has-text-centered mt-2 mb-2 is-size-5 ">{{ storeO.store_name }}</p>
|
||||
<p class="has-text-centered mt-2 mb-2 is-size-5">{{ storeO.store_name }}</p>
|
||||
|
||||
<form class="columns m-0 is-multiline is-mobile" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="field column is-6-desktop is-6-mobile">
|
||||
<div class="control ">
|
||||
<input id="searchQS" value="{{ djsearchQS }}" name="searchQS" class="input is-fullwidth" type="text" placeholder="نام محصول">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field column is-3-desktop is-6-mobile">
|
||||
<div class="control ">
|
||||
<div class="select is-fullwidth">
|
||||
<select id="mojodi" name="mojodi">
|
||||
<option value="">وضعیت موجودی</option>
|
||||
<option value="0">عدم موجودی</option>
|
||||
<option value="1">نقطه سفارش</option>
|
||||
</select>
|
||||
<div class="field column is-6-desktop is-6-mobile">
|
||||
<div class="control">
|
||||
<input id="searchQS" value="{{ djsearchQS }}" name="searchQS" class="input is-fullwidth" type="text" placeholder="نام محصول">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field column is-3-desktop is-12-mobile">
|
||||
<div class="control ">
|
||||
<input type="submit" class="button is-link is-fullwidth" value="جستجو"/>
|
||||
<div class="field column is-3-desktop is-6-mobile">
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select id="mojodi" name="mojodi">
|
||||
<option value="">وضعیت موجودی</option>
|
||||
<option value="0">عدم موجودی</option>
|
||||
<option value="1">نقطه سفارش</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
<div class="field column is-3-desktop is-12-mobile">
|
||||
<div class="control">
|
||||
<input type="submit" class="button is-link is-fullwidth" value="جستجو"/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<a class="button is-info mb-3 {% if searchStatus %} {% else %} is-hidden {% endif %}" href="{% url 'admin_panel:store' s_id %}">لغو جستجو و مشاهده همه</a>
|
||||
<a class="button is-info mb-3 {% if searchStatus %}{% else %}is-hidden{% endif %}" href="{% url 'admin_panel:store' s_id %}">لغو جستجو و مشاهده همه</a>
|
||||
|
||||
<div class="table-container">
|
||||
<table class="table m-auto has-text-centered table is-bordered is-striped is-narrow is-fullwidth mojodiTB ">
|
||||
<table class="table m-auto has-text-centered table is-bordered is-striped is-narrow is-fullwidth mojodiTB">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="min-width: 200px;">نام محصول</th>
|
||||
<th>قیمت</th>
|
||||
<th>موجودی</th>
|
||||
<th style="min-width: 100px;">عملیات</th>
|
||||
<th>موجودی فیزیکی</th>
|
||||
<th>در انتظار خروج (رزرو)</th>
|
||||
<th>در انتظار ورود (در راه)</th>
|
||||
<th style="min-width: 100px;">عملیات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th>نام محصول</th>
|
||||
<th>قیمت</th>
|
||||
<th>موجودی</th>
|
||||
<th>موجودی فیزیکی</th>
|
||||
<th>در انتظار خروج (رزرو)</th>
|
||||
<th>در انتظار ورود (در راه)</th>
|
||||
<th>عملیات</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
<tbody>
|
||||
{% for prd in productSum %}
|
||||
<tr prd_id="{{ prd.id }}" class="openPrdEdit" p_cat="{{ prd.ProductCategory.id }}">
|
||||
<td >
|
||||
<a {% if perms.admin_panel.ViewStore %}href="{% url 'admin_panel:operationProduct' s_id prd.id %}"{% endif %}>
|
||||
<span class="product_name">{{ prd.product_name }}</span>
|
||||
</a>
|
||||
<td>
|
||||
<a {% if perms.admin_panel.OnlyAnbar %}href="{% url 'admin_panel:operationProduct' s_id prd.id %}"{% endif %}>
|
||||
<span class="product_name font-weight-bold">{{ prd.product_name }}</span>
|
||||
</a>
|
||||
</td>
|
||||
<td class="product_price">{{ prd.product_price }} </td>
|
||||
<td class="product_mojodi" >{{ prd.mojodi }}</td>
|
||||
|
||||
<td>
|
||||
<td class="product_price">{{ prd.product_price }}</td>
|
||||
|
||||
<td class="product_mojodi has-text-weight-bold is-size-6">
|
||||
{{ prd.mojodi }}
|
||||
</td>
|
||||
|
||||
{% if perms.admin_panel.ViewStore %}
|
||||
<span class="Oedit_prd"><img style="width: 20px; height: 20px;" src="{% static 'admin_panel/icons/edit.png' %}"></span>
|
||||
|
||||
|
||||
<span class="delet_prd"><img style="width: 20px; height: 20px;" src="{% static 'admin_panel/icons/delet.png' %}"></span>
|
||||
<td>
|
||||
{% if prd.pending_out > 0 %}
|
||||
<span class="tag is-warning is-medium" title="ارسال شده به انبار دیگر و منتظر تایید">
|
||||
{{ prd.pending_out }} -
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="has-text-grey-light">0</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
|
||||
<td>
|
||||
{% if prd.pending_in > 0 %}
|
||||
<span class="tag is-info is-medium" title="ارسال شده به این انبار و منتظر تایید شما">
|
||||
{{ prd.pending_in }} +
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="has-text-grey-light">0</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{% if perms.admin_panel.ViewStore or perms.admin_panel.OnlyAnbar %}
|
||||
<span class="Oedit_prd" style="cursor: pointer;"><img style="width: 20px; height: 20px;" src="{% static 'admin_panel/icons/edit.png' %}"></span>
|
||||
<span class="delet_prd" style="cursor: pointer;"><img style="width: 20px; height: 20px;" src="{% static 'admin_panel/icons/delet.png' %}"></span>
|
||||
<a href="{% url 'admin_panel:newOperation' s_id prd.id %}">
|
||||
<span><img style="width: 20px; height: 20px;" src="{% static 'admin_panel/icons/add.png' %}"></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
|
||||
</td>
|
||||
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -97,10 +114,8 @@
|
||||
<button class="delete close_edit" aria-label="close"></button>
|
||||
</header>
|
||||
<form action="{% url 'admin_panel:productEdit' %}" id="formcsEdit" class="has-background-white brad5" method="post">
|
||||
<section class="modal-card-body">
|
||||
|
||||
<section class="modal-card-body">
|
||||
{% csrf_token %}
|
||||
|
||||
<input id="i_pcode" type="hidden" name="prd_code" value="">
|
||||
|
||||
<div id="userDT" class="is-hidden-mobile">
|
||||
@@ -111,11 +126,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="field column is-12-desktop is-12-mobile m-0">
|
||||
<label class="label">دسته کالا</label>
|
||||
<div class="control">
|
||||
|
||||
<div class="select is-fullwidth">
|
||||
<select id="Editcategory" name="category" required>
|
||||
<option value=""></option>
|
||||
@@ -124,7 +137,6 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -134,51 +146,47 @@
|
||||
<input id="Editproduct_price" required value="" name="price" class="input an_name" type="text" placeholder="قیمت">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
<input value="ذخیره" type="submit" class="button is-success">
|
||||
<button class="button close_edit">لغو</button>
|
||||
</footer>
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
<input value="ذخیره" type="submit" class="button is-success">
|
||||
<button type="button" class="button close_edit">لغو</button>
|
||||
</footer>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
{% if not mojodi %}
|
||||
var mojodiVar = '';
|
||||
{% else %}
|
||||
var mojodiVar = {{ mojodi }};
|
||||
{% endif %}
|
||||
var objpname = null;
|
||||
{% if not mojodi %}
|
||||
var mojodiVar = '';
|
||||
{% else %}
|
||||
var mojodiVar = {{ mojodi }};
|
||||
{% endif %}
|
||||
var objpname = null;
|
||||
|
||||
$('#mojodi').val(mojodiVar);
|
||||
|
||||
$('#mojodi').val( mojodiVar );
|
||||
$(".delet_prd").click(function(){
|
||||
var prd_id = $(this).parent().parent().attr('prd_id');
|
||||
if ( confirm("از حذف مطمئن هستید؟") ){
|
||||
window.location.href = '/panel/product/del/'+prd_id+"/"+"{{ s_id }}";
|
||||
if (confirm("از حذف مطمئن هستید؟")) {
|
||||
window.location.href = '/panel/product/del/' + prd_id + "/" + "{{ s_id }}";
|
||||
}
|
||||
});
|
||||
|
||||
$(".close_edit").click(function(){
|
||||
$("#edit_prd").removeClass('is-active');
|
||||
});
|
||||
|
||||
$(".Oedit_prd").click(function(){
|
||||
$("#edit_prd").addClass('is-active');
|
||||
prd_id = $(this).parent().parent().attr("prd_id");
|
||||
p_cat = $(this).parent().parent().attr("p_cat");
|
||||
objpname = $(this).parent().parent().find('.product_name')
|
||||
//alert($(this).parent().parent().find('.product_name'))
|
||||
var prd_id = $(this).parent().parent().attr("prd_id");
|
||||
var p_cat = $(this).parent().parent().attr("p_cat");
|
||||
objpname = $(this).parent().parent().find('.product_name');
|
||||
|
||||
$("#i_pcode").val(prd_id);
|
||||
$("#EditnamePrd").val( $(this).parent().parent().find('.product_name').html() );
|
||||
$("#Editproduct_price").val( $(this).parent().parent().find('.product_price').html() );
|
||||
$("#Editcategory").val( p_cat );
|
||||
$("#EditnamePrd").val($(this).parent().parent().find('.product_name').text().trim());
|
||||
$("#Editproduct_price").val($(this).parent().parent().find('.product_price').text().trim());
|
||||
$("#Editcategory").val(p_cat);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,224 @@
|
||||
{% extends "./baseStore.html" %}
|
||||
{% load custom_tags %}
|
||||
{% load static %}
|
||||
|
||||
{% block storeBody %}
|
||||
<div class="container is-fluid pt-3">
|
||||
|
||||
{% if messages %}
|
||||
<div class="mb-4">
|
||||
{% for message in messages %}
|
||||
<div class="notification {% if message.tags == 'success' %}is-success{% elif message.tags == 'error' %}is-danger{% elif message.tags == 'warning' %}is-warning{% else %}is-info{% endif %} is-light">
|
||||
<button class="delete"></button>
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="box mb-5 border-top-primary">
|
||||
<h3 class="title is-5 has-text-primary mb-3">
|
||||
📥 بارهای ورودی در انتظار تایید شما
|
||||
<span class="tag is-primary is-rounded ml-2">{{ incoming_pending.count }}</span>
|
||||
</h3>
|
||||
|
||||
{% if incoming_pending %}
|
||||
<div class="table-container">
|
||||
<table class="table is-bordered is-striped is-narrow is-fullwidth has-text-centered">
|
||||
<thead class="has-background-light">
|
||||
<tr>
|
||||
<th>کد درخواست</th>
|
||||
<th>کالا</th>
|
||||
<th>تعداد</th>
|
||||
<th>ارسال کننده (انبار مبدا)</th>
|
||||
<th>کاربر ثبتکننده</th>
|
||||
<th>زمان ارسال</th>
|
||||
<th style="min-width: 180px;">اقدام انباردار</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in incoming_pending %}
|
||||
<tr>
|
||||
<td class="has-text-weight-bold">#{{ item.id }}</td>
|
||||
<td class="has-text-weight-bold has-text-link">{{ item.product.product_name }}</td>
|
||||
<td class="is-size-5 has-text-weight-bold has-text-success">{{ item.quantity }}</td>
|
||||
<td>{{ item.source_store.store_name }}</td>
|
||||
<td>{{ item.requester.first_name }} {{ item.requester.last_name }}</td>
|
||||
<td dir="ltr">{{ item.created_at|toJalali }}</td>
|
||||
<td>
|
||||
<div class="buttons is-centered m-0">
|
||||
<form action="{% url 'admin_panel:process_transfer_action' item.id %}" method="post" class="m-0" onsubmit="return confirm('آیا از تایید و ورود این کالا به انبار خود اطمینان دارید؟');">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="approve">
|
||||
<button type="submit" class="button is-success is-small ml-1">✓ تایید و دریافت</button>
|
||||
</form>
|
||||
|
||||
<button type="button" class="button is-danger is-small btn-reject" data-id="{{ item.id }}" data-name="{{ item.product.product_name }}" data-qty="{{ item.quantity }}">
|
||||
✕ رد بار
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="has-text-grey has-text-centered py-4">هیچ بار ورودی جدیدی در انتظار تایید شما نیست.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="box mb-5">
|
||||
<h3 class="title is-5 has-text-info mb-3">
|
||||
📤 بارهای ارسالی شما (در انتظار تایید انبار مقصد)
|
||||
<span class="tag is-info is-rounded ml-2">{{ outgoing_pending.count }}</span>
|
||||
</h3>
|
||||
|
||||
{% if outgoing_pending %}
|
||||
<div class="table-container">
|
||||
<table class="table is-bordered is-striped is-narrow is-fullwidth has-text-centered">
|
||||
<thead class="has-background-light">
|
||||
<tr>
|
||||
<th>کد درخواست</th>
|
||||
<th>کالا</th>
|
||||
<th>تعداد</th>
|
||||
<th>انبار مقصد</th>
|
||||
<th>زمان ارسال</th>
|
||||
<th>توضیحات شما</th>
|
||||
<th>وضعیت</th>
|
||||
<th>عملیات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in outgoing_pending %}
|
||||
<tr>
|
||||
<td>#{{ item.id }}</td>
|
||||
<td class="has-text-weight-bold">{{ item.product.product_name }}</td>
|
||||
<td class="has-text-weight-bold">{{ item.quantity }}</td>
|
||||
<td class="has-text-info has-text-weight-bold">{{ item.target_store.store_name }}</td>
|
||||
<td dir="ltr">{{ item.created_at|toJalali }}</td>
|
||||
<td>{{ item.desc|default:"-" }}</td>
|
||||
<td><span class="tag is-warning">منتظر تحویل</span></td>
|
||||
<td>
|
||||
<form action="{% url 'admin_panel:process_transfer_action' item.id %}" method="post" class="m-0" onsubmit="return confirm('آیا قصد انصراف و بازگرداندن بار از مسیر انتقال را دارید؟');">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="cancel">
|
||||
<button type="submit" class="button is-warning is-small">↩ انصراف از ارسال</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="has-text-grey has-text-centered py-4">هیچ بار ارسالی معلقی ندارید.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h3 class="title is-6 has-text-grey-dark mb-3">🕒 تاریخچه ۵۰ انتقال اخیر انبار</h3>
|
||||
<div class="table-container">
|
||||
<table class="table is-bordered is-narrow is-fullwidth has-text-centered is-size-7">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>کد</th>
|
||||
<th>کالا</th>
|
||||
<th>تعداد</th>
|
||||
<th>مبدا</th>
|
||||
<th>مقصد</th>
|
||||
<th>وضعیت</th>
|
||||
<th>تعیینتکلیف کننده</th>
|
||||
<th>زمان اقدام</th>
|
||||
<th>توضیحات / دلیل رد</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in history_list %}
|
||||
<tr>
|
||||
<td>#{{ item.id }}</td>
|
||||
<td class="has-text-weight-bold">{{ item.product.product_name }}</td>
|
||||
<td>{{ item.quantity }}</td>
|
||||
<td>{% if item.source_store_id == storeO.id %}<span class="tag is-light">انبار شما</span>{% else %}{{ item.source_store.store_name }}{% endif %}</td>
|
||||
<td>{% if item.target_store_id == storeO.id %}<span class="tag is-light">انبار شما</span>{% else %}{{ item.target_store.store_name }}{% endif %}</td>
|
||||
<td>
|
||||
{% if item.status == 'APPROVED' %}
|
||||
<span class="tag is-success is-light">✓ تایید و تحویل شد</span>
|
||||
{% elif item.status == 'REJECTED' %}
|
||||
<span class="tag is-danger is-light">✕ رد شد</span>
|
||||
{% elif item.status == 'CANCELED' %}
|
||||
<span class="tag is-warning is-light">انصراف مبدا</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.approver.first_name }} {{ item.approver.last_name }}</td>
|
||||
<td dir="ltr">{{ item.action_date|toJalali|default:"-" }}</td>
|
||||
<td class="has-text-right">{{ item.rejection_reason|default:item.desc|default:"-" }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="9" class="has-text-grey">هیچ تاریخچهای یافت نشد.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="modal-reject" class="modal">
|
||||
<div class="modal-background close-modal"></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head has-background-danger-light">
|
||||
<p class="modal-card-title has-text-danger-dark is-size-5">⚠️ اعلام دلیل رد دریافت بار</p>
|
||||
<button class="delete close-modal" aria-label="close"></button>
|
||||
</header>
|
||||
<form id="form-reject" action="" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="reject">
|
||||
|
||||
<section class="modal-card-body">
|
||||
<p class="mb-3">شما در حال رد کردن ورود <strong id="modal-qty"></strong> عدد <strong id="modal-prd-name"></strong> به انبار خود هستید.</p>
|
||||
<div class="field">
|
||||
<label class="label">علت رد بار (الزام جهت اطلاع انبار مبدا): <span class="has-text-danger">*</span></label>
|
||||
<div class="control">
|
||||
<textarea name="reason" class="textarea" required placeholder="مثال: مغایرت در تعداد ارسالی / خرابی فیزیکی کالا / ارسال اشتباهی به این انبار..." rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="modal-card-foot">
|
||||
<button type="submit" class="button is-danger">تایید و رد بار</button>
|
||||
<button type="button" class="button close-modal">انصراف</button>
|
||||
</footer>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// بستن پیامهای هشدار
|
||||
$('.notification .delete').click(function() {
|
||||
$(this).parent().fadeOut();
|
||||
});
|
||||
|
||||
// باز کردن مودال رد بار و تنظیم اطلاعات
|
||||
$('.btn-reject').click(function() {
|
||||
var id = $(this).data('id');
|
||||
var name = $(this).data('name');
|
||||
var qty = $(this).data('qty');
|
||||
|
||||
$('#modal-prd-name').text(name);
|
||||
$('#modal-qty').text(qty);
|
||||
// تنظیم اکشن فرم به آدرس مخصوص همین تراکنش
|
||||
$('#form-reject').attr('action', '/panel/transfer/' + id + '/action/');
|
||||
|
||||
$('#modal-reject').addClass('is-active');
|
||||
});
|
||||
|
||||
// بستن مودال
|
||||
$('.close-modal').click(function() {
|
||||
$('#modal-reject').removeClass('is-active');
|
||||
$('#form-reject').find('textarea').val('');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -53,6 +53,9 @@ urlpatterns = [
|
||||
path('store/<int:s_id>/operations/<int:p_id>', views.operationProduct, name='operationProduct'),
|
||||
path('store/<int:s_id>/operation/<int:p_id>/new', views.newOperation, name='newOperation'),
|
||||
path('store/new', views.newStore, name='newStore'),
|
||||
# === مسیرهای مدیریت انتقال بین انبارها ===
|
||||
path('store/<int:s_id>/transfers/', views.store_transfers, name='store_transfers'),
|
||||
path('transfer/<int:transfer_id>/action/', views.process_transfer_action, name='process_transfer_action'),
|
||||
path('product/add', views.productAdd, name='productAdd'),
|
||||
path('product/edit', views.productEdit, name='productEdit'),
|
||||
path('operation', views.operation, name='operation'),
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+253
-96
@@ -4,9 +4,11 @@ from django.urls import reverse # تکراری در کد اصلی شما بود
|
||||
from persiantools.jdatetime import JalaliDate
|
||||
from persiantools import characters, digits
|
||||
import datetime
|
||||
from django.contrib import messages
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.utils import timezone # جدید
|
||||
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
|
||||
from django.core.exceptions import ValidationError # جدید
|
||||
from django.core.exceptions import PermissionDenied, ValidationError # جدید
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest # بخشBadRequest جدید
|
||||
@@ -18,8 +20,9 @@ from django.contrib.auth.decorators import login_required, permission_required
|
||||
import os
|
||||
import json # جدید
|
||||
import logging # جدید
|
||||
|
||||
from .models import *
|
||||
from .services import create_transfer_request, approve_transfer_request, reject_transfer_request, cancel_transfer_request
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -883,6 +886,7 @@ def entezar(request):
|
||||
return render(request, 'admin_panel/entezar.html', {'customers': customerHa, 'searchStatus': searchStatus, 'full_path': full_path, 'searchVal': searchVal, 'searchOn':searchOn})
|
||||
|
||||
@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]
|
||||
@@ -891,29 +895,87 @@ def stores(request):
|
||||
data = {'all_store': all_store}
|
||||
return render(request, 'admin_panel/stores.html', data)
|
||||
|
||||
from django.db.models import Sum, Subquery, OuterRef, IntegerField, Value
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from .models import StoreRoom, ProductCategory, Product, ProductInStore, StoreTransferRequest
|
||||
|
||||
@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 = StoreRoom.objects.get(id=s_id)
|
||||
storeO = get_object_or_404(StoreRoom, id=s_id)
|
||||
searchStatus = False
|
||||
searchQS = request.POST.get('searchQS', '')
|
||||
searchQS = request.POST.get('searchQS', '').strip()
|
||||
mojodi = request.POST.get('mojodi', '')
|
||||
#return HttpResponse(mojodi)
|
||||
if searchQS !='':
|
||||
productSum = Product.objects.filter(productinstore__StoreRoom=storeO ,productinstore__year=1400,product_name__contains=searchQS).annotate(mojodi=( Sum('productinstore__s_in') - Sum('productinstore__s_out') ))
|
||||
if mojodi == "0":
|
||||
productSum = productSum.filter(mojodi__lte=0)
|
||||
|
||||
# ۱. محاسبه موجودی فیزیکی کل کالاها با یک کوئری تکضرب و سریع 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
|
||||
else:
|
||||
productSum = Product.objects.filter(productinstore__StoreRoom=storeO ,productinstore__year=1400).annotate(mojodi=( Sum('productinstore__s_in') - Sum('productinstore__s_out') ))
|
||||
if mojodi == "0":
|
||||
searchStatus = True
|
||||
productSum = productSum.filter(mojodi__lte=0)
|
||||
data = {'storeO': storeO,'productSum': productSum, 's_id': s_id, 'searchStatus': searchStatus, "mojodi": mojodi, 'djsearchQS':searchQS, 'productCategory': productCategory}
|
||||
|
||||
# 🛠️ اصلاح فرآیند ترتیب: استفاده از '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.ViewStore', raise_exception=True)
|
||||
@permission_required('admin_panel.OnlyAnbar', raise_exception=True)
|
||||
def operations(request, s_id):
|
||||
page = request.GET.get('page', 1)
|
||||
productCategory = ProductCategory.objects.all()
|
||||
@@ -942,7 +1004,7 @@ def operations(request, s_id):
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewStore', raise_exception=True)
|
||||
@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)
|
||||
@@ -961,7 +1023,7 @@ def operationProduct(request, s_id, p_id):
|
||||
return render(request, 'admin_panel/operations.html', data)
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewStore', raise_exception=True)
|
||||
@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)
|
||||
@@ -970,6 +1032,92 @@ def newOperation(request, s_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):
|
||||
@@ -981,7 +1129,7 @@ def newStore(request):
|
||||
return redirect('/panel/store/')
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewStore', raise_exception=True)
|
||||
@permission_required('admin_panel.OnlyAnbar', raise_exception=True)
|
||||
def productAdd(request):
|
||||
namePrd = request.POST.get('namePrd', '')
|
||||
category = request.POST.get('category', '')
|
||||
@@ -1031,109 +1179,118 @@ def productDel(request, p_id, s_id):
|
||||
return redirect('/panel/store/'+str(s_id))
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewStore', raise_exception=True)
|
||||
@permission_required('admin_panel.OnlyAnbar', raise_exception=True)
|
||||
def operation(request):
|
||||
if request.method != 'POST':
|
||||
return HttpResponseBadRequest("متد ارسالی نامعتبر است.")
|
||||
|
||||
# استفاده از توابع امن اختصاصی پروژه شما برای جداسازی مقادیر عددی
|
||||
# استفاده از توابع امن اختصاصی پروژه برای جداسازی مقادیر عددی
|
||||
prd_id = _safe_int(request.POST.get('prd_id'))
|
||||
actionType = request.POST.get('actionType', '')
|
||||
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', '')
|
||||
desc = request.POST.get('desc', '').strip()
|
||||
|
||||
if tedad <= 0:
|
||||
return HttpResponseBadRequest("تعداد وارد شده باید بیشتر از صفر باشد.")
|
||||
|
||||
sRoom = get_object_or_404(StoreRoom, id=azanbar)
|
||||
productO = get_object_or_404(Product, id=prd_id)
|
||||
s_room = get_object_or_404(StoreRoom, id=azanbar)
|
||||
product_obj = get_object_or_404(Product, id=prd_id)
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
|
||||
# ساخت کوئری پایه برای دریافت تراکنشهای سال جاری کالا
|
||||
qs = ProductInStore.objects.filter(
|
||||
Product=productO, StoreRoom=sRoom, year=1400
|
||||
)
|
||||
|
||||
# بررسی هوشمند پشتیبانی دیتابیس از قابلیت قفل ردیف (سازگار با SQLite و PostgreSQL)
|
||||
if connection.features.has_select_for_update:
|
||||
qs = qs.select_for_update()
|
||||
|
||||
# بارگذاری امن تراکنشها در حافظه (RAM) جهت جلوگیری از خطای توابع تجمیعی همزمان با قفل دیتابیس
|
||||
locked_records = list(qs)
|
||||
|
||||
# --- عملیات ورود دستی ---
|
||||
if actionType == 'vorod':
|
||||
pInStore = ProductInStore(
|
||||
StoreRoom=sRoom,
|
||||
Product=productO,
|
||||
year=1400
|
||||
)
|
||||
pInStore.s_in = tedad
|
||||
pInStore.desc = "ورود: "+str(tedad)+" به انبار " + str(sRoom.store_name) + "#" + str(desc)
|
||||
pInStore.movement_type = ProductInStore.MovementType.DIRECT_IN
|
||||
pInStore.save()
|
||||
|
||||
# --- عملیات خروج دستی یا انتقال بین انبارها ---
|
||||
elif actionType in ['khoroj', 'transfer']:
|
||||
# =====================================================================
|
||||
# بخش اول: انتقال بین انبارها (واگذاری کامل به لایه سرویس)
|
||||
# =====================================================================
|
||||
if action_type == 'transfer':
|
||||
if not beanbar:
|
||||
return HttpResponse("انبار مقصد مشخص نشده است.")
|
||||
|
||||
# محاسبات دقیق و سریع موجودی کالا در سطح پایتون بدون درگیر کردن دیسک
|
||||
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
|
||||
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))
|
||||
|
||||
if mojodi_prd < tedad:
|
||||
# تطابق ۱۰۰ درصدی پاسخ کدهای قدیمی با فرانتاند شما
|
||||
return HttpResponse("موجودی کافی نیست، موجودی فعلی: "+ str(mojodi_prd))
|
||||
# =====================================================================
|
||||
# بخش دوم: ورود و خروج دستی (کنترل مستقیم با Transaction و Lock)
|
||||
# =====================================================================
|
||||
elif action_type in ['vorod', 'khoroj']:
|
||||
with transaction.atomic():
|
||||
# ساخت کوئری پایه برای دریافت تراکنشهای سال جاری کالا
|
||||
qs = ProductInStore.objects.filter(
|
||||
Product=product_obj, StoreRoom=s_room, year=1400
|
||||
)
|
||||
|
||||
# الف) خروج دستی ساده
|
||||
if actionType == 'khoroj':
|
||||
pInStore = ProductInStore(
|
||||
StoreRoom=sRoom,
|
||||
Product=productO,
|
||||
year=1400
|
||||
)
|
||||
pInStore.s_out = tedad
|
||||
pInStore.desc = "خروج: "+str(tedad)+" از انبار " + str(sRoom.store_name) + "#" + str(desc)
|
||||
pInStore.movement_type = ProductInStore.MovementType.DIRECT_OUT
|
||||
pInStore.save()
|
||||
# قفلگذاری ردیفها برای جلوگیری از Race Condition (در دیتابیسهای پشتیبان)
|
||||
if connection.features.has_select_for_update:
|
||||
qs = qs.select_for_update()
|
||||
|
||||
# ب) انتقال بین دو انبار مجزا
|
||||
elif actionType == 'transfer':
|
||||
if not beanbar:
|
||||
return HttpResponse("انبار مقصد مشخص نشده است.")
|
||||
|
||||
beanbarOjb = get_object_or_404(StoreRoom, id=_safe_int(beanbar))
|
||||
|
||||
# ردیف اول: ثبت تراکنش خروج از انبار مبدأ
|
||||
pInStore = ProductInStore(
|
||||
StoreRoom=sRoom,
|
||||
Product=productO,
|
||||
year=1400
|
||||
)
|
||||
pInStore.s_out = tedad
|
||||
pInStore.desc = "خروج: "+str(tedad)+" از انبار " + str(sRoom.store_name) + " جهت انتقال به انبار " + str(beanbarOjb.store_name) + "#" + str(desc)
|
||||
pInStore.movement_type = ProductInStore.MovementType.TRANSFER
|
||||
pInStore.save()
|
||||
locked_records = list(qs)
|
||||
|
||||
# ردیف دوم: ثبت تراکنش ورود به انبار مقصد (محافظت شده در بلاک اتمیک)
|
||||
# --- الف) عملیات ورود دستی ---
|
||||
if action_type == 'vorod':
|
||||
ProductInStore.objects.create(
|
||||
StoreRoom=beanbarOjb,
|
||||
Product=productO,
|
||||
StoreRoom=s_room,
|
||||
Product=product_obj,
|
||||
s_in=tedad,
|
||||
s_out=0,
|
||||
year=1400,
|
||||
desc="ورود: "+str(tedad)+" از انبار " + str(sRoom.store_name) + "#" + str(desc),
|
||||
movement_type=ProductInStore.MovementType.TRANSFER
|
||||
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"Error in store operation view: {str(e)}", exc_info=True)
|
||||
return HttpResponse("خطای سیستمی رخ داد. تراکنش لغو شد.")
|
||||
logger.error(f"Critical error in store operation view: {str(e)}", exc_info=True)
|
||||
return HttpResponse("خطای سیستمی رخ داد. تراکنش لغو شد.", status=500)
|
||||
|
||||
return redirect('/panel/store/'+str(sRoom.id))
|
||||
return redirect('/panel/store/' + str(s_room.id))
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def newFactor(request, c_id):
|
||||
|
||||
Reference in new issue
Block a user