gozaresh anbar va eslahfactor
This commit is contained in:
1 parent
739104115a
commit
7a305b8e19
7 files changed
+571
-62
No files matched your search
+175
-6
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user