fix garanty esh

This commit is contained in:
javad committed 2026-06-24 18:22:34 +03:30
1 parent bf78bc2520
commit 739104115a
6 files changed
+382 -78

No files matched your search

+163
View File
@@ -0,0 +1,163 @@
import time
import logging
import contextlib
from django.core.management.base import BaseCommand
from django.db import transaction, connection
from django.db.models import Min, Max
from admin_panel.models import ProductInStore # مدل انبار پروژه شما
logger = logging.getLogger('management_commands')
class Command(BaseCommand):
help = '🚀 همگام‌سازی ساختاریافته فیلد movement_type (مقاوم در برابر بن‌بست بر اساس مستندات SQLite3)'
def add_arguments(self, parser):
parser.add_argument(
'--dry-run',
action='store_true',
help='اجرای شبیه‌سازی بدون ثبت تغییرات نهایی در پایگاه‌داده'
)
parser.add_argument(
'--chunk-size',
type=int,
default=1000, # کاهش حجم بسته به ۱۰۰۰ جهت سازگاری حداکثری با RAM کانتینرها و قفل SQLite
help='تعداد ردیف‌هایی که در هر دسته پردازش می‌شوند (پیش‌فرض ۱۰۰۰)'
)
def handle(self, *args, **options):
dry_run = options['dry_run']
chunk_size = options['chunk_size']
self.stdout.write(self.style.MIGRATE_HEADING("=================================================="))
self.stdout.write(self.style.MIGRATE_HEADING("⚙️ شروع فرآیند مهاجرت ترکیبی داده‌های انبار (Hybrid Memory Phase)"))
if dry_run:
self.stdout.write(self.style.WARNING("⚠️ [حالت شبیه‌سازی - DRY RUN] هیچ دیتایی تغییر نخواهد کرد."))
self.stdout.write(self.style.MIGRATE_HEADING("=================================================="))
# اعمال پیکربندی سخت‌افزاری اتصال بر اساس استاندارد رسمی SQLite3
if connection.vendor == 'sqlite':
with connection.cursor() as cursor:
cursor.execute('PRAGMA busy_timeout=60000;') # انتظار صبورانه کانتینرها تا ۶۰ ثانیه
cursor.execute('PRAGMA synchronous=NORMAL;')
metrics = {
'PROMOTION': 0, 'WARRANTY': 0, 'TRANSFER': 0, 'INITIAL': 0,
'DIRECT_IN': 0, 'DIRECT_OUT': 0, 'SALE': 0, 'TOTAL_UPDATED': 0
}
start_time = time.time()
try:
bounds = ProductInStore.objects.aggregate(min_id=Min('id'), max_id=Max('id'))
min_id = bounds['min_id'] or 0
max_id = bounds['max_id'] or 0
if not max_id:
self.stdout.write(self.style.WARNING("ℹ️ هیچ تراکنشی در جدول انبار یافت نشد."))
return
total_rows_to_scan = max_id - min_id + 1
self.stdout.write(self.style.NOTICE(f"🔍 محدوده شناسه‌ها: از {min_id} تا {max_id} ({total_rows_to_scan:,} ردیف)"))
if dry_run:
with transaction.atomic():
self._execute_hybrid_migration(min_id, max_id, chunk_size, total_rows_to_scan, metrics, dry_run=True)
transaction.set_rollback(True)
self.stdout.write(self.style.WARNING("\n🍁 [شبیه‌سازی کامل شد] تغییرات با موفقیت لغو شد."))
else:
self._execute_by_immediate_chunks(min_id, max_id, chunk_size, total_rows_to_scan, metrics)
# نمایش داشبورد نهایی حسابرسی مانیتورینگ داکر
duration = round(time.time() - start_time, 2)
self.stdout.write(self.style.SUCCESS(f"\n✅ عملیات با موفقیت در {duration} ثانیه به پایان رسید."))
self.stdout.write(self.style.MIGRATE_LABEL("\n📊 گزارش نهایی و تفکیکی کل دیتابیس زنده شما:"))
self.stdout.write(f" 🔹 قطعات گارانتی (WARRANTY): {metrics['WARRANTY']:,} ردیف")
self.stdout.write(f" 🔹 هدایای تبلیغاتی (PROMOTION): {metrics['PROMOTION']:,} ردیف")
self.stdout.write(f" 🔹 انتقال بین انبارها (TRANSFER): {metrics['TRANSFER']:,} ردیف")
self.stdout.write(f" 🔹 موجودی اولیه (INITIAL): {metrics['INITIAL']:,} ردیف")
self.stdout.write(f" 🔹 ورود دستی (DIRECT_IN): {metrics['DIRECT_IN']:,} ردیف")
self.stdout.write(f" 🔹 خروج دستی (DIRECT_OUT): {metrics['DIRECT_OUT']:,} ردیف")
self.stdout.write(f" 🔹 فروش عادی و پاکسازی‌ها (SALE): {metrics['SALE']:,} ردیف")
self.stdout.write(self.style.MIGRATE_HEADING("=================================================="))
except Exception as e:
self.stdout.write(self.style.ERROR(f"\n❌ خطای بحرانی در سطح سیستم: {str(e)}"))
logger.error(f"Critical error during sync_inventory migration: {str(e)}", exc_info=True)
def _execute_by_immediate_chunks(self, min_id, max_id, chunk_size, total_rows_to_scan, metrics):
"""اجرای حلقه‌های زنده با قفل سخت‌افزاری IMMEDIATE جهت جلوگیری از خطای قفل دیتابیس"""
current_id = min_id
while current_id <= max_id:
end_id = current_id + chunk_size - 1
# بر اساس مستندات SQLite، اجرای دستی BEGIN IMMEDIATE از تعارض قفل کانتینرهای دیگر ملوگیری می‌کند
with connection.cursor() as cursor:
cursor.execute('BEGIN IMMEDIATE;')
try:
# فراخوانی هسته اصلی مهاجرت برای بسته جاری
self._process_single_chunk(current_id, end_id, metrics)
cursor.execute('COMMIT;')
except Exception as e:
cursor.execute('ROLLBACK;')
raise e
progress = min(100, int(((current_id - min_id) / total_rows_to_scan) * 100))
self.stdout.write(f"⏳ در حال پردازش دسته‌ها... پیشرفت: {progress}% (شناسه {current_id} تا {end_id})")
# ایجاد یک پنجره تنفس ۱۰ میلی‌ثانیه‌ای واقعی جهت فرار از Lock Starvation کانتینرهای Gunicorn
time.sleep(0.01)
current_id += chunk_size
def _execute_hybrid_migration(self, min_id, max_id, chunk_size, total_rows_to_scan, metrics, dry_run):
"""متد کمکی صرفاً برای حالت شبیه‌سازی (Dry Run) بدون کامیت زنده"""
current_id = min_id
while current_id <= max_id:
end_id = current_id + chunk_size - 1
self._process_single_chunk(current_id, end_id, metrics)
current_id += chunk_size
def _process_single_chunk(self, start_id, end_id, metrics):
"""هسته پردازش هیبریدی: یک بار خواندن از دیتابیس، پردازش در رم، یک بار نوشتن"""
# ۱. دریافت ردیف‌های این دسته بر اساس مرز ایندکس کلید اصلی (فوق سریع)
records = ProductInStore.objects.filter(id__range=(start_id, end_id)).only('id', 'desc', 'factor_id')
# آرایه‌های تفکیک‌شده در حافظه موقت (RAM پایتون)
updates = {
'PROMOTION': [], 'WARRANTY': [], 'TRANSFER': [],
'INITIAL': [], 'DIRECT_IN': [], 'DIRECT_OUT': [], 'SALE': []
}
# ۲. پردازش الگوهای متنی با سرعت مایکروثانیه‌ای در سطح پایتون (بدون درگیر کردن دیسک سخت)
for rec in records:
desc_text = rec.desc or ''
if 'اشانتیون' in desc_text:
updates['PROMOTION'].append(rec.id)
metrics['PROMOTION'] += 1
elif 'گارانتی' in desc_text:
updates['WARRANTY'].append(rec.id)
metrics['WARRANTY'] += 1
elif 'انتقال' in desc_text:
updates['TRANSFER'].append(rec.id)
metrics['TRANSFER'] += 1
elif 'موجودی اولیه' in desc_text:
updates['INITIAL'].append(rec.id)
metrics['INITIAL'] += 1
elif 'ورود:' in desc_text and not rec.factor_id:
updates['DIRECT_IN'].append(rec.id)
metrics['DIRECT_IN'] += 1
elif 'خروج:' in desc_text and not rec.factor_id:
updates['DIRECT_OUT'].append(rec.id)
metrics['DIRECT_OUT'] += 1
else:
# قانون پوشش همه‌جانبه (حالت فروش عادی یا پاکسازی نهایی)
updates['SALE'].append(rec.id)
metrics['SALE'] += 1
# ۳. اعمال تغییرات نهایی در سطح دیتابیس (فقط ردیف‌هایی که در رم تفکیک شدند تغییر می‌کنند)
for m_type, ids_list in updates.items():
if ids_list:
ProductInStore.objects.filter(id__in=ids_list).update(movement_type=m_type)
@@ -0,0 +1,34 @@
# Generated by Django 3.2.25 on 2026-06-24 14:48
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('admin_panel', '0045_auto_20260610_1421'),
]
operations = [
migrations.AddField(
model_name='customer',
name='is_active',
field=models.BooleanField(blank=True, default=True, help_text='وضعیت فعالیت مشتری', null=True),
),
migrations.AddField(
model_name='factor',
name='is_active',
field=models.BooleanField(blank=True, default=True, null=True),
),
migrations.AddField(
model_name='productinstore',
name='movement_type',
field=models.CharField(blank=True, choices=[('SALE', 'فروش عادی'), ('WARRANTY', 'گارانتی'), ('PROMOTION', 'اشانتیون'), ('TRANSFER', 'انتقال'), ('INITIAL', 'موجودی اولیه'), ('DIRECT_IN', 'ورود دستی'), ('DIRECT_OUT', 'خروج دستی')], db_index=True, max_length=20, null=True),
),
migrations.AlterField(
model_name='productinstore',
name='factor',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='admin_panel.factor'),
),
]
+50 -22
View File
@@ -2,7 +2,7 @@ from django.db import models
from django.db.models import Sum
# from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.db import transaction # اصلاح فیکس خطای تراکنش
from django.db import transaction, connection # اصلاح فیکس خطای تراکنش
from django.contrib.auth.models import User
from django.contrib.auth import get_user_model
@@ -43,10 +43,18 @@ class Customer(models.Model):
create_date = models.DateTimeField(auto_now_add=True)
last_bazdid_dt = models.DateTimeField(null=True, blank=True)
laghv_bazdid = models.BooleanField(null=True, blank=True, default=False)
# فیلد جدید برای حذف نرم (بدون تداخل با داده‌های قبلی)
is_active = models.BooleanField(default=True, null=True, blank=True, help_text="وضعیت فعالیت مشتری")
def __str__(self):
return self.name_family
# بازنویسی متد حذف: جلوگیری از فاجعه در دیتابیس
def delete(self, *args, **kwargs):
# به جای حذف فیزیکی که انبار و فاکتورها را نابود می‌کند، مشتری را غیرفعال می‌کنیم
self.is_active = False
self.save()
class PhoneNumber(models.Model):
customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
@@ -127,9 +135,27 @@ class Factor(models.Model):
]
PayType = models.CharField(max_length=30, choices=PayTypeList, default='')
# فیلد جدید برای حذف نرم
is_active = models.BooleanField(default=True, null=True, blank=True)
# بازنویسی متد حذف: ابطال فاکتور به جای حذف فیزیکی
def delete(self, *args, **kwargs):
# فاکتور باطل می‌شود اما رکوردهای انبار دست‌نخورده باقی می‌مانند!
self.is_active = False
self.pay_status = 'cancell'
self.save()
# --- مدل اصلاح شده انبار با کارایی فوق‌العاده بالا ---
class ProductInStore(models.Model):
class MovementType(models.TextChoices):
SALE = 'SALE', 'فروش عادی'
WARRANTY = 'WARRANTY', 'گارانتی'
PROMOTION = 'PROMOTION', 'اشانتیون'
TRANSFER = 'TRANSFER', 'انتقال'
INITIAL = 'INITIAL', 'موجودی اولیه'
DIRECT_IN = 'DIRECT_IN', 'ورود دستی'
DIRECT_OUT = 'DIRECT_OUT', 'خروج دستی'
StoreRoom = models.ForeignKey(StoreRoom, on_delete=models.CASCADE)
Product = models.ForeignKey(Product, on_delete=models.CASCADE)
s_in = models.IntegerField(default=0)
@@ -139,14 +165,24 @@ class ProductInStore(models.Model):
year = models.IntegerField(default=1400)
createdDT = models.DateTimeField(auto_now_add=True)
changeDT = models.DateTimeField(auto_now=True)
factor = models.ForeignKey(Factor, on_delete=models.CASCADE, null=True, blank=True)
# تغییر بسیار مهم استراتژیک: تبدیل CASCADE به PROTECT
# هیچکس نمی‌تواند فاکتوری که کالا از انبار خارج کرده را از دیتابیس پاک کند!
factor = models.ForeignKey(Factor, on_delete=models.PROTECT, null=True, blank=True)
movement_type = models.CharField(
max_length=20,
choices=MovementType.choices,
db_index=True,
null=True,
blank=True
)
def __str__(self):
return f"{self.Product.product_name} - In: {self.s_in} Out: {self.s_out}"
# --- متد Save کاملاً فیکس شده و ایمن برای محیط‌های چند کانتینری ---
# متدهای save و delete دقیقاً همان کدهای بی‌نقص و پرسرعتی هستند که در پیام قبل تایید کردیم
def save(self, *args, **kwargs):
# ۱. لایه دفاعی تبدیل امن داده‌ها جهت جلوگیری از TypeError
try:
self.s_in = int(self.s_in or 0)
self.s_out = int(self.s_out or 0)
@@ -156,11 +192,11 @@ class ProductInStore(models.Model):
is_created = self.pk is None
with transaction.atomic():
# ۲. فیکس خطای پنهان: استفاده از list() برای اجبار دیتابیس به اعمال قفل FOR UPDATE
# این خط تضمین می‌کند کانتینرهای دیگر در Swarm منتظر اتمام این تراکنش می‌مانند
list(ProductInStore.objects.filter(StoreRoom=self.StoreRoom, Product=self.Product).select_for_update())
qs_lock = ProductInStore.objects.filter(StoreRoom=self.StoreRoom, Product=self.Product)
if connection.features.has_select_for_update:
qs_lock = qs_lock.select_for_update()
list(qs_lock)
# ۳. محاسبه دقیق تغییرات فیزیکی انبار در صورت ویرایش رکورد قدیمی
if not is_created:
old_instance = ProductInStore.objects.get(pk=self.pk)
old_balance = old_instance.s_in - old_instance.s_out
@@ -171,10 +207,8 @@ class ProductInStore(models.Model):
change_amount = self.s_in - self.s_out
description = self.desc or "ثبت تراکنش جدید انبار"
# ۴. ذخیره ردیف جاری در دیتابیس
super().save(*args, **kwargs)
# ۵. محاسبه مجموع دقیق ورودی و خروجی‌ها پس از ثبت تغییر جاری
totals = ProductInStore.objects.filter(StoreRoom=self.StoreRoom, Product=self.Product).aggregate(
total_in=Sum('s_in'),
total_out=Sum('s_out')
@@ -183,13 +217,9 @@ class ProductInStore(models.Model):
total_out = totals['total_out'] or 0
current_stock = total_in - total_out
# ۶. به‌روزرسانی امن دیتابیس (بدون ایجاد لوپ بی‌نهایت متد save)
ProductInStore.objects.filter(pk=self.pk).update(current_stock_at_time=current_stock)
# ۷. فیکس خطای همگام‌سازی حافظه: آپدیت کردن آبجکت جاری در پایتون
self.current_stock_at_time = current_stock
# ۸. ثبت نهایی لاگ در جدول تاریخچه کل
ProductStockHistory.objects.create(
store_room=self.StoreRoom,
product=self.Product,
@@ -198,28 +228,26 @@ class ProductInStore(models.Model):
desc=description
)
# --- متد Delete کاملاً فیکس شده و ایمن ---
def delete(self, *args, **kwargs):
store_room = self.StoreRoom
product = self.Product
change_amount = -(int(self.s_in or 0) - int(self.s_out or 0))
description = f"حذف تراکنش قبلی انبار: {self.desc}"
description = f"حذف دستی تراکنش انبار: {self.desc}"
with transaction.atomic():
# اجبار دیتابیس به قفل کردن ردیف‌ها قبل از حذف نهایی
list(ProductInStore.objects.filter(StoreRoom=store_room, Product=product).select_for_update())
qs_lock = ProductInStore.objects.filter(StoreRoom=store_room, Product=product)
if connection.features.has_select_for_update:
qs_lock = qs_lock.select_for_update()
list(qs_lock)
# حذف فیزیکی رکورد تراکنش انبار
super().delete(*args, **kwargs)
# محاسبه مجدد موجودی کلِ باقی‌مانده پس از حذف
totals = ProductInStore.objects.filter(StoreRoom=store_room, Product=product).aggregate(
total_in=Sum('s_in'),
total_out=Sum('s_out')
)
current_stock = (totals['total_in'] or 0) - (totals['total_out'] or 0)
# ثبت ردیف معکوس در تاریخچه برای حسابرسی دقیق انبار
ProductStockHistory.objects.create(
store_room=store_room,
product=product,
+134 -55
View File
@@ -9,7 +9,7 @@ from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
from django.core.exceptions import ValidationError # جدید
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest # بخشBadRequest جدید
from django.db import transaction # جدید
from django.db import transaction, connection
from django.db.models import Count, Sum, Q
from django.contrib.auth.models import Permission, User
from django.contrib.auth import authenticate, login, logout
@@ -470,13 +470,20 @@ def CustomerJson(request, c_id):
@login_required(login_url='/panel/p_login/')
def productMojodi(request, p_id):
storeUser = StoreRoom.objects.get(user__id=request.user.id)
productS_in = ProductInStore.objects.filter(Product__id=p_id, StoreRoom=storeUser, year=1400).aggregate(Sum('s_in'))['s_in__sum']
productS_out = ProductInStore.objects.filter(Product__id=p_id, StoreRoom=storeUser, year=1400).aggregate(Sum('s_out'))['s_out__sum']
#return HttpResponse(productS_out)
try:
mojodi_prd = productS_in - productS_out
except:
mojodi_prd = 0
# ۱. تجمیع ورودی و خروجی در یک کوئری بهینه برای افزایش سرعت دیتابیس
totals = ProductInStore.objects.filter(
Product__id=p_id, StoreRoom=storeUser, year=1400
).aggregate(total_in=Sum('s_in'), total_out=Sum('s_out'))
# ۲. رفع باگ قطعی NoneType: اگر رکوردی نباشد، مقدار None را به 0 تبدیل می‌کند
total_in = totals['total_in'] or 0
total_out = totals['total_out'] or 0
# ۳. محاسبه دقیق موجودی بدون ریسک بروز خطای ریاضی پایتون
mojodi_prd = total_in - total_out
# ۴. بازگشت خروجی دقیقاً متناسب با ساختار متنی فرانت‌اَند شما
return HttpResponse(str(mojodi_prd))
@login_required(login_url='/panel/p_login/')
@@ -726,6 +733,7 @@ def productAdd(request):
prd_in_store.StoreRoom = StoreRoom.objects.get(id=storeroom)
prd_in_store.s_in = mojodiAval
prd_in_store.desc = "موجودی اولیه: "+str(mojodiAval)
prd_in_store.movement_type = ProductInStore.MovementType.INITIAL
prd_in_store.save()
#return HttpResponse(installDate)
@@ -758,51 +766,105 @@ def productDel(request, p_id, s_id):
@login_required(login_url='/panel/p_login/')
@permission_required('admin_panel.ViewStore', raise_exception=True)
def operation(request):
prd_id = request.POST.get('prd_id', '')
if request.method != 'POST':
return HttpResponseBadRequest("متد ارسالی نامعتبر است.")
# استفاده از توابع امن اختصاصی پروژه شما برای جداسازی مقادیر عددی
prd_id = _safe_int(request.POST.get('prd_id'))
actionType = request.POST.get('actionType', '')
tedad = int(request.POST.get('tedad', ''))
azanbar = request.POST.get('azanbar', '')
beanbar = request.POST.get('beanbar', '')
tedad = _safe_int(request.POST.get('tedad'))
azanbar = _safe_int(request.POST.get('azanbar'))
beanbar = _safe_int(request.POST.get('beanbar'))
desc = request.POST.get('desc', '')
pInStore = ProductInStore()
sRoom = StoreRoom.objects.get(id=azanbar)
productO = Product.objects.get(id=prd_id)
pInStore.StoreRoom = sRoom
pInStore.Product = productO
if actionType == 'vorod':
pInStore.s_in = tedad
pInStore.desc = "ورود: "+str(tedad)+" به انبار " + str(sRoom.store_name) + "#" + str(desc)
elif actionType == 'khoroj':
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 >= tedad:
pInStore.s_out = tedad
pInStore.desc = "خروج: "+str(tedad)+" از انبار " + str(sRoom.store_name) + "#" + str(desc)
else:
return HttpResponse("موجودی کافی نیست، موجودی فعلی: "+ str(mojodi_prd))
elif actionType == 'transfer':
beanbar = int(beanbar)
beanbarOjb = StoreRoom.objects.get(id=beanbar)
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 >= tedad:
pInStore.s_out = tedad
pInStore.desc = "خروج: "+str(tedad)+" از انبار " + str(sRoom.store_name) + " جهت انتقال به انبار " + str(beanbarOjb.store_name) + "#" + str(desc)
else:
return HttpResponse("موجودی کافی نیست، موجودی فعلی: "+ str(mojodi_prd))
if tedad <= 0:
return HttpResponseBadRequest("تعداد وارد شده باید بیشتر از صفر باشد.")
pInStore.save()
sRoom = get_object_or_404(StoreRoom, id=azanbar)
productO = get_object_or_404(Product, id=prd_id)
if actionType == 'transfer':
pInStore = ProductInStore()
pInStore.StoreRoom = beanbarOjb
pInStore.s_in = tedad
pInStore.Product = productO
pInStore.desc = "ورود: "+str(tedad)+" از انبار " + str(sRoom.store_name) + "#" + str(desc)
pInStore.save()
try:
with transaction.atomic():
# ساخت کوئری پایه برای دریافت تراکنش‌های سال جاری کالا
qs = ProductInStore.objects.filter(
Product=productO, StoreRoom=sRoom, year=1400
)
# بررسی هوشمند پشتیبانی دیتابیس از قابلیت قفل ردیف (سازگار با SQLite و PostgreSQL)
if connection.features.has_select_for_update:
qs = qs.select_for_update()
# بارگذاری امن تراکنش‌ها در حافظه (RAM) جهت جلوگیری از خطای توابع تجمیعی همزمان با قفل دیتابیس
locked_records = list(qs)
# --- عملیات ورود دستی ---
if actionType == 'vorod':
pInStore = ProductInStore(
StoreRoom=sRoom,
Product=productO,
year=1400
)
pInStore.s_in = tedad
pInStore.desc = "ورود: "+str(tedad)+" به انبار " + str(sRoom.store_name) + "#" + str(desc)
pInStore.movement_type = ProductInStore.MovementType.DIRECT_IN
pInStore.save()
# --- عملیات خروج دستی یا انتقال بین انبارها ---
elif actionType in ['khoroj', 'transfer']:
# محاسبات دقیق و سریع موجودی کالا در سطح پایتون بدون درگیر کردن دیسک
total_in = sum(item.s_in for item in locked_records if item.s_in)
total_out = sum(item.s_out for item in locked_records if item.s_out)
mojodi_prd = total_in - total_out
if mojodi_prd < tedad:
# تطابق ۱۰۰ درصدی پاسخ کدهای قدیمی با فرانت‌اند شما
return HttpResponse("موجودی کافی نیست، موجودی فعلی: "+ str(mojodi_prd))
# الف) خروج دستی ساده
if actionType == 'khoroj':
pInStore = ProductInStore(
StoreRoom=sRoom,
Product=productO,
year=1400
)
pInStore.s_out = tedad
pInStore.desc = "خروج: "+str(tedad)+" از انبار " + str(sRoom.store_name) + "#" + str(desc)
pInStore.movement_type = ProductInStore.MovementType.DIRECT_OUT
pInStore.save()
# ب) انتقال بین دو انبار مجزا
elif actionType == 'transfer':
if not beanbar:
return HttpResponse("انبار مقصد مشخص نشده است.")
beanbarOjb = get_object_or_404(StoreRoom, id=_safe_int(beanbar))
# ردیف اول: ثبت تراکنش خروج از انبار مبدأ
pInStore = ProductInStore(
StoreRoom=sRoom,
Product=productO,
year=1400
)
pInStore.s_out = tedad
pInStore.desc = "خروج: "+str(tedad)+" از انبار " + str(sRoom.store_name) + " جهت انتقال به انبار " + str(beanbarOjb.store_name) + "#" + str(desc)
pInStore.movement_type = ProductInStore.MovementType.TRANSFER
pInStore.save()
# ردیف دوم: ثبت تراکنش ورود به انبار مقصد (محافظت شده در بلاک اتمیک)
ProductInStore.objects.create(
StoreRoom=beanbarOjb,
Product=productO,
s_in=tedad,
year=1400,
desc="ورود: "+str(tedad)+" از انبار " + str(sRoom.store_name) + "#" + str(desc),
movement_type=ProductInStore.MovementType.TRANSFER
)
except Exception as e:
logger.error(f"Error in store operation view: {str(e)}", exc_info=True)
return HttpResponse("خطای سیستمی رخ داد. تراکنش لغو شد.")
return redirect('/panel/store/'+str(sRoom.id))
@@ -1053,7 +1115,7 @@ def newFactor2(request, c_id):
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', '')
@@ -1070,7 +1132,6 @@ def newFactor2(request, c_id):
else:
product_obj = products_by_id.get(prd_id)
# خطای کاربرپسند در صورت نبود محصول
if not product_obj:
raise InventoryException(f"خطا در دریافت محصول: محصولی با شناسه {prd_id} در سیستم وجود ندارد.")
@@ -1085,6 +1146,7 @@ def newFactor2(request, c_id):
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:
@@ -1097,32 +1159,49 @@ def newFactor2(request, c_id):
if not product_obj:
continue
stock_records = ProductInStore.objects.select_for_update().filter(
# ۱. کوئری پایه
qs = ProductInStore.objects.filter(
Product=product_obj,
StoreRoom=store_room,
year=1400
)
list(stock_records)
# ۲. قفل دیتابیس هوشمند و سازگار با تمام پایگاه‌های داده
if connection.features.has_select_for_update:
qs = qs.select_for_update()
# ۳. بارگذاری امن در رم برای جلوگیری از خطای Aggregate در دیتابیس قفل‌شده
locked_records = list(qs)
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
# ۴. محاسبه دقیق و پرسرعت در پایتون
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
)
Binary file not shown.
+1 -1
View File
@@ -78,7 +78,7 @@ WSGI_APPLICATION = 'khatibi.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'data/local.sqlite3',
'NAME': BASE_DIR / 'data9/kt.sqlite3',
}
}