fix garanty esh
This commit is contained in:
1 parent
bf78bc2520
commit
739104115a
6 files changed
+374
-70
No files matched your search
@@ -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
@@ -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
|
||||
|
||||
@@ -44,9 +44,17 @@ class Customer(models.Model):
|
||||
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,
|
||||
|
||||
+126
-47
@@ -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 tedad <= 0:
|
||||
return HttpResponseBadRequest("تعداد وارد شده باید بیشتر از صفر باشد.")
|
||||
|
||||
sRoom = get_object_or_404(StoreRoom, id=azanbar)
|
||||
productO = get_object_or_404(Product, id=prd_id)
|
||||
|
||||
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)
|
||||
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.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)
|
||||
else:
|
||||
return HttpResponse("موجودی کافی نیست، موجودی فعلی: "+ str(mojodi_prd))
|
||||
pInStore.movement_type = ProductInStore.MovementType.DIRECT_OUT
|
||||
pInStore.save()
|
||||
|
||||
# ب) انتقال بین دو انبار مجزا
|
||||
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:
|
||||
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)
|
||||
else:
|
||||
return HttpResponse("موجودی کافی نیست، موجودی فعلی: "+ str(mojodi_prd))
|
||||
|
||||
pInStore.movement_type = ProductInStore.MovementType.TRANSFER
|
||||
pInStore.save()
|
||||
|
||||
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()
|
||||
# ردیف دوم: ثبت تراکنش ورود به انبار مقصد (محافظت شده در بلاک اتمیک)
|
||||
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)
|
||||
|
||||
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
|
||||
# ۲. قفل دیتابیس هوشمند و سازگار با تمام پایگاههای داده
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
Binary file not shown.
+1
-1
@@ -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',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user