diff --git a/admin_panel/management/commands/gt_esh.py b/admin_panel/management/commands/gt_esh.py new file mode 100644 index 0000000..f05089f --- /dev/null +++ b/admin_panel/management/commands/gt_esh.py @@ -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) \ No newline at end of file diff --git a/admin_panel/migrations/0046_auto_20260624_1818.py b/admin_panel/migrations/0046_auto_20260624_1818.py new file mode 100644 index 0000000..81ade93 --- /dev/null +++ b/admin_panel/migrations/0046_auto_20260624_1818.py @@ -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'), + ), + ] diff --git a/admin_panel/models.py b/admin_panel/models.py index 4874720..bc5935d 100644 --- a/admin_panel/models.py +++ b/admin_panel/models.py @@ -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, diff --git a/admin_panel/views.py b/admin_panel/views.py index ec8add1..7706833 100644 --- a/admin_panel/views.py +++ b/admin_panel/views.py @@ -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 ) diff --git a/data9/db222.sqlite344 b/data9/db222.sqlite344 deleted file mode 100644 index 5fd55b2..0000000 Binary files a/data9/db222.sqlite344 and /dev/null differ diff --git a/khatibi/settings.py b/khatibi/settings.py index 72115ed..1561548 100644 --- a/khatibi/settings.py +++ b/khatibi/settings.py @@ -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', } }