import json from django.db import transaction, connection from django.db.models import Sum, Q from django.shortcuts import redirect, get_object_or_404 from django.http import HttpResponse, HttpResponseBadRequest from django.views.decorators.http import require_POST from django.contrib.auth.models import User from django.contrib.auth.decorators import login_required, permission_required from persiantools.jdatetime import JalaliDate from persiantools import digits from ..models import (Customer, StoreRoom, Factor, FactorIteam, Product, ProductInStore, Article, Sarresid) from ..sms.triggers import notify_factor_survey, notify_payment_thanks from ._helpers import logger, _safe_int, _parse_jalali_date, _parse_timestamp, InventoryException @login_required(login_url='/panel/p_login/') def newFactor(request, c_id): prdsList = request.POST.get('prdsList', '') sarResidDate = request.POST.get('sarResidDate', None) pardakhti = request.POST.get('pardakhti', None) #return HttpResponse(pardakhti) if pardakhti: pardakhti=int(pardakhti) if pardakhti>=1: pass else: pardakhti = None #return HttpResponse(sarResidDate) 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 #return HttpResponse(sarResidDate[1]) #sarResidDate = JalaliDate(sarResidDate[0], sarResidDate[1], sarResidDate[2]).to_gregorian() payType = request.POST.get('payType', '') FactorDate = request.POST.get('FactorDate', '') FactorType = request.POST.get('FactorType', '') FactorDesc = request.POST.get('FactorDesc', '') Factorfail = False customerObj=Customer.objects.get(id=c_id) storeUser = StoreRoom.objects.get(user__id=request.user.id) import json 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 if payType in ('naghd', 'kartKhan', 'cardToCard'): newFactor.pay_status = 'paid' else: newFactor.pay_status = 'unpaid' newFactor.FactorType = FactorType newFactor.PayType = payType newFactor.desc = FactorDesc 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 if Factorfail != True: #accFactor To article is_naghd = False articleHesabDari = Article() articleHesabDari.debit = newFactor.amount articleHesabDari.credit = 0 articleHesabDari.customer = customerObj articleHesabDari.factor = newFactor if payType in ('naghd', 'kartKhan', 'cardToCard'): is_naghd = True articleHesabDari.type = payType articleHesabDari.desc = str(FactorType) + " " + str(customerObj.name_family) + " طی فاکتور شماره: " + str(newFactor.id) articleHesabDari.save() if is_naghd: articleHesabDari2 = Article() articleHesabDari2.debit = 0 articleHesabDari2.credit = newFactor.amount articleHesabDari2.customer = customerObj articleHesabDari2.type = payType articleHesabDari2.factor = newFactor articleHesabDari2.desc = str(FactorType) + " " + str(customerObj.name_family) + " تسویه نقدی فاکتور شماره: " + str(newFactor.id) articleHesabDari2.save() elif pardakhti != None and pardakhti!='' and pardakhti != ' ': articleHesabDari2 = Article() articleHesabDari2.debit = 0 articleHesabDari2.credit = pardakhti articleHesabDari2.customer = customerObj articleHesabDari2.type = payType articleHesabDari2.factor = newFactor articleHesabDari2.desc = str(customerObj.name_family) + " پرداخت نقد در محل برا فاکتور " + str(newFactor.id) articleHesabDari2.save() 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)) return redirect('/panel/customer/'+str(c_id)+'/factors/') @login_required(login_url='/panel/p_login/') def newFactor2(request, c_id): if request.method != 'POST': return HttpResponseBadRequest("متد ارسالی باید POST باشد.") user_op = request.user try: 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 نامعتبر است.") factor_type = request.POST.get('FactorType', '') factor_desc = request.POST.get('FactorDesc', '') sar_resid_date = _parse_jalali_date(request.POST.get('sarResidDate')) customer_obj = get_object_or_404(Customer, id=c_id) store_room = get_object_or_404(StoreRoom, owners=user_op) # رفع مشکل متنی/عددی در استخراج شناسه کالاها product_ids = [] for item in prds_list: p_id = item.get('prd_id') if p_id and _safe_int(p_id) != 9999: product_ids.append(_safe_int(p_id)) products_by_id = {p.id: p for p in Product.objects.filter(id__in=product_ids)} 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() ) new_factor = None payment_articles = [] try: 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 ) 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 ) payment_map = { 'l_naghd': 'نقد', 'l_kartbekart': 'کارت به کارت', 'l_kartKhan': 'کارت خوان', 'l_takhfif': 'تخفیف' } for key, amount_raw in json_pardakht_ha.items(): amount = _safe_int(amount_raw) if amount <= 0: continue if key in payment_map: nahve_pardakht = payment_map[key] payment_articles.append((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 ), nahve_pardakht)) 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' sar_resid_dt = _parse_timestamp(json_pardakht_ha.get(ts_key)) Sarresid.objects.create( customer=customer_obj, price=amount, payStatus="false", type=type_str, user=user_op, factor=new_factor, sarresidDT=sar_resid_dt ) 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 = _safe_int(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 InventoryException(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) # فرآیند کسر از انبار (Enterprise Level) for item in prds_list: prd_id = _safe_int(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) if not product_obj: continue # ۱. کوئری پایه qs = ProductInStore.objects.filter( Product=product_obj, StoreRoom=store_room, year=1400 ) # ۲. قفل دیتابیس هوشمند و سازگار با تمام پایگاه‌های داده if connection.features.has_select_for_update: qs = qs.select_for_update() # ۳. بارگذاری امن در رم برای جلوگیری از خطای Aggregate در دیتابیس قفل‌شده locked_records = list(qs) # ۴. محاسبه دقیق و پرسرعت در پایتون total_in = sum(rec.s_in for rec in locked_records if rec.s_in) total_out = sum(rec.s_out for rec in locked_records if rec.s_out) 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}" if "اشانتیون" in str(garanty_esh): m_type = ProductInStore.MovementType.PROMOTION else: m_type = ProductInStore.MovementType.WARRANTY else: desc_text = f"خروج: {prd_count}#فروش#{customer_obj.name_family} * {user_fullname}" m_type = ProductInStore.MovementType.SALE # ۶. ثبت تراکنش خروجی انبار ProductInStore.objects.create( StoreRoom=store_room, Product=product_obj, factor=new_factor, s_out=prd_count, desc=desc_text, movement_type=m_type, # ثبت ساختاریافته نوع خروج برای سرعت در گزارش‌گیری year=1400 ) except InventoryException as e: 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) # تریگرهای پیامک پس از تأیید تراکنش (خارج از atomic تا در صورت rollback ساخته نشوند). if new_factor is not None: # نظرسنجی بی‌واسطه پس از ثبت فاکتور. notify_factor_survey(customer_obj, new_factor, created_by=user_op) # تشکر برای هر پرداخت نقدی/کارتیِ ثبت‌شده روی همین فاکتور (تخفیف مستثناست). for art, method in payment_articles: notify_payment_thanks(customer_obj, art.credit, article_id=art.id, method=method, created_by=user_op) return redirect(f'/panel/customer/{c_id}/factors/') @login_required(login_url='/panel/p_login/') @permission_required('admin_panel.DelFactor', raise_exception=True) @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: 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)