diff --git a/admin_panel/admin.py b/admin_panel/admin.py index ef162ea..33abdbe 100644 --- a/admin_panel/admin.py +++ b/admin_panel/admin.py @@ -6,7 +6,6 @@ from .models import * admin.site.register(Customer) admin.site.register(PhoneNumber) -admin.site.register(StoreRoom) admin.site.register(ProductCategory) admin.site.register(Product) admin.site.register(ProductInStore) @@ -49,4 +48,30 @@ class StoreTransferRequestAdmin(admin.ModelAdmin): search_fields = ['id', 'desc', 'product__product_name', 'requester__username'] # ۶. غیرقابل ادیت کردن فیلدهای حساس مالی در پنل ادمین (اختیاری اما برای Enterprise الزامی است) - readonly_fields = ['created_at', 'updated_at'] \ No newline at end of file + readonly_fields = ['created_at', 'updated_at'] + +class StoreRoomOwnerInline(admin.TabularInline): + """ + این کلاس باعث می‌شود مالکین انبار به صورت یک جدول کوچک + در داخل همان صفحه مدیریت انبار نمایش داده شوند. + """ + model = StoreRoomOwner + extra = 1 # تعداد ردیف‌های خالی برای افزودن مالک جدید + verbose_name = "مالک انبار" + verbose_name_plural = "مالکین انبار" + +@admin.register(StoreRoom) +class StoreRoomAdmin(admin.ModelAdmin): + list_display = ('store_name', 'get_owners_count') + search_fields = ('store_name',) + + # فیلد user یک M2M معمولی است، پس filter_horizontal روی آن عالی کار می‌کند + filter_horizontal = ('user',) + + # فیلد owners چون جدول واسط دارد، باید از طریق Inline مدیریت شود + inlines = [StoreRoomOwnerInline] + + def get_owners_count(self, obj): + """نمایش تعداد مالکین در لیست اصلی انبارها""" + return obj.owners.count() + get_owners_count.short_description = "تعداد مالکین" \ No newline at end of file diff --git a/admin_panel/migrations/0051_auto_20260705_1356.py b/admin_panel/migrations/0051_auto_20260705_1356.py new file mode 100644 index 0000000..9dbb698 --- /dev/null +++ b/admin_panel/migrations/0051_auto_20260705_1356.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.25 on 2026-07-05 10:26 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('admin_panel', '0050_storetransferrequest'), + ] + + operations = [ + migrations.AlterModelOptions( + name='storetransferrequest', + options={'ordering': ['-created_at']}, + ), + migrations.AlterField( + model_name='storeroom', + name='user', + field=models.ManyToManyField(blank=True, help_text='از این برای دسترسی مشاهده آن اتبار استفاده می شود', to=settings.AUTH_USER_MODEL, verbose_name='دسترسی مشاهده انبار احتمالا'), + ), + ] diff --git a/admin_panel/migrations/0052_storeroomowner.py b/admin_panel/migrations/0052_storeroomowner.py new file mode 100644 index 0000000..c92201d --- /dev/null +++ b/admin_panel/migrations/0052_storeroomowner.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.25 on 2026-07-05 10:28 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('admin_panel', '0051_auto_20260705_1356'), + ] + + operations = [ + migrations.CreateModel( + name='StoreRoomOwner', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('storeroom', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='admin_panel.storeroom')), + ('user', models.OneToOneField(error_messages={'unique': 'این کاربر در حال حاضر مالک انبار دیگری است.'}, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/admin_panel/migrations/0053_storeroom_owners.py b/admin_panel/migrations/0053_storeroom_owners.py new file mode 100644 index 0000000..8160253 --- /dev/null +++ b/admin_panel/migrations/0053_storeroom_owners.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.25 on 2026-07-05 10:29 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('admin_panel', '0052_storeroomowner'), + ] + + operations = [ + migrations.AddField( + model_name='storeroom', + name='owners', + field=models.ManyToManyField(blank=True, help_text='این کاربران اگر فاکتور بزنند از این انبار کسر می\u200cگردد', related_name='owned_storerooms', through='admin_panel.StoreRoomOwner', to=settings.AUTH_USER_MODEL, verbose_name='مالکین انبار در فاکتور و موجودی و...'), + ), + ] diff --git a/admin_panel/models.py b/admin_panel/models.py index 3194cec..cc646b6 100644 --- a/admin_panel/models.py +++ b/admin_panel/models.py @@ -71,12 +71,40 @@ class StoreRoom(models.Model): ("DelStore", "DelStore") ] store_name = models.CharField(max_length=200) - user = models.ManyToManyField(User, blank=True) + user = models.ManyToManyField(User, + blank=True, + verbose_name="دسترسی مشاهده انبار احتمالا", + help_text="از این برای دسترسی مشاهده آن اتبار استفاده می شود" + ) + owners = models.ManyToManyField( + User, + through='StoreRoomOwner', # کلید حل مشکل بدون شکستن کدهای قبلی + blank=True, + related_name="owned_storerooms", + verbose_name="مالکین انبار در فاکتور و موجودی و...", + help_text="این کاربران اگر فاکتور بزنند از این انبار کسر می‌گردد" + ) def __str__(self): return self.store_name +class StoreRoomOwner(models.Model): + """ + این جدول واسط، پشت صحنه قرار می‌گیرد تا فقط قانون مالکیت را مدیریت کند. + کدهای دیگر پروژه اصلاً نیازی ندارند مستقیم با این جدول درگیر شوند. + """ + storeroom = models.ForeignKey(StoreRoom, on_delete=models.CASCADE) + + # OneToOneField تضمین می‌کند یک کاربر نمی‌تواند در این جدول (و هیچ انبار دیگری) تکرار شود + user = models.OneToOneField( + User, + on_delete=models.CASCADE, + error_messages={ + 'unique': 'این کاربر در حال حاضر مالک انبار دیگری است.' + } + ) + class ProductCategory(models.Model): category_name = models.CharField(max_length=50) diff --git a/admin_panel/views.py b/admin_panel/views.py index fb83d52..6652522 100644 --- a/admin_panel/views.py +++ b/admin_panel/views.py @@ -739,21 +739,28 @@ 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) - # ۱. تجمیع ورودی و خروجی در یک کوئری بهینه برای افزایش سرعت دیتابیس + # پیدا کردن انبار کاربر با متد filter به جای get برای جلوگیری از کرش سیستم + store_user = StoreRoom.objects.filter(owners=request.user).first() + + # اگر کاربر هنوز مالک هیچ انباری نشده است + if not store_user: + return HttpResponse("کاربر مالکیت انبار ندارد") # یا هر مقداری که فرانت‌اند انتظار دارد + + # محاسبه موجودی برای انبار پیدا شده totals = ProductInStore.objects.filter( - Product__id=p_id, StoreRoom=storeUser, year=1400 - ).aggregate(total_in=Sum('s_in'), total_out=Sum('s_out')) + Product_id=p_id, + StoreRoom=store_user, + 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/') @@ -1455,7 +1462,7 @@ def newFactor2(request, c_id): 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, user=user_op) + store_room = get_object_or_404(StoreRoom, owners=user_op) # رفع مشکل متنی/عددی در استخراج شناسه کالاها product_ids = []