diff --git a/admin_panel/views.py b/admin_panel/views.py index 43a84c7..dbc748f 100644 --- a/admin_panel/views.py +++ b/admin_panel/views.py @@ -1,24 +1,63 @@ -from django.shortcuts import redirect, render +from django.shortcuts import redirect, render, get_object_or_404 from django.urls.base import reverse +from django.urls import reverse # تکراری در کد اصلی شما بود، اما جهت عدم تغییر حفظ شد from persiantools.jdatetime import JalaliDate from persiantools import characters, digits import datetime +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 -# Create your views here. -from django.http import HttpResponse -from django.urls import reverse -from django.http import Http404, HttpResponseRedirect -from django.db.models import Count, Sum +from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest # بخشBadRequest جدید +from django.db import transaction # جدید +from django.db.models import Count, Sum, Q from django.contrib.auth.models import Permission, User from django.contrib.auth import authenticate, login, logout from django.contrib.auth.decorators import login_required, permission_required import os -from django.shortcuts import get_object_or_404 -from django.db.models import Q +import json # جدید +import logging # جدید from .models import * +logger = logging.getLogger(__name__) + +class InventoryException(Exception): + """خطای اختصاصی برای کنترل موجودی انبار""" + pass + +def _safe_int(value, default=0): + """تابع امن برای تبدیل مقادیر دریافتی از جاوااسکریپت به عدد""" + if value is None: + return default + try: + clean_val = str(value).strip() + return int(clean_val) if clean_val else default + except (ValueError, TypeError): + return default + +def _parse_jalali_date(date_str): + """تبدیل امن تاریخ ارسالی از pDatepicker به میلادی""" + if not date_str or not str(date_str).strip(): + return None + try: + clean_date = digits.fa_to_en(digits.ar_to_fa(str(date_str).strip())) + y, m, d = map(int, clean_date.split("/")) + return JalaliDate(y, m, d).to_gregorian() + except Exception as e: + logger.warning(f"Error parsing Jalali date '{date_str}': {e}") + return None + +def _parse_timestamp(ts_value): + """تبدیل یونیکس تایم‌استمپ‌های ارسالی از جاوااسکریپت""" + if not ts_value: + return None + try: + # تبدیل میلی‌ثانیه جاوااسکریپت به ثانیه پایتون + return datetime.datetime.fromtimestamp(int(ts_value) / 1000.0, tz=timezone.get_current_timezone()) + except (ValueError, TypeError): + return None + def p_login(request): username = request.POST.get('username', None ) password = request.POST.get('password', None ) @@ -915,233 +954,191 @@ def newFactor(request, c_id): @login_required(login_url='/panel/p_login/') def newFactor2(request, c_id): - import json - prdsList = request.POST.get('prdsList', '') - sarResidDate = request.POST.get('sarResidDate', None) + if request.method != 'POST': + return HttpResponseBadRequest("متد ارسالی باید POST باشد.") + user_op = request.user + # ۱. پارس داده‌های JSON ارسال شده از فرم (پشتیبانی کامل از ساختار paylist و orderList جاوااسکریپت) try: - sarResidDate = digits.fa_to_en(digits.ar_to_fa(sarResidDate)) - #return HttpResponse(sarResidDate) - sarResidDate = sarResidDate.split("/") - #return HttpResponse(sarResidDate[2]) - sarResidDate = JalaliDate(int(sarResidDate[0]), int(sarResidDate[1]), int(sarResidDate[2])).to_gregorian() - #return HttpResponse(sarResidDate) - except: - sarResidDate = None + prds_list = json.loads(request.POST.get('prdsList') or '[]') + json_pardakht_ha = json.loads(request.POST.get('PardakhtHa') or '{}') + except json.JSONDecodeError: + return HttpResponseBadRequest("فرمت داده‌های ارسالی JSON نامعتبر است.") - #return HttpResponse(sarResidDate[1]) - #sarResidDate = JalaliDate(sarResidDate[0], sarResidDate[1], sarResidDate[2]).to_gregorian() + factor_type = request.POST.get('FactorType', '') + factor_desc = request.POST.get('FactorDesc', '') + sar_resid_date = _parse_jalali_date(request.POST.get('sarResidDate')) - FactorDate = request.POST.get('FactorDate', '') - FactorType = request.POST.get('FactorType', '') - FactorDesc = request.POST.get('FactorDesc', '') - PardakhtHa = request.POST.get('PardakhtHa', '') - jsonPardakhtHa = json.loads(PardakhtHa) + # ۲. واکشی اطلاعات پایه‌ای + customer_obj = get_object_or_404(Customer, id=c_id) + store_room = get_object_or_404(StoreRoom, user=user_op) - #return HttpResponse(PardakhtHa) + # ۳. واکشی دسته‌جمعی محصولات برای جلوگیری از فشار به دیتابیس + product_ids = [item.get('prd_id') for item in prds_list if item.get('prd_id') and item.get('prd_id') != 9999] + products_by_id = {p.id: p for p in Product.objects.filter(id__in=product_ids)} - #print(datetime.datetime.fromtimestamp(int(jsonPardakhtHa['l_checkPriceSarResid'])/1000.0)) - #return HttpResponse(type(jsonPardakhtHa['l_nesiyeDateTime'])) - #for pardakhti in jsonPardakhtHa: - #if(jsonPardakhtHa[pardakhti] != None): - #print(str(pardakhti)+" --> " +str(jsonPardakhtHa[pardakhti])) + # ۴. محاسبه مبلغ کل با نادیده گرفتن محصولاتی که گارانتی/اشانتیون هستند + sum_price = sum( + _safe_int(item.get('prd_count')) * _safe_int(item.get('price')) + for item in prds_list + if not str(item.get('garanty_Esh', '')).strip() + ) - #return HttpResponse(PardakhtHa) - - Factorfail = False - customerObj=Customer.objects.get(id=c_id) - storeUser = StoreRoom.objects.get(user__id=request.user.id) - - prdsList = json.loads(prdsList) - sum_price = 0 - for prd in prdsList: - if prd['garanty_Esh'] == ' ' or prd['garanty_Esh'] == '' or prd['garanty_Esh'] == None: - sum_price += prd['prd_count'] * prd['price'] - - #return HttpResponse(prd_code) try: - newFactor = Factor() - newFactor.customer= customerObj - newFactor.amount = sum_price + # ۵. باز کردن یک تراکنش امن دیتابیس + with transaction.atomic(): + + # ثبت آبجکت فاکتور + new_factor = Factor.objects.create( + customer=customer_obj, + amount=sum_price, + FactorType=factor_type, + desc=factor_desc, + deadline=sar_resid_date, + user=user_op + ) - newFactor.FactorType = FactorType - - newFactor.desc = FactorDesc + # ثبت بدهکاری کلی در سند حسابداری + Article.objects.create( + debit=sum_price, + credit=0, + customer=customer_obj, + factor=new_factor, + desc=f"{customer_obj.name_family} فاکتور شماره: {new_factor.id}", + user=user_op + ) - if sarResidDate != None: - newFactor.deadline = sarResidDate - newFactor.user = User.objects.get(id=request.user.id) - newFactor.save() - except: - try: - Factorfail=True - newFactor.delete() - except: - pass + # ثبت ریزپرداختی‌ها بر اساس دیکشنری paylist جاوااسکریپت + payment_map = { + 'l_naghd': 'نقد', + 'l_kartbekart': 'کارت به کارت', + 'l_kartKhan': 'کارت خوان', + 'l_takhfif': 'تخفیف' + } - #ثبت مبالغ پرداختی - if Factorfail != True: - - #مبلغ کلی فاکتور وارد می شود - is_naghd = False - '''articleHesabDari = Article() - articleHesabDari.debit = newFactor.amount - articleHesabDari.credit = 0 - articleHesabDari.customer = customerObj - articleHesabDari.factor = newFactor + for key, amount_raw in json_pardakht_ha.items(): + amount = _safe_int(amount_raw) + if amount <= 0: + continue - articleHesabDari.desc = str(FactorType) + " " + str(customerObj.name_family) + " طی فاکتور شماره: " + str(newFactor.id) - articleHesabDari.save()''' - #ابتدا مانده حساب بستانکار قبل گرفته میشود از فاکتور کم بشه - debit = Article.objects.filter(customer=customerObj).aggregate(Sum('debit'))['debit__sum'] - credit = Article.objects.filter(customer=customerObj).aggregate(Sum('credit'))['credit__sum'] - - mandehesabCs = 0 - try: - if (debit - credit) < 0: - mandehesabCs = abs(debit - credit) - except: - pass - - #کل مبلغ فاکتور بدهکار میشه اول - # اگر مشتری بستانکار بود ازش کم میشه - articleHesabDari2 = Article() - articleHesabDari2.debit = (sum_price) - articleHesabDari2.credit = 0 - articleHesabDari2.customer = customerObj - articleHesabDari2.factor = newFactor - strDes = str(customerObj.name_family) + " فاکتور شماره: " + str(newFactor.id) + "" - #if mandehesabCs > 1: - # strDes += " مبلغ کل فاکتور" +str(sum_price)+ " - " + str(mandehesabCs)+" بستانکار از قبل " - articleHesabDari2.desc = strDes - articleHesabDari2.user = user_op - articleHesabDari2.save() - - #مبالغ پرداختی وارد می شود - for pardakhti in jsonPardakhtHa: - - if(jsonPardakhtHa[pardakhti] != None): - #بستانکارها - if str(pardakhti) in ['l_naghd', 'l_kartbekart', 'l_kartKhan']: - nahvePardakht = '' - if str(pardakhti) == 'l_naghd': - nahvePardakht = 'نقد' - elif str(pardakhti) == 'l_kartbekart': - nahvePardakht = 'کارت به کارت' - elif str(pardakhti) == 'l_kartKhan': - nahvePardakht = 'کارت خوان' - elif str(pardakhti) == 'l_takhfif': - nahvePardakht = 'تخفیف' - - articleHesabDari2 = Article() - articleHesabDari2.debit = 0 - articleHesabDari2.credit = int(jsonPardakhtHa[pardakhti]) - articleHesabDari2.customer = customerObj - articleHesabDari2.factor = newFactor - articleHesabDari2.desc = nahvePardakht + " " + str(customerObj.name_family) + " فاکتور شماره: " + str(newFactor.id) - articleHesabDari2.nahvePardakht = nahvePardakht - articleHesabDari2.payStatus = "true" - articleHesabDari2.user = user_op - articleHesabDari2.save() - - elif str(pardakhti) in ['l_takhfif']: - if str(pardakhti) == 'l_takhfif': - nahvePardakht = 'تخفیف' - - articleHesabDari2 = Article() - articleHesabDari2.debit = 0 - articleHesabDari2.credit = int(jsonPardakhtHa[pardakhti]) - articleHesabDari2.customer = customerObj - articleHesabDari2.factor = newFactor - articleHesabDari2.desc = nahvePardakht + " " + str(customerObj.name_family) + " فاکتور شماره: " + str(newFactor.id) - articleHesabDari2.nahvePardakht = nahvePardakht - articleHesabDari2.payStatus = "true" - articleHesabDari2.user = user_op - articleHesabDari2.save() - - pass -#چک یا نیسیه بدهکار - elif str(pardakhti) in ['l_nesiye', 'l_checkPrice']: - nahvePardakht = '' - sarResidDateTime01 = None - if str(pardakhti) == 'l_nesiye': - sarResidDateTime01 = datetime.datetime.fromtimestamp(int(jsonPardakhtHa['l_nesiyeSarResid'])/1000.0) - nahvePardakht = 'nesiye' - elif str(pardakhti) == 'l_checkPrice': - sarResidDateTime01 = datetime.datetime.fromtimestamp(int(jsonPardakhtHa['l_checkPriceSarResid'])/1000.0) - nahvePardakht = 'check' - - - sarresidINC = Sarresid() - sarresidINC.customer = customerObj - sarresidINC.price = int(jsonPardakhtHa[pardakhti]) - sarresidINC.payStatus = "false" - sarresidINC.type = nahvePardakht - sarresidINC.user = request.user - sarresidINC.factor = newFactor - sarresidINC.sarresidDT = sarResidDateTime01 - sarresidINC.save() + if key in payment_map: + nahve_pardakht = payment_map[key] + Article.objects.create( + debit=0, + credit=amount, + customer=customer_obj, + factor=new_factor, + desc=f"{nahve_pardakht} {customer_obj.name_family} فاکتور شماره: {new_factor.id}", + nahvePardakht=nahve_pardakht, + payStatus="true", + user=user_op + ) + + # ثبت اسناد چک و نسیه + elif key in ['l_nesiye', 'l_checkPrice']: + is_nesiye = (key == 'l_nesiye') + type_str = 'nesiye' if is_nesiye else 'check' + ts_key = 'l_nesiyeSarResid' if is_nesiye else 'l_checkPriceSarResid' - #articleHesabDari2 = Article() - #articleHesabDari2.debit = int(jsonPardakhtHa[pardakhti]) - #articleHesabDari2.credit = 0 - #articleHesabDari2.customer = customerObj - #articleHesabDari2.factor = newFactor - #articleHesabDari2.desc = nahvePardakht + " " + str(customerObj.name_family) + " فاکتور شماره: " + str(newFactor.id) - #articleHesabDari2.nahvePardakht = nahvePardakht - #articleHesabDari2.payStatus = "false" - #articleHesabDari2.sarresid = sarResidDateTime01 - #print(sarResidDateTime01) - #articleHesabDari2.save() + sar_resid_dt = _parse_timestamp(json_pardakht_ha.get(ts_key)) - #ایتم های فاکتور - for prd in prdsList: - try: - fIteams = FactorIteam() - fIteams.factor = newFactor - if prd['prd_id'] == 9999: - Gproduct = None - fIteams.product = Gproduct - fIteams.product_name = prd['product_name'] - fIteams.num = prd['prd_count'] - fIteams.amount = int(prd['priceKol']) - else: - Gproduct = Product.objects.get(id=prd['prd_id']) - fIteams.product = Gproduct - fIteams.product_name = Gproduct.product_name - fIteams.num = prd['prd_count'] - fIteams.amount = prd['prd_count'] * Gproduct.product_price - fIteams.garantyEsh = prd['garanty_Esh'] - fIteams.save() - except: - Factorfail = True - newFactor.delete() - break - #کم کردن موجودی انبار - if Factorfail != True: - for prd in prdsList: - if prd['prd_id'] != 9999: - pInStore = ProductInStore() - sRoom = storeUser - productO = Product.objects.get(id=prd['prd_id']) - pInStore.StoreRoom = sRoom - pInStore.Product = productO - pInStore.factor = newFactor - productS_in = ProductInStore.objects.filter(Product=productO, StoreRoom=sRoom, year=1400).aggregate(Sum('s_in'))['s_in__sum'] - productS_out = ProductInStore.objects.filter(Product=productO, StoreRoom=sRoom, year=1400).aggregate(Sum('s_out'))['s_out__sum'] - mojodi_prd = productS_in - productS_out - if mojodi_prd >= prd['prd_count']: - pInStore.s_out = prd['prd_count'] - if prd['garanty_Esh'] != ' ' and prd['garanty_Esh'] != '' and prd['garanty_Esh'] != None: - pInStore.desc = "خروج: " + str(prd['prd_count']) + "#"+str(prd['garanty_Esh'])+ "#" + str(customerObj.name_family) + " * " + str(request.user.first_name) + " " + str(request.user.last_name) - else: - pInStore.desc = "خروج: " + str(prd['prd_count']) + "#فروش" + "#" + str(customerObj.name_family) + " * " + str(request.user.first_name) + " " + str(request.user.last_name) - pInStore.save() - else: - return HttpResponse("موجودی کافی نیست، موجودی فعلی: "+ str(mojodi_prd)) + Sarresid.objects.create( + customer=customer_obj, + price=amount, + payStatus="false", + type=type_str, + user=user_op, + factor=new_factor, + sarresidDT=sar_resid_dt + ) - return redirect('/panel/customer/'+str(c_id)+'/factors/') + # ثبت آیتم‌های فاکتور با کارایی بالا (املای کلاس FactorIteam دقیقا منطبق بر models.py) + factor_items_pool = [] + user_fullname = f"{user_op.first_name} {user_op.last_name}".strip() or user_op.username + + for item in prds_list: + prd_id = item.get('prd_id') + prd_count = _safe_int(item.get('prd_count')) + garanty_esh = item.get('garanty_Esh', '') + + if prd_id == 9999: + factor_items_pool.append(FactorIteam( + factor=new_factor, + product=None, + product_name=item.get('product_name', ''), + num=prd_count, + amount=_safe_int(item.get('priceKol')), + garantyEsh=garanty_esh + )) + else: + product_obj = products_by_id.get(prd_id) + if not product_obj: + raise ValidationError(f"محصولی با شناسه {prd_id} یافت نشد.") + + factor_items_pool.append(FactorIteam( + factor=new_factor, + product=product_obj, + product_name=product_obj.product_name, + num=prd_count, + amount=prd_count * _safe_int(product_obj.product_price), + garantyEsh=garanty_esh + )) + + # ثبت یکباره تمام آیتم‌ها + FactorIteam.objects.bulk_create(factor_items_pool, batch_size=500) + + # چک کردن موجودی انبار و ثبت کسر کالا + for item in prds_list: + prd_id = item.get('prd_id') + if prd_id == 9999: + continue + + prd_count = _safe_int(item.get('prd_count')) + garanty_esh = item.get('garanty_Esh', '') + product_obj = products_by_id.get(prd_id) + + # قفل کردن ردیف برای اطمینان ۱۰۰ درصدی از عدم تداخل پیش از متد Save مدل + stock_records = ProductInStore.objects.select_for_update().filter( + Product=product_obj, + StoreRoom=store_room, + year=1400 + ) + list(stock_records) + + totals = stock_records.aggregate(total_in=Sum('s_in'), total_out=Sum('s_out')) + total_in = totals['total_in'] or 0 + total_out = totals['total_out'] or 0 + mojodi_prd = total_in - total_out + + if mojodi_prd < prd_count: + raise InventoryException(f"موجودی کافی نیست، موجودی فعلی {product_obj.product_name}: {mojodi_prd}") + + if garanty_esh and str(garanty_esh).strip(): + desc_text = f"خروج: {prd_count}#{garanty_esh}#{customer_obj.name_family} * {user_fullname}" + else: + desc_text = f"خروج: {prd_count}#فروش#{customer_obj.name_family} * {user_fullname}" + + # این خط به‌طور خودکار متد حرفه‌ای Save شما در models.py را فراخوانی می‌کند + # و جداول تاریخچه انبارداری را نیز بروزرسانی می‌نماید + ProductInStore.objects.create( + StoreRoom=store_room, + Product=product_obj, + factor=new_factor, + s_out=prd_count, + desc=desc_text, + year=1400 + ) + + except InventoryException as e: + # اگر خطا دهد دیتابیس Rollback می‌شود و فرم در قالب همان پیغام HTML به کاربر برمی‌گردد + return HttpResponse(str(e), status=400) + except Exception as e: + logger.error(f"Error creating factor for customer {c_id}: {str(e)}", exc_info=True) + return HttpResponse("خطایی غیرمنتظره در سرور رخ داد. اطلاعات ذخیره نشد.", status=500) + + # بازگشت موفقیت‌آمیز دقیقاً به همان آدرس فایل urls.py + return redirect(f'/panel/customer/{c_id}/factors/') @login_required(login_url='/panel/p_login/') @permission_required('admin_panel.DelFactor', raise_exception=True) diff --git a/khatibi/production_settings.py b/khatibi/production_settings.py index b4c6aa5..b97f2f4 100644 --- a/khatibi/production_settings.py +++ b/khatibi/production_settings.py @@ -4,5 +4,12 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': Path('/app/sqlite/db.sqlite3'), + 'OPTIONS': { + # ۱. افزایش زمان انتظار (Timeout) به ثانیه + # با این کار اگر دیتابیس قفل باشد، کاربر دوم کرش نمی‌کند بلکه تا ۳۰ ثانیه منتظر باز شدن قفل می‌ماند + 'timeout': 30, + # ۲. فعال‌سازی حالت WAL برای بالا بردن قدرت پردازش هم‌زمانی در SQLite + 'init_command': 'PRAGMA journal_mode=WAL;', + }, } } \ No newline at end of file