479 lines
22 KiB
Python
479 lines
22 KiB
Python
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)
|