+
نحوه پرداخت
-
-
-
-
- نقد
- کارت به کارت
- کارت خوان
- نیسیه
- چک
+
+
+ {{ factorO.PayType }}
-
-
-
سررسید
+
-
-
توضیحات فاکتور فاکتور
+
-
- {% if perms.admin_panel.DelFactor %}
-
حذف فاکتور و اصلاح انبار
+
+ {% if factorO.is_active and factorO.pay_status != 'cancell' %}
+ {% if perms.admin_panel.DelFactor %}
+
+ ابطال فاکتور و صدور اسناد اصلاحی
+
+ {% endif %}
+ {% else %}
+
+ این فاکتور باطل شده است. (اسناد مالی و انبار معکوس شدهاند)
+
{% endif %}
-
+
+
+
+
+
+
+
+
+ قانون حذف فیزیکی: در سیستمهای مالی چیزی حذف نمیشود؛ تایید این فرم مستقیماً اسناد معکوس در دفتر روزنامه و کاردکس انبار ثبت میکند. لطفاً با دقت انتخاب کنید.
+
+
+
+ {% csrf_token %}
+
+
+
گام اول: ماهیت ابطال
+
+
+
+
+ الف) اشتباه کاربری در ثبت فاکتور
+
+
• اثر انبار: کالا تحت عنوان "اصلاح خطای ثبت" بازمیگردد.
+
• اثر مالی: مبلغ فاکتور معکوس و تمامی پرداختیها لغو میشوند.
+
+
+
+
+
+ ب) برگشت از فروش واقعی (مرجوعی)
+
+
• اثر انبار: کالا رسماً تحت عنوان "برگشت از فروش" ثبت میشود.
+
• اثر مالی: مشتری بستانکار میشود. وضعیت وجه او در گام دوم مشخص میگردد.
+
+
+
+
+
+
+
+
+
گام دوم: وضعیت مبالغ مالی
+
+
+ تلهی حسابداری: اگر پولی از مشتری گرفته نشده (یا عودت داده شده)، گزینه اول را انتخاب کنید تا از بستانکاری کاذب جلوگیری شود.
+
+
+
+
+
+ سند معکوس پرداختی صادر شود (پولی دریافت نشده بود / وجه عودت داده شد)
+
+
+
+
+ پول به عنوان اعتبار در حساب بماند (مشتری وجه را پرداخت کرده و پس نمیگیرد)
+
+
+
+
+
+
+
+ اینجانب تایید میکنم که راهنما را مطالعه کردهام و مسئولیت ثبت این سند اصلاحیِ انبار و مالی را میپذیرم.
+
+
+
+
+
+
+
+
{% endblock %}
\ No newline at end of file
diff --git a/admin_panel/templates/admin_panel/report_inventory_movements.html b/admin_panel/templates/admin_panel/report_inventory_movements.html
new file mode 100644
index 0000000..efed2c3
--- /dev/null
+++ b/admin_panel/templates/admin_panel/report_inventory_movements.html
@@ -0,0 +1,202 @@
+{% extends "./base.html" %}
+{% load custom_tags %}
+
+{% block bodyPage %}
+
+
+
گزارش تفکیکی گردش و خروج کالا از انبار
+
+
+
+
+
نوع تراکنش کالا
+
+
+ -- همه موارد --
+ {% for code, label in movement_choices %}
+
+ {{ label }}
+
+ {% endfor %}
+
+
+
+
+
+
کاربر صادرکننده
+
+
+ -- همه کاربران --
+ {% for u in all_users %}
+
+ {{ u.first_name }} {{ u.last_name }} ({{ u.username }})
+
+ {% endfor %}
+
+
+
+
+
+
انبار
+
+
+ -- همه انبارها --
+ {% for s in all_stores %}
+
+ {{ s.store_name }}
+
+ {% endfor %}
+
+
+
+
+
+ از تاریخ
+
+
+
+
+
+ تا تاریخ
+
+
+
+
+
+
+ اعمال فیلتر
+
+
+
+
+
+
+
+
+ جمع کل خروجی در این گزارش:
+ {{ summary_stats.total_out_qty|default:0 }} عدد
+
+
+
+
+ جمع کل ورودی در این گزارش:
+ {{ summary_stats.total_in_qty|default:0 }} عدد
+
+
+
+
+
+
+
+
+ #
+ کد کالا
+ نام کالا
+ نوع تراکنش
+ خروج (-)
+ ورود (+)
+ موجودی لحظهای
+ مشتری / گیرنده
+ کاربر ثبتکننده
+ انبار
+ تاریخ ثبت
+ توضیحات سند
+
+
+
+ {% for item in page_obj %}
+
+ {{ forloop.counter }}
+ {{ item.Product.id }}
+ {{ item.Product.product_name }}
+
+
+ {{ item.get_movement_type_display }}
+
+
+ {{ item.s_out|default:"-" }}
+ {{ item.s_in|default:"-" }}
+ {{ item.current_stock_at_time }}
+
+ {% if item.factor and item.factor.customer %}
+
+ {{ item.factor.customer.name_family }}
+
+ {% else %}
+ ثبت دستی/داخلی
+ {% endif %}
+
+
+ {% if item.factor and item.factor.user %}
+ {{ item.factor.user.first_name }} {{ item.factor.user.last_name }}
+ {% else %}
+ سیستم / نامشخص
+ {% endif %}
+
+ {{ item.StoreRoom.store_name }}
+ {{ item.createdDT|toJalali }}
+ {{ item.desc }}
+
+ {% empty %}
+
+ هیچ تراکنشی با این مشخصات یافت نشد.
+
+ {% endfor %}
+
+
+
+
+ {% if page_obj.has_other_pages %}
+
+ {% endif %}
+
+
+{% endblock %}
+
+{% block jsEnd %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/admin_panel/urls.py b/admin_panel/urls.py
index f938399..1625511 100644
--- a/admin_panel/urls.py
+++ b/admin_panel/urls.py
@@ -11,6 +11,7 @@ urlpatterns = [
path('p_logout/', views.p_logout, name='p_logout'),
path('bedehi/', views.bedehi, name='bedehi'),
path('report/', views.report, name='report'),
+ path('report/inventory-movements/', views.inventory_movements_report, name='inventory_movements_report'),
path('hesabdari/', views.hesabdari, name='hesabdari'),
path('hesabdari/sarresid_nashode/', views.hesabdari_residnashone, name='hesabdari_residnashone'),
path('hesabdari/check/pass/', views.hesabdari_checkPass, name='hesabdari_check_pass'),
diff --git a/admin_panel/views.py b/admin_panel/views.py
index 7706833..4c08ae2 100644
--- a/admin_panel/views.py
+++ b/admin_panel/views.py
@@ -8,6 +8,7 @@ from django.utils import timezone # جدید
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
from django.core.exceptions import 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 جدید
from django.db import transaction, connection
from django.db.models import Count, Sum, Q
@@ -183,7 +184,83 @@ def report(request):
data = {'nahvePardakhat': nahvePardakhat, 'userHa':userHa}
return render(request, 'admin_panel/report.html', data)
+@login_required(login_url='/panel/p_login/')
+@permission_required('admin_panel.ViewReportMali', raise_exception=True)
+def inventory_movements_report(request):
+ """
+ گزارش جامع گردش و خروج کالا از انبار به تفکیک نوع تراکنش (گارانتی، اشانتیون، فروش و...)،
+ کاربر ثبتکننده، انبار و بازه زمانی شمسی.
+ """
+ # واکشی اطلاعات اولیه برای 드롭داونهای فیلتر در قالب
+ all_users = User.objects.all().order_by('first_name')
+ all_stores = StoreRoom.objects.all()
+ movement_choices = ProductInStore.MovementType.choices
+ # دریافت پارامترهای فیلتر از فرانتاند
+ selected_type = request.GET.get('movement_type', '')
+ selected_user = request.GET.get('user_id', '')
+ selected_store = request.GET.get('store_id', '')
+ az_tarikh = request.GET.get('azTarikh', '')
+ ta_tarikh = request.GET.get('taTarikh', '')
+
+ # ۱. کوئری پایه: اعمال select_related جهت جلوگیری از فاجعه N+1 در پایگاه داده
+ movements_list = ProductInStore.objects.select_related(
+ 'Product', 'StoreRoom', 'factor__customer', 'factor__user'
+ ).filter(year=1400).order_by('-createdDT')
+
+ # ۲. اعمال فیلترهای پویای کاربر
+ if selected_type:
+ movements_list = movements_list.filter(movement_type=selected_type)
+
+ if selected_user:
+ # فیلتر بر اساس کاربری که فاکتور خروج کالا را در سیستم ثبت کرده است
+ movements_list = movements_list.filter(factor__user__id=_safe_int(selected_user))
+
+ if selected_store:
+ movements_list = movements_list.filter(StoreRoom__id=_safe_int(selected_store))
+
+ # ۳. پردازش بازه زمانی (تبدیل تایماستمپ جاوااسکریپت به تاریخ میلادی)
+ if az_tarikh:
+ try:
+ az_dt = datetime.datetime.fromtimestamp(int(az_tarikh) / 1000.0)
+ movements_list = movements_list.filter(createdDT__gte=az_dt.strftime('%Y-%m-%d 00:00:00'))
+ except (ValueError, TypeError):
+ pass
+
+ if ta_tarikh:
+ try:
+ ta_dt = datetime.datetime.fromtimestamp(int(ta_tarikh) / 1000.0)
+ movements_list = movements_list.filter(createdDT__lte=ta_dt.strftime('%Y-%m-%d 23:59:59'))
+ except (ValueError, TypeError):
+ pass
+
+ # ۴. محاسبه آماری سریع روی نتایج فیلتر شده (بدون نیاز به پایش دستی در پایتون)
+ summary_stats = movements_list.aggregate(
+ total_out_qty=Sum('s_out'),
+ total_in_qty=Sum('s_in')
+ )
+
+ # ۵. صفحهبندی نتایج (۵۰ رکورد در هر صفحه جهت حفظ سرعت لود مرورگر)
+ paginator = Paginator(movements_list, 50)
+ page_number = request.GET.get('page')
+ page_obj = paginator.get_page(page_number)
+
+ context = {
+ 'page_obj': page_obj,
+ 'all_users': all_users,
+ 'all_stores': all_stores,
+ 'movement_choices': movement_choices,
+ 'summary_stats': summary_stats,
+ 'filters': {
+ 'movement_type': selected_type,
+ 'user_id': _safe_int(selected_user) if selected_user else '',
+ 'store_id': _safe_int(selected_store) if selected_store else '',
+ 'azTarikh': az_tarikh,
+ 'taTarikh': ta_tarikh,
+ }
+ }
+
+ return render(request, 'admin_panel/report_inventory_movements.html', context)
@login_required(login_url='/panel/p_login/')
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
@@ -1215,13 +1292,105 @@ def newFactor2(request, c_id):
@login_required(login_url='/panel/p_login/')
@permission_required('admin_panel.DelFactor', raise_exception=True)
-def FactorDel(request, c_id ,f_id):
+@require_POST
+def FactorDel(request, c_id, f_id):
+ customer_obj = get_object_or_404(Customer, id=c_id)
+ factor_initial = get_object_or_404(Factor, id=f_id, customer=customer_obj)
+
+ if factor_initial.pay_status == 'cancell' or not factor_initial.is_active:
+ return HttpResponseBadRequest("این فاکتور قبلاً باطل یا برگشت داده شده است.")
+
+ user_fullname = f"{request.user.first_name} {request.user.last_name}".strip() or request.user.username
+ cancel_reason = request.POST.get('cancel_reason', 'error')
+ payment_action = request.POST.get('payment_action', 'keep')
+
+ if cancel_reason == 'return':
+ reason_text = "برگشت از فروش (مرجوعی مشتری)"
+ inv_movement = ProductInStore.MovementType.RETURN
+ else:
+ reason_text = "اصلاح خطای کاربری"
+ inv_movement = ProductInStore.MovementType.ERROR_CORRECTION
+
try:
- #ProductInStore.objects.filter(factor__id= f_id).delete()
- Factor.objects.get(id=f_id).delete()
- return redirect('/panel/customer/'+str(c_id)+'/factors/')
- except:
- return HttpResponse("خطا در حذف فاکتور")
+ with transaction.atomic():
+ if connection.features.has_select_for_update:
+ factor = Factor.objects.select_for_update().get(pk=factor_initial.pk)
+ else:
+ factor = Factor.objects.get(pk=factor_initial.pk)
+
+ if factor.pay_status == 'cancell' or not factor.is_active:
+ return HttpResponseBadRequest("خطای همزمانی: این فاکتور در لحظه فرستادن درخواست باطل شده بود.")
+
+ factor.is_active = False
+ factor.pay_status = 'cancell'
+ factor.save(update_fields=['is_active', 'pay_status'])
+
+ # واکشی اسناد پرداخت قبل از ایجاد سند جدید (جلوگیری از باگ تکرار سند)
+ payments_to_reverse = []
+ if payment_action == 'reverse' or cancel_reason == 'error':
+ payments_to_reverse = list(Article.objects.filter(
+ Q(debit=0) | Q(debit__isnull=True),
+ factor=factor,
+ credit__gt=0
+ ))
+
+ # ۱. صدور سند معکوس مالی برای کل مبلغ فاکتور
+ factor_amount = int(factor.amount or 0)
+ if factor_amount > 0:
+ Article.objects.create(
+ debit=0,
+ credit=factor_amount,
+ customer=customer_obj,
+ factor=factor,
+ type=factor.PayType,
+ desc=f"برگشت از فروش و ابطال فاکتور شماره {factor.id} (مرجوعی کالا توسط مشتری) - کاربر: {user_fullname}" if cancel_reason == 'return' else f"اصلاح خطای سیستم: ابطال فاکتور اشتباه شماره {factor.id} (بدون جابجایی واقعی کالا) - کاربر: {user_fullname}",
+ user=request.user,
+ payStatus="true"
+ )
+
+ # ۲. خنثی کردن پرداختهای اولیه فاکتور
+ for p in payments_to_reverse:
+ Article.objects.create(
+ debit=p.credit,
+ credit=0,
+ customer=customer_obj,
+ factor=factor,
+ type=p.type,
+ nahvePardakht=p.nahvePardakht,
+ desc=f"عودت وجه پرداخت شده / ابطال رسید فاکتور {factor.id} (به دلیل مرجوعی کالا) - کاربر: {user_fullname}" if cancel_reason == 'return' else f"اصلاح خطای سیستم: ابطال رسید پرداخت فاکتور اشتباه شماره {factor.id} - کاربر: {user_fullname}",
+ user=request.user,
+ payStatus="true"
+ )
+
+ # ۳. 🛠️ اصلاح انبار (حل باگ عدم تغییر موجودی سیستم)
+ stock_movements = list(ProductInStore.objects.filter(factor=factor, s_out__gt=0))
+
+ for movement in stock_movements:
+ pInStore = ProductInStore(
+ StoreRoom=movement.StoreRoom,
+ Product=movement.Product,
+ factor=factor,
+ s_in=movement.s_out, # برگرداندن تعداد کسر شده به ستون ورودی
+ s_out=0,
+ year=movement.year, # 💡 فیکس قطعی: استفاده دقیق از سال مالی تراکنش اصلی (۱۴۰۰) جهت همخوانی با توابع سیستم
+ desc=f"ورود به انبار بابت برگشت از فروش کالا (فاکتور {factor.id}) - کاربر: {user_fullname}" if cancel_reason == 'return' else f"ورود به انبار بابت اصلاح خطای ثبت فاکتور {factor.id} - کاربر: {user_fullname}",
+ movement_type=inv_movement
+ )
+ pInStore.save() # فراخوانی متد save مدل شما که محاسبات تاریخچه و موجودی لحظهای را انجام میدهد
+
+ # ۴. ابطال اقساط و سررسیدهای پاس نشده
+ Sarresid.objects.filter(factor=factor, payStatus="false").update(
+ payStatus="cancell",
+ desc=f"ابطال بابت {reason_text} فاکتور {factor.id}"
+ )
+
+ logger.info(f"Factor {factor.id} successfully cancelled. Reason: {cancel_reason}, Payment: {payment_action}")
+
+ except Exception as e:
+ logger.error(f"Critical error in FactorDel for factor {f_id}: {str(e)}", exc_info=True)
+ return HttpResponse("خطای سیستمی در فرآیند ابطال و صدور اسناد اصلاحی رخ داد.", status=500)
+
+ return redirect('admin_panel:customerFactors', c_id=c_id)
@login_required(login_url='/panel/p_login/')
@permission_required('admin_panel.DelCustomer', raise_exception=True)