From 2228279696db53f9d1a59c50265c9190b2e8e1d7 Mon Sep 17 00:00:00 2001 From: javad Date: Mon, 22 Jun 2026 13:05:23 +0330 Subject: [PATCH] fix models.py and add dockerfile --- .dockerignore | 6 ++ Dockerfile | 67 +++++++++++++++++++ admin_panel/models.py | 152 +++++++++++++++++++++++------------------- entrypoint.sh | 15 +++++ 4 files changed, 173 insertions(+), 67 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 entrypoint.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6dcd316 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.git +.gitignore +__pycache__/ +*.pyc +venv/ +.venv/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8d73d2f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,67 @@ +# ========================================== +# Stage 1: Build dependencies +# ========================================== +FROM python:3.11-slim AS builder + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +WORKDIR /app + +# بهینه‌سازی ۱: استفاده از کش سیستم‌عامل برای apt +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + libpq-dev + +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +COPY requirements.txt . + +# بهینه‌سازی ۲: استفاده از مکانیزم کش BuildKit برای pip +RUN --mount=type=cache,target=/root/.cache/pip \ + pip install --default-timeout=1000 -i https://package-mirror.liara.ir/repository/pypi/simple -r requirements.txt + +# ========================================== +# Stage 2: Final Production Image +# ========================================== +FROM python:3.11-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PATH="/opt/venv/bin:$PATH" + +WORKDIR /app + +# بهینه‌سازی ۳: ساخت کاربر سیستم غیر ریشه در بالاترین بخش برای حفظ کِش داکر +RUN useradd -m -u 1000 django + +# بهینه‌سازی ۴: استفاده از کش apt و ساخت دایرکتوری‌ها به صورت یکپارچه +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt-get update && apt-get install -y --no-install-recommends \ + libpq5 \ + supervisor && \ + mkdir -p /app/staticfiles /app/mediafiles /etc/supervisor /var/log/supervisor && \ + chown -R django:django /app /etc/supervisor /var/log/supervisor + +# کپی کردن و واگذاری مالکیت محیط مجازی به کاربر django +COPY --from=builder --chown=django:django /opt/venv /opt/venv + + +# کپی کردن تمام کدهای پروژه با مالکیت کاربر محدود شده +COPY --chown=django:django . . + +# حل مشکل خطای Permission Denied اسکریپت ورود +RUN chmod +x /app/entrypoint.sh + +# سوئیچ به کاربر غیر ریشه برای امنیت پروداکشن +USER django + +# اکسپوز پورت وب‌سرور برای وب‌پروکسی Coolify +EXPOSE 8000 + +# نقطه ورود اصلی کانتینر +ENTRYPOINT ["/app/entrypoint.sh"] \ No newline at end of file diff --git a/admin_panel/models.py b/admin_panel/models.py index aa0acce..4874720 100644 --- a/admin_panel/models.py +++ b/admin_panel/models.py @@ -1,6 +1,6 @@ from django.db import models from django.db.models import Sum -from django.db.models.signals import post_save, post_delete +# from django.db.models.signals import post_save, post_delete from django.dispatch import receiver from django.db import transaction # اصلاح فیکس خطای تراکنش from django.contrib.auth.models import User @@ -134,10 +134,7 @@ class ProductInStore(models.Model): Product = models.ForeignKey(Product, on_delete=models.CASCADE) s_in = models.IntegerField(default=0) s_out = models.IntegerField(default=0) - - # اضافه شدن این فیلد دیتابیسی، نیاز به کوئری‌های سنگین در تمپلیت را صفر می‌کند current_stock_at_time = models.IntegerField(default=0, help_text="موجودی انبار در لحظه این تراکنش") - desc = models.CharField(max_length=50, default='') year = models.IntegerField(default=1400) createdDT = models.DateTimeField(auto_now_add=True) @@ -147,6 +144,90 @@ class ProductInStore(models.Model): def __str__(self): return f"{self.Product.product_name} - In: {self.s_in} Out: {self.s_out}" + # --- متد Save کاملاً فیکس شده و ایمن برای محیط‌های چند کانتینری --- + 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) + except (ValueError, TypeError): + self.s_in, self.s_out = 0, 0 + + 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()) + + # ۳. محاسبه دقیق تغییرات فیزیکی انبار در صورت ویرایش رکورد قدیمی + if not is_created: + old_instance = ProductInStore.objects.get(pk=self.pk) + old_balance = old_instance.s_in - old_instance.s_out + new_balance = self.s_in - self.s_out + change_amount = new_balance - old_balance + description = self.desc or f"ویرایش تراکنش: {self.desc}" + else: + 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') + ) + total_in = totals['total_in'] or 0 + 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, + quantity_changed=change_amount, + current_stock=current_stock, + 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}" + + with transaction.atomic(): + # اجبار دیتابیس به قفل کردن ردیف‌ها قبل از حذف نهایی + list(ProductInStore.objects.filter(StoreRoom=store_room, Product=product).select_for_update()) + + # حذف فیزیکی رکورد تراکنش انبار + 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, + quantity_changed=change_amount, + current_stock=current_stock, + desc=description + ) + class ProductStockHistory(models.Model): store_room = models.ForeignKey(StoreRoom, on_delete=models.CASCADE, related_name='stock_histories') @@ -162,69 +243,6 @@ class ProductStockHistory(models.Model): def __str__(self): return f"{self.product.product_name} in {self.store_room.store_name} - Stock: {self.current_stock}" - -# تابع کمکی بهینه‌سازی شده برای محاسبه و ثبت همزمان در تراکنش و تاریخچه -def recalculate_and_log_stock(store_room, product, change_amount, description, current_instance=None): - with transaction.atomic(): - # قفل کردن ردیف‌ها برای همزمانی ایمن - qs = ProductInStore.objects.filter(StoreRoom=store_room, Product=product).select_for_update() - - totals = qs.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 - current_stock = total_in - total_out - - # اگر این تابع از سمت سِیو تراکنش صدا زده شده، فیلد لحظه‌ای آن را پر سرعتمان آپدیت می‌کنیم - if current_instance: - ProductInStore.objects.filter(pk=current_instance.pk).update(current_stock_at_time=current_stock) - - # ثبت ردیف در جدول تاریخچه کل - ProductStockHistory.objects.create( - store_room=store_room, - product=product, - quantity_changed=change_amount, - current_stock=current_stock, - desc=description - ) - - -@receiver(post_save, sender=ProductInStore) -def update_stock_history_on_save(sender, instance, created, **kwargs): - if created: - change = instance.s_in - instance.s_out - recalculate_and_log_stock( - store_room=instance.StoreRoom, - product=instance.Product, - change_amount=change, - description=instance.desc or "ثبت تراکنش جدید انبار", - current_instance=instance - ) - else: - recalculate_and_log_stock( - store_room=instance.StoreRoom, - product=instance.Product, - change_amount=0, - description=f"ویرایش تراکنش قبلی: {instance.desc}", - current_instance=instance - ) - - -@receiver(post_delete, sender=ProductInStore) -def update_stock_history_on_delete(sender, instance, **kwargs): - recalculate_and_log_stock( - store_room=instance.StoreRoom, - product=instance.Product, - change_amount=-(instance.s_in - instance.s_out), - description=f"حذف تراکنش قبلی انبار: {instance.desc}" - ) - - -# --- ادامه مدل‌های سیستم شما بدون تغییر بسامدی --- - class FactorIteam(models.Model): # پیشنهاد: در آینده املای نام این کلاس را به FactorItem اصلاح کنید factor = models.ForeignKey(Factor, on_delete=models.CASCADE) product = models.ForeignKey(Product, on_delete=models.CASCADE, null=True, blank=True) diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..02223ff --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# متوقف کردن اسکریپت در صورت بروز هرگونه خطا در مراحل آماده‌سازی +set -e + +echo "==> Collecting Static Files..." +# ۱. انتقال به ابتدای اسکریپت +python manage.py collectstatic --noinput --clear + +echo "==> Running Database Migrations..." +# ۲. این دستور معمولاً باید بعد از استاتیک‌فایل‌ها یا در مرحله‌ای مجزا اجرا شود +python manage.py migrate --noinput + +echo "==> Starting Gunicorn..." +exec gunicorn --bind 0.0.0.0:8000 config.wsgi:application \ No newline at end of file