refactor
This commit is contained in:
1 parent
6f36db63a1
commit
250cdbab7f
42 files changed
+96363
-11699
No files matched your search
@@ -6,6 +6,8 @@ data/*.sqlite3
|
||||
*.sqlite3
|
||||
*.zip
|
||||
data9
|
||||
.venv
|
||||
.venv/*
|
||||
|
||||
# graphify: ignore regenerable cache and dated backups, keep the graph itself
|
||||
graphify-out/cache/
|
||||
|
||||
@@ -12,11 +12,13 @@ data9/
|
||||
*.zip
|
||||
khatibi.zip
|
||||
db.json
|
||||
PROJECT_TREE.txt
|
||||
|
||||
# Python caches
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Backups and scratch copies
|
||||
backups/
|
||||
admin_panel/views - Copy.py
|
||||
admin_panel/viewsBK.py
|
||||
+49
-9
@@ -1,11 +1,11 @@
|
||||
Project Root: khatibi
|
||||
Generated At: 2026-10-09T13:46:35+00:00
|
||||
Python Files: 95
|
||||
Django Files: 13
|
||||
Generated At: 2026-10-09T15:40:01+00:00
|
||||
Python Files: 126
|
||||
Django Files: 16
|
||||
JavaScript/TypeScript Files: 14
|
||||
HTML Files: 50
|
||||
Total Files: 412
|
||||
Total Directories: 66
|
||||
Total Files: 447
|
||||
Total Directories: 71
|
||||
|
||||
khatibi
|
||||
├── .cursor
|
||||
@@ -95,6 +95,16 @@ khatibi
|
||||
│ │ ├── 0055_action_nextnazardt.py
|
||||
│ │ ├── 0056_auto_20260731_1824.py
|
||||
│ │ └── __init__.py
|
||||
│ ├── models
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── actions.py
|
||||
│ │ ├── customers.py
|
||||
│ │ ├── factors.py
|
||||
│ │ ├── finance.py
|
||||
│ │ ├── products.py
|
||||
│ │ ├── store.py
|
||||
│ │ ├── system.py
|
||||
│ │ └── users.py
|
||||
│ ├── static
|
||||
│ │ └── admin_panel
|
||||
│ │ ├── DataTables
|
||||
@@ -230,22 +240,42 @@ khatibi
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── custom_tags.py
|
||||
│ │ └── remove_q_p.py
|
||||
│ ├── views
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── _helpers.py
|
||||
│ │ ├── actions.py
|
||||
│ │ ├── auth.py
|
||||
│ │ ├── cheque_inquiry.py
|
||||
│ │ ├── customers.py
|
||||
│ │ ├── factors.py
|
||||
│ │ ├── finance.py
|
||||
│ │ └── store.py
|
||||
│ ├── __init__.py
|
||||
│ ├── admin.py
|
||||
│ ├── apps.py
|
||||
│ ├── context_processor.py
|
||||
│ ├── models.py
|
||||
│ ├── services.py
|
||||
│ ├── tests.py
|
||||
│ ├── urls.py
|
||||
│ ├── views - Copy.py
|
||||
│ ├── views.py
|
||||
│ └── viewsBK.py
|
||||
├── backups
|
||||
│ └── refactor-2026-10-09
|
||||
│ ├── admin.py
|
||||
│ ├── apps.py
|
||||
│ ├── context_processor.py
|
||||
│ ├── MANIFEST.sha256
|
||||
│ ├── models.py
|
||||
│ ├── services.py
|
||||
│ ├── urls.py
|
||||
│ └── views.py
|
||||
├── data9
|
||||
│ ├── db2 (2).sqlite32
|
||||
│ ├── db2.sqlite32
|
||||
│ ├── db222.sqlite3
|
||||
│ └── kt.sqlite3
|
||||
│ ├── kt.sqlite3
|
||||
│ ├── kt.sqlite3-shm
|
||||
│ └── kt.sqlite3-wal
|
||||
├── graphify-out
|
||||
│ ├── 2026-10-09
|
||||
│ │ ├── .graphify_analysis.json
|
||||
@@ -467,6 +497,15 @@ khatibi
|
||||
│ └── wsgi.py
|
||||
├── static
|
||||
│ └── .gitkeep
|
||||
├── tests
|
||||
│ ├── __init__.py
|
||||
│ ├── factories.py
|
||||
│ ├── test_customers.py
|
||||
│ ├── test_factors.py
|
||||
│ ├── test_inventory.py
|
||||
│ ├── test_models_contract.py
|
||||
│ ├── test_urls_contract.py
|
||||
│ └── test_view_smoke.py
|
||||
├── .dockerignore
|
||||
├── .gitignore
|
||||
├── .graphifyignore
|
||||
@@ -485,4 +524,5 @@ khatibi
|
||||
├── project_tree.py
|
||||
├── reconciliation_tool.py
|
||||
├── requirements.txt
|
||||
└── requirements2.txt
|
||||
├── requirements2.txt
|
||||
└── ripwire - README.md
|
||||
@@ -1,515 +0,0 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
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, connection # اصلاح فیکس خطای تراکنش
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.validators import RegexValidator
|
||||
|
||||
userModel = get_user_model()
|
||||
|
||||
|
||||
# اصلاح مدل کاربر: تبدیل به پروفایل برای حفظ امنیت و اتصال به سیستم پیشفرض جنگو
|
||||
class UserProfile(models.Model):
|
||||
UserTypeList = [
|
||||
('admin', 'admin'),
|
||||
('office', 'office'),
|
||||
('operator', 'operator'),
|
||||
]
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
|
||||
user_type = models.CharField(max_length=10, choices=UserTypeList)
|
||||
name_family = models.CharField(max_length=200)
|
||||
create_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username} ({self.user_type})"
|
||||
|
||||
|
||||
class Customer(models.Model):
|
||||
class Meta:
|
||||
permissions = [
|
||||
("AddCustomer", "AddCustomer"),
|
||||
("DelCustomer", "DelCustomer"),
|
||||
("EditCustomer", "EditCustomer"),
|
||||
("ViewCustomer", "ViewCustomer")
|
||||
]
|
||||
name_family = models.CharField(max_length=200)
|
||||
address = models.CharField(max_length=200)
|
||||
phone1 = models.CharField(max_length=20, default='')
|
||||
phone2 = models.CharField(max_length=20, default='', null=True, blank=True)
|
||||
phone3 = models.CharField(max_length=20, default='', null=True, blank=True)
|
||||
idCode = models.CharField(max_length=20, default='', null=True, blank=True)
|
||||
installDate = models.DateField(auto_now_add=True)
|
||||
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, related_name='phones')
|
||||
title = models.CharField(max_length=50, default='تلفن', blank=True, null=True, help_text="عنوان شماره (مثل: موبایل پدر)")
|
||||
phone_number = models.CharField(max_length=20)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.title}: {self.phone_number}"
|
||||
|
||||
class StoreRoom(models.Model):
|
||||
class Meta:
|
||||
permissions = [
|
||||
("ViewStore", "ViewStore"),
|
||||
("AddStore", "AddStore"),
|
||||
("EditStore", "EditStore"),
|
||||
("DelStore", "DelStore")
|
||||
]
|
||||
store_name = models.CharField(max_length=200)
|
||||
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)
|
||||
|
||||
def __str__(self):
|
||||
return self.category_name
|
||||
|
||||
|
||||
class Product(models.Model):
|
||||
class Meta:
|
||||
permissions = [
|
||||
("ViewProduct", "ViewProduct"),
|
||||
]
|
||||
ProductCategory = models.ForeignKey(ProductCategory, on_delete=models.CASCADE)
|
||||
product_name = models.CharField(max_length=50)
|
||||
product_price = models.IntegerField()
|
||||
year = models.IntegerField(default=1400)
|
||||
createdDT = models.DateTimeField(auto_now_add=True)
|
||||
changeDT = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.product_name
|
||||
|
||||
|
||||
class Factor(models.Model):
|
||||
class Meta:
|
||||
permissions = [
|
||||
("ViewFactor", "ViewFactor"),
|
||||
("DelFactor", "DelFactor"),
|
||||
("EditFactor", "EditFactor"),
|
||||
("AddFactor", "AddFactor")
|
||||
]
|
||||
customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
|
||||
create_date = models.DateTimeField(auto_now_add=True)
|
||||
amount = models.IntegerField()
|
||||
discount = models.IntegerField(null=True, blank=True)
|
||||
deadline = models.DateTimeField(null=True, blank=True)
|
||||
desc = models.CharField(max_length=100, default='')
|
||||
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
statusList = [
|
||||
('unpaid', 'unpaid'),
|
||||
('paid', 'paid'),
|
||||
('cancell', 'cancell'),
|
||||
]
|
||||
pay_status = models.CharField(max_length=10, choices=statusList, default='')
|
||||
FactorTypeList = [
|
||||
('Nasb', 'Nasb'),
|
||||
('Khadamat', 'Khadamat'),
|
||||
('Garranty', 'Garranty'),
|
||||
]
|
||||
FactorType = models.CharField(max_length=10, choices=FactorTypeList, default='')
|
||||
PayTypeList = [
|
||||
('naghd', 'naghd'),
|
||||
('cardToCard', 'cardToCard'),
|
||||
('kartKhan', 'kartKhan'),
|
||||
('nesiye', 'nesiye'),
|
||||
('check', 'check'),
|
||||
]
|
||||
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 Meta:
|
||||
permissions = [
|
||||
("ViewProductInStore", "ViewProductInStore"),
|
||||
("EditProductInStore", "EditProductInStore"),
|
||||
("OnlyAnbar", "OnlyAnbar"),
|
||||
]
|
||||
class MovementType(models.TextChoices):
|
||||
SALE = 'SALE', 'فروش عادی'
|
||||
WARRANTY = 'WARRANTY', 'گارانتی'
|
||||
PROMOTION = 'PROMOTION', 'اشانتیون'
|
||||
TRANSFER = 'TRANSFER', 'انتقال'
|
||||
INITIAL = 'INITIAL', 'موجودی اولیه'
|
||||
DIRECT_IN = 'DIRECT_IN', 'ورود دستی'
|
||||
DIRECT_OUT = 'DIRECT_OUT', 'خروج دستی'
|
||||
RETURN = 'RETURN', 'برگشت از فروش'
|
||||
ERROR_CORRECTION = 'ERROR_CORRECTION', 'اصلاح خطای ثبت'
|
||||
|
||||
StoreRoom = models.ForeignKey(StoreRoom, on_delete=models.CASCADE)
|
||||
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)
|
||||
changeDT = models.DateTimeField(auto_now=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 و delete دقیقاً همان کدهای بینقص و پرسرعتی هستند که در پیام قبل تایید کردیم
|
||||
def save(self, *args, **kwargs):
|
||||
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():
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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():
|
||||
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,
|
||||
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')
|
||||
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='stock_histories')
|
||||
quantity_changed = models.IntegerField(help_text="مقدار تغییر یافته (مثبت برای ورود، منفی برای خروج)")
|
||||
current_stock = models.IntegerField(help_text="موجودی کل انبار در این لحظه")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
desc = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.product.product_name} in {self.store_room.store_name} - Stock: {self.current_stock}"
|
||||
|
||||
|
||||
class StoreTransferRequest(models.Model):
|
||||
class Status(models.TextChoices):
|
||||
PENDING = 'PENDING', 'در انتظار تایید انبار مقصد'
|
||||
APPROVED = 'APPROVED', 'تایید شده و منتقل شده'
|
||||
REJECTED = 'REJECTED', 'رد شده توسط انبار مقصد'
|
||||
CANCELED = 'CANCELED', 'انصراف توسط مبدا'
|
||||
|
||||
source_store = models.ForeignKey(
|
||||
'StoreRoom',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='outgoing_transfers',
|
||||
verbose_name="انبار مبدا"
|
||||
)
|
||||
target_store = models.ForeignKey(
|
||||
'StoreRoom',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='incoming_transfers',
|
||||
verbose_name="انبار مقصد"
|
||||
)
|
||||
product = models.ForeignKey(
|
||||
'Product',
|
||||
on_delete=models.PROTECT,
|
||||
verbose_name="کالا"
|
||||
)
|
||||
quantity = models.PositiveIntegerField(verbose_name="تعداد انتقال")
|
||||
|
||||
status = models.CharField(
|
||||
max_length=15,
|
||||
choices=Status.choices,
|
||||
default=Status.PENDING,
|
||||
db_index=True,
|
||||
verbose_name="وضعیت"
|
||||
)
|
||||
desc = models.CharField(max_length=500, blank=True, null=True, verbose_name="توضیحات درخواست")
|
||||
rejection_reason = models.CharField(max_length=500, blank=True, null=True, verbose_name="دلیل رد درخواست")
|
||||
|
||||
requester = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='requested_transfers',
|
||||
verbose_name="کاربر درخواستدهنده"
|
||||
)
|
||||
approver = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='processed_transfers',
|
||||
verbose_name="کاربر تعیینتکلیفکننده"
|
||||
)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True, db_index=True, verbose_name="زمان ثبت درخواست")
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="زمان آخرین بهروزرسانی")
|
||||
action_date = models.DateTimeField(null=True, blank=True, db_index=True, verbose_name="زمان تعیین تکلیف")
|
||||
|
||||
out_transaction = models.ForeignKey(
|
||||
'ProductInStore',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='transfer_out_ref',
|
||||
verbose_name="سند خروج انبار مبدا"
|
||||
)
|
||||
in_transaction = models.ForeignKey(
|
||||
'ProductInStore',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='transfer_in_ref',
|
||||
verbose_name="سند ورود انبار مقصد"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"[{self.get_status_display()}] {self.quantity} عدد {self.product.product_name} از {self.source_store.store_name} به {self.target_store.store_name}"
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if self.source_store_id == self.target_store_id:
|
||||
raise ValidationError("انبار مبدا و مقصد نمیتوانند یکسان باشند.")
|
||||
if self.quantity and self.quantity <= 0:
|
||||
raise ValidationError("تعداد انتقال باید بیشتر از صفر باشد.")
|
||||
|
||||
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)
|
||||
product_name = models.CharField(max_length=200, default='')
|
||||
num = models.IntegerField()
|
||||
amount = models.IntegerField()
|
||||
garantyEsh = models.CharField(max_length=100, default='', null=True, blank=True)
|
||||
|
||||
|
||||
class VisitQueue(models.Model):
|
||||
customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
|
||||
dateTime = models.DateTimeField()
|
||||
statusList = [
|
||||
('waiting', 'waiting'),
|
||||
('finish', 'finish'),
|
||||
('cancell', 'cancell'),
|
||||
]
|
||||
status = models.CharField(max_length=10, choices=statusList, default='waiting')
|
||||
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
GradeList = [
|
||||
('3', '3'),
|
||||
('2', '2'),
|
||||
('1', '1'),
|
||||
]
|
||||
grade = models.CharField(max_length=1, choices=GradeList)
|
||||
grade_desc = models.CharField(max_length=200)
|
||||
|
||||
|
||||
class ArticleType(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
|
||||
|
||||
class Article(models.Model):
|
||||
class Meta:
|
||||
permissions = [
|
||||
("ViewReportMali", "ViewReportMali")
|
||||
]
|
||||
debit = models.IntegerField(null=True, blank=True)
|
||||
credit = models.IntegerField(null=True, blank=True)
|
||||
customer = models.ForeignKey(Customer, on_delete=models.CASCADE, null=True, blank=True)
|
||||
factor = models.ForeignKey(Factor, on_delete=models.CASCADE, null=True, blank=True)
|
||||
type = models.CharField(max_length=50, null=True, blank=True)
|
||||
typePy = models.CharField(max_length=50, null=True, blank=True)
|
||||
checkNum = models.IntegerField(null=True, blank=True)
|
||||
nahvePardakht = models.CharField(max_length=50, null=True, blank=True)
|
||||
sarresid = models.DateTimeField(null=True, blank=True)
|
||||
payStatus = models.CharField(max_length=50, null=True, blank=True)
|
||||
createdDT = models.DateTimeField(auto_now_add=True)
|
||||
changeDT = models.DateTimeField(auto_now=True)
|
||||
desc = models.CharField(max_length=200)
|
||||
user = models.ForeignKey(userModel, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
|
||||
|
||||
class Action(models.Model):
|
||||
actionType = models.CharField(max_length=50, null=True, blank=True)
|
||||
forUser = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
customer = models.ForeignKey(Customer, related_name='actionrlt', on_delete=models.CASCADE, null=True, blank=True)
|
||||
createdDT = models.DateTimeField(auto_now_add=True)
|
||||
changeDT = models.DateTimeField(auto_now=True)
|
||||
actionDateTime = models.DateTimeField(null=True, blank=True)
|
||||
finishDateTime = models.DateTimeField(null=True, blank=True)
|
||||
desc = models.CharField(max_length=500, null=True, blank=True)
|
||||
result = models.CharField(max_length=500, null=True, blank=True)
|
||||
status = models.CharField(max_length=50, null=True, blank=True)
|
||||
nazar = models.CharField(max_length=50, null=True, blank=True, default=None)
|
||||
nextNazarDT = models.DateTimeField(null=True, blank=True, default=None)
|
||||
nazarDT = models.DateTimeField(null=True, blank=True, default=None)
|
||||
grade = models.IntegerField(null=True, blank=True, default=None)
|
||||
|
||||
|
||||
class Sarresid(models.Model):
|
||||
customer = models.ForeignKey('Customer', on_delete=models.CASCADE, null=True, blank=True)
|
||||
createdDT = models.DateTimeField(auto_now_add=True)
|
||||
changeDT = models.DateTimeField(auto_now=True)
|
||||
price = models.IntegerField(null=False, blank=False)
|
||||
payStatus = models.CharField(max_length=50, null=True, blank=True)
|
||||
type = models.CharField(max_length=50, null=True, blank=True)
|
||||
factor = models.ForeignKey('Factor', on_delete=models.CASCADE, null=True, blank=True)
|
||||
sarresidDT = models.DateTimeField(null=True, blank=True)
|
||||
user = models.ForeignKey(userModel, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
desc = models.TextField(null=True, blank=True)
|
||||
|
||||
# ---------------- فیلدهای استاندارد و ایمنشده چک ----------------
|
||||
check_row_number = models.CharField(
|
||||
max_length=50,
|
||||
null=True, blank=True,
|
||||
verbose_name="شماره ردیف چک"
|
||||
)
|
||||
check_sayyad_id = models.CharField(
|
||||
max_length=16,
|
||||
validators=[
|
||||
RegexValidator(regex=r'^\d{16}$', message='شناسه صیادی باید دقیقاً ۱۶ رقم عدد باشد.')
|
||||
],
|
||||
null=True, blank=True,
|
||||
verbose_name="شناسه صیادی"
|
||||
)
|
||||
check_owner_name = models.CharField(
|
||||
max_length=100,
|
||||
null=True, blank=True,
|
||||
verbose_name="نام مالک چک"
|
||||
)
|
||||
check_owner_phone = models.CharField(
|
||||
max_length=11,
|
||||
validators=[
|
||||
RegexValidator(regex=r'^\d{11}$', message='شماره تلفن باید ۱۱ رقم و فقط شامل اعداد باشد.')
|
||||
],
|
||||
null=True, blank=True,
|
||||
verbose_name="تلفن مالک چک"
|
||||
)
|
||||
|
||||
class CacheTable(models.Model):
|
||||
key = models.CharField(max_length=50, unique=True)
|
||||
value = models.CharField(max_length=400)
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.key} # {self.value}'
|
||||
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
پکیج مدلهای admin_panel.
|
||||
|
||||
این پکیج جایگزین فایل تکپارچهٔ models.py است و آن را به ماژولهای کوچکتر
|
||||
دامنهای تقسیم میکند. همهٔ نامها اینجا re-export میشوند تا تمام
|
||||
مصرفکنندههای فعلی (`from .models import *`, `from admin_panel.models import X`)
|
||||
بدون هیچ تغییری کار کنند و app_label و مایگریشنها ثابت بمانند.
|
||||
|
||||
نکته: عمداً __all__ تعریف نشده تا رفتار star-import دقیقاً مانند فایل قبلی باشد
|
||||
(نامهایی مثل `models`, `User`, `Sum` نیز مثل قبل منتشر میشوند).
|
||||
"""
|
||||
from .users import * # noqa: F401,F403
|
||||
from .customers import * # noqa: F401,F403
|
||||
from .products import * # noqa: F401,F403
|
||||
from .store import * # noqa: F401,F403
|
||||
from .factors import * # noqa: F401,F403
|
||||
from .finance import * # noqa: F401,F403
|
||||
from .actions import * # noqa: F401,F403
|
||||
from .system import * # noqa: F401,F403
|
||||
@@ -0,0 +1,38 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
class VisitQueue(models.Model):
|
||||
customer = models.ForeignKey('Customer', on_delete=models.CASCADE)
|
||||
dateTime = models.DateTimeField()
|
||||
statusList = [
|
||||
('waiting', 'waiting'),
|
||||
('finish', 'finish'),
|
||||
('cancell', 'cancell'),
|
||||
]
|
||||
status = models.CharField(max_length=10, choices=statusList, default='waiting')
|
||||
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
GradeList = [
|
||||
('3', '3'),
|
||||
('2', '2'),
|
||||
('1', '1'),
|
||||
]
|
||||
grade = models.CharField(max_length=1, choices=GradeList)
|
||||
grade_desc = models.CharField(max_length=200)
|
||||
|
||||
|
||||
class Action(models.Model):
|
||||
actionType = models.CharField(max_length=50, null=True, blank=True)
|
||||
forUser = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
customer = models.ForeignKey('Customer', related_name='actionrlt', on_delete=models.CASCADE, null=True, blank=True)
|
||||
createdDT = models.DateTimeField(auto_now_add=True)
|
||||
changeDT = models.DateTimeField(auto_now=True)
|
||||
actionDateTime = models.DateTimeField(null=True, blank=True)
|
||||
finishDateTime = models.DateTimeField(null=True, blank=True)
|
||||
desc = models.CharField(max_length=500, null=True, blank=True)
|
||||
result = models.CharField(max_length=500, null=True, blank=True)
|
||||
status = models.CharField(max_length=50, null=True, blank=True)
|
||||
nazar = models.CharField(max_length=50, null=True, blank=True, default=None)
|
||||
nextNazarDT = models.DateTimeField(null=True, blank=True, default=None)
|
||||
nazarDT = models.DateTimeField(null=True, blank=True, default=None)
|
||||
grade = models.IntegerField(null=True, blank=True, default=None)
|
||||
@@ -0,0 +1,42 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Customer(models.Model):
|
||||
class Meta:
|
||||
permissions = [
|
||||
("AddCustomer", "AddCustomer"),
|
||||
("DelCustomer", "DelCustomer"),
|
||||
("EditCustomer", "EditCustomer"),
|
||||
("ViewCustomer", "ViewCustomer")
|
||||
]
|
||||
name_family = models.CharField(max_length=200)
|
||||
address = models.CharField(max_length=200)
|
||||
phone1 = models.CharField(max_length=20, default='')
|
||||
phone2 = models.CharField(max_length=20, default='', null=True, blank=True)
|
||||
phone3 = models.CharField(max_length=20, default='', null=True, blank=True)
|
||||
idCode = models.CharField(max_length=20, default='', null=True, blank=True)
|
||||
installDate = models.DateField(auto_now_add=True)
|
||||
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, related_name='phones')
|
||||
title = models.CharField(max_length=50, default='تلفن', blank=True, null=True, help_text="عنوان شماره (مثل: موبایل پدر)")
|
||||
phone_number = models.CharField(max_length=20)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.title}: {self.phone_number}"
|
||||
@@ -0,0 +1,58 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
class Factor(models.Model):
|
||||
class Meta:
|
||||
permissions = [
|
||||
("ViewFactor", "ViewFactor"),
|
||||
("DelFactor", "DelFactor"),
|
||||
("EditFactor", "EditFactor"),
|
||||
("AddFactor", "AddFactor")
|
||||
]
|
||||
customer = models.ForeignKey('Customer', on_delete=models.CASCADE)
|
||||
create_date = models.DateTimeField(auto_now_add=True)
|
||||
amount = models.IntegerField()
|
||||
discount = models.IntegerField(null=True, blank=True)
|
||||
deadline = models.DateTimeField(null=True, blank=True)
|
||||
desc = models.CharField(max_length=100, default='')
|
||||
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
statusList = [
|
||||
('unpaid', 'unpaid'),
|
||||
('paid', 'paid'),
|
||||
('cancell', 'cancell'),
|
||||
]
|
||||
pay_status = models.CharField(max_length=10, choices=statusList, default='')
|
||||
FactorTypeList = [
|
||||
('Nasb', 'Nasb'),
|
||||
('Khadamat', 'Khadamat'),
|
||||
('Garranty', 'Garranty'),
|
||||
]
|
||||
FactorType = models.CharField(max_length=10, choices=FactorTypeList, default='')
|
||||
PayTypeList = [
|
||||
('naghd', 'naghd'),
|
||||
('cardToCard', 'cardToCard'),
|
||||
('kartKhan', 'kartKhan'),
|
||||
('nesiye', 'nesiye'),
|
||||
('check', 'check'),
|
||||
]
|
||||
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 FactorIteam(models.Model): # پیشنهاد: در آینده املای نام این کلاس را به FactorItem اصلاح کنید
|
||||
factor = models.ForeignKey(Factor, on_delete=models.CASCADE)
|
||||
product = models.ForeignKey('Product', on_delete=models.CASCADE, null=True, blank=True)
|
||||
product_name = models.CharField(max_length=200, default='')
|
||||
num = models.IntegerField()
|
||||
amount = models.IntegerField()
|
||||
garantyEsh = models.CharField(max_length=100, default='', null=True, blank=True)
|
||||
@@ -0,0 +1,71 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.validators import RegexValidator
|
||||
|
||||
userModel = get_user_model()
|
||||
|
||||
|
||||
class ArticleType(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
|
||||
|
||||
class Article(models.Model):
|
||||
class Meta:
|
||||
permissions = [
|
||||
("ViewReportMali", "ViewReportMali")
|
||||
]
|
||||
debit = models.IntegerField(null=True, blank=True)
|
||||
credit = models.IntegerField(null=True, blank=True)
|
||||
customer = models.ForeignKey('Customer', on_delete=models.CASCADE, null=True, blank=True)
|
||||
factor = models.ForeignKey('Factor', on_delete=models.CASCADE, null=True, blank=True)
|
||||
type = models.CharField(max_length=50, null=True, blank=True)
|
||||
typePy = models.CharField(max_length=50, null=True, blank=True)
|
||||
checkNum = models.IntegerField(null=True, blank=True)
|
||||
nahvePardakht = models.CharField(max_length=50, null=True, blank=True)
|
||||
sarresid = models.DateTimeField(null=True, blank=True)
|
||||
payStatus = models.CharField(max_length=50, null=True, blank=True)
|
||||
createdDT = models.DateTimeField(auto_now_add=True)
|
||||
changeDT = models.DateTimeField(auto_now=True)
|
||||
desc = models.CharField(max_length=200)
|
||||
user = models.ForeignKey(userModel, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
|
||||
|
||||
class Sarresid(models.Model):
|
||||
customer = models.ForeignKey('Customer', on_delete=models.CASCADE, null=True, blank=True)
|
||||
createdDT = models.DateTimeField(auto_now_add=True)
|
||||
changeDT = models.DateTimeField(auto_now=True)
|
||||
price = models.IntegerField(null=False, blank=False)
|
||||
payStatus = models.CharField(max_length=50, null=True, blank=True)
|
||||
type = models.CharField(max_length=50, null=True, blank=True)
|
||||
factor = models.ForeignKey('Factor', on_delete=models.CASCADE, null=True, blank=True)
|
||||
sarresidDT = models.DateTimeField(null=True, blank=True)
|
||||
user = models.ForeignKey(userModel, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
desc = models.TextField(null=True, blank=True)
|
||||
|
||||
# ---------------- فیلدهای استاندارد و ایمنشده چک ----------------
|
||||
check_row_number = models.CharField(
|
||||
max_length=50,
|
||||
null=True, blank=True,
|
||||
verbose_name="شماره ردیف چک"
|
||||
)
|
||||
check_sayyad_id = models.CharField(
|
||||
max_length=16,
|
||||
validators=[
|
||||
RegexValidator(regex=r'^\d{16}$', message='شناسه صیادی باید دقیقاً ۱۶ رقم عدد باشد.')
|
||||
],
|
||||
null=True, blank=True,
|
||||
verbose_name="شناسه صیادی"
|
||||
)
|
||||
check_owner_name = models.CharField(
|
||||
max_length=100,
|
||||
null=True, blank=True,
|
||||
verbose_name="نام مالک چک"
|
||||
)
|
||||
check_owner_phone = models.CharField(
|
||||
max_length=11,
|
||||
validators=[
|
||||
RegexValidator(regex=r'^\d{11}$', message='شماره تلفن باید ۱۱ رقم و فقط شامل اعداد باشد.')
|
||||
],
|
||||
null=True, blank=True,
|
||||
verbose_name="تلفن مالک چک"
|
||||
)
|
||||
@@ -0,0 +1,148 @@
|
||||
from django.db import models, transaction, connection
|
||||
from django.db.models import Sum
|
||||
|
||||
|
||||
class ProductCategory(models.Model):
|
||||
category_name = models.CharField(max_length=50)
|
||||
|
||||
def __str__(self):
|
||||
return self.category_name
|
||||
|
||||
|
||||
class Product(models.Model):
|
||||
class Meta:
|
||||
permissions = [
|
||||
("ViewProduct", "ViewProduct"),
|
||||
]
|
||||
ProductCategory = models.ForeignKey(ProductCategory, on_delete=models.CASCADE)
|
||||
product_name = models.CharField(max_length=50)
|
||||
product_price = models.IntegerField()
|
||||
year = models.IntegerField(default=1400)
|
||||
createdDT = models.DateTimeField(auto_now_add=True)
|
||||
changeDT = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.product_name
|
||||
|
||||
|
||||
class ProductInStore(models.Model):
|
||||
class Meta:
|
||||
permissions = [
|
||||
("ViewProductInStore", "ViewProductInStore"),
|
||||
("EditProductInStore", "EditProductInStore"),
|
||||
("OnlyAnbar", "OnlyAnbar"),
|
||||
]
|
||||
class MovementType(models.TextChoices):
|
||||
SALE = 'SALE', 'فروش عادی'
|
||||
WARRANTY = 'WARRANTY', 'گارانتی'
|
||||
PROMOTION = 'PROMOTION', 'اشانتیون'
|
||||
TRANSFER = 'TRANSFER', 'انتقال'
|
||||
INITIAL = 'INITIAL', 'موجودی اولیه'
|
||||
DIRECT_IN = 'DIRECT_IN', 'ورود دستی'
|
||||
DIRECT_OUT = 'DIRECT_OUT', 'خروج دستی'
|
||||
RETURN = 'RETURN', 'برگشت از فروش'
|
||||
ERROR_CORRECTION = 'ERROR_CORRECTION', 'اصلاح خطای ثبت'
|
||||
|
||||
StoreRoom = models.ForeignKey('StoreRoom', on_delete=models.CASCADE)
|
||||
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)
|
||||
changeDT = models.DateTimeField(auto_now=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 و delete دقیقاً همان کدهای بینقص و پرسرعتی هستند که در پیام قبل تایید کردیم
|
||||
def save(self, *args, **kwargs):
|
||||
from .store import ProductStockHistory
|
||||
|
||||
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():
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
from .store import ProductStockHistory
|
||||
|
||||
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():
|
||||
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,
|
||||
quantity_changed=change_amount,
|
||||
current_stock=current_stock,
|
||||
desc=description
|
||||
)
|
||||
@@ -0,0 +1,148 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
class StoreRoom(models.Model):
|
||||
class Meta:
|
||||
permissions = [
|
||||
("ViewStore", "ViewStore"),
|
||||
("AddStore", "AddStore"),
|
||||
("EditStore", "EditStore"),
|
||||
("DelStore", "DelStore")
|
||||
]
|
||||
store_name = models.CharField(max_length=200)
|
||||
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 ProductStockHistory(models.Model):
|
||||
store_room = models.ForeignKey(StoreRoom, on_delete=models.CASCADE, related_name='stock_histories')
|
||||
product = models.ForeignKey('Product', on_delete=models.CASCADE, related_name='stock_histories')
|
||||
quantity_changed = models.IntegerField(help_text="مقدار تغییر یافته (مثبت برای ورود، منفی برای خروج)")
|
||||
current_stock = models.IntegerField(help_text="موجودی کل انبار در این لحظه")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
desc = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.product.product_name} in {self.store_room.store_name} - Stock: {self.current_stock}"
|
||||
|
||||
|
||||
class StoreTransferRequest(models.Model):
|
||||
class Status(models.TextChoices):
|
||||
PENDING = 'PENDING', 'در انتظار تایید انبار مقصد'
|
||||
APPROVED = 'APPROVED', 'تایید شده و منتقل شده'
|
||||
REJECTED = 'REJECTED', 'رد شده توسط انبار مقصد'
|
||||
CANCELED = 'CANCELED', 'انصراف توسط مبدا'
|
||||
|
||||
source_store = models.ForeignKey(
|
||||
'StoreRoom',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='outgoing_transfers',
|
||||
verbose_name="انبار مبدا"
|
||||
)
|
||||
target_store = models.ForeignKey(
|
||||
'StoreRoom',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='incoming_transfers',
|
||||
verbose_name="انبار مقصد"
|
||||
)
|
||||
product = models.ForeignKey(
|
||||
'Product',
|
||||
on_delete=models.PROTECT,
|
||||
verbose_name="کالا"
|
||||
)
|
||||
quantity = models.PositiveIntegerField(verbose_name="تعداد انتقال")
|
||||
|
||||
status = models.CharField(
|
||||
max_length=15,
|
||||
choices=Status.choices,
|
||||
default=Status.PENDING,
|
||||
db_index=True,
|
||||
verbose_name="وضعیت"
|
||||
)
|
||||
desc = models.CharField(max_length=500, blank=True, null=True, verbose_name="توضیحات درخواست")
|
||||
rejection_reason = models.CharField(max_length=500, blank=True, null=True, verbose_name="دلیل رد درخواست")
|
||||
|
||||
requester = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='requested_transfers',
|
||||
verbose_name="کاربر درخواستدهنده"
|
||||
)
|
||||
approver = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='processed_transfers',
|
||||
verbose_name="کاربر تعیینتکلیفکننده"
|
||||
)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True, db_index=True, verbose_name="زمان ثبت درخواست")
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="زمان آخرین بهروزرسانی")
|
||||
action_date = models.DateTimeField(null=True, blank=True, db_index=True, verbose_name="زمان تعیین تکلیف")
|
||||
|
||||
out_transaction = models.ForeignKey(
|
||||
'ProductInStore',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='transfer_out_ref',
|
||||
verbose_name="سند خروج انبار مبدا"
|
||||
)
|
||||
in_transaction = models.ForeignKey(
|
||||
'ProductInStore',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='transfer_in_ref',
|
||||
verbose_name="سند ورود انبار مقصد"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"[{self.get_status_display()}] {self.quantity} عدد {self.product.product_name} از {self.source_store.store_name} به {self.target_store.store_name}"
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if self.source_store_id == self.target_store_id:
|
||||
raise ValidationError("انبار مبدا و مقصد نمیتوانند یکسان باشند.")
|
||||
if self.quantity and self.quantity <= 0:
|
||||
raise ValidationError("تعداد انتقال باید بیشتر از صفر باشد.")
|
||||
@@ -0,0 +1,9 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class CacheTable(models.Model):
|
||||
key = models.CharField(max_length=50, unique=True)
|
||||
value = models.CharField(max_length=400)
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.key} # {self.value}'
|
||||
@@ -0,0 +1,18 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
# اصلاح مدل کاربر: تبدیل به پروفایل برای حفظ امنیت و اتصال به سیستم پیشفرض جنگو
|
||||
class UserProfile(models.Model):
|
||||
UserTypeList = [
|
||||
('admin', 'admin'),
|
||||
('office', 'office'),
|
||||
('operator', 'operator'),
|
||||
]
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
|
||||
user_type = models.CharField(max_length=10, choices=UserTypeList)
|
||||
name_family = models.CharField(max_length=200)
|
||||
create_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username} ({self.user_type})"
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
پکیج ویوهای اپ admin_panel.
|
||||
|
||||
این پکیج جایگزین فایل تکپارچهٔ سابق `admin_panel/views.py` شده است. هر ویو در ماژول
|
||||
دامنهای مربوط به خود قرار گرفته و در اینجا همهٔ آنها re-export میشوند تا مصرفکنندهٔ
|
||||
فعلی (`from . import views` در urls.py) بدون هیچ تغییری به `views.X` دسترسی داشته باشد.
|
||||
"""
|
||||
|
||||
from ._helpers import ( # noqa: F401
|
||||
logger,
|
||||
InventoryException,
|
||||
_safe_int,
|
||||
_parse_jalali_date,
|
||||
_parse_timestamp,
|
||||
)
|
||||
|
||||
from .auth import * # noqa: F401,F403
|
||||
from .customers import * # noqa: F401,F403
|
||||
from .factors import * # noqa: F401,F403
|
||||
from .store import * # noqa: F401,F403
|
||||
from .finance import * # noqa: F401,F403
|
||||
from .actions import * # noqa: F401,F403
|
||||
from .cheque_inquiry import * # noqa: F401,F403
|
||||
@@ -0,0 +1,49 @@
|
||||
"""توابع و ابزارهای مشترک لایهٔ ویوها."""
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from django.utils import timezone
|
||||
from persiantools.jdatetime import JalaliDate
|
||||
from persiantools import digits
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InventoryException(Exception):
|
||||
"""خطای اختصاصی برای کنترل موجودی انبار"""
|
||||
pass
|
||||
|
||||
|
||||
def _safe_int(value, default=0):
|
||||
"""تابع امن برای تبدیل مقادیر دریافتی از جاوااسکریپت به عدد"""
|
||||
if value is None:
|
||||
return default
|
||||
try:
|
||||
clean_val = str(value).strip()
|
||||
return int(clean_val) if clean_val else default
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
|
||||
|
||||
def _parse_jalali_date(date_str):
|
||||
"""تبدیل امن تاریخ ارسالی از pDatepicker به میلادی"""
|
||||
if not date_str or not str(date_str).strip():
|
||||
return None
|
||||
try:
|
||||
clean_date = digits.fa_to_en(digits.ar_to_fa(str(date_str).strip()))
|
||||
y, m, d = map(int, clean_date.split("/"))
|
||||
return JalaliDate(y, m, d).to_gregorian()
|
||||
except Exception as e:
|
||||
logger.warning(f"Error parsing Jalali date '{date_str}': {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _parse_timestamp(ts_value):
|
||||
"""تبدیل یونیکس تایماستمپهای ارسالی از جاوااسکریپت"""
|
||||
if not ts_value:
|
||||
return None
|
||||
try:
|
||||
# تبدیل میلیثانیه جاوااسکریپت به ثانیه پایتون
|
||||
return datetime.datetime.fromtimestamp(int(ts_value) / 1000.0, tz=timezone.get_current_timezone())
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
@@ -0,0 +1,263 @@
|
||||
import datetime
|
||||
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.http import HttpResponse
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
from ..models import Action, Customer
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customerAction(request, c_id):
|
||||
# استفاده از get_object_or_404 برای امنیت بیشتر
|
||||
customer = get_object_or_404(Customer, id=c_id)
|
||||
|
||||
# دریافت پارامترهای فیلتر از URL (پیشفرض: all)
|
||||
filter_type = request.GET.get('actionType', 'all')
|
||||
filter_status = request.GET.get('status', 'all')
|
||||
|
||||
# کوئری پایه برای اقدامات این مشتری
|
||||
actions = Action.objects.filter(customer=customer)
|
||||
|
||||
# اعمال فیلتر بر اساس نوع اقدام (بازدید، نصب و ...)
|
||||
if filter_type != 'all':
|
||||
actions = actions.filter(actionType=filter_type)
|
||||
|
||||
# اعمال فیلتر بر اساس وضعیت (انجام شده، در انتظار و ...)
|
||||
if filter_status != 'all':
|
||||
actions = actions.filter(status=filter_status)
|
||||
|
||||
# مرتبسازی نزولی بر اساس تاریخ ایجاد
|
||||
actions = actions.order_by("-createdDT")
|
||||
|
||||
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
|
||||
|
||||
data = {
|
||||
'customer': customer,
|
||||
'actions': actions,
|
||||
'allUsers': allUsers,
|
||||
# ارسال مقادیر فیلتر به قالب برای حفظ وضعیت انتخابشده در دراپداونها
|
||||
'filter_type': filter_type,
|
||||
'filter_status': filter_status
|
||||
}
|
||||
|
||||
return render(request, 'admin_panel/customerAction.html', data)
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customerNazar(request ,c_id):
|
||||
customer = Customer.objects.get(id=c_id)
|
||||
actions = Action.objects.filter(customer=customer, nazarDT__isnull=False).order_by("-createdDT")
|
||||
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
|
||||
data = {'customer': customer, 'actions': actions, 'allUsers': allUsers}
|
||||
return render(request, 'admin_panel/customer/nazar.html', data)
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customerNewAction(request ,c_id):
|
||||
actionType = request.POST.get('actionType', None)
|
||||
dateTimeAnjam = request.POST.get('dateTimeAnjam', None)
|
||||
#return HttpResponse(float(dateTimeAnjam+".0"))
|
||||
try:
|
||||
dateTimeAnjam = datetime.datetime.fromtimestamp(int(dateTimeAnjam)/1e3)
|
||||
except:
|
||||
pass
|
||||
desc = request.POST.get('desc', None)
|
||||
userID = request.POST.get('userID', None)
|
||||
is_done = request.POST.get('is_done', None)
|
||||
result = request.POST.get('result', None)
|
||||
FinishDateTime = request.POST.get('FinishDateTime', None)
|
||||
try:
|
||||
FinishDateTime = datetime.datetime.fromtimestamp(int(FinishDateTime)/1e3)
|
||||
except:
|
||||
pass
|
||||
|
||||
customer_obj = Customer.objects.get(id=int(c_id))
|
||||
newAction = Action()
|
||||
newAction.actionType = actionType
|
||||
newAction.actionDateTime = dateTimeAnjam
|
||||
newAction.desc = desc
|
||||
newAction.forUser = User.objects.get(id=int(userID))
|
||||
newAction.customer = customer_obj
|
||||
if is_done:
|
||||
newAction.status = 'finish'
|
||||
newAction.finishDateTime = FinishDateTime
|
||||
newAction.result = result
|
||||
else:
|
||||
newAction.status = 'waite'
|
||||
newAction.save()
|
||||
|
||||
if newAction.status == 'finish' and newAction.actionType == 'bazdid':
|
||||
customer_obj.last_bazdid_dt = newAction.finishDateTime
|
||||
customer_obj.save()
|
||||
|
||||
return redirect('/panel/customer/'+str(c_id)+'/action')
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def ActionList(request):
|
||||
actions = Action.objects.filter(forUser__id=request.user.id, status='waite').order_by("-createdDT")
|
||||
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
|
||||
data = {'actions': actions, 'allUsers': allUsers}
|
||||
return render(request, 'admin_panel/ActionList.html', data)
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def ActionListUser(request, u_id):
|
||||
actions = Action.objects.filter(forUser__id=u_id, status='waite').order_by("-createdDT")
|
||||
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
|
||||
data = {'actions': actions, 'allUsers': allUsers}
|
||||
return render(request, 'admin_panel/ActionList.html', data)
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customerEditAction(request ,c_id, action_id):
|
||||
|
||||
actionType = request.POST.get('actionType', None)
|
||||
dateTimeAnjam = request.POST.get('dateTimeAnjam', None)
|
||||
print("ok")
|
||||
#return HttpResponse(float(dateTimeAnjam+".0"))
|
||||
try:
|
||||
dateTimeAnjam = datetime.datetime.fromtimestamp(int(dateTimeAnjam)/1e3)
|
||||
except:
|
||||
pass
|
||||
desc = request.POST.get('desc', None)
|
||||
userID = request.POST.get('userID', None)
|
||||
is_done = request.POST.get('is_done', None)
|
||||
result = request.POST.get('result', None)
|
||||
FinishDateTime = request.POST.get('FinishDateTime', None)
|
||||
try:
|
||||
FinishDateTime = datetime.datetime.fromtimestamp(int(FinishDateTime)/1e3)
|
||||
except:
|
||||
pass
|
||||
|
||||
customer_obj = Customer.objects.get(id=int(c_id))
|
||||
newAction = Action.objects.get(id=action_id)
|
||||
newAction.actionType = actionType
|
||||
newAction.actionDateTime = dateTimeAnjam
|
||||
newAction.desc = desc
|
||||
newAction.forUser = User.objects.get(id=int(userID))
|
||||
newAction.customer = customer_obj
|
||||
if is_done:
|
||||
newAction.status = 'finish'
|
||||
newAction.finishDateTime = FinishDateTime
|
||||
newAction.result = result
|
||||
else:
|
||||
newAction.status = 'waite'
|
||||
newAction.save()
|
||||
|
||||
if newAction.status == 'finish' and newAction.actionType == 'bazdid':
|
||||
customer_obj.last_bazdid_dt = newAction.finishDateTime
|
||||
customer_obj.save()
|
||||
#return redirect('/panel/customer/'+str(c_id)+'/action')
|
||||
return redirect(request.META.get('HTTP_REFERER', 'redirect_if_referer_not_found'))
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def ActionListAll(request):
|
||||
actions = Action.objects.filter().order_by("-id")
|
||||
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
|
||||
data = {'actions': actions, 'allUsers': allUsers}
|
||||
return render(request, 'admin_panel/ActionList.html', data)
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def ActionListToday(request):
|
||||
startTodayDT= datetime.datetime.today().strftime('%Y-%m-%d 00:00:00')
|
||||
endTodayDT= datetime.datetime.today().strftime('%Y-%m-%d 23:59:59')
|
||||
#startTodayDT = datetime.datetime.strptime(startTodayDT, '%Y-%m-%d-00:00:00')
|
||||
#endTodayDT = datetime.datetime.strptime(endTodayDT, '%Y-%m-%d-23:59:59')
|
||||
#return HttpResponse(todayDT)
|
||||
actions = Action.objects.filter(actionDateTime__gte=startTodayDT,actionDateTime__lte=endTodayDT, forUser__id=request.user.id, status='waite').order_by("-actionDateTime")[:50]
|
||||
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
|
||||
data = {'actions': actions, 'allUsers': allUsers}
|
||||
return render(request, 'admin_panel/ActionList.html', data)
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def ActionListUserToday(request, u_id):
|
||||
dateTimeAction = request.GET.get('dateTime', None)
|
||||
status = request.GET.get('status', None)
|
||||
unixADT = dateTimeAction
|
||||
if dateTimeAction:
|
||||
dateTimeAction = datetime.datetime.fromtimestamp(int(dateTimeAction)/1e3)
|
||||
startTodayDT = dateTimeAction.strftime('%Y-%m-%d 00:00:00')
|
||||
endTodayDT= dateTimeAction.strftime('%Y-%m-%d 23:59:59')
|
||||
else:
|
||||
startTodayDT= datetime.datetime.today().strftime('%Y-%m-%d 00:00:00')
|
||||
endTodayDT= datetime.datetime.today().strftime('%Y-%m-%d 23:59:59')
|
||||
#startTodayDT = datetime.datetime.strptime(startTodayDT, '%Y-%m-%d-00:00:00')
|
||||
#endTodayDT = datetime.datetime.strptime(endTodayDT, '%Y-%m-%d-23:59:59')
|
||||
#return HttpResponse(todayDT)
|
||||
if status == 'waite' or status == None or status == '' or status == ' ':
|
||||
status = 'waite'
|
||||
actions = Action.objects.filter(actionDateTime__gte=startTodayDT,actionDateTime__lte=endTodayDT, forUser__id=u_id, status=status).order_by("-actionDateTime")[:50]
|
||||
actionPr = Action.objects.filter(actionDateTime__lt=startTodayDT, forUser__id=u_id, status=status).order_by("-actionDateTime")[:200]
|
||||
elif status == 'finish':
|
||||
actionPr = None
|
||||
actions = Action.objects.filter(actionDateTime__gte=startTodayDT,actionDateTime__lte=endTodayDT, forUser__id=u_id, status=status).order_by("-actionDateTime")[:50]
|
||||
|
||||
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
|
||||
data = {'actions': actions, 'allUsers': allUsers, 'u_id': u_id, "unixADT": unixADT, "status": status, 'actionPr': actionPr}
|
||||
return render(request, 'admin_panel/ActionList.html', data)
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def rezayat(request):
|
||||
statusRezayat = request.GET.get('status', 'waite')
|
||||
now = datetime.datetime.now()
|
||||
|
||||
# محاسبه بازههای زمانی ۳۰ روز و ۶۰ روز قبل
|
||||
dt_30_days_ago = now - datetime.timedelta(days=30)
|
||||
dt_60_days_ago = now - datetime.timedelta(days=60)
|
||||
|
||||
if statusRezayat == 'finish':
|
||||
# نمایش نظرسنجیهای تعیینتکلیف شده (ثبت شده یا کنسل شده)
|
||||
actions = Action.objects.exclude(actionType='vosol').filter(
|
||||
status='finish',
|
||||
nazarDT__isnull=False
|
||||
).order_by("-nazarDT")[:500]
|
||||
else:
|
||||
statusRezayat = 'waite'
|
||||
# نمایش نظرسنجیهای در انتظار با اعمال شرط عدم انقضای زمان
|
||||
actions = Action.objects.exclude(actionType='vosol').filter(
|
||||
status='finish',
|
||||
nazarDT__isnull=True, # نظرسنجی هنوز نهایی نشده
|
||||
createdDT__gte=dt_60_days_ago # حداکثر ۶۰ روز از ایجاد گذشته باشد
|
||||
).filter(
|
||||
Q(finishDateTime__isnull=True) | Q(finishDateTime__gte=dt_30_days_ago) # حداکثر ۳۰ روز از اتمام گذشته باشد
|
||||
).filter(
|
||||
Q(nextNazarDT__isnull=True) | Q(nextNazarDT__lte=now) # موعد پیگیری رسیده یا تعویقی نداشته باشد
|
||||
).order_by("-id")[:500]
|
||||
|
||||
data = {'actions': actions, "status": statusRezayat}
|
||||
return render(request, 'admin_panel/customerRezayat.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def rezayatEdit(request, ac_id):
|
||||
csGrade = request.POST.get('csGrade', None)
|
||||
csDesc = request.POST.get('csDesc', None)
|
||||
|
||||
if csDesc and csGrade:
|
||||
action = get_object_or_404(Action, id=ac_id)
|
||||
|
||||
if csGrade == '-1':
|
||||
# عدم موفقیت: تنظیم nextNazarDT برای ۳ روز بعد (nazarDT پر نمیشود تا نهایی نشود)
|
||||
action.nazar = csDesc
|
||||
action.grade = int(csGrade)
|
||||
action.nextNazarDT = datetime.datetime.now() + datetime.timedelta(days=3)
|
||||
|
||||
elif csGrade == '-2':
|
||||
# کنسل کردن: پر کردن nazarDT برای خروج کامل از لیست کارهای در انتظار
|
||||
action.nazar = csDesc
|
||||
action.grade = int(csGrade)
|
||||
action.nazarDT = datetime.datetime.now()
|
||||
action.nextNazarDT = None
|
||||
|
||||
else:
|
||||
# ثبت امتیاز عادی
|
||||
action.nazar = csDesc
|
||||
action.grade = int(csGrade)
|
||||
action.nazarDT = datetime.datetime.now()
|
||||
action.nextNazarDT = None
|
||||
|
||||
action.save()
|
||||
return redirect('/panel/rezayat/')
|
||||
else:
|
||||
return HttpResponse('لطفا همه فیلد ها را پر کنید')
|
||||
@@ -0,0 +1,29 @@
|
||||
from django.shortcuts import redirect, render
|
||||
from django.contrib.auth import authenticate, login, logout
|
||||
|
||||
|
||||
def p_login(request):
|
||||
username = request.POST.get('username', None )
|
||||
password = request.POST.get('password', None )
|
||||
if username != None and password != None:
|
||||
if not request.user.is_authenticated:
|
||||
user = authenticate(request, username=username, password=password)
|
||||
if user is not None:
|
||||
login(request, user)
|
||||
return redirect("/panel/ActionList/"+str(request.user.id)+"/today")
|
||||
else:
|
||||
data = {'message': 'یوزنیم یا پسورد اشتباه است'}
|
||||
return render(request, 'admin_panel/login.html', data)
|
||||
else:
|
||||
return redirect("/panel/")
|
||||
else:
|
||||
if request.user.is_authenticated:
|
||||
return redirect("/panel/")
|
||||
else:
|
||||
return render(request, 'admin_panel/login.html')
|
||||
#return logout(request)
|
||||
|
||||
|
||||
def p_logout(request):
|
||||
logout(request)
|
||||
return redirect("/panel/p_login/")
|
||||
@@ -0,0 +1,273 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
import cv2
|
||||
import ddddocr
|
||||
import numpy as np
|
||||
from asgiref.sync import async_to_sync
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.http import JsonResponse
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
from ._helpers import logger, _safe_int
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# توابع کمکی و منطق استعلام چک (برگرفته از اسکریپتهای شما)
|
||||
# =====================================================================
|
||||
|
||||
def solve_base64_captcha(base64_string: str) -> str:
|
||||
try:
|
||||
if ',' in base64_string:
|
||||
base64_string = base64_string.split(',')[1]
|
||||
|
||||
image_bytes = base64.b64decode(base64_string)
|
||||
np_arr = np.frombuffer(image_bytes, np.uint8)
|
||||
img = cv2.imdecode(np_arr, cv2.IMREAD_COLOR)
|
||||
|
||||
if img is None:
|
||||
return ""
|
||||
|
||||
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
||||
blur = cv2.medianBlur(gray, 3)
|
||||
_, thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
|
||||
|
||||
kernel = np.ones((2, 2), np.uint8)
|
||||
cleaned_img = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=1)
|
||||
final_img = cv2.bitwise_not(cleaned_img)
|
||||
|
||||
success, encoded_image = cv2.imencode('.png', final_img)
|
||||
if not success:
|
||||
return ""
|
||||
|
||||
processed_bytes = encoded_image.tobytes()
|
||||
ocr = ddddocr.DdddOcr(show_ad=False)
|
||||
extracted_text = ocr.classification(processed_bytes)
|
||||
|
||||
numbers = re.findall(r'\d+', extracted_text)
|
||||
if len(numbers) >= 2:
|
||||
num1, num2 = int(numbers[0]), int(numbers[1])
|
||||
if '+' in extracted_text:
|
||||
ans = num1 + num2
|
||||
elif '-' in extracted_text:
|
||||
ans = num1 - num2
|
||||
elif '*' in extracted_text or 'x' in extracted_text.lower():
|
||||
ans = num1 * num2
|
||||
else:
|
||||
ans = num1 + num2
|
||||
|
||||
return str(ans)
|
||||
return ""
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def parse_cheque_result_html(html_content: str) -> dict:
|
||||
sayad_id = re.search(r'شناسه\s*<b>?\s*(\d+)\s*</b>?', html_content)
|
||||
owner_name = re.search(r'متعلق به\s*<b>?\s*(.*?)\s*</b>?\s*می باشد', html_content)
|
||||
validity_date = re.search(r'تا تاریخ\s*<b>?\s*([\d/]+)\s*</b>?', html_content)
|
||||
status = re.search(r'در وضعیت\s*<b>?\s*(.*?)\s*</b>?\s*در پایگاه', html_content)
|
||||
|
||||
clean_full_text = re.sub(r'<[^>]+>', '', html_content).strip()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"sayad_id": sayad_id.group(1) if sayad_id else "",
|
||||
"owner_name": owner_name.group(1).strip() if owner_name else "",
|
||||
"validity_date": validity_date.group(1) if validity_date else "",
|
||||
"status": status.group(1).strip() if status else "",
|
||||
"raw_message": clean_full_text
|
||||
},
|
||||
"error": None
|
||||
}
|
||||
|
||||
async def run_cbi_sayad_inquiry_logic(sayad_id: str, max_retries: int = 7) -> str:
|
||||
lightpanda_url = os.environ.get("LIGHTPANDA_URL") or "ws://127.0.0.1:9222"
|
||||
async with async_playwright() as p:
|
||||
try:
|
||||
browser = await p.chromium.connect_over_cdp(lightpanda_url)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "data": None, "error": f"عدم اتصال به Lightpanda: {str(e)}"})
|
||||
|
||||
for attempt in range(1, max_retries + 1):
|
||||
context = await browser.new_context(ignore_https_errors=True, bypass_csp=True)
|
||||
page = await context.new_page()
|
||||
|
||||
try:
|
||||
url = "https://cbi.ir/EstelamSayad/19689.aspx"
|
||||
await page.goto(url, wait_until="domcontentloaded", timeout=20000)
|
||||
|
||||
captcha_selector = "#ctl00_ucBody_ucContent_ctl00_imgcpatcha"
|
||||
await page.wait_for_selector(captcha_selector, state="visible", timeout=15000)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
base64_src = await page.locator(captcha_selector).get_attribute("src")
|
||||
viewstate = await page.locator("#__VIEWSTATE").get_attribute("value") or ""
|
||||
viewstate_gen = await page.locator("#__VIEWSTATEGENERATOR").get_attribute("value") or "453B48D8"
|
||||
|
||||
if not base64_src or not viewstate:
|
||||
await context.close()
|
||||
continue
|
||||
|
||||
captcha_answer = solve_base64_captcha(base64_src)
|
||||
if not captcha_answer:
|
||||
await context.close()
|
||||
continue
|
||||
|
||||
ajax_raw_response = await page.evaluate("""
|
||||
async ({ sayadId, captchaAns, viewState, viewStateGen }) => {
|
||||
try {
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('ctl00$ucBody$ucContent$ctl00$ScriptManager1', 'ctl00$ucBody$ucContent$ctl00$UpdatePanel1|ctl00$ucBody$ucContent$ctl00$btnSearch');
|
||||
formData.append('__EVENTTARGET', '');
|
||||
formData.append('__EVENTARGUMENT', '');
|
||||
formData.append('__VIEWSTATE', viewState);
|
||||
formData.append('__VIEWSTATEGENERATOR', viewStateGen);
|
||||
formData.append('__VIEWSTATEENCRYPTED', '');
|
||||
formData.append('ctl00$ucHeader$txtQuickSearch', '');
|
||||
formData.append('ctl00$ucBody$ucContent$ctl00$txtChequeNumber', sayadId);
|
||||
formData.append('ctl00$ucBody$ucContent$ctl00$txtCaptchaInput', captchaAns);
|
||||
formData.append('__ASYNCPOST', 'true');
|
||||
formData.append('ctl00$ucBody$ucContent$ctl00$btnSearch', 'استعلام چک صیادی');
|
||||
|
||||
const response = await fetch(window.location.href, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Cache-Control': 'no-cache'
|
||||
},
|
||||
body: formData.toString()
|
||||
});
|
||||
return await response.text();
|
||||
} catch (err) {
|
||||
return 'FETCH_ERROR: ' + err.message;
|
||||
}
|
||||
}
|
||||
""", {'sayadId': sayad_id, 'captchaAns': captcha_answer, 'viewState': viewstate, 'viewStateGen': viewstate_gen})
|
||||
|
||||
if "FETCH_ERROR:" in ajax_raw_response:
|
||||
await context.close()
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
|
||||
error_match = re.search(r'id="ctl00_ucBody_ucContent_ctl00_lblError"[^>]*>(.*?)</span>', ajax_raw_response, re.DOTALL)
|
||||
if error_match and error_match.group(1).strip():
|
||||
clean_error = re.sub(r'<[^>]+>', '', error_match.group(1)).strip()
|
||||
if "عبارت محاسباتی" in clean_error:
|
||||
await context.close()
|
||||
continue
|
||||
else:
|
||||
await context.close()
|
||||
await browser.close()
|
||||
return json.dumps({"success": False, "data": None, "error": clean_error})
|
||||
|
||||
result_match = re.search(r'id="ctl00_ucBody_ucContent_ctl00_lblChequeNumberResult"[^>]*>(.*?)</span>', ajax_raw_response, re.DOTALL)
|
||||
if result_match:
|
||||
raw_html = result_match.group(1)
|
||||
parsed_result = parse_cheque_result_html(raw_html)
|
||||
await context.close()
|
||||
await browser.close()
|
||||
return json.dumps(parsed_result)
|
||||
|
||||
await context.close()
|
||||
except Exception:
|
||||
await context.close()
|
||||
await asyncio.sleep(1)
|
||||
|
||||
await browser.close()
|
||||
return json.dumps({"success": False, "data": None, "error": "استعلام ناموفق بود (خطای شبکه یا تشخیص کپچا)"})
|
||||
|
||||
|
||||
async def run_bpi_cheque_inquiry_logic(national_id: str, sayad_id: str, id_type: int = 1):
|
||||
lightpanda_url = os.environ.get("LIGHTPANDA_URL") or "ws://127.0.0.1:9222"
|
||||
async with async_playwright() as p:
|
||||
try:
|
||||
browser = await p.chromium.connect_over_cdp(lightpanda_url)
|
||||
except Exception as e:
|
||||
return {"success": False, "error": f"عدم اتصال به مرورگر Lightpanda: {str(e)}"}
|
||||
|
||||
context = await browser.new_context()
|
||||
page = await context.new_page()
|
||||
|
||||
try:
|
||||
await page.goto("https://vbank.bpi.ir/public/inquiries/inquiry-bounced-cheques", wait_until="domcontentloaded", timeout=20000)
|
||||
api_url = f"https://sec.bpi.ir/prls/api/v1/inquiry/chequeStatus?IdCode={national_id}&IdType={id_type}&SayadId={sayad_id}"
|
||||
|
||||
response = await context.request.get(
|
||||
api_url,
|
||||
headers={
|
||||
"Referer": "https://vbank.bpi.ir/public/inquiries/inquiry-bounced-cheques",
|
||||
"Accept": "application/json, text/plain, */*"
|
||||
}
|
||||
)
|
||||
|
||||
if response.ok:
|
||||
data = await response.json()
|
||||
return {"success": True, "data": data}
|
||||
else:
|
||||
error_text = await response.text()
|
||||
return {"success": False, "error": f"سرور خطا برگرداند (کد {response.status}): {error_text}"}
|
||||
|
||||
except Exception as e:
|
||||
return {"success": False, "error": f"خطایی در فرآیند استعلام رخ داد: {str(e)}"}
|
||||
finally:
|
||||
await context.close()
|
||||
await browser.close()
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# View های مربوط به استعلام (API)
|
||||
# =====================================================================
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewStore', raise_exception=True)
|
||||
def api_check_status_inquiry(request):
|
||||
"""
|
||||
استعلام وضعیت چک از بانک مرکزی
|
||||
پارامتر ورودی: sayad_id (GET)
|
||||
"""
|
||||
sayad_id = request.GET.get('sayad_id', '').strip()
|
||||
|
||||
if not sayad_id or not sayad_id.isdigit() or len(sayad_id) != 16:
|
||||
return JsonResponse({"success": False, "error": "شناسه صیادی باید دقیقاً ۱۶ رقم باشد."})
|
||||
|
||||
try:
|
||||
# اجرای تابع Async به صورت Sync
|
||||
result_json_str = async_to_sync(run_cbi_sayad_inquiry_logic)(sayad_id)
|
||||
# تبدیل رشته جیسون به دیکشنری برای ارسال مجدد با JsonResponse
|
||||
result_data = json.loads(result_json_str)
|
||||
return JsonResponse(result_data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in CBI Check Status Inquiry: {str(e)}", exc_info=True)
|
||||
return JsonResponse({"success": False, "error": "خطای سرور در برقراری ارتباط با بانک مرکزی."})
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewStore', raise_exception=True)
|
||||
def api_check_sabt_inquiry(request):
|
||||
"""
|
||||
استعلام ثبت چک از بانک پاسارگاد
|
||||
پارامترهای ورودی: sayad_id, national_id, id_type (GET)
|
||||
"""
|
||||
sayad_id = request.GET.get('sayad_id', '').strip()
|
||||
national_id = request.GET.get('national_id', '').strip()
|
||||
id_type = _safe_int(request.GET.get('id_type', 1)) # عدد 1 حقیقی، 2 حقوقی
|
||||
|
||||
# اگر کدملی از فرانتاند خالی ارسال شد، از یک کدملی پیشفرض استفاده میکنیم
|
||||
if not national_id:
|
||||
national_id = "1219968218"
|
||||
|
||||
if not sayad_id:
|
||||
return JsonResponse({"success": False, "error": "شناسه صیادی برای استعلام ثبت الزامی است."})
|
||||
|
||||
try:
|
||||
# اجرای تابع Async به صورت Sync
|
||||
result_data = async_to_sync(run_bpi_cheque_inquiry_logic)(national_id, sayad_id, id_type)
|
||||
return JsonResponse(result_data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in BPI Check Registration Inquiry: {str(e)}", exc_info=True)
|
||||
return JsonResponse({"success": False, "error": "خطای سرور در برقراری ارتباط با سامانه بانک."})
|
||||
@@ -0,0 +1,463 @@
|
||||
import datetime
|
||||
import os
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Sum, Q
|
||||
from django.shortcuts import redirect, render, get_object_or_404
|
||||
from django.core.paginator import Paginator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from persiantools.jdatetime import JalaliDate
|
||||
|
||||
from ..models import (Customer, PhoneNumber, ProductCategory, Product, Factor,
|
||||
FactorIteam, Article, Sarresid, Action, CacheTable)
|
||||
from ._helpers import logger, _parse_jalali_date
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customers(request):
|
||||
searchVal = request.GET.get('searchVal', '')
|
||||
searchOn = request.GET.get('searchOn', '')
|
||||
searchStatus = True
|
||||
full_path = request.get_full_path()
|
||||
|
||||
if searchOn == 'code':
|
||||
all_customers = Customer.objects.filter(idCode__contains=searchVal).order_by('-create_date')
|
||||
elif searchOn == 'phone':
|
||||
all_customers = Customer.objects.filter(Q(phone1__contains=searchVal) | Q(phone2__contains=searchVal) | Q(phone3__contains=searchVal) ).order_by('-create_date')
|
||||
elif searchOn == 'name':
|
||||
all_customers = Customer.objects.filter(name_family__contains=searchVal).order_by('-create_date')
|
||||
elif searchOn == 'address':
|
||||
all_customers = Customer.objects.filter(address__contains=searchVal).order_by('-create_date')
|
||||
elif searchOn == 'laghv':
|
||||
all_customers = Customer.objects.filter(laghv_bazdid=True).order_by('-create_date')
|
||||
else:
|
||||
searchStatus = False
|
||||
all_customers = Customer.objects.filter(is_active=True).order_by('-create_date')
|
||||
|
||||
paginator = Paginator(all_customers, 25) # Show 25 contacts per page.
|
||||
page_number = request.GET.get('page', None)
|
||||
all_customers = paginator.get_page(page_number)
|
||||
|
||||
|
||||
data = {'customersD': all_customers, 'searchStatus': searchStatus, 'full_path': full_path, 'searchVal': searchVal, 'searchOn':searchOn}
|
||||
return render(request, 'admin_panel/customers.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customersSearch(request, searchVal,searchOn):
|
||||
searchStatus = True
|
||||
|
||||
if searchOn == 'code':
|
||||
all_customers = Customer.objects.filter(idCode__contains=searchVal).order_by('-create_date')
|
||||
elif searchOn == 'phone':
|
||||
all_customers = Customer.objects.filter(phone1__contains=searchVal).order_by('-create_date')
|
||||
elif searchOn == 'name':
|
||||
all_customers = Customer.objects.filter(name_family__contains=searchVal).order_by('-create_date')
|
||||
elif searchOn == 'address':
|
||||
all_customers = Customer.objects.filter(address__contains=searchVal).order_by('-create_date')
|
||||
else:
|
||||
searchStatus = False
|
||||
all_customers = Customer.objects.all().order_by('-create_date')
|
||||
|
||||
paginator = Paginator(all_customers, 25) # Show 25 contacts per page.
|
||||
page_number = request.GET.get('page', None)
|
||||
all_customers = paginator.get_page(page_number)
|
||||
|
||||
|
||||
data = {'customersD': all_customers, 'searchStatus': searchStatus}
|
||||
return render(request, 'admin_panel/customers.html', data)
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customer(request ,c_id):
|
||||
customer = Customer.objects.get(id=c_id)
|
||||
categorys = ProductCategory.objects.all()
|
||||
factorha = Factor.objects.filter(customer__id = c_id)
|
||||
#all_prd = serializers.serialize("json", all_prda)
|
||||
#return JsonResponse(all_prd,safe = False)
|
||||
dateNow = datetime.datetime.now().date
|
||||
data = {'customer': customer, 'categorys': categorys, 'dateNow': dateNow, 'factorha': factorha}
|
||||
return render(request, 'admin_panel/customerID.html', data)
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customerNew(request ,c_id):
|
||||
customer = Customer.objects.get(id=c_id)
|
||||
categorys = ProductCategory.objects.all()
|
||||
factorha = Factor.objects.filter(customer__id = c_id)
|
||||
#all_prd = serializers.serialize("json", all_prda)
|
||||
#return JsonResponse(all_prd,safe = False)
|
||||
dateNow = datetime.datetime.now().date
|
||||
data = {'customer': customer, 'categorys': categorys, 'dateNow': dateNow, 'factorha': factorha}
|
||||
return render(request, 'admin_panel/customerNew.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customerNew2(request ,c_id):
|
||||
customer = get_object_or_404(Customer, id=c_id, is_active=True)
|
||||
categorys = ProductCategory.objects.all()
|
||||
factorha = Factor.objects.filter(customer__id = c_id)
|
||||
|
||||
debit = Article.objects.filter(customer=customer).aggregate(Sum('debit'))['debit__sum']
|
||||
credit = Article.objects.filter(customer=customer).aggregate(Sum('credit'))['credit__sum']
|
||||
mandehesabCs = 0
|
||||
try:
|
||||
if (debit - credit) < 0:
|
||||
mandehesabCs = abs(debit - credit)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
#all_prd = serializers.serialize("json", all_prda)
|
||||
#return JsonResponse(all_prd,safe = False)
|
||||
dateNow = datetime.datetime.now().date
|
||||
data = {'customer': customer, 'categorys': categorys, 'dateNow': dateNow, 'factorha': factorha, 'mandehesabCs':mandehesabCs}
|
||||
return render(request, 'admin_panel/customerNew2.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customerFactors(request, c_id):
|
||||
customer = get_object_or_404(Customer, id=c_id)
|
||||
categorys = ProductCategory.objects.all()
|
||||
|
||||
# کوئری پایه برای دریافت همه فاکتورهای این مشتری
|
||||
factor_qs = Factor.objects.filter(customer=customer)
|
||||
|
||||
# دریافت پارامترهای فیلتر و جستجو از آدرس URL
|
||||
search_query = request.GET.get('search_factor', '').strip()
|
||||
filter_status = request.GET.get('status', 'all')
|
||||
filter_type = request.GET.get('type', 'all')
|
||||
az_tarikh = request.GET.get('az_tarikh', '').strip()
|
||||
ta_tarikh = request.GET.get('ta_tarikh', '').strip()
|
||||
|
||||
# ۱. اعمال فیلتر جستجو (شناسه فاکتور یا متن توضیحات)
|
||||
if search_query:
|
||||
if search_query.isdigit():
|
||||
factor_qs = factor_qs.filter(models.Q(id=int(search_query)) | models.Q(desc__icontains=search_query))
|
||||
else:
|
||||
factor_qs = factor_qs.filter(models.Q(desc__icontains=search_query))
|
||||
|
||||
# ۲. اعمال فیلتر وضعیت پرداخت و حذف نرم
|
||||
if filter_status != 'all':
|
||||
if filter_status == 'deleted':
|
||||
factor_qs = factor_qs.filter(is_active=False)
|
||||
else:
|
||||
factor_qs = factor_qs.filter(pay_status=filter_status, is_active=True)
|
||||
|
||||
# ۳. اعمال فیلتر نوع فاکتور (نصب، خدمات، گارانتی)
|
||||
if filter_type != 'all':
|
||||
factor_qs = factor_qs.filter(FactorType=filter_type)
|
||||
|
||||
# ۴. اعمال فیلتر تاریخ شروع (از تاریخ)
|
||||
if az_tarikh:
|
||||
az_gregorian = _parse_jalali_date(az_tarikh)
|
||||
if az_gregorian:
|
||||
az_datetime = datetime.datetime.combine(az_gregorian, datetime.time.min)
|
||||
factor_qs = factor_qs.filter(create_date__gte=az_datetime)
|
||||
|
||||
# ۵. اعمال فیلتر تاریخ پایان (تا تاریخ)
|
||||
if ta_tarikh:
|
||||
ta_gregorian = _parse_jalali_date(ta_tarikh)
|
||||
if ta_gregorian:
|
||||
ta_datetime = datetime.datetime.combine(ta_gregorian, datetime.time.max)
|
||||
factor_qs = factor_qs.filter(create_date__lte=ta_datetime)
|
||||
|
||||
# مرتبسازی بر اساس آخرین تاریخ ایجاد
|
||||
factorha = factor_qs.order_by('-create_date')
|
||||
|
||||
dateNow = datetime.datetime.now().date
|
||||
|
||||
data = {
|
||||
'customer': customer,
|
||||
'categorys': categorys,
|
||||
'dateNow': dateNow,
|
||||
'factorha': factorha,
|
||||
'search_query': search_query,
|
||||
'filter_status': filter_status,
|
||||
'filter_type': filter_type,
|
||||
'az_tarikh': az_tarikh,
|
||||
'ta_tarikh': ta_tarikh
|
||||
}
|
||||
return render(request, 'admin_panel/customerIDFactor.html', data)
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customerFactor(request ,c_id, f_id):
|
||||
customer = Customer.objects.get(id=c_id)
|
||||
categorys = ProductCategory.objects.all()
|
||||
factorO = Factor.objects.get(customer=customer, id=f_id)
|
||||
fiteam = FactorIteam.objects.filter(factor=factorO)
|
||||
#all_prd = serializers.serialize("json", all_prda)
|
||||
#return JsonResponse(all_prd,safe = False)
|
||||
dateNow = datetime.datetime.now().date
|
||||
data = {'customer': customer, 'categorys': categorys, 'dateNow': dateNow, 'factorO': factorO, 'fiteam': fiteam}
|
||||
return render(request, 'admin_panel/customerFactor.html', data)
|
||||
|
||||
|
||||
def productsJson(request):
|
||||
from django.core import serializers
|
||||
from django.http import JsonResponse
|
||||
all_prd = Product.objects.all()
|
||||
all_prd = serializers.serialize("json", all_prd)
|
||||
return JsonResponse(all_prd,safe = False)
|
||||
|
||||
def ActionJson(request, action_id):
|
||||
from django.core import serializers
|
||||
from django.http import JsonResponse
|
||||
AcJson = Action.objects.filter(id=action_id)
|
||||
AcJson = serializers.serialize("json", AcJson)
|
||||
return JsonResponse(AcJson,safe = False)
|
||||
|
||||
def CustomerJson(request, c_id):
|
||||
from django.core import serializers
|
||||
from django.http import JsonResponse
|
||||
customerJson = Customer.objects.filter(id=c_id)
|
||||
customerJson = serializers.serialize("json", customerJson)
|
||||
return JsonResponse(customerJson,safe = False)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customersAdd(request):
|
||||
an_name = request.POST.get('an_name', '')
|
||||
an_address = request.POST.get('an_address', '')
|
||||
installDate = request.POST.get('installDate', '')
|
||||
idnCode = request.POST.get('idnCode', '')
|
||||
|
||||
# دریافت آرایههای عنوان و شماره تلفن
|
||||
titles_list = request.POST.getlist('phone_titles')
|
||||
phones_list = request.POST.getlist('phones')
|
||||
|
||||
# ترکیب عنوانها و شمارهها و حذف ردیفهای خالی
|
||||
valid_phones = []
|
||||
for title, phone in zip(titles_list, phones_list):
|
||||
if phone.strip():
|
||||
valid_phones.append({
|
||||
'title': title.strip() if title.strip() else 'تلفن',
|
||||
'phone': phone.strip()
|
||||
})
|
||||
|
||||
year, month, day = map(int, installDate.split('/'))
|
||||
installDate_gregorian = JalaliDate(year, month, day).to_gregorian()
|
||||
|
||||
newCs = Customer()
|
||||
newCs.name_family = an_name
|
||||
newCs.address = an_address
|
||||
newCs.installDate = installDate_gregorian
|
||||
newCs.idCode = idnCode
|
||||
|
||||
# حفظ سازگاری با کدهای قدیمی (ریختن ۳ شماره اول در فیلدهای قدیمی)
|
||||
newCs.phone1 = valid_phones[0]['phone'] if len(valid_phones) > 0 else ''
|
||||
newCs.phone2 = valid_phones[1]['phone'] if len(valid_phones) > 1 else ''
|
||||
newCs.phone3 = valid_phones[2]['phone'] if len(valid_phones) > 2 else ''
|
||||
newCs.save()
|
||||
|
||||
# ذخیره شمارهها به صورت نامحدود در جدول PhoneNumber
|
||||
for item in valid_phones:
|
||||
PhoneNumber.objects.create(customer=newCs, title=item['title'], phone_number=item['phone'])
|
||||
|
||||
return redirect('/panel/customers/')
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customerEdit(request, c_id):
|
||||
an_name = request.POST.get('an_name', '')
|
||||
an_address = request.POST.get('an_address', '')
|
||||
installDate = request.POST.get('installDate', '')
|
||||
idnCode = request.POST.get('idnCode', '')
|
||||
laghv_bazdid = request.POST.get('laghv_bazdid', False)
|
||||
|
||||
# دریافت آرایههای عنوان و شماره تلفن
|
||||
titles_list = request.POST.getlist('phone_titles')
|
||||
phones_list = request.POST.getlist('phones')
|
||||
|
||||
valid_phones = []
|
||||
for title, phone in zip(titles_list, phones_list):
|
||||
if phone.strip():
|
||||
valid_phones.append({
|
||||
'title': title.strip() if title.strip() else 'تلفن',
|
||||
'phone': phone.strip()
|
||||
})
|
||||
|
||||
year, month, day = map(int, installDate.split('/'))
|
||||
installDate_gregorian = JalaliDate(year, month, day).to_gregorian()
|
||||
|
||||
newCs = get_object_or_404(Customer, id=c_id)
|
||||
newCs.name_family = an_name
|
||||
newCs.address = an_address
|
||||
newCs.installDate = installDate_gregorian
|
||||
newCs.idCode = idnCode
|
||||
newCs.laghv_bazdid = True if laghv_bazdid == 'true' else False
|
||||
|
||||
# آپدیت فیلدهای قدیمی جهت حفظ سازگاری
|
||||
newCs.phone1 = valid_phones[0]['phone'] if len(valid_phones) > 0 else ''
|
||||
newCs.phone2 = valid_phones[1]['phone'] if len(valid_phones) > 1 else ''
|
||||
newCs.phone3 = valid_phones[2]['phone'] if len(valid_phones) > 2 else ''
|
||||
newCs.save()
|
||||
|
||||
# بروزرسانی جدول بینهایت (حذف قبلیها و درج مجدد)
|
||||
PhoneNumber.objects.filter(customer=newCs).delete()
|
||||
for item in valid_phones:
|
||||
PhoneNumber.objects.create(customer=newCs, title=item['title'], phone_number=item['phone'])
|
||||
|
||||
return redirect('/panel/customer/'+str(c_id))
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def entezar(request):
|
||||
a = datetime.datetime.now()
|
||||
|
||||
searchVal = request.GET.get('searchVal', '')
|
||||
searchOn = request.GET.get('searchOn', '')
|
||||
searchStatus = True
|
||||
full_path = request.get_full_path()
|
||||
|
||||
from django.db.models import Q
|
||||
|
||||
bazdid = os.environ.get('bazdid', 9)
|
||||
|
||||
bazdid = int ( int(bazdid) * 4.3 )
|
||||
|
||||
dateTime6MonthAgo = a - datetime.timedelta(weeks=bazdid)
|
||||
''' customers = Customer.objects.filter(name_family__isnull=False)
|
||||
m6mbazdid = []
|
||||
for customerObj in customers:
|
||||
try:
|
||||
actionAa = Action.objects.filter(customer=customerObj, actionType='bazdid', status='finish').last()
|
||||
if actionAa.status == 'finish':
|
||||
customerObj.last_bazdid_dt = actionAa.actionDateTime
|
||||
customerObj.save()
|
||||
if actionAa.actionDateTime.date() < dateTime6MonthAgo.date():
|
||||
m6mbazdid.append(actionAa)
|
||||
except:
|
||||
pass '''
|
||||
|
||||
if searchOn == 'address':
|
||||
print("addressaddressaddressaddressaddress")
|
||||
customers_objj = Customer.objects.filter(Q(last_bazdid_dt__lte=dateTime6MonthAgo) | Q(last_bazdid_dt__isnull=True),
|
||||
installDate__lte=dateTime6MonthAgo,
|
||||
laghv_bazdid=False, name_family__isnull=False, address__contains=searchVal).order_by('last_bazdid_dt')
|
||||
else:
|
||||
searchStatus = False
|
||||
customers_objj = Customer.objects.filter(Q(last_bazdid_dt__lte=dateTime6MonthAgo) | Q(last_bazdid_dt__isnull=True),
|
||||
installDate__lte=dateTime6MonthAgo,
|
||||
laghv_bazdid=False, name_family__isnull=False).order_by('last_bazdid_dt')
|
||||
|
||||
#customers_objj = Customer.objects.filter(name_family__isnull=False, last_bazdid_dt__lte=dateTime6MonthAgo)
|
||||
|
||||
try:
|
||||
entezar_bazdid = CacheTable.objects.get(key='9mahbazdid')
|
||||
|
||||
entezar_bazdid.value = Customer.objects.filter(
|
||||
Q(last_bazdid_dt__lte=dateTime6MonthAgo) | Q(last_bazdid_dt__isnull=True),
|
||||
installDate__lte=dateTime6MonthAgo,
|
||||
laghv_bazdid=False, name_family__isnull=False).count()
|
||||
|
||||
entezar_bazdid.save()
|
||||
except:
|
||||
pass
|
||||
|
||||
paginator = Paginator(customers_objj, 20) # Show 25 contacts per page.
|
||||
page_number = request.GET.get('page', None)
|
||||
customerHa = paginator.get_page(page_number)
|
||||
|
||||
return render(request, 'admin_panel/entezar.html', {'customers': customerHa, 'searchStatus': searchStatus, 'full_path': full_path, 'searchVal': searchVal, 'searchOn':searchOn})
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.DelCustomer', raise_exception=True)
|
||||
def customerDel(request, c_id):
|
||||
customer_obj = get_object_or_404(Customer, id=c_id)
|
||||
|
||||
try:
|
||||
# ۱. بررسی وجود هرگونه سابقه مالی/حسابداری (حتی با مانده صفر)
|
||||
has_articles = Article.objects.filter(customer=customer_obj).exists()
|
||||
|
||||
# ۲. بررسی وجود هرگونه سابقه چک یا نسیه (حتی در صورت پاس شدن و تسویه کامل)
|
||||
has_sarresids = Sarresid.objects.filter(customer=customer_obj).exists()
|
||||
|
||||
# ۳. بررسی وجود هرگونه فاکتور ثبت شده برای این مشتری
|
||||
has_factors = Factor.objects.filter(customer=customer_obj).exists()
|
||||
|
||||
# ۴. بررسی وجود هرگونه سابقه اقدام، بازدید یا نظرسنجی
|
||||
has_actions = Action.objects.filter(customer=customer_obj).exists()
|
||||
|
||||
# ۵. اگر هر کدام از این سوابق وجود داشته باشد، به هیچ عنوان اجازه غیرفعالسازی/حذف نمیدهیم
|
||||
if has_articles or has_sarresids or has_factors or has_actions:
|
||||
error_msg = f"عملیات ناموفق: مشتری '{customer_obj.name_family}' به دلیل داشتن سابقه در سیستم قابل حذف نیست! دلایل: "
|
||||
reasons = []
|
||||
|
||||
if has_articles:
|
||||
reasons.append("داشتن تراکنش مالی یا گردش حسابداری")
|
||||
if has_sarresids:
|
||||
reasons.append("داشتن سابقه چک یا نسیه (حتی تسویه شده)")
|
||||
if has_factors:
|
||||
reasons.append("داشتن فاکتور ثبتشده")
|
||||
if has_actions:
|
||||
reasons.append("داشتن سابقه اقدامات، بازدید یا نظرسنجی")
|
||||
|
||||
error_msg += "، ".join(reasons) + "."
|
||||
|
||||
# ارسال پیام خطا به تمپلیت و بازگرداندن کاربر به صفحه قبلی
|
||||
messages.error(request, error_msg)
|
||||
return redirect(request.META.get('HTTP_REFERER', '/panel/customers/'))
|
||||
|
||||
# ۶. در صورتی که مشتری کاملاً خام بود و هیچ سابقهای در هیچ بخشی نداشت، حذف میشود
|
||||
customer_obj.delete()
|
||||
messages.success(request, f"مشتری '{customer_obj.name_family}' که فاقد هرگونه سابقهای بود، با موفقیت از سیستم حذف شد.")
|
||||
return redirect('/panel/customers/')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in deleting customer {c_id}: {str(e)}", exc_info=True)
|
||||
messages.error(request, "خطای سیستمی در بررسی سوابق و حذف مشتری رخ داد.")
|
||||
return redirect(request.META.get('HTTP_REFERER', '/panel/customers/'))
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customerHesabdari(request ,c_id):
|
||||
customer = Customer.objects.get(id=c_id)
|
||||
try:
|
||||
debit = Article.objects.filter(customer=customer).aggregate(Sum('debit'))['debit__sum']
|
||||
credit = Article.objects.filter(customer=customer).aggregate(Sum('credit'))['credit__sum']
|
||||
mandehesabCs = debit - credit
|
||||
articls = Article.objects.filter(customer=customer).order_by('-createdDT')
|
||||
except:
|
||||
articls =None
|
||||
mandehesabCs = 0
|
||||
|
||||
dateNow = datetime.datetime.now().date
|
||||
data = {'customer': customer, 'mandehesabCs': mandehesabCs, 'articls': articls}
|
||||
return render(request, 'admin_panel/customerHesabDari.html', data)
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def sarresidh(request ,c_id):
|
||||
customer = Customer.objects.get(id=c_id)
|
||||
try:
|
||||
all_sarresid = Sarresid.objects.filter(customer=customer).order_by('-sarresidDT')
|
||||
except:
|
||||
all_sarresid = None
|
||||
|
||||
data = {'customer': customer, 'all_sarresid': all_sarresid}
|
||||
return render(request, 'admin_panel/customerSarresid.html', data)
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customerHesabdariAdd(request ,c_id):
|
||||
user_op = request.user
|
||||
price = request.POST.get('price', None)
|
||||
type = request.POST.get('type', None)
|
||||
typePy = request.POST.get('typePy', None)
|
||||
desc = request.POST.get('desc', None)
|
||||
nahvePardakhat = request.POST.get('nahvePardakhat', None)
|
||||
|
||||
newArticl = Article()
|
||||
newArticl.customer = Customer.objects.get(id=c_id)
|
||||
newArticl.desc = desc
|
||||
newArticl.typePy = typePy
|
||||
if type == 'credit':
|
||||
newArticl.credit = price
|
||||
newArticl.debit = 0
|
||||
elif type == 'debit':
|
||||
newArticl.debit = price
|
||||
newArticl.credit = 0
|
||||
newArticl.user = user_op
|
||||
newArticl.nahvePardakht = nahvePardakhat
|
||||
newArticl.save()
|
||||
return redirect('/panel/customer/'+str(c_id)+'/hesabdari')
|
||||
@@ -0,0 +1,465 @@
|
||||
import json
|
||||
|
||||
from django.db import transaction, connection
|
||||
from django.db.models import Sum, Q
|
||||
from django.shortcuts import redirect, get_object_or_404
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from persiantools.jdatetime import JalaliDate
|
||||
from persiantools import digits
|
||||
|
||||
from ..models import (Customer, StoreRoom, Factor, FactorIteam, Product,
|
||||
ProductInStore, Article, Sarresid)
|
||||
from ._helpers import logger, _safe_int, _parse_jalali_date, _parse_timestamp, InventoryException
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def newFactor(request, c_id):
|
||||
prdsList = request.POST.get('prdsList', '')
|
||||
sarResidDate = request.POST.get('sarResidDate', None)
|
||||
pardakhti = request.POST.get('pardakhti', None)
|
||||
#return HttpResponse(pardakhti)
|
||||
if pardakhti:
|
||||
pardakhti=int(pardakhti)
|
||||
if pardakhti>=1:
|
||||
pass
|
||||
else:
|
||||
pardakhti = None
|
||||
#return HttpResponse(sarResidDate)
|
||||
try:
|
||||
sarResidDate = digits.fa_to_en(digits.ar_to_fa(sarResidDate))
|
||||
#return HttpResponse(sarResidDate)
|
||||
sarResidDate = sarResidDate.split("/")
|
||||
#return HttpResponse(sarResidDate[2])
|
||||
sarResidDate = JalaliDate(int(sarResidDate[0]), int(sarResidDate[1]), int(sarResidDate[2])).to_gregorian()
|
||||
#return HttpResponse(sarResidDate)
|
||||
except:
|
||||
sarResidDate = None
|
||||
|
||||
#return HttpResponse(sarResidDate[1])
|
||||
#sarResidDate = JalaliDate(sarResidDate[0], sarResidDate[1], sarResidDate[2]).to_gregorian()
|
||||
|
||||
payType = request.POST.get('payType', '')
|
||||
FactorDate = request.POST.get('FactorDate', '')
|
||||
FactorType = request.POST.get('FactorType', '')
|
||||
FactorDesc = request.POST.get('FactorDesc', '')
|
||||
Factorfail = False
|
||||
customerObj=Customer.objects.get(id=c_id)
|
||||
storeUser = StoreRoom.objects.get(user__id=request.user.id)
|
||||
import json
|
||||
prdsList = json.loads(prdsList)
|
||||
sum_price = 0
|
||||
for prd in prdsList:
|
||||
if prd['garanty_Esh'] == ' ' or prd['garanty_Esh'] == '' or prd['garanty_Esh'] == None:
|
||||
sum_price += prd['prd_count'] * prd['price']
|
||||
|
||||
#return HttpResponse(prd_code)
|
||||
try:
|
||||
newFactor = Factor()
|
||||
newFactor.customer= customerObj
|
||||
newFactor.amount = sum_price
|
||||
if payType in ('naghd', 'kartKhan', 'cardToCard'):
|
||||
newFactor.pay_status = 'paid'
|
||||
else:
|
||||
newFactor.pay_status = 'unpaid'
|
||||
newFactor.FactorType = FactorType
|
||||
newFactor.PayType = payType
|
||||
newFactor.desc = FactorDesc
|
||||
if sarResidDate != None:
|
||||
newFactor.deadline = sarResidDate
|
||||
newFactor.user = User.objects.get(id=request.user.id)
|
||||
newFactor.save()
|
||||
except:
|
||||
try:
|
||||
Factorfail=True
|
||||
newFactor.delete()
|
||||
except:
|
||||
pass
|
||||
|
||||
if Factorfail != True:
|
||||
#accFactor To article
|
||||
is_naghd = False
|
||||
articleHesabDari = Article()
|
||||
articleHesabDari.debit = newFactor.amount
|
||||
articleHesabDari.credit = 0
|
||||
articleHesabDari.customer = customerObj
|
||||
articleHesabDari.factor = newFactor
|
||||
|
||||
if payType in ('naghd', 'kartKhan', 'cardToCard'):
|
||||
is_naghd = True
|
||||
|
||||
articleHesabDari.type = payType
|
||||
articleHesabDari.desc = str(FactorType) + " " + str(customerObj.name_family) + " طی فاکتور شماره: " + str(newFactor.id)
|
||||
articleHesabDari.save()
|
||||
|
||||
if is_naghd:
|
||||
articleHesabDari2 = Article()
|
||||
articleHesabDari2.debit = 0
|
||||
articleHesabDari2.credit = newFactor.amount
|
||||
articleHesabDari2.customer = customerObj
|
||||
articleHesabDari2.type = payType
|
||||
articleHesabDari2.factor = newFactor
|
||||
articleHesabDari2.desc = str(FactorType) + " " + str(customerObj.name_family) + " تسویه نقدی فاکتور شماره: " + str(newFactor.id)
|
||||
articleHesabDari2.save()
|
||||
elif pardakhti != None and pardakhti!='' and pardakhti != ' ':
|
||||
articleHesabDari2 = Article()
|
||||
articleHesabDari2.debit = 0
|
||||
articleHesabDari2.credit = pardakhti
|
||||
articleHesabDari2.customer = customerObj
|
||||
articleHesabDari2.type = payType
|
||||
articleHesabDari2.factor = newFactor
|
||||
articleHesabDari2.desc = str(customerObj.name_family) + " پرداخت نقد در محل برا فاکتور " + str(newFactor.id)
|
||||
articleHesabDari2.save()
|
||||
|
||||
for prd in prdsList:
|
||||
try:
|
||||
fIteams = FactorIteam()
|
||||
fIteams.factor = newFactor
|
||||
if prd['prd_id'] == 9999:
|
||||
Gproduct = None
|
||||
fIteams.product = Gproduct
|
||||
fIteams.product_name = prd['product_name']
|
||||
fIteams.num = prd['prd_count']
|
||||
fIteams.amount = int(prd['priceKol'])
|
||||
else:
|
||||
Gproduct = Product.objects.get(id=prd['prd_id'])
|
||||
fIteams.product = Gproduct
|
||||
fIteams.product_name = Gproduct.product_name
|
||||
fIteams.num = prd['prd_count']
|
||||
fIteams.amount = prd['prd_count'] * Gproduct.product_price
|
||||
fIteams.garantyEsh = prd['garanty_Esh']
|
||||
fIteams.save()
|
||||
except:
|
||||
Factorfail = True
|
||||
newFactor.delete()
|
||||
break
|
||||
|
||||
if Factorfail != True:
|
||||
for prd in prdsList:
|
||||
if prd['prd_id'] != 9999:
|
||||
pInStore = ProductInStore()
|
||||
sRoom = storeUser
|
||||
productO = Product.objects.get(id=prd['prd_id'])
|
||||
pInStore.StoreRoom = sRoom
|
||||
pInStore.Product = productO
|
||||
pInStore.factor = newFactor
|
||||
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 >= prd['prd_count']:
|
||||
pInStore.s_out = prd['prd_count']
|
||||
if prd['garanty_Esh'] != ' ' and prd['garanty_Esh'] != '' and prd['garanty_Esh'] != None:
|
||||
pInStore.desc = "خروج: " + str(prd['prd_count']) + "#"+str(prd['garanty_Esh'])+ "#" + str(customerObj.name_family) + " * " + str(request.user.first_name) + " " + str(request.user.last_name)
|
||||
else:
|
||||
pInStore.desc = "خروج: " + str(prd['prd_count']) + "#فروش" + "#" + str(customerObj.name_family) + " * " + str(request.user.first_name) + " " + str(request.user.last_name)
|
||||
pInStore.save()
|
||||
else:
|
||||
return HttpResponse("موجودی کافی نیست، موجودی فعلی: "+ str(mojodi_prd))
|
||||
|
||||
return redirect('/panel/customer/'+str(c_id)+'/factors/')
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def newFactor2(request, c_id):
|
||||
if request.method != 'POST':
|
||||
return HttpResponseBadRequest("متد ارسالی باید POST باشد.")
|
||||
|
||||
user_op = request.user
|
||||
|
||||
try:
|
||||
prds_list = json.loads(request.POST.get('prdsList') or '[]')
|
||||
json_pardakht_ha = json.loads(request.POST.get('PardakhtHa') or '{}')
|
||||
except json.JSONDecodeError:
|
||||
return HttpResponseBadRequest("فرمت دادههای ارسالی JSON نامعتبر است.")
|
||||
|
||||
factor_type = request.POST.get('FactorType', '')
|
||||
factor_desc = request.POST.get('FactorDesc', '')
|
||||
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, owners=user_op)
|
||||
|
||||
# رفع مشکل متنی/عددی در استخراج شناسه کالاها
|
||||
product_ids = []
|
||||
for item in prds_list:
|
||||
p_id = item.get('prd_id')
|
||||
if p_id and _safe_int(p_id) != 9999:
|
||||
product_ids.append(_safe_int(p_id))
|
||||
|
||||
products_by_id = {p.id: p for p in Product.objects.filter(id__in=product_ids)}
|
||||
|
||||
sum_price = sum(
|
||||
_safe_int(item.get('prd_count')) * _safe_int(item.get('price'))
|
||||
for item in prds_list
|
||||
if not str(item.get('garanty_Esh', '')).strip()
|
||||
)
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
|
||||
new_factor = Factor.objects.create(
|
||||
customer=customer_obj,
|
||||
amount=sum_price,
|
||||
FactorType=factor_type,
|
||||
desc=factor_desc,
|
||||
deadline=sar_resid_date,
|
||||
user=user_op
|
||||
)
|
||||
|
||||
Article.objects.create(
|
||||
debit=sum_price,
|
||||
credit=0,
|
||||
customer=customer_obj,
|
||||
factor=new_factor,
|
||||
desc=f"{customer_obj.name_family} فاکتور شماره: {new_factor.id}",
|
||||
user=user_op
|
||||
)
|
||||
|
||||
payment_map = {
|
||||
'l_naghd': 'نقد',
|
||||
'l_kartbekart': 'کارت به کارت',
|
||||
'l_kartKhan': 'کارت خوان',
|
||||
'l_takhfif': 'تخفیف'
|
||||
}
|
||||
|
||||
for key, amount_raw in json_pardakht_ha.items():
|
||||
amount = _safe_int(amount_raw)
|
||||
if amount <= 0:
|
||||
continue
|
||||
|
||||
if key in payment_map:
|
||||
nahve_pardakht = payment_map[key]
|
||||
Article.objects.create(
|
||||
debit=0,
|
||||
credit=amount,
|
||||
customer=customer_obj,
|
||||
factor=new_factor,
|
||||
desc=f"{nahve_pardakht} {customer_obj.name_family} فاکتور شماره: {new_factor.id}",
|
||||
nahvePardakht=nahve_pardakht,
|
||||
payStatus="true",
|
||||
user=user_op
|
||||
)
|
||||
|
||||
elif key in ['l_nesiye', 'l_checkPrice']:
|
||||
is_nesiye = (key == 'l_nesiye')
|
||||
type_str = 'nesiye' if is_nesiye else 'check'
|
||||
ts_key = 'l_nesiyeSarResid' if is_nesiye else 'l_checkPriceSarResid'
|
||||
|
||||
sar_resid_dt = _parse_timestamp(json_pardakht_ha.get(ts_key))
|
||||
|
||||
Sarresid.objects.create(
|
||||
customer=customer_obj,
|
||||
price=amount,
|
||||
payStatus="false",
|
||||
type=type_str,
|
||||
user=user_op,
|
||||
factor=new_factor,
|
||||
sarresidDT=sar_resid_dt
|
||||
)
|
||||
|
||||
factor_items_pool = []
|
||||
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', '')
|
||||
|
||||
if prd_id == 9999:
|
||||
factor_items_pool.append(FactorIteam(
|
||||
factor=new_factor,
|
||||
product=None,
|
||||
product_name=item.get('product_name', ''),
|
||||
num=prd_count,
|
||||
amount=_safe_int(item.get('priceKol')),
|
||||
garantyEsh=garanty_esh
|
||||
))
|
||||
else:
|
||||
product_obj = products_by_id.get(prd_id)
|
||||
|
||||
if not product_obj:
|
||||
raise InventoryException(f"خطا در دریافت محصول: محصولی با شناسه {prd_id} در سیستم وجود ندارد.")
|
||||
|
||||
factor_items_pool.append(FactorIteam(
|
||||
factor=new_factor,
|
||||
product=product_obj,
|
||||
product_name=product_obj.product_name,
|
||||
num=prd_count,
|
||||
amount=prd_count * _safe_int(product_obj.product_price),
|
||||
garantyEsh=garanty_esh
|
||||
))
|
||||
|
||||
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:
|
||||
continue
|
||||
|
||||
prd_count = _safe_int(item.get('prd_count'))
|
||||
garanty_esh = item.get('garanty_Esh', '')
|
||||
product_obj = products_by_id.get(prd_id)
|
||||
|
||||
if not product_obj:
|
||||
continue
|
||||
|
||||
# ۱. کوئری پایه
|
||||
qs = ProductInStore.objects.filter(
|
||||
Product=product_obj,
|
||||
StoreRoom=store_room,
|
||||
year=1400
|
||||
)
|
||||
|
||||
# ۲. قفل دیتابیس هوشمند و سازگار با تمام پایگاههای داده
|
||||
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
|
||||
)
|
||||
|
||||
except InventoryException as e:
|
||||
return HttpResponse(str(e), status=400)
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating factor for customer {c_id}: {str(e)}", exc_info=True)
|
||||
return HttpResponse("خطایی غیرمنتظره در سرور رخ داد. اطلاعات ذخیره نشد.", status=500)
|
||||
|
||||
return redirect(f'/panel/customer/{c_id}/factors/')
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.DelFactor', raise_exception=True)
|
||||
@require_POST
|
||||
def FactorDel(request, c_id, f_id):
|
||||
customer_obj = get_object_or_404(Customer, id=c_id)
|
||||
factor_initial = get_object_or_404(Factor, id=f_id, customer=customer_obj)
|
||||
|
||||
if factor_initial.pay_status == 'cancell' or not factor_initial.is_active:
|
||||
return HttpResponseBadRequest("این فاکتور قبلاً باطل یا برگشت داده شده است.")
|
||||
|
||||
user_fullname = f"{request.user.first_name} {request.user.last_name}".strip() or request.user.username
|
||||
cancel_reason = request.POST.get('cancel_reason', 'error')
|
||||
payment_action = request.POST.get('payment_action', 'keep')
|
||||
|
||||
if cancel_reason == 'return':
|
||||
reason_text = "برگشت از فروش (مرجوعی مشتری)"
|
||||
inv_movement = ProductInStore.MovementType.RETURN
|
||||
else:
|
||||
reason_text = "اصلاح خطای کاربری"
|
||||
inv_movement = ProductInStore.MovementType.ERROR_CORRECTION
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
if connection.features.has_select_for_update:
|
||||
factor = Factor.objects.select_for_update().get(pk=factor_initial.pk)
|
||||
else:
|
||||
factor = Factor.objects.get(pk=factor_initial.pk)
|
||||
|
||||
if factor.pay_status == 'cancell' or not factor.is_active:
|
||||
return HttpResponseBadRequest("خطای همزمانی: این فاکتور در لحظه فرستادن درخواست باطل شده بود.")
|
||||
|
||||
factor.is_active = False
|
||||
factor.pay_status = 'cancell'
|
||||
factor.save(update_fields=['is_active', 'pay_status'])
|
||||
|
||||
# واکشی اسناد پرداخت قبل از ایجاد سند جدید (جلوگیری از باگ تکرار سند)
|
||||
payments_to_reverse = []
|
||||
if payment_action == 'reverse' or cancel_reason == 'error':
|
||||
payments_to_reverse = list(Article.objects.filter(
|
||||
Q(debit=0) | Q(debit__isnull=True),
|
||||
factor=factor,
|
||||
credit__gt=0
|
||||
))
|
||||
|
||||
# ۱. صدور سند معکوس مالی برای کل مبلغ فاکتور
|
||||
factor_amount = int(factor.amount or 0)
|
||||
if factor_amount > 0:
|
||||
Article.objects.create(
|
||||
debit=0,
|
||||
credit=factor_amount,
|
||||
customer=customer_obj,
|
||||
factor=factor,
|
||||
type=factor.PayType,
|
||||
desc=f"برگشت از فروش و ابطال فاکتور شماره {factor.id} (مرجوعی کالا توسط مشتری) - کاربر: {user_fullname}" if cancel_reason == 'return' else f"اصلاح خطای سیستم: ابطال فاکتور اشتباه شماره {factor.id} (بدون جابجایی واقعی کالا) - کاربر: {user_fullname}",
|
||||
user=request.user,
|
||||
payStatus="true"
|
||||
)
|
||||
|
||||
# ۲. خنثی کردن پرداختهای اولیه فاکتور
|
||||
for p in payments_to_reverse:
|
||||
Article.objects.create(
|
||||
debit=p.credit,
|
||||
credit=0,
|
||||
customer=customer_obj,
|
||||
factor=factor,
|
||||
type=p.type,
|
||||
nahvePardakht=p.nahvePardakht,
|
||||
desc=f"عودت وجه پرداخت شده / ابطال رسید فاکتور {factor.id} (به دلیل مرجوعی کالا) - کاربر: {user_fullname}" if cancel_reason == 'return' else f"اصلاح خطای سیستم: ابطال رسید پرداخت فاکتور اشتباه شماره {factor.id} - کاربر: {user_fullname}",
|
||||
user=request.user,
|
||||
payStatus="true"
|
||||
)
|
||||
|
||||
# ۳. 🛠️ اصلاح انبار (حل باگ عدم تغییر موجودی سیستم)
|
||||
stock_movements = list(ProductInStore.objects.filter(factor=factor, s_out__gt=0))
|
||||
|
||||
for movement in stock_movements:
|
||||
pInStore = ProductInStore(
|
||||
StoreRoom=movement.StoreRoom,
|
||||
Product=movement.Product,
|
||||
factor=factor,
|
||||
s_in=movement.s_out, # برگرداندن تعداد کسر شده به ستون ورودی
|
||||
s_out=0,
|
||||
year=movement.year, # 💡 فیکس قطعی: استفاده دقیق از سال مالی تراکنش اصلی (۱۴۰۰) جهت همخوانی با توابع سیستم
|
||||
desc=f"ورود به انبار بابت برگشت از فروش کالا (فاکتور {factor.id}) - کاربر: {user_fullname}" if cancel_reason == 'return' else f"ورود به انبار بابت اصلاح خطای ثبت فاکتور {factor.id} - کاربر: {user_fullname}",
|
||||
movement_type=inv_movement
|
||||
)
|
||||
pInStore.save() # فراخوانی متد save مدل شما که محاسبات تاریخچه و موجودی لحظهای را انجام میدهد
|
||||
|
||||
# ۴. ابطال اقساط و سررسیدهای پاس نشده
|
||||
Sarresid.objects.filter(factor=factor, payStatus="false").update(
|
||||
payStatus="cancell",
|
||||
desc=f"ابطال بابت {reason_text} فاکتور {factor.id}"
|
||||
)
|
||||
|
||||
logger.info(f"Factor {factor.id} successfully cancelled. Reason: {cancel_reason}, Payment: {payment_action}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Critical error in FactorDel for factor {f_id}: {str(e)}", exc_info=True)
|
||||
return HttpResponse("خطای سیستمی در فرآیند ابطال و صدور اسناد اصلاحی رخ داد.", status=500)
|
||||
|
||||
return redirect('admin_panel:customerFactors', c_id=c_id)
|
||||
@@ -0,0 +1,974 @@
|
||||
import datetime
|
||||
import json
|
||||
|
||||
from django.db import transaction
|
||||
from django.db.models import (Sum, Q, Count, Avg, Min, F, Subquery, OuterRef,
|
||||
IntegerField, Value, Case, When)
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.shortcuts import redirect, render, get_object_or_404
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from persiantools.jdatetime import JalaliDate
|
||||
|
||||
from ..models import (Article, Sarresid, Customer, Factor, ProductInStore,
|
||||
StoreRoom, Action)
|
||||
from ._helpers import logger, _safe_int
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||||
def report(request):
|
||||
|
||||
nahvePardakhat = [
|
||||
{'fa': 'تخفیف', 'en': 'تخفیف'},
|
||||
{'fa': 'نسیه', 'en': 'نسیه'},
|
||||
{'fa': 'چک', 'en': 'چک'},
|
||||
{'fa': 'کارتخوان', 'en': 'کارت خوان'},
|
||||
{'fa': 'کارت به کارت', 'en': 'کارت به کارت'},
|
||||
{'fa': 'نقدی', 'en': 'نقد'},
|
||||
]
|
||||
|
||||
userHa = User.objects.filter()
|
||||
|
||||
if request.method == "GET":
|
||||
nahvePardakht = request.GET.get("nahvepardakht", None)
|
||||
user = request.GET.get("user", None)
|
||||
azTarikh = request.GET.get("azTarikh", None)
|
||||
taTarikh = request.GET.get("taTarikh", None)
|
||||
pagenumber = request.GET.get("page", None)
|
||||
|
||||
|
||||
if azTarikh and nahvePardakht:
|
||||
|
||||
query_data = {
|
||||
'nahvePardakht': nahvePardakht,
|
||||
'user': user,
|
||||
'azTarikh': azTarikh,
|
||||
'taTarikh': taTarikh,
|
||||
}
|
||||
|
||||
azDateTime = datetime.datetime.fromtimestamp(int(azTarikh) / 1e3)
|
||||
azDateTime = azDateTime.strftime('%Y-%m-%d 00:00:00')
|
||||
|
||||
taDateTime = datetime.datetime.fromtimestamp(int(taTarikh) / 1e3)
|
||||
taDateTime = taDateTime.strftime('%Y-%m-%d 23:59:59')
|
||||
print("b")
|
||||
user_a = User.objects.get(username=user)
|
||||
print(user_a)
|
||||
|
||||
if nahvePardakht == "نسیه" or nahvePardakht == "چک":
|
||||
if nahvePardakht == 'نسیه':
|
||||
typeSarresid = 'nesiye'
|
||||
elif nahvePardakht == 'چک':
|
||||
typeSarresid = 'check'
|
||||
|
||||
|
||||
sumSarresidha = Sarresid.objects.filter(
|
||||
type=typeSarresid,
|
||||
user=user_a,
|
||||
createdDT__lte=taDateTime,
|
||||
createdDT__gte=azDateTime,
|
||||
payStatus='false'
|
||||
).aggregate(Sum('price'))
|
||||
|
||||
sarresidha = Sarresid.objects.filter(
|
||||
type=typeSarresid,
|
||||
user=user_a,
|
||||
createdDT__lte=taDateTime,
|
||||
createdDT__gte=azDateTime,
|
||||
payStatus='false'
|
||||
)
|
||||
|
||||
paginator = Paginator(sarresidha, 25) # Show 25 contacts per page.
|
||||
page_number = request.GET.get('page', None)
|
||||
all_sarresidha = paginator.get_page(pagenumber)
|
||||
data = {'sarresidha': all_sarresidha ,'sumSarresidha': sumSarresidha, 'nahvePardakhat': nahvePardakhat, 'userHa':userHa, 'query_data': query_data}
|
||||
return render(request, 'admin_panel/reportCheckNesiye.html', data)
|
||||
|
||||
else:
|
||||
|
||||
sumAricles = Article.objects.filter(
|
||||
nahvePardakht=nahvePardakht,
|
||||
user=user_a,
|
||||
createdDT__lte=taDateTime,
|
||||
createdDT__gte=azDateTime,
|
||||
).aggregate(Sum('credit'))
|
||||
|
||||
aricles1 = Article.objects.filter(
|
||||
nahvePardakht=nahvePardakht,
|
||||
user=user_a,
|
||||
createdDT__lte=taDateTime,
|
||||
createdDT__gte=azDateTime,
|
||||
)
|
||||
|
||||
paginator = Paginator(aricles1, 25) # Show 25 contacts per page.
|
||||
page_number = request.GET.get('page', None)
|
||||
all_aricles = paginator.get_page(page_number)
|
||||
data = {'aricles': all_aricles,'sum_credit': sumAricles, 'nahvePardakhat': nahvePardakhat, 'userHa':userHa, 'query_data': query_data}
|
||||
return render(request, 'admin_panel/report.html', data)
|
||||
else:
|
||||
data = {'nahvePardakhat': nahvePardakhat, 'userHa':userHa}
|
||||
return render(request, 'admin_panel/report.html', data)
|
||||
|
||||
|
||||
data = {'nahvePardakhat': nahvePardakhat, 'userHa':userHa}
|
||||
return render(request, 'admin_panel/report.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||||
def report_debtors_no_vosol(request):
|
||||
now = datetime.datetime.now()
|
||||
|
||||
# --- ۱. دریافت و اعتبارسنجی پارامترهای فیلتر از فرانتاند ---
|
||||
try:
|
||||
days_threshold = int(request.GET.get('days', 20))
|
||||
if days_threshold < 0:
|
||||
days_threshold = 20
|
||||
except ValueError:
|
||||
days_threshold = 20
|
||||
|
||||
filter_type = request.GET.get('filter_type', 'all') # all, never, expired
|
||||
sort_field = request.GET.get('sort', 'amount') # amount, oldest_date, last_action
|
||||
sort_dir = request.GET.get('dir', 'desc') # asc, desc
|
||||
|
||||
threshold_date = now - datetime.timedelta(days=days_threshold)
|
||||
|
||||
# --- ۲. ساخت Subquery های فوقسریع و ایمن ---
|
||||
sq_debit = Article.objects.filter(customer=OuterRef('pk')).values('customer').annotate(s=Sum('debit')).values('s')
|
||||
sq_credit = Article.objects.filter(customer=OuterRef('pk')).values('customer').annotate(s=Sum('credit')).values('s')
|
||||
|
||||
sq_overdue_amt = Sarresid.objects.filter(
|
||||
customer=OuterRef('pk'), payStatus='false', sarresidDT__lte=now
|
||||
).values('customer').annotate(s=Sum('price')).values('s')
|
||||
|
||||
sq_oldest_date = Sarresid.objects.filter(
|
||||
customer=OuterRef('pk'), payStatus='false', sarresidDT__lte=now
|
||||
).values('customer').annotate(min_dt=Min('sarresidDT')).values('min_dt')
|
||||
|
||||
sq_overdue_count = Sarresid.objects.filter(
|
||||
customer=OuterRef('pk'), payStatus='false', sarresidDT__lte=now
|
||||
).values('customer').annotate(c=Count('id')).values('c')
|
||||
|
||||
# استخراج آخرین سابقه اکشن وصول
|
||||
latest_vosol_qs = Action.objects.filter(
|
||||
customer=OuterRef('pk'), actionType='vosol'
|
||||
).order_by('-createdDT')
|
||||
|
||||
# --- ۳. ساخت کوئری پایه (فقط مشتریان فعال و دارای معوقه) ---
|
||||
base_qs = Customer.objects.filter(is_active=True).annotate(
|
||||
t_debit=Coalesce(Subquery(sq_debit, output_field=IntegerField()), Value(0)),
|
||||
t_credit=Coalesce(Subquery(sq_credit, output_field=IntegerField()), Value(0)),
|
||||
overdue_amount=Coalesce(Subquery(sq_overdue_amt, output_field=IntegerField()), Value(0)),
|
||||
overdue_count=Coalesce(Subquery(sq_overdue_count, output_field=IntegerField()), Value(0)),
|
||||
oldest_overdue_date=Subquery(sq_oldest_date),
|
||||
last_vosol_date=Subquery(latest_vosol_qs.values('createdDT')[:1]),
|
||||
last_vosol_desc=Subquery(latest_vosol_qs.values('desc')[:1])
|
||||
).annotate(
|
||||
net_debt=F('t_debit') - F('t_credit')
|
||||
).filter(
|
||||
overdue_amount__gt=0 # شرط اساسی: فقط کسانی که معوقه سررسید گذشته دارند
|
||||
)
|
||||
|
||||
# استخراج مشتریانی که نیازمند پیگیری هستند (هرگز پیگیری نشدهاند یا موعد پیگیریشان گذشته است)
|
||||
action_needed_qs = base_qs.filter(
|
||||
Q(last_vosol_date__isnull=True) | Q(last_vosol_date__lt=threshold_date)
|
||||
)
|
||||
|
||||
# --- ۴. محاسبه آمار دقیق (بدون خطا) برای داشبورد ---
|
||||
# استفاده از Case/When برای جلوگیری کامل از خطای None در Aggregate
|
||||
stats = action_needed_qs.aggregate(
|
||||
never_count=Coalesce(Sum(Case(When(last_vosol_date__isnull=True, then=1), default=Value(0), output_field=IntegerField())), Value(0)),
|
||||
never_amount=Coalesce(Sum(Case(When(last_vosol_date__isnull=True, then=F('overdue_amount')), default=Value(0), output_field=IntegerField())), Value(0)),
|
||||
expired_count=Coalesce(Sum(Case(When(last_vosol_date__isnull=False, then=1), default=Value(0), output_field=IntegerField())), Value(0)),
|
||||
expired_amount=Coalesce(Sum(Case(When(last_vosol_date__isnull=False, then=F('overdue_amount')), default=Value(0), output_field=IntegerField())), Value(0)),
|
||||
)
|
||||
|
||||
# --- ۵. اعمال فیلتر کشویی (Dropdown) ---
|
||||
if filter_type == 'never':
|
||||
final_qs = action_needed_qs.filter(last_vosol_date__isnull=True)
|
||||
elif filter_type == 'expired':
|
||||
final_qs = action_needed_qs.filter(last_vosol_date__isnull=False)
|
||||
else: # all
|
||||
final_qs = action_needed_qs
|
||||
|
||||
# --- ۶. اعمال مرتبسازی (Sorting) پویا ---
|
||||
if sort_field == 'amount':
|
||||
order_expr = F('overdue_amount').desc() if sort_dir == 'desc' else F('overdue_amount').asc()
|
||||
elif sort_field == 'oldest_date':
|
||||
order_expr = F('oldest_overdue_date').desc(nulls_last=True) if sort_dir == 'desc' else F('oldest_overdue_date').asc(nulls_last=True)
|
||||
elif sort_field == 'last_action':
|
||||
order_expr = F('last_vosol_date').desc(nulls_last=True) if sort_dir == 'desc' else F('last_vosol_date').asc(nulls_first=True)
|
||||
else:
|
||||
order_expr = F('overdue_amount').desc()
|
||||
|
||||
final_qs = final_qs.order_by(order_expr)
|
||||
|
||||
# --- ۷. صفحهبندی ایمن ---
|
||||
paginator = Paginator(final_qs, 50)
|
||||
page_number = request.GET.get('page', 1)
|
||||
try:
|
||||
page_obj = paginator.page(page_number)
|
||||
except (PageNotAnInteger, EmptyPage):
|
||||
page_obj = paginator.page(1)
|
||||
|
||||
context = {
|
||||
'page_obj': page_obj,
|
||||
'page_name': 'debtors_no_vosol',
|
||||
'stats': stats,
|
||||
# پارامترهای کنترل پنل
|
||||
'current_days': days_threshold,
|
||||
'current_filter': filter_type,
|
||||
'current_sort': sort_field,
|
||||
'current_dir': sort_dir,
|
||||
}
|
||||
|
||||
return render(request, 'admin_panel/report_debtors_no_vosol.html', context)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||||
def inventory_movements_report(request):
|
||||
"""
|
||||
گزارش جامع گردش و خروج کالا از انبار به همراه فیلتر متنی نام کالا،
|
||||
نوع تراکنش، کاربر، انبار و بازه زمانی شمسی.
|
||||
"""
|
||||
all_users = User.objects.all().order_by('first_name')
|
||||
all_stores = StoreRoom.objects.all()
|
||||
movement_choices = ProductInStore.MovementType.choices
|
||||
|
||||
# دریافت پارامترهای فیلتر از فرانتاند
|
||||
selected_type = request.GET.get('movement_type', '')
|
||||
selected_user = request.GET.get('user_id', '')
|
||||
selected_store = request.GET.get('store_id', '')
|
||||
selected_product_name = request.GET.get('product_name', '').strip() # فیلد جدید جستجوی نام کالا
|
||||
az_tarikh = request.GET.get('azTarikh', '')
|
||||
ta_tarikh = request.GET.get('taTarikh', '')
|
||||
|
||||
# ۱. کوئری پایه بهینه
|
||||
movements_list = ProductInStore.objects.select_related(
|
||||
'Product', 'StoreRoom', 'factor__customer', 'factor__user'
|
||||
).filter(year=1400).order_by('-createdDT')
|
||||
|
||||
# ۲. اعمال فیلترهای پویا
|
||||
if selected_type:
|
||||
movements_list = movements_list.filter(movement_type=selected_type)
|
||||
|
||||
if selected_user:
|
||||
movements_list = movements_list.filter(factor__user__id=_safe_int(selected_user))
|
||||
|
||||
if selected_store:
|
||||
movements_list = movements_list.filter(StoreRoom__id=_safe_int(selected_store))
|
||||
|
||||
if selected_product_name:
|
||||
# فیلتر متنی حساس نبودن به حروف بزرگ و کوچک (Case-Insensitive Contains)
|
||||
movements_list = movements_list.filter(Product__product_name__icontains=selected_product_name)
|
||||
|
||||
# ۳. پردازش بازه زمانی
|
||||
if az_tarikh:
|
||||
try:
|
||||
az_dt = datetime.datetime.fromtimestamp(int(az_tarikh) / 1000.0)
|
||||
movements_list = movements_list.filter(createdDT__gte=az_dt.strftime('%Y-%m-%d 00:00:00'))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
if ta_tarikh:
|
||||
try:
|
||||
ta_dt = datetime.datetime.fromtimestamp(int(ta_tarikh) / 1000.0)
|
||||
movements_list = movements_list.filter(createdDT__lte=ta_dt.strftime('%Y-%m-%d 23:59:59'))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# ۴. محاسبه آماری سریع دیتابیس روی نتایج فیلتر شده
|
||||
summary_stats = movements_list.aggregate(
|
||||
total_out_qty=Sum('s_out'),
|
||||
total_in_qty=Sum('s_in')
|
||||
)
|
||||
|
||||
# ۵. صفحهبندی نتایج
|
||||
paginator = Paginator(movements_list, 50)
|
||||
page_number = request.GET.get('page')
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
context = {
|
||||
'page_obj': page_obj,
|
||||
'all_users': all_users,
|
||||
'all_stores': all_stores,
|
||||
'movement_choices': movement_choices,
|
||||
'summary_stats': summary_stats,
|
||||
'filters': {
|
||||
'movement_type': selected_type,
|
||||
'user_id': _safe_int(selected_user) if selected_user else '',
|
||||
'store_id': _safe_int(selected_store) if selected_store else '',
|
||||
'product_name': selected_product_name, # ارسال فیلد جدید به قالب برای حفظ مقدار ورودی
|
||||
'azTarikh': az_tarikh,
|
||||
'taTarikh': ta_tarikh,
|
||||
}
|
||||
}
|
||||
|
||||
return render(request, 'admin_panel/report_inventory_movements.html', context)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||||
def financial_dashboard(request):
|
||||
"""
|
||||
داشبورد هوش مالی و مدیریت نقدینگی (BI Dashboard)
|
||||
مجهز به هسته حسابرسی ۳۶۰ درجه برای کشف تمامی مغایرتها (بدهی رو هوا، چکهای الکی، پیشدریافتها)
|
||||
"""
|
||||
az_tarikh = request.GET.get('azTarikh', '')
|
||||
ta_tarikh = request.GET.get('taTarikh', '')
|
||||
|
||||
az_dt = None
|
||||
ta_dt = None
|
||||
|
||||
if az_tarikh:
|
||||
try:
|
||||
az_dt = datetime.datetime.fromtimestamp(int(az_tarikh) / 1000.0)
|
||||
az_dt = az_dt.replace(hour=0, minute=0, second=0)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
if ta_tarikh:
|
||||
try:
|
||||
ta_dt = datetime.datetime.fromtimestamp(int(ta_tarikh) / 1000.0)
|
||||
ta_dt = ta_dt.replace(hour=23, minute=59, second=59)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
anchor_dt = ta_dt if ta_dt else datetime.datetime.now()
|
||||
|
||||
# =========================================================================
|
||||
# محاسبه روند ۱۲ ماهه غلتان (TTM)
|
||||
# =========================================================================
|
||||
jalali_months = ['فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور', 'مهر', 'آبان', 'آذر', 'دی', 'بهمن', 'اسفند']
|
||||
trend_labels = []
|
||||
trend_data = []
|
||||
|
||||
anchor_jd = JalaliDate(anchor_dt.date())
|
||||
curr_y = int(anchor_jd.year)
|
||||
curr_m = int(anchor_jd.month)
|
||||
|
||||
for _ in range(12):
|
||||
if curr_m < 1:
|
||||
curr_m = 12
|
||||
curr_y -= 1
|
||||
|
||||
label = f"{jalali_months[curr_m - 1]} {curr_y}"
|
||||
trend_labels.insert(0, label)
|
||||
|
||||
last_day = JalaliDate.days_in_month(curr_m, curr_y)
|
||||
end_of_month_jd = JalaliDate(curr_y, curr_m, last_day)
|
||||
end_of_month_gd = end_of_month_jd.to_gregorian()
|
||||
end_dt_str = end_of_month_gd.strftime('%Y-%m-%d 23:59:59')
|
||||
|
||||
totals = Article.objects.filter(createdDT__lte=end_dt_str).aggregate(
|
||||
t_deb=Sum('debit'),
|
||||
t_cred=Sum('credit')
|
||||
)
|
||||
t_deb = totals['t_deb'] or 0
|
||||
t_cred = totals['t_cred'] or 0
|
||||
month_total_debt = t_deb - t_cred
|
||||
|
||||
trend_data.insert(0, month_total_debt)
|
||||
curr_m -= 1
|
||||
|
||||
# =========================================================================
|
||||
# فیلترهای پایه (دادههای زماندار و داشبورد)
|
||||
# =========================================================================
|
||||
sarresid_qs = Sarresid.objects.filter(factor__productinstore__year=1400).distinct()
|
||||
factor_qs = Factor.objects.filter(productinstore__year=1400, is_active=True).distinct()
|
||||
article_qs = Article.objects.filter(factor__productinstore__year=1400).distinct()
|
||||
store_qs = ProductInStore.objects.filter(year=1400)
|
||||
|
||||
if az_dt:
|
||||
sarresid_qs = sarresid_qs.filter(createdDT__gte=az_dt)
|
||||
factor_qs = factor_qs.filter(create_date__gte=az_dt)
|
||||
article_qs = article_qs.filter(createdDT__gte=az_dt)
|
||||
store_qs = store_qs.filter(createdDT__gte=az_dt)
|
||||
|
||||
if ta_dt:
|
||||
sarresid_qs = sarresid_qs.filter(createdDT__lte=ta_dt)
|
||||
factor_qs = factor_qs.filter(create_date__lte=ta_dt)
|
||||
article_qs = article_qs.filter(createdDT__lte=ta_dt)
|
||||
store_qs = store_qs.filter(createdDT__lte=ta_dt)
|
||||
|
||||
# کارتهای شاخص عملکرد عمومی
|
||||
total_unpaid_debts = sarresid_qs.filter(payStatus="false").aggregate(Sum('price'))['price__sum'] or 0
|
||||
total_sales_period = factor_qs.aggregate(Sum('amount'))['amount__sum'] or 0
|
||||
overdue_checks_count = sarresid_qs.filter(payStatus="false", type="check", sarresidDT__lte=anchor_dt).count()
|
||||
|
||||
# =========================================================================
|
||||
# 👑 هسته حسابرسی ۳۶۰ درجه (برگرفته از Ghost Hunter / Deep Audit)
|
||||
# =========================================================================
|
||||
|
||||
# الف) تراز کل ریاضی سیستم
|
||||
g_art = Article.objects.aggregate(d=Sum('debit'), c=Sum('credit'))
|
||||
global_mande = (g_art['d'] or 0) - (g_art['c'] or 0)
|
||||
g_sar = Sarresid.objects.filter(payStatus="false").aggregate(s=Sum('price'))['s'] or 0
|
||||
global_diff = global_mande - g_sar
|
||||
|
||||
# ب) محاسبات Subquery فوقسریع برای تمام مشتریان
|
||||
sq_debit = Article.objects.filter(customer=OuterRef('pk')).values('customer').annotate(s=Sum('debit')).values('s')
|
||||
sq_credit = Article.objects.filter(customer=OuterRef('pk')).values('customer').annotate(s=Sum('credit')).values('s')
|
||||
sq_sarresid = Sarresid.objects.filter(customer=OuterRef('pk'), payStatus="false").values('customer').annotate(s=Sum('price')).values('s')
|
||||
|
||||
base_customers = Customer.objects.filter(is_active=True).annotate(
|
||||
t_debit=Coalesce(Subquery(sq_debit, output_field=IntegerField()), 0),
|
||||
t_credit=Coalesce(Subquery(sq_credit, output_field=IntegerField()), 0),
|
||||
t_sarresid=Coalesce(Subquery(sq_sarresid, output_field=IntegerField()), 0),
|
||||
).annotate(
|
||||
net_debt=F('t_debit') - F('t_credit') # تراز نهایی
|
||||
).annotate(
|
||||
diff=F('net_debt') - F('t_sarresid') # میزان کسر/اضافه سند
|
||||
)
|
||||
|
||||
# 🔴 گروه 1: بدهکارانی که کسر سند/چک دارند (بدهی خطرناک روی هوا) [diff > 0]
|
||||
group1_qs = base_customers.filter(diff__gt=0).order_by('-diff')
|
||||
group1_count = group1_qs.count()
|
||||
group1_total = sum(group1_qs.values_list('diff', flat=True)) or 0
|
||||
group1_sample = group1_qs[:20]
|
||||
|
||||
# 🟡 گروه 2: چک پاسنشده در سیستم بیشتر از بدهی است (خطای ثبتی اپراتور) [diff < 0 AND net_debt >= 0]
|
||||
group2_qs = base_customers.filter(diff__lt=0, net_debt__gte=0).annotate(
|
||||
fake_amount=F('t_sarresid') - F('net_debt')
|
||||
).order_by('-fake_amount')
|
||||
group2_count = group2_qs.count()
|
||||
group2_total = sum(group2_qs.values_list('fake_amount', flat=True)) or 0
|
||||
group2_sample = group2_qs[:20]
|
||||
|
||||
# 🟢 گروه 3: پیشدریافت خالص (بستانکار است و هیچ چکی ندارد) [diff < 0 AND net_debt < 0 AND t_sarresid = 0]
|
||||
group3_qs = base_customers.filter(diff__lt=0, net_debt__lt=0, t_sarresid=0).order_by('net_debt') # net_debt منفی است
|
||||
group3_count = group3_qs.count()
|
||||
group3_total = sum(group3_qs.values_list('net_debt', flat=True)) or 0
|
||||
group3_sample = group3_qs[:20]
|
||||
|
||||
# 🟣 گروه 4: عجیبترین حالت! مشتری بستانکار است، اما چک پاسنشده هم در سیستم دارد! [diff < 0 AND net_debt < 0 AND t_sarresid > 0]
|
||||
group4_qs = base_customers.filter(diff__lt=0, net_debt__lt=0, t_sarresid__gt=0).order_by('net_debt')
|
||||
group4_count = group4_qs.count()
|
||||
group4_total = sum(group4_qs.values_list('diff', flat=True)) or 0
|
||||
group4_sample = group4_qs[:20]
|
||||
|
||||
# =========================================================================
|
||||
# نمودارها و آمار تکمیلی داشبورد
|
||||
# =========================================================================
|
||||
debt_future = sarresid_qs.filter(payStatus="false", sarresidDT__gt=anchor_dt).aggregate(Sum('price'))['price__sum'] or 0
|
||||
dt_30_days_ago = anchor_dt - datetime.timedelta(days=30)
|
||||
debt_0_30 = sarresid_qs.filter(payStatus="false", sarresidDT__lte=anchor_dt, sarresidDT__gte=dt_30_days_ago).aggregate(Sum('price'))['price__sum'] or 0
|
||||
dt_90_days_ago = anchor_dt - datetime.timedelta(days=90)
|
||||
debt_30_90 = sarresid_qs.filter(payStatus="false", sarresidDT__lt=dt_30_days_ago, sarresidDT__gte=dt_90_days_ago).aggregate(Sum('price'))['price__sum'] or 0
|
||||
debt_above_90 = sarresid_qs.filter(payStatus="false", sarresidDT__lt=dt_90_days_ago).aggregate(Sum('price'))['price__sum'] or 0
|
||||
|
||||
payment_articles = article_qs.filter(credit__gt=0).values('nahvePardakht').annotate(total_amount=Sum('credit')).order_by('-total_amount')
|
||||
pay_labels = []
|
||||
pay_data = []
|
||||
for item in payment_articles:
|
||||
label = item['nahvePardakht'] or 'سایر/نامشخص'
|
||||
pay_labels.append(label)
|
||||
pay_data.append(item['total_amount'])
|
||||
|
||||
store_leaks = store_qs.filter(
|
||||
movement_type__in=[ProductInStore.MovementType.WARRANTY, ProductInStore.MovementType.PROMOTION]
|
||||
).values('movement_type').annotate(total_qty=Sum('s_out'))
|
||||
|
||||
leak_data = {'WARRANTY': 0, 'PROMOTION': 0}
|
||||
for leak in store_leaks:
|
||||
leak_data[leak['movement_type']] = leak['total_qty']
|
||||
|
||||
context = {
|
||||
'kpi_total_unpaid': total_unpaid_debts,
|
||||
'kpi_total_sales': total_sales_period,
|
||||
'kpi_overdue_checks': overdue_checks_count,
|
||||
|
||||
# آمار کلان ریاضیاتی
|
||||
'global_mande': global_mande,
|
||||
'g_sar': g_sar,
|
||||
'global_diff': global_diff,
|
||||
|
||||
# متغیرهای ۴ گروه حسابرسی
|
||||
'group1_count': group1_count, 'group1_total': group1_total, 'group1_sample': group1_sample,
|
||||
'group2_count': group2_count, 'group2_total': group2_total, 'group2_sample': group2_sample,
|
||||
'group3_count': group3_count, 'group3_total': group3_total, 'group3_sample': group3_sample,
|
||||
'group4_count': group4_count, 'group4_total': group4_total, 'group4_sample': group4_sample,
|
||||
|
||||
'trend_labels': trend_labels,
|
||||
'trend_data': trend_data,
|
||||
|
||||
'aging_data': [debt_future, debt_0_30, debt_30_90, debt_above_90],
|
||||
'pay_labels': pay_labels,
|
||||
'pay_data': pay_data,
|
||||
'warranty_qty': leak_data['WARRANTY'],
|
||||
'promotion_qty': leak_data['PROMOTION'],
|
||||
|
||||
'filters': {
|
||||
'azTarikh': az_tarikh,
|
||||
'taTarikh': ta_tarikh,
|
||||
}
|
||||
}
|
||||
|
||||
return render(request, 'admin_panel/financial_dashboard.html', context)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||||
def bedehi(request):
|
||||
try:
|
||||
debit = Article.objects.filter().aggregate(Sum('debit'))['debit__sum']
|
||||
credit = Article.objects.filter().aggregate(Sum('credit'))['credit__sum']
|
||||
mandehesabCs = debit - credit
|
||||
articls = Article.objects.filter().order_by('-createdDT')
|
||||
articlsPaginator = Paginator(articls, 10)
|
||||
pageNumber = request.GET.get("page", 1)
|
||||
articl_objs = articlsPaginator.get_page(pageNumber)
|
||||
|
||||
except:
|
||||
articls =None
|
||||
mandehesabCs = 0
|
||||
|
||||
data = {'articls': articl_objs, 'mandehesabKP': mandehesabCs}
|
||||
return render(request, 'admin_panel/bedehi.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||||
def hesabdari(request):
|
||||
page_name = 'sarresidShodeha'
|
||||
anchor_dt = datetime.datetime.now()
|
||||
|
||||
# تعریف محدودههای زمانی ماتریس ریسک (سن مطالبات)
|
||||
dt_30_days_ago = anchor_dt - datetime.timedelta(days=30)
|
||||
dt_90_days_ago = anchor_dt - datetime.timedelta(days=90)
|
||||
|
||||
# کوئری پایه: تمام سررسیدهای پرداختنشده که از موعد آنها گذشته است
|
||||
base_qs = Sarresid.objects.filter(payStatus="false", sarresidDT__lte=anchor_dt)
|
||||
|
||||
# محاسبه پرسرعت مجموع مبالغ در هر دسته (توسط موتور دیتابیس)
|
||||
matrix_totals = base_qs.aggregate(
|
||||
minor_delay=Sum('price', filter=Q(sarresidDT__gte=dt_30_days_ago)),
|
||||
high_risk=Sum('price', filter=Q(sarresidDT__lt=dt_30_days_ago, sarresidDT__gte=dt_90_days_ago)),
|
||||
critical=Sum('price', filter=Q(sarresidDT__lt=dt_90_days_ago)),
|
||||
total_sum=Sum('price')
|
||||
)
|
||||
|
||||
# دریافت فیلتر انتخابی کاربر از URL
|
||||
risk_filter = request.GET.get('risk_level', 'all')
|
||||
|
||||
# اعمال فیلتر روی لیست نمایش داده شده
|
||||
if risk_filter == 'minor':
|
||||
filtered_qs = base_qs.filter(sarresidDT__gte=dt_30_days_ago)
|
||||
elif risk_filter == 'high':
|
||||
filtered_qs = base_qs.filter(sarresidDT__lt=dt_30_days_ago, sarresidDT__gte=dt_90_days_ago)
|
||||
elif risk_filter == 'critical':
|
||||
filtered_qs = base_qs.filter(sarresidDT__lt=dt_90_days_ago)
|
||||
else:
|
||||
filtered_qs = base_qs
|
||||
|
||||
# مرتبسازی: قدیمیترینها (خطرناکترینها) در صدر جدول قرار میگیرند
|
||||
filtered_qs = filtered_qs.order_by('-sarresidDT')
|
||||
|
||||
page_number = request.GET.get('page', 1)
|
||||
paginator = Paginator(filtered_qs, per_page=50)
|
||||
all_sarresid_pgi = paginator.get_page(page_number)
|
||||
|
||||
data = {
|
||||
'page_name': page_name,
|
||||
'dateNow': anchor_dt.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'all_sarresid': all_sarresid_pgi,
|
||||
'current_filter': risk_filter,
|
||||
# متغیرهای ماتریس ریسک
|
||||
'sum_sarresid': matrix_totals['total_sum'] or 0,
|
||||
'minor_sum': matrix_totals['minor_delay'] or 0,
|
||||
'high_sum': matrix_totals['high_risk'] or 0,
|
||||
'critical_sum': matrix_totals['critical'] or 0,
|
||||
}
|
||||
|
||||
return render(request, 'admin_panel/hesabdari.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||||
def hesabdari_checkPass(request):
|
||||
|
||||
page_name = 'checkPass'
|
||||
|
||||
dateNow = datetime.datetime.today().strftime('%Y-%m-%d 23:59:59')
|
||||
all_sarresid = Sarresid.objects.filter(payStatus="true", type='check').order_by('-sarresidDT')
|
||||
|
||||
page_number = request.GET.get('page', None)
|
||||
paginator = Paginator(all_sarresid, per_page=50)
|
||||
|
||||
all_sarresid_pgi = paginator.get_page(page_number)
|
||||
|
||||
#paginator = Paginator(all_sarresid, per_page=200)
|
||||
|
||||
data = {'all_sarresid': all_sarresid_pgi, "dateNow":dateNow, "page_name":page_name}
|
||||
return render(request, 'admin_panel/hesabdari.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||||
def hesabdari_checkPassFail(request):
|
||||
|
||||
page_name = 'checkPassFaild'
|
||||
|
||||
dateNow = datetime.datetime.today().strftime('%Y-%m-%d 23:59:59')
|
||||
all_sarresid = Sarresid.objects.filter(payStatus="false", type='check').order_by('sarresidDT')
|
||||
|
||||
count_check_passfail = all_sarresid.count()
|
||||
|
||||
sum_check_passfail = all_sarresid.aggregate(Sum('price'))['price__sum']
|
||||
|
||||
page_number = request.GET.get('page', None)
|
||||
paginator = Paginator(all_sarresid, per_page=50)
|
||||
|
||||
all_sarresid_pgi = paginator.get_page(page_number)
|
||||
|
||||
#paginator = Paginator(all_sarresid, per_page=200)
|
||||
|
||||
data = {'all_sarresid': all_sarresid_pgi, 'sum_check_passfail':sum_check_passfail, 'count_check_passfail': count_check_passfail, "dateNow":dateNow, "page_name":page_name}
|
||||
return render(request, 'admin_panel/hesabdari.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||||
def hesabdari_residnashone(request):
|
||||
|
||||
page_name = 'sarresidNaShodeha'
|
||||
|
||||
dateNow = datetime.datetime.today().strftime('%Y-%m-%d 23:59:59')
|
||||
all_sarresid = Sarresid.objects.filter(payStatus="false", sarresidDT__gte=dateNow).order_by('sarresidDT')
|
||||
|
||||
sarresidNashode = all_sarresid.aggregate(Sum('price'))['price__sum']
|
||||
page_number = request.GET.get('page', None)
|
||||
paginator = Paginator(all_sarresid, per_page=50)
|
||||
|
||||
all_sarresid_pgi = paginator.get_page(page_number)
|
||||
|
||||
#paginator = Paginator(all_sarresid, per_page=200)
|
||||
|
||||
data = {'sarresidNashode': sarresidNashode,'all_sarresid': all_sarresid_pgi, "dateNow":dateNow, "page_name":page_name}
|
||||
return render(request, 'admin_panel/hesabdari.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||||
def bedehiKhadamat(request):
|
||||
#bedehiKhadamat
|
||||
try:
|
||||
debit = Article.objects.filter(typePy='khadamat').aggregate(Sum('debit'))['debit__sum']
|
||||
credit = Article.objects.filter(typePy='khadamat').aggregate(Sum('credit'))['credit__sum']
|
||||
mandehesabCs = debit - credit
|
||||
articls = Article.objects.filter(typePy='khadamat').order_by('-createdDT')[:100]
|
||||
except:
|
||||
articls =None
|
||||
mandehesabCs = 0
|
||||
|
||||
data = {'articls': articls, 'mandehesabKP': mandehesabCs}
|
||||
return render(request, 'admin_panel/bedehi.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||||
def bedehiNasb(request):
|
||||
try:
|
||||
debit = Article.objects.filter(typePy='nasb').aggregate(Sum('debit'))['debit__sum']
|
||||
credit = Article.objects.filter(typePy='nasb').aggregate(Sum('credit'))['credit__sum']
|
||||
mandehesabCs = debit - credit
|
||||
articls = Article.objects.filter(typePy='nasb').order_by('-createdDT')[:100]
|
||||
except:
|
||||
articls =None
|
||||
mandehesabCs = 0
|
||||
|
||||
data = {'articls': articls, 'mandehesabKP': mandehesabCs}
|
||||
return render(request, 'admin_panel/bedehi.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewStore', raise_exception=True)
|
||||
def passCheck(request, articl_id):
|
||||
|
||||
sarresidINC = Sarresid.objects.get(id=articl_id)
|
||||
user_op = request.user
|
||||
|
||||
desc = request.POST.get('message')
|
||||
|
||||
articleHesabDari2 = Article()
|
||||
articleHesabDari2.debit = 0
|
||||
articleHesabDari2.credit = sarresidINC.price
|
||||
articleHesabDari2.customer = sarresidINC.customer
|
||||
articleHesabDari2.factor = sarresidINC.factor
|
||||
articleHesabDari2.desc = 'وصول چک' + " " + str(sarresidINC.customer.name_family)
|
||||
if sarresidINC.factor:
|
||||
articleHesabDari2.desc += " فاکتور شماره: " + str(sarresidINC.factor.id)
|
||||
articleHesabDari2.nahvePardakht = 'وصول چک'
|
||||
articleHesabDari2.payStatus = "true"
|
||||
articleHesabDari2.user = user_op
|
||||
articleHesabDari2.save()
|
||||
|
||||
sarresidINC.payStatus = "true"
|
||||
sarresidINC.desc = desc
|
||||
sarresidINC.save()
|
||||
|
||||
return HttpResponse("ok")
|
||||
#return redirect(request.META.get('HTTP_REFERER'))
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewStore', raise_exception=True)
|
||||
@require_POST
|
||||
def update_check_info(request, sarresid_id):
|
||||
# دریافت رکورد مورد نظر
|
||||
sarresid = get_object_or_404(Sarresid, id=sarresid_id)
|
||||
|
||||
# اطمینان از اینکه آیتم حتما چک است
|
||||
if sarresid.type != 'check':
|
||||
messages.error(request, "عملیات نامعتبر: این سند چک نیست.")
|
||||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||||
|
||||
# دریافت مقادیر فرم با حذف فاصلههای اضافی
|
||||
check_row_number = request.POST.get('check_row_number', '').strip()
|
||||
check_sayyad_id = request.POST.get('check_sayyad_id', '').strip()
|
||||
check_owner_name = request.POST.get('check_owner_name', '').strip()
|
||||
check_owner_phone = request.POST.get('check_owner_phone', '').strip()
|
||||
|
||||
# اعتبارسنجی فیلدهای اجباری
|
||||
if not check_row_number or not check_sayyad_id or not check_owner_name:
|
||||
messages.error(request, "شماره ردیف، شناسه صیادی و نام مالک اجباری است.")
|
||||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||||
|
||||
# اعتبارسنجی شناسه صیادی (باید دقیقا ۱۶ رقم عدد باشد)
|
||||
if not check_sayyad_id.isdigit() or len(check_sayyad_id) != 16:
|
||||
messages.error(request, "شناسه صیادی باید دقیقاً ۱۶ رقم عدد باشد.")
|
||||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||||
|
||||
# اعتبارسنجی شماره تلفن (در صورت وارد شدن باید فقط عدد باشد)
|
||||
if check_owner_phone and not check_owner_phone.isdigit():
|
||||
messages.error(request, "شماره تلفن مالک چک باید فقط شامل اعداد باشد.")
|
||||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||||
|
||||
try:
|
||||
# بروزرسانی اطلاعات
|
||||
sarresid.check_row_number = check_row_number
|
||||
sarresid.check_sayyad_id = check_sayyad_id
|
||||
sarresid.check_owner_name = check_owner_name
|
||||
sarresid.check_owner_phone = check_owner_phone
|
||||
# آپدیت فیلد توضیحات با اطلاعات جدید
|
||||
sarresid.desc = f"صیادی: {check_sayyad_id} | مالک: {check_owner_name} | تلفن: {check_owner_phone or 'ندارد'}"
|
||||
|
||||
sarresid.save()
|
||||
messages.success(request, "اطلاعات چک با موفقیت ثبت/بروزرسانی شد.")
|
||||
except Exception as e:
|
||||
messages.error(request, "خطای سیستمی در بروزرسانی اطلاعات رخ داد.")
|
||||
|
||||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewStore', raise_exception=True)
|
||||
def addSarresid(request, cs_id):
|
||||
if request.method != 'POST':
|
||||
return HttpResponseBadRequest("متد ارسالی نامعتبر است.")
|
||||
|
||||
try:
|
||||
customerObj = get_object_or_404(Customer, id=cs_id)
|
||||
|
||||
# دریافت فیلدهای اصلی
|
||||
sarrresidUNIX = request.POST.get('n_sarresidDateTime')
|
||||
sarresidPrice = request.POST.get('n_sarresidPrice')
|
||||
sarresidType = request.POST.get('n_sarresidType')
|
||||
|
||||
# دریافت و پاکسازی فیلدهای چک (جلوگیری از باگ فاصلههای خالی)
|
||||
check_row_number = request.POST.get('check_row_number', '').strip()
|
||||
check_sayyad_id = request.POST.get('check_sayyad_id', '').strip()
|
||||
check_owner_name = request.POST.get('check_owner_name', '').strip()
|
||||
check_owner_phone = request.POST.get('check_owner_phone', '').strip()
|
||||
|
||||
# --- لایه اول: اعتبارسنجی فیلدهای پایه ---
|
||||
if not sarrresidUNIX or not sarresidPrice or not sarresidType:
|
||||
messages.error(request, "لطفا فیلدهای اصلی (مبلغ، نوع و تاریخ) را پر کنید.")
|
||||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||||
|
||||
# --- لایه دوم: اعتبارسنجی دقیق و امنیتی فیلدهای چک ---
|
||||
if sarresidType == "check":
|
||||
if not check_row_number or not check_sayyad_id or not check_owner_name:
|
||||
messages.error(request, "برای ثبت چک، وارد کردن شماره ردیف، شناسه صیادی و نام مالک اجباری است.")
|
||||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||||
|
||||
# بررسی ۱۶ رقمی و عددی بودن شناسه صیادی
|
||||
if not check_sayyad_id.isdigit() or len(check_sayyad_id) != 16:
|
||||
messages.error(request, "شناسه صیادی باید دقیقاً ۱۶ رقم عدد باشد.")
|
||||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||||
|
||||
# بررسی عددی بودن شماره تلفن (در صورت وارد شدن)
|
||||
if check_owner_phone and not check_owner_phone.isdigit():
|
||||
messages.error(request, "شماره تلفن مالک چک باید فقط شامل اعداد باشد.")
|
||||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||||
|
||||
# تبدیل نوع دادهها با مدیریت خطا (جلوگیری از کرش سرور 500)
|
||||
try:
|
||||
sarResidDateTime01 = datetime.datetime.fromtimestamp(int(sarrresidUNIX) / 1000.0)
|
||||
price_int = int(sarresidPrice.replace(',', ''))
|
||||
except ValueError:
|
||||
messages.error(request, "مقادیر تاریخ یا مبلغ نامعتبر است.")
|
||||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||||
|
||||
# --- لایه سوم: ذخیرهسازی اتمیک (تضمین جامعیت دادهها) ---
|
||||
with transaction.atomic():
|
||||
# ۱. ثبت سند حسابداری
|
||||
articleHesabDariObj = Article()
|
||||
articleHesabDariObj.debit = price_int
|
||||
articleHesabDariObj.credit = 0
|
||||
articleHesabDariObj.customer = customerObj
|
||||
articleHesabDariObj.user = request.user
|
||||
|
||||
if sarresidType == "check":
|
||||
articleHesabDariObj.desc = f"بدهی بابت ثبت چک دستی (صیادی: {check_sayyad_id})"
|
||||
elif sarresidType == "nesiye":
|
||||
articleHesabDariObj.desc = "بدهی بابت ثبت نسیه دستی"
|
||||
|
||||
articleHesabDariObj.save()
|
||||
|
||||
# ۲. ثبت سررسید / چک
|
||||
sarresidINC = Sarresid()
|
||||
sarresidINC.customer = customerObj
|
||||
sarresidINC.price = price_int
|
||||
sarresidINC.payStatus = "false"
|
||||
sarresidINC.type = sarresidType
|
||||
sarresidINC.user = request.user
|
||||
sarresidINC.sarresidDT = sarResidDateTime01
|
||||
|
||||
if sarresidType == "check":
|
||||
sarresidINC.check_row_number = check_row_number
|
||||
sarresidINC.check_sayyad_id = check_sayyad_id
|
||||
sarresidINC.check_owner_name = check_owner_name
|
||||
sarresidINC.check_owner_phone = check_owner_phone
|
||||
sarresidINC.desc = f"صیادی: {check_sayyad_id} | مالک: {check_owner_name} | تلفن: {check_owner_phone or 'ندارد'}"
|
||||
|
||||
sarresidINC.save()
|
||||
|
||||
messages.success(request, "اطلاعات با موفقیت ثبت شد.")
|
||||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in addSarresid: {str(e)}", exc_info=True)
|
||||
messages.error(request, "خطای سیستمی رخ داد. لطفا مجددا تلاش کنید.")
|
||||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewStore', raise_exception=True)
|
||||
def pardakhtNesiye(request, cs_id):
|
||||
try:
|
||||
customerObj = Customer.objects.get(id=cs_id)
|
||||
user_op = request.user
|
||||
|
||||
sarrresidUNIX = request.POST.get('next_sarresidDateTime', None)
|
||||
sarresidPrice = int(request.POST.get('next_sarresidPrice', None))
|
||||
sr_articlID = request.POST.get('sr_articlID', None)
|
||||
nahvePardakht = request.POST.get('nahvePardakht', None)
|
||||
desc_sar = request.POST.get('desc_sar', None)
|
||||
|
||||
sarResidDateTime01 = datetime.datetime.fromtimestamp(int(sarrresidUNIX)/1000.0)
|
||||
|
||||
SarresidObj = Sarresid.objects.get(id=sr_articlID)
|
||||
#return HttpResponse(sarresidPrice)
|
||||
debitNow = SarresidObj.price - sarresidPrice
|
||||
#return HttpResponse(debitNow)
|
||||
|
||||
if debitNow==0:
|
||||
#توسیه بدهی بخورد
|
||||
SarresidObj.price = 0
|
||||
SarresidObj.payStatus = "true"
|
||||
elif debitNow >= 1:
|
||||
SarresidObj.price = debitNow
|
||||
SarresidObj.sarresidDT = sarResidDateTime01
|
||||
else:
|
||||
return HttpResponse("خطا در مورد مبلغ")
|
||||
SarresidObj.save()
|
||||
|
||||
|
||||
if str(nahvePardakht) == 'naghd':
|
||||
nahvePardakht = 'نقد'
|
||||
elif str(nahvePardakht) == 'kartbekart':
|
||||
nahvePardakht = 'کارت به کارت'
|
||||
elif str(nahvePardakht) == 'kartkhan':
|
||||
nahvePardakht = 'کارت خوان'
|
||||
elif str(nahvePardakht) == 'takhfif':
|
||||
nahvePardakht = 'تخفیف'
|
||||
elif str(nahvePardakht) == 'check':
|
||||
nahvePardakht = 'چک'
|
||||
|
||||
articleHesabDariObj = Article()
|
||||
articleHesabDariObj.debit = 0
|
||||
articleHesabDariObj.credit = int(sarresidPrice)
|
||||
|
||||
if desc_sar:
|
||||
articleHesabDariObj.desc = "پرداختی بابت نسیه" + " | " + desc_sar
|
||||
else:
|
||||
articleHesabDariObj.desc = "پرداختی بابت نسیه"
|
||||
|
||||
articleHesabDariObj.customer = customerObj
|
||||
articleHesabDariObj.nahvePardakht = nahvePardakht
|
||||
articleHesabDariObj.user = user_op
|
||||
articleHesabDariObj.save()
|
||||
|
||||
return redirect(request.META.get('HTTP_REFERER'))
|
||||
|
||||
except:
|
||||
return HttpResponse("خطا در ثبت ")
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||||
def rezayat_dashboard(request):
|
||||
# دریافت تمامی نظرسنجیهای ثبت شده معتبر (که تاریخ نظرسنجی دارند)
|
||||
valid_surveys = Action.objects.filter(
|
||||
nazarDT__isnull=False,
|
||||
grade__in=[5, 10, 15, 20]
|
||||
)
|
||||
|
||||
# ۱. محاسبات شاخصهای کلیدی (KPIs)
|
||||
total_surveys = valid_surveys.count()
|
||||
avg_grade = valid_surveys.aggregate(Avg('grade'))['grade__avg'] or 0
|
||||
|
||||
# ۲. استخراج دادهها برای نمودار توزیع رضایت
|
||||
grade_distribution = valid_surveys.values('grade').annotate(count=Count('id'))
|
||||
chart_dict = {20: 0, 15: 0, 10: 0, 5: 0}
|
||||
for item in grade_distribution:
|
||||
g = item['grade']
|
||||
if g in chart_dict:
|
||||
chart_dict[g] = item['count']
|
||||
|
||||
chart_labels = ['عالی (۲۰)', 'متوسط (۱۵)', 'ضعیف (۱۰)', 'بد (۵)']
|
||||
chart_data_list = [chart_dict[20], chart_dict[15], chart_dict[10], chart_dict[5]]
|
||||
|
||||
# ۳. تبدیل امن آرایهها به JSON برای Chart.js
|
||||
chart_labels_json = json.dumps(chart_labels, ensure_ascii=False)
|
||||
chart_data_json = json.dumps(chart_data_list)
|
||||
has_chart_data = sum(chart_data_list) > 0
|
||||
|
||||
# ۴. واکشی لیست مشتریان نیازمند پیگیری (شامل متوسط ۱۵، ضعیف ۱۰ و بد ۵)
|
||||
low_score_actions = Action.objects.filter(
|
||||
nazarDT__isnull=False,
|
||||
grade__in=[5, 10, 15] # <-- امتیاز ۱۵ (متوسط) نیز اضافه شد
|
||||
).order_by('-nazarDT')
|
||||
|
||||
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
|
||||
|
||||
context = {
|
||||
'total_surveys': total_surveys,
|
||||
'avg_grade': round(avg_grade, 1),
|
||||
'chart_labels_json': chart_labels_json,
|
||||
'chart_data_json': chart_data_json,
|
||||
'has_chart_data': has_chart_data,
|
||||
'low_score_actions': low_score_actions,
|
||||
# تعداد کل موارد نیازمند پیگیری (متوسط + ضعیف + بد)
|
||||
'unhappy_count': chart_dict[15] + chart_dict[10] + chart_dict[5],
|
||||
'allUsers': allUsers,
|
||||
}
|
||||
|
||||
return render(request, 'admin_panel/report_rezayat.html', context)
|
||||
@@ -0,0 +1,458 @@
|
||||
from django.db import transaction, connection
|
||||
from django.db.models import Sum, Q
|
||||
from django.shortcuts import redirect, render, get_object_or_404
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
|
||||
from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
|
||||
from ..models import StoreRoom, ProductCategory, Product, ProductInStore, StoreTransferRequest
|
||||
from ..services import (create_transfer_request, approve_transfer_request,
|
||||
reject_transfer_request, cancel_transfer_request)
|
||||
from ._helpers import logger, _safe_int
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.OnlyAnbar', raise_exception=True)
|
||||
def stores(request):
|
||||
if request.user.username not in ('admin', 'office1', 'office2', 'office3', 'khatibi'):
|
||||
all_store = StoreRoom.objects.filter(user__id=request.user.id).order_by('id')[:20]
|
||||
else:
|
||||
all_store = StoreRoom.objects.all().order_by('id')[:20]
|
||||
data = {'all_store': all_store}
|
||||
return render(request, 'admin_panel/stores.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.OnlyAnbar', raise_exception=True)
|
||||
def store(request, s_id):
|
||||
productCategory = ProductCategory.objects.all()
|
||||
storeO = get_object_or_404(StoreRoom, id=s_id)
|
||||
searchStatus = False
|
||||
searchQS = request.POST.get('searchQS', '').strip()
|
||||
mojodi = request.POST.get('mojodi', '')
|
||||
|
||||
# ۱. محاسبه موجودی فیزیکی کل کالاها با یک کوئری تکضرب و سریع GROUP BY
|
||||
stock_balances = ProductInStore.objects.filter(
|
||||
StoreRoom=storeO, year=1400
|
||||
).values('Product_id').annotate(
|
||||
total=Sum('s_in') - Sum('s_out')
|
||||
).order_by()
|
||||
stock_dict = {item['Product_id']: item['total'] for item in stock_balances}
|
||||
|
||||
# ۲. محاسبه کل کالاهای در انتظار خروج
|
||||
pending_out_balances = StoreTransferRequest.objects.filter(
|
||||
source_store=storeO, status=StoreTransferRequest.Status.PENDING
|
||||
).values('product_id').annotate(
|
||||
total=Sum('quantity')
|
||||
).order_by()
|
||||
pending_out_dict = {item['product_id']: item['total'] for item in pending_out_balances}
|
||||
|
||||
# ۳. محاسبه کل کالاهای در انتظار ورود
|
||||
pending_in_balances = StoreTransferRequest.objects.filter(
|
||||
target_store=storeO, status=StoreTransferRequest.Status.PENDING
|
||||
).values('product_id').annotate(
|
||||
total=Sum('quantity')
|
||||
).order_by()
|
||||
pending_in_dict = {item['product_id']: item['total'] for item in pending_in_balances}
|
||||
|
||||
# ۴. استخراج سریع تمام آیدیهای منحصربهفرد مرتبط با این انبار در سطح حافظه
|
||||
all_pids = set(stock_dict.keys()) | set(pending_out_dict.keys()) | set(pending_in_dict.keys())
|
||||
|
||||
# ۵. کوئری نهایی فوقبهینه برای خواندن کالاها (حل باگ N+1 با دستور select_related)
|
||||
products_qs = Product.objects.select_related('ProductCategory').filter(id__in=all_pids)
|
||||
|
||||
# اعمال فیلتر متنی جستجو در سطح دیتابیس
|
||||
if searchQS:
|
||||
products_qs = products_qs.filter(product_name__icontains=searchQS)
|
||||
searchStatus = True
|
||||
|
||||
# 🛠️ اصلاح فرآیند ترتیب: استفاده از 'id' جهت انطباق ۱۰۰٪ با ترتیب پیشفرض و سنتی دیتابیس شما
|
||||
# نکته: اگر مایلید بر اساس زمان ایجاد مرتب شود، میتوانید آن را به 'createdDT' یا '-createdDT' تغییر دهید.
|
||||
products_list = list(products_qs.order_by('id'))
|
||||
|
||||
# ۶. معماری زمان ثابت O(1): چسباندن پرسرعت دیتاها در حافظه رم سرور
|
||||
for prd in products_list:
|
||||
prd.mojodi = stock_dict.get(prd.id, 0)
|
||||
prd.pending_out = pending_out_dict.get(prd.id, 0)
|
||||
prd.pending_in = pending_in_dict.get(prd.id, 0)
|
||||
|
||||
# ۷. اعمال فیلترهای منطقی موجودی در لایه پایتون
|
||||
if mojodi == "0":
|
||||
products_list = [p for p in products_list if p.mojodi <= 0]
|
||||
searchStatus = True
|
||||
elif mojodi == "1":
|
||||
products_list = [p for p in products_list if p.mojodi > 0]
|
||||
searchStatus = True
|
||||
|
||||
data = {
|
||||
'storeO': storeO,
|
||||
'productSum': products_list,
|
||||
's_id': s_id,
|
||||
'searchStatus': searchStatus,
|
||||
"mojodi": mojodi,
|
||||
'djsearchQS': searchQS,
|
||||
'productCategory': productCategory
|
||||
}
|
||||
return render(request, 'admin_panel/store.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.OnlyAnbar', raise_exception=True)
|
||||
def operations(request, s_id):
|
||||
page = request.GET.get('page', 1)
|
||||
productCategory = ProductCategory.objects.all()
|
||||
storeO = get_object_or_404(StoreRoom, id=s_id)
|
||||
|
||||
# بهینهسازی کلیدی: استفاده از select_related برای جلوگیری از کندی و پر شدن RAM دیتابیس
|
||||
operationsList = ProductInStore.objects.filter(
|
||||
StoreRoom=storeO, year=1400
|
||||
).select_related('Product').order_by('-createdDT')
|
||||
|
||||
paginator = Paginator(operationsList, 25)
|
||||
try:
|
||||
data_list = paginator.page(page)
|
||||
except PageNotAnInteger:
|
||||
data_list = paginator.page(1)
|
||||
except EmptyPage:
|
||||
data_list = paginator.page(paginator.num_pages)
|
||||
|
||||
data = {
|
||||
'storeO': storeO,
|
||||
'operationsList': data_list,
|
||||
's_id': s_id,
|
||||
'productCategory': productCategory
|
||||
}
|
||||
return render(request, 'admin_panel/operations.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.OnlyAnbar', raise_exception=True)
|
||||
def operationProduct(request, s_id, p_id):
|
||||
productCategory = ProductCategory.objects.all()
|
||||
storeO = get_object_or_404(StoreRoom, id=s_id)
|
||||
|
||||
# بهینهسازی کلیدی در فیلتر اختصاصی محصول
|
||||
operationsList = ProductInStore.objects.filter(
|
||||
StoreRoom=storeO, year=1400, Product__id=p_id
|
||||
).select_related('Product').order_by('-createdDT')
|
||||
|
||||
data = {
|
||||
'storeO': storeO,
|
||||
'operationsList': operationsList,
|
||||
's_id': s_id,
|
||||
'productCategory': productCategory
|
||||
}
|
||||
return render(request, 'admin_panel/operations.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.OnlyAnbar', raise_exception=True)
|
||||
def newOperation(request, s_id, p_id):
|
||||
productCategory = ProductCategory.objects.all()
|
||||
storeO = StoreRoom.objects.get(id=s_id)
|
||||
allStores = StoreRoom.objects.exclude(id=s_id)
|
||||
product = Product.objects.get(id=p_id)
|
||||
data = {'s_id': s_id,'p_id': p_id, 'allStores': allStores, 'storeO': storeO, 'product':product, 'productCategory': productCategory}
|
||||
return render(request, 'admin_panel/newOperation.html', data )
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.OnlyAnbar', raise_exception=True)
|
||||
def store_transfers(request, s_id):
|
||||
"""
|
||||
داشبورد و کارتابل جامع مدیریت انتقالهای انبار
|
||||
"""
|
||||
storeO = get_object_or_404(StoreRoom, id=s_id)
|
||||
|
||||
# ۱. کارتابل ورودی (بارهایی که به سمت این انبار آمده و منتظر تایید ما هستند)
|
||||
# استفاده از select_related برای جلوگیری از صدها کوئری اضافی به دیتابیس
|
||||
incoming_pending = StoreTransferRequest.objects.filter(
|
||||
target_store=storeO,
|
||||
status=StoreTransferRequest.Status.PENDING
|
||||
).select_related('product', 'source_store', 'requester').order_by('-created_at')
|
||||
|
||||
# ۲. کارتابل خروجی (بارهایی که ما فرستادیم و منتظر تایید انبار مقصد هستیم)
|
||||
outgoing_pending = StoreTransferRequest.objects.filter(
|
||||
source_store=storeO,
|
||||
status=StoreTransferRequest.Status.PENDING
|
||||
).select_related('product', 'target_store', 'requester').order_by('-created_at')
|
||||
|
||||
# ۳. تاریخچه انتقالات اخیر (۵۰ مورد آخر تعیین تکلیف شده برای این انبار)
|
||||
history_list = StoreTransferRequest.objects.filter(
|
||||
Q(source_store=storeO) | Q(target_store=storeO)
|
||||
).exclude(
|
||||
status=StoreTransferRequest.Status.PENDING
|
||||
).select_related('product', 'source_store', 'target_store', 'approver').order_by('-action_date')[:50]
|
||||
|
||||
data = {
|
||||
'storeO': storeO,
|
||||
's_id': s_id,
|
||||
'incoming_pending': incoming_pending,
|
||||
'outgoing_pending': outgoing_pending,
|
||||
'history_list': history_list,
|
||||
}
|
||||
return render(request, 'admin_panel/store_transfers.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.OnlyAnbar', raise_exception=True)
|
||||
@require_POST
|
||||
def process_transfer_action(request, transfer_id):
|
||||
"""
|
||||
پردازش امن اقدامات انباردار (تایید، رد، انصراف) با تراکنشهای اتمیک
|
||||
"""
|
||||
action = request.POST.get('action')
|
||||
reason = request.POST.get('reason', '').strip()
|
||||
|
||||
# دریافت درخواست انتقال
|
||||
transfer = get_object_or_404(StoreTransferRequest, id=transfer_id)
|
||||
|
||||
# تعیین انباری که کاربر از آنجا اقدام کرده جهت ریدایرکت صحیح
|
||||
redirect_store_id = transfer.target_store_id if action in ['approve', 'reject'] else transfer.source_store_id
|
||||
|
||||
try:
|
||||
if action == 'approve':
|
||||
approve_transfer_request(request_id=transfer.id, approver=request.user)
|
||||
messages.success(request, f"انتقال {transfer.quantity} عدد {transfer.product.product_name} با موفقیت تایید و به موجودی انبار اضافه شد.")
|
||||
|
||||
elif action == 'reject':
|
||||
if not reason:
|
||||
messages.error(request, "برای رد کردن بار، وارد کردن علت رد الزامی است.")
|
||||
return redirect('admin_panel:store_transfers', s_id=redirect_store_id)
|
||||
|
||||
reject_transfer_request(request_id=transfer.id, rejector=request.user, reason=reason)
|
||||
messages.warning(request, f"درخواست انتقال بار رد شد و به انبار مبدا اطلاع داده شد.")
|
||||
|
||||
elif action == 'cancel':
|
||||
cancel_transfer_request(request_id=transfer.id, user=request.user)
|
||||
messages.info(request, "درخواست انتقال توسط شما لغو گردید.")
|
||||
|
||||
else:
|
||||
messages.error(request, "دستور ارسالی نامعتبر است.")
|
||||
|
||||
except ValidationError as e:
|
||||
# نمایش خطاهای منطقی (مثلاً کمبود موجودی انبار مبدا در لحظه تایید)
|
||||
error_text = ' '.join(e.messages) if hasattr(e, 'messages') else str(e)
|
||||
messages.error(request, f"خطا در انجام عملیات: {error_text}")
|
||||
except PermissionError as e:
|
||||
messages.error(request, str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing transfer action #{transfer_id}: {str(e)}", exc_info=True)
|
||||
messages.error(request, "خطای سیستمی در پردازش تراکنش رخ داد.")
|
||||
|
||||
return redirect('admin_panel:store_transfers', s_id=redirect_store_id)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewStore', raise_exception=True)
|
||||
def newStore(request):
|
||||
storeName = request.POST.get('storeName', '')
|
||||
newStore = StoreRoom()
|
||||
newStore.store_name = storeName
|
||||
newStore.save()
|
||||
|
||||
return redirect('/panel/store/')
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.OnlyAnbar', raise_exception=True)
|
||||
def productAdd(request):
|
||||
namePrd = request.POST.get('namePrd', '')
|
||||
category = request.POST.get('category', '')
|
||||
price = request.POST.get('price', '')
|
||||
mojodiAval = request.POST.get('mojodiAval', '')
|
||||
storeroom = request.POST.get('storeroom', '')
|
||||
|
||||
newProduct = Product()
|
||||
newProduct.product_name = namePrd
|
||||
newProduct.ProductCategory = ProductCategory.objects.get(id=category)
|
||||
newProduct.product_price = price
|
||||
newProduct.save()
|
||||
|
||||
prd_in_store = ProductInStore()
|
||||
prd_in_store.Product = newProduct
|
||||
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)
|
||||
return redirect('/panel/store/1')
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewStore', raise_exception=True)
|
||||
def productEdit(request):
|
||||
prd_code = request.POST.get('prd_code', '')
|
||||
namePrd = request.POST.get('namePrd', '')
|
||||
category = request.POST.get('category', '')
|
||||
price = request.POST.get('price', '')
|
||||
|
||||
#return HttpResponse(prd_code)
|
||||
|
||||
editProduct = Product.objects.get(id=prd_code)
|
||||
editProduct.product_name = namePrd
|
||||
editProduct.ProductCategory = ProductCategory.objects.get(id=category)
|
||||
editProduct.product_price = price
|
||||
editProduct.save()
|
||||
|
||||
return redirect('/panel/store/1')
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewStore', raise_exception=True)
|
||||
def productDel(request, p_id, s_id):
|
||||
Product.objects.get(id=p_id).delete()
|
||||
return redirect('/panel/store/'+str(s_id))
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.OnlyAnbar', raise_exception=True)
|
||||
def operation(request):
|
||||
if request.method != 'POST':
|
||||
return HttpResponseBadRequest("متد ارسالی نامعتبر است.")
|
||||
|
||||
user_op = request.user
|
||||
user_store_room = get_object_or_404(StoreRoom, owners=user_op)
|
||||
|
||||
# استفاده از توابع امن اختصاصی پروژه برای جداسازی مقادیر عددی
|
||||
prd_id = _safe_int(request.POST.get('prd_id'))
|
||||
action_type = request.POST.get('actionType', '')
|
||||
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', '').strip()
|
||||
|
||||
if tedad <= 0:
|
||||
return HttpResponseBadRequest("تعداد وارد شده باید بیشتر از صفر باشد.")
|
||||
|
||||
s_room = get_object_or_404(StoreRoom, id=azanbar)
|
||||
product_obj = get_object_or_404(Product, id=prd_id)
|
||||
|
||||
try:
|
||||
# =====================================================================
|
||||
# بخش اول: انتقال بین انبارها (واگذاری کامل به لایه سرویس)
|
||||
# =====================================================================
|
||||
if action_type == 'transfer':
|
||||
if not beanbar:
|
||||
return HttpResponse("انبار مقصد مشخص نشده است.")
|
||||
|
||||
target_store = get_object_or_404(StoreRoom, id=_safe_int(beanbar))
|
||||
|
||||
# فراخوانی لایه سرویس برای ثبت درخواست انتقال (بدون تغییر مستقیم موجودی)
|
||||
create_transfer_request(
|
||||
source_store=s_room,
|
||||
target_store=target_store,
|
||||
product=product_obj,
|
||||
quantity=tedad,
|
||||
requester=request.user,
|
||||
desc=desc or f"درخواست انتقال از انبار {s_room.store_name} به {target_store.store_name}"
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Transfer request created: {tedad}x {product_obj.product_name} from Store #{s_room.id} to Store #{target_store.id} by User #{request.user.id}"
|
||||
)
|
||||
return redirect('/panel/store/' + str(s_room.id))
|
||||
|
||||
# =====================================================================
|
||||
# بخش دوم: ورود و خروج دستی (کنترل مستقیم با Transaction و Lock)
|
||||
# =====================================================================
|
||||
elif action_type in ['vorod', 'khoroj']:
|
||||
user_s_room = get_object_or_404(StoreRoom, owners=user_op)
|
||||
if not user_op.is_superuser and s_room != user_s_room:
|
||||
return HttpResponse("شما دسترسی ورود / خروج به این انبار ندارید")
|
||||
with transaction.atomic():
|
||||
# ساخت کوئری پایه برای دریافت تراکنشهای سال جاری کالا
|
||||
qs = ProductInStore.objects.filter(
|
||||
Product=product_obj, StoreRoom=s_room, year=1400
|
||||
)
|
||||
|
||||
# قفلگذاری ردیفها برای جلوگیری از Race Condition (در دیتابیسهای پشتیبان)
|
||||
if connection.features.has_select_for_update:
|
||||
qs = qs.select_for_update()
|
||||
|
||||
locked_records = list(qs)
|
||||
|
||||
# --- الف) عملیات ورود دستی ---
|
||||
if action_type == 'vorod':
|
||||
ProductInStore.objects.create(
|
||||
StoreRoom=s_room,
|
||||
Product=product_obj,
|
||||
s_in=tedad,
|
||||
s_out=0,
|
||||
year=1400,
|
||||
desc=f"ورود: {tedad} به انبار {s_room.store_name}#{desc}",
|
||||
movement_type=ProductInStore.MovementType.DIRECT_IN
|
||||
)
|
||||
|
||||
# --- ب) عملیات خروج دستی ---
|
||||
elif action_type == 'khoroj':
|
||||
# محاسبه سریع موجودی در رم بدون فشار مجدد به دیتابیس
|
||||
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))
|
||||
|
||||
ProductInStore.objects.create(
|
||||
StoreRoom=s_room,
|
||||
Product=product_obj,
|
||||
s_in=0,
|
||||
s_out=tedad,
|
||||
year=1400,
|
||||
desc=f"خروج: {tedad} از انبار {s_room.store_name}#{desc}",
|
||||
movement_type=ProductInStore.MovementType.DIRECT_OUT
|
||||
)
|
||||
|
||||
else:
|
||||
return HttpResponseBadRequest("نوع عملیات نامعتبر است.")
|
||||
|
||||
except ValidationError as e:
|
||||
# مدیریت خطاهای اعتبارسنجی (مثل کمبود موجودی در لایه سرویس یا یکسان بودن انبارها)
|
||||
error_msg = ' '.join(e.messages) if hasattr(e, 'messages') else str(e)
|
||||
logger.warning(f"Validation error in store operation: {error_msg}")
|
||||
return HttpResponse(error_msg)
|
||||
|
||||
except PermissionDenied as e:
|
||||
# مدیریت خطای دسترسی کاربر به انبار
|
||||
logger.warning(f"Permission denied in store operation for user {request.user.id}: {str(e)}")
|
||||
return HttpResponse("شما دسترسی لازم برای انجام این عملیات در این انبار را ندارید.", status=403)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Critical error in store operation view: {str(e)}", exc_info=True)
|
||||
return HttpResponse("خطای سیستمی رخ داد. تراکنش لغو شد.", status=500)
|
||||
|
||||
return redirect('/panel/store/' + str(s_room.id))
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def productMojodi(request, p_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=store_user,
|
||||
year=1400
|
||||
).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
|
||||
mojodi_prd = total_in - total_out
|
||||
|
||||
return HttpResponse(str(mojodi_prd))
|
||||
@@ -1 +1,95 @@
|
||||
{"0": "User Action Views", "1": "Inventory and Product Models", "2": "Daily Reporting Command", "3": "Customer Admin Templates", "4": "Database Schema Migrations", "5": "Admin Panel URLs", "6": "Database Backup Command", "7": "Initial Database Migrations", "8": "User Relation Migrations", "9": "Financial and Accounting Views", "10": "Invoice Management Logic", "11": "Django Admin Configuration", "12": "Core Application Models", "13": "Resource Cleanup Command", "14": "Django App Configurations", "15": "Timezone-related Migrations", "16": "Application Entrypoint Scripts", "17": "Core Admin App", "18": "Project URL Configuration", "19": "Graphify Tooling", "20": "Data Validation Migration", "21": "Application Tests", "22": "Database Schema Migration", "23": "Database Schema Migration", "24": "Remove StoreRoom User Relation", "25": "Database Schema Migration", "26": "Add Action Type Field", "27": "Add Action Grade Field", "28": "Database Schema Migration", "29": "Add Article Payment Status", "30": "Add Article Payment Method", "31": "Add Customer Last Visit", "32": "Database Schema Migration", "33": "Add Sarresid Description", "34": "Update ProductInStore Options", "35": "Update ProductInStore Options", "36": "AI Tooling Documentation", "37": "Admin Migrations Package", "38": "Customer Action Partial", "39": "New Customer Base Template", "40": "Customer Feedback Partial", "41": "Customer Invoice Detail", "42": "Customer Accounting Template", "43": "Customer Invoice List", "44": "Customer Feedback Page", "45": "Management Commands Package", "46": "Management Package", "47": "Admin Panel Migrations Package", "48": "Action List Template", "49": "New Customer Invoice Page", "50": "New Customer Invoice V2", "51": "Customer Satisfaction List", "52": "Customer Satisfaction Partial", "53": "Customer Debt Management", "54": "Customer List Page", "55": "Customer List Backup Template", "56": "Waiting List Backup Template", "57": "Financial Dashboard", "58": "Accounting Main View", "59": "Admin Panel Index Page", "60": "New Inventory Operation", "61": "Inventory Operations List", "62": "Operations Backup Template", "63": "Financial Report Template", "64": "Debt Collection Dashboard", "65": "Inventory Movement Report", "66": "Customer Satisfaction Dashboard", "67": "Check and Credit Report", "68": "Satisfaction List Template", "69": "Store Inventory Template", "70": "Store Transfers Template", "71": "Stores List Template", "72": "Template Tags Package", "73": "Customer Detail URL", "74": "New Store URL", "75": "Process Transfer URL", "76": "Edit Satisfaction URL", "77": "Main Project Package", "78": "Extended Requirements File", "79": "Project Requirements File"}
|
||||
{
|
||||
"0": "views/store.py",
|
||||
"1": "project_tree.py",
|
||||
"2": "sqlite_db_backup.py",
|
||||
"3": "django_db",
|
||||
"4": "Customer ID Backup/New Factor Template",
|
||||
"5": "views/customers.py",
|
||||
"6": "cheque_inquiry.py",
|
||||
"7": "admin_panel/templates/admin_panel/baseHesabdari.html",
|
||||
"8": "django_db_models_deletion",
|
||||
"9": "django_conf",
|
||||
"10": "admin_panel/storeBK1.html",
|
||||
"11": "ViewSmokeTests",
|
||||
"12": "admin_panel/templates/admin_panel/customerAction.html",
|
||||
"13": "User",
|
||||
"14": "admin_panel/templates/admin_panel/bedehi.html",
|
||||
"15": "django_contrib",
|
||||
"16": "models/__init__.py",
|
||||
"17": "admin_panel/templates/admin_panel/customerBase.html",
|
||||
"18": "hesabdariCheckPassFail.html",
|
||||
"19": "Reference guide",
|
||||
"20": "ProductInStore",
|
||||
"21": "graphify",
|
||||
"22": "hesabdariResidNashode.html",
|
||||
"23": "0017_factoriteam_garantyesh.py",
|
||||
"24": "django_apps",
|
||||
"25": "0002_auto_20220114_1616.py",
|
||||
"26": "0021_alter_article_type.py",
|
||||
"27": "Rip'n Fast. Fewer Tokens. Better Code.",
|
||||
"28": "0027_alter_action_nazar.py",
|
||||
"29": "admin_panel/entezar.html",
|
||||
"30": "views/finance.py",
|
||||
"31": "Customer Modals UI",
|
||||
"32": "0029_alter_article_options.py",
|
||||
"33": "0034_alter_article_sarresid.py",
|
||||
"34": "0005_auto_20220121_2054.py",
|
||||
"35": "0035_alter_article_sarresid.py",
|
||||
"36": "0009_remove_storeroom_user.py",
|
||||
"37": "0010_auto_20220122_1546.py",
|
||||
"38": "0040_customer_laghv_bazdid.py",
|
||||
"39": "0043_sarresid_user.py",
|
||||
"40": "0048_alter_productinstore_options.py",
|
||||
"41": "0032_article_paystatus.py",
|
||||
"42": "0033_article_nahvepardakht.py",
|
||||
"45": "0044_sarresid_desc.py",
|
||||
"48": "GEMINI.md",
|
||||
"49": "admin/migrations/__init__.py",
|
||||
"50": "Customer Action Include",
|
||||
"51": "New Customer Base Template",
|
||||
"52": "Customer Feedback Partial",
|
||||
"53": "Customer Factor Detail",
|
||||
"54": "Customer Accounting Template",
|
||||
"55": "Customer Factor List",
|
||||
"56": "Customer Feedback Page",
|
||||
"57": "commands/__init__.py",
|
||||
"58": "management/__init__.py",
|
||||
"59": "admin_panel/migrations/__init__.py",
|
||||
"60": "Action List Template",
|
||||
"61": "base.html",
|
||||
"62": "Customer New Factor",
|
||||
"63": "Customer New Factor V2",
|
||||
"64": "Customer Satisfaction List",
|
||||
"65": "Customer Satisfaction Partial",
|
||||
"66": "Customer Check/Debt Management",
|
||||
"67": "admin_panel/templates/admin_panel/customerBaseNew.html",
|
||||
"68": "Customer List",
|
||||
"69": "admin_panel/templates/admin_panel/entezarBK.html",
|
||||
"70": "Financial Dashboard",
|
||||
"71": "Accounting Main View",
|
||||
"72": "admin_panel/templates/admin_panel/index.html",
|
||||
"73": "New Inventory Operation",
|
||||
"74": "Inventory Operations List",
|
||||
"75": "Operations Backup Template",
|
||||
"76": "Financial Report Template",
|
||||
"77": "Debt Collection Dashboard",
|
||||
"78": "Inventory Movement Report",
|
||||
"79": "Customer Satisfaction Dashboard",
|
||||
"80": "Check and Credit Report Template",
|
||||
"81": "Satisfaction List Template",
|
||||
"82": "Store Inventory Template",
|
||||
"83": "Store Transfers Template",
|
||||
"84": "Stores List Template",
|
||||
"85": "toCurrency",
|
||||
"86": "templatetags/__init__.py",
|
||||
"87": "URL: newStore",
|
||||
"88": "URL: process_transfer_action",
|
||||
"89": "URL: rezayatEdit",
|
||||
"90": "khatibi/__init__.py",
|
||||
"91": "Extended Requirements File",
|
||||
"92": "Requirements File",
|
||||
"99": "ModelsPackageContractTests",
|
||||
"102": "UrlContractTests",
|
||||
"104": "system.py",
|
||||
"107": "tests/__init__.py"
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
{"0": "dc38416453f219cd", "1": "17677da135e176af", "2": "a173e495d86da278", "3": "2ac4a8f7fd725f39", "4": "7cd0b8c5a73aaae2", "5": "0666b74f90562078", "6": "c3f18528373c1e6e", "7": "1aa60a8e7dec6306", "8": "0ecf8267dd379fc7", "9": "110a96d098e68da1", "10": "019e40911d267493", "11": "3cc770d8cc99f8ae", "12": "d8399135bcf129db", "13": "dbfbf8557361f5ea", "14": "1cdbbc0afb5b81e2", "15": "a19c211702401a5b", "16": "a9087ce814efe96b", "17": "28ebb54ffa686f35", "18": "2db67510f080898c", "19": "8c2d0656a6e54e1f", "20": "cfbcf8cef78c5757", "21": "37de28eabfcc656a", "22": "6886ad7a5751faca", "23": "9fdcab2ed5d20e49", "24": "a1a1f3c1cdff72cf", "25": "2f1621907db6f86f", "26": "9cc7a507cfecd8fb", "27": "edb619bf5ac0fc49", "28": "8cb6b3db3aadb973", "29": "d5c0f5f6714c9991", "30": "9948fb8b548535ed", "31": "52502a4c1ef75ce6", "32": "8d20e5c1bce8916d", "33": "90e57f26f669372d", "34": "bf60a8731033e467", "35": "6cef5bc37e710e62", "36": "66ed76491a412a72", "37": "e67668b62ee6c62c", "38": "68aee39cd24db354", "39": "09b7297ea62919f2", "40": "054401d27ffaa4b2", "41": "dace81c1995fb321", "42": "19e301aeace95bb2", "43": "d6fd0cec4634a0b0", "44": "0230d355a6dabb54", "45": "5cebbb5528321795", "46": "536fe28d24c361f5", "47": "b08bdc2712cb0f12", "48": "6ce8444f504022cc", "49": "d8e34a3b3eb43ee0", "50": "056b8d6b5072289b", "51": "171c34c74a95481b", "52": "73e9eca6f2ca1c9b", "53": "dcc2230ae751c29a", "54": "2f5417a69eb1d15e", "55": "864760002345205a", "56": "d55cb638532fdfeb", "57": "06cae4389208269f", "58": "e9c0a7be255e9b26", "59": "455b4ca39e55ecb0", "60": "8105c55538c34081", "61": "74304670b54c018f", "62": "a27f876eabb044b2", "63": "73f74dd7a40776d6", "64": "29d433d164b99f89", "65": "3f44aa48aa05d597", "66": "ca8a7efba0a3e579", "67": "5cc770af9d184d77", "68": "2d31fa723128b164", "69": "9d814c2520329846", "70": "f97d1a5c3a066598", "71": "14f84a6e4cd311d6", "72": "0bf6f9d0aeab4fd8", "73": "f16cb34aa8bf540c", "74": "8b491bb3ff0649dd", "75": "638812cee15d1b9b", "76": "3dd16d68928a479c", "77": "72dad91d66d471ce", "78": "353a9d09eaae47ea", "79": "2d29260874184e4a"}
|
||||
{"0": "c3a36a9368a041d9", "1": "90337c8ac61b6b73", "2": "6076efb85d7e0cf6", "3": "102a7faeadcd9f37", "4": "c87f3f051be1d6cf", "5": "38d8150b6fcc29dd", "6": "8f0f51ff611eb425", "7": "828492c80f59c13e", "8": "5a34a83a0a037abe", "9": "ab8fcea052c3ff15", "10": "9701f1591d8fda6e", "11": "bf7d35550596f299", "12": "59f959de834a65cb", "13": "1b1f9f2676dbbf2f", "14": "138ab97b0f9bf63e", "15": "3cc770d8cc99f8ae", "16": "b3f1fcf44a1ef48c", "17": "42f1e68ac27c0389", "18": "90146abe85d9419f", "19": "21f888d34b160f31", "20": "a6cdf08fcc949de1", "21": "0f632fc57e9feeb0", "22": "066f74b181692719", "23": "a800ecffc01fc9d5", "24": "1cdbbc0afb5b81e2", "25": "a19c211702401a5b", "26": "89a02225d25a53c3", "27": "c9e7bf6ac46f263c", "28": "be1b722cf1a01583", "29": "7db54a3d7ae0017c", "30": "c6c68925b6415da0", "31": "03f8896e9b60ca96", "32": "cde232c2541ef2d0", "33": "c02bc9595eaedd36", "34": "6886ad7a5751faca", "35": "7714d721fe893ef5", "36": "a1a1f3c1cdff72cf", "37": "2f1621907db6f86f", "38": "ef8221c5fdeee7f8", "39": "eb04d6a3e3a79bf8", "40": "bf60a8731033e467", "41": "d5c0f5f6714c9991", "42": "9948fb8b548535ed", "45": "90e57f26f669372d", "48": "66ed76491a412a72", "49": "e67668b62ee6c62c", "50": "68aee39cd24db354", "51": "09b7297ea62919f2", "52": "054401d27ffaa4b2", "53": "dace81c1995fb321", "54": "19e301aeace95bb2", "55": "d6fd0cec4634a0b0", "56": "0230d355a6dabb54", "57": "5cebbb5528321795", "58": "536fe28d24c361f5", "59": "b08bdc2712cb0f12", "60": "6ce8444f504022cc", "61": "8021f52559633383", "62": "d8e34a3b3eb43ee0", "63": "056b8d6b5072289b", "64": "171c34c74a95481b", "65": "73e9eca6f2ca1c9b", "66": "dcc2230ae751c29a", "67": "2a617b932f5d92b2", "68": "2f5417a69eb1d15e", "69": "d55cb638532fdfeb", "70": "06cae4389208269f", "71": "e9c0a7be255e9b26", "72": "455b4ca39e55ecb0", "73": "8105c55538c34081", "74": "74304670b54c018f", "75": "a27f876eabb044b2", "76": "73f74dd7a40776d6", "77": "29d433d164b99f89", "78": "3f44aa48aa05d597", "79": "ca8a7efba0a3e579", "80": "5cc770af9d184d77", "81": "2d31fa723128b164", "82": "9d814c2520329846", "83": "f97d1a5c3a066598", "84": "14f84a6e4cd311d6", "85": "3a592d27b1f4ccc9", "86": "0bf6f9d0aeab4fd8", "87": "8b491bb3ff0649dd", "88": "638812cee15d1b9b", "89": "3dd16d68928a479c", "90": "72dad91d66d471ce", "91": "353a9d09eaae47ea", "92": "2d29260874184e4a", "99": "28cdf3baa198be4d", "102": "2de2cdee1890b458", "104": "c5743041b2101132", "107": "43bbf35b96423d46"}
|
||||
@@ -1 +1 @@
|
||||
D:\Projects\khatibi
|
||||
.
|
||||
+200
-123
@@ -1,67 +1,82 @@
|
||||
# Graph Report - khatibi (2026-10-09)
|
||||
|
||||
## Corpus Check
|
||||
- cluster-only mode — file stats not available
|
||||
- 124 files · ~93,934 words
|
||||
- Verdict: corpus is large enough that graph structure adds value.
|
||||
- Unclassified: 7 file(s) not represented in the graph (top: (none) 4, .env 2, .csv 1)
|
||||
|
||||
## Summary
|
||||
- 548 nodes · 887 edges · 80 communities (14 shown, 66 thin omitted)
|
||||
- Extraction: 85% EXTRACTED · 15% INFERRED · 0% AMBIGUOUS · INFERRED: 131 edges (avg confidence: 0.92)
|
||||
- Token cost: 2,543 input · 5,433 output
|
||||
- 821 nodes · 1291 edges · 93 communities (26 shown, 67 thin omitted)
|
||||
- Extraction: 92% EXTRACTED · 8% INFERRED · 0% AMBIGUOUS · INFERRED: 103 edges (avg confidence: 0.86)
|
||||
- Token cost: 0 input · 0 output
|
||||
|
||||
## Graph Freshness
|
||||
- Built from commit: `f19d8ee6`
|
||||
- Built from commit: `6f36db63`
|
||||
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
|
||||
- Run `graphify update .` after code changes (no API cost).
|
||||
|
||||
## Community Hubs (Navigation)
|
||||
- User Action Views
|
||||
- Inventory and Product Models
|
||||
- Daily Reporting Command
|
||||
- Customer Admin Templates
|
||||
- Database Schema Migrations
|
||||
- Admin Panel URLs
|
||||
- Database Backup Command
|
||||
- Initial Database Migrations
|
||||
- User Relation Migrations
|
||||
- Financial and Accounting Views
|
||||
- Invoice Management Logic
|
||||
- Django Admin Configuration
|
||||
- Core Application Models
|
||||
- Resource Cleanup Command
|
||||
- Django App Configurations
|
||||
- Timezone-related Migrations
|
||||
- Application Entrypoint Scripts
|
||||
- Graphify Tooling
|
||||
- Data Validation Migration
|
||||
- Database Schema Migration
|
||||
- Database Schema Migration
|
||||
- Remove StoreRoom User Relation
|
||||
- Database Schema Migration
|
||||
- Add Action Type Field
|
||||
- Add Action Grade Field
|
||||
- Database Schema Migration
|
||||
- Add Article Payment Status
|
||||
- Add Article Payment Method
|
||||
- Add Customer Last Visit
|
||||
- Database Schema Migration
|
||||
- Add Sarresid Description
|
||||
- Update ProductInStore Options
|
||||
- Update ProductInStore Options
|
||||
- AI Tooling Documentation
|
||||
- Customer Action Partial
|
||||
- views/store.py
|
||||
- project_tree.py
|
||||
- sqlite_db_backup.py
|
||||
- django_db
|
||||
- Customer ID Backup/New Factor Template
|
||||
- views/customers.py
|
||||
- cheque_inquiry.py
|
||||
- admin_panel/templates/admin_panel/baseHesabdari.html
|
||||
- django_db_models_deletion
|
||||
- django_conf
|
||||
- admin_panel/storeBK1.html
|
||||
- ViewSmokeTests
|
||||
- admin_panel/templates/admin_panel/customerAction.html
|
||||
- User
|
||||
- admin_panel/templates/admin_panel/bedehi.html
|
||||
- django_contrib
|
||||
- models/__init__.py
|
||||
- admin_panel/templates/admin_panel/customerBase.html
|
||||
- hesabdariCheckPassFail.html
|
||||
- Reference guide
|
||||
- ProductInStore
|
||||
- graphify
|
||||
- hesabdariResidNashode.html
|
||||
- 0017_factoriteam_garantyesh.py
|
||||
- django_apps
|
||||
- 0002_auto_20220114_1616.py
|
||||
- 0021_alter_article_type.py
|
||||
- Rip'n Fast. Fewer Tokens. Better Code.
|
||||
- 0027_alter_action_nazar.py
|
||||
- admin_panel/entezar.html
|
||||
- views/finance.py
|
||||
- Customer Modals UI
|
||||
- 0029_alter_article_options.py
|
||||
- 0034_alter_article_sarresid.py
|
||||
- 0005_auto_20220121_2054.py
|
||||
- 0035_alter_article_sarresid.py
|
||||
- 0009_remove_storeroom_user.py
|
||||
- 0010_auto_20220122_1546.py
|
||||
- 0040_customer_laghv_bazdid.py
|
||||
- 0043_sarresid_user.py
|
||||
- 0048_alter_productinstore_options.py
|
||||
- 0032_article_paystatus.py
|
||||
- 0033_article_nahvepardakht.py
|
||||
- 0044_sarresid_desc.py
|
||||
- GEMINI.md
|
||||
- Customer Action Include
|
||||
- New Customer Base Template
|
||||
- Customer Feedback Partial
|
||||
- Customer Invoice Detail
|
||||
- Customer Factor Detail
|
||||
- Customer Accounting Template
|
||||
- Customer Invoice List
|
||||
- Customer Factor List
|
||||
- Customer Feedback Page
|
||||
- Action List Template
|
||||
- New Customer Invoice Page
|
||||
- New Customer Invoice V2
|
||||
- base.html
|
||||
- Customer New Factor
|
||||
- Customer New Factor V2
|
||||
- Customer Satisfaction List
|
||||
- Customer Satisfaction Partial
|
||||
- Customer Debt Management
|
||||
- Customer List Page
|
||||
- Customer Check/Debt Management
|
||||
- admin_panel/templates/admin_panel/customerBaseNew.html
|
||||
- Customer List
|
||||
- Financial Dashboard
|
||||
- Accounting Main View
|
||||
- New Inventory Operation
|
||||
@@ -71,37 +86,41 @@
|
||||
- Debt Collection Dashboard
|
||||
- Inventory Movement Report
|
||||
- Customer Satisfaction Dashboard
|
||||
- Check and Credit Report
|
||||
- Check and Credit Report Template
|
||||
- Satisfaction List Template
|
||||
- Store Inventory Template
|
||||
- Store Transfers Template
|
||||
- Stores List Template
|
||||
- toCurrency
|
||||
- Extended Requirements File
|
||||
- Project Requirements File
|
||||
- Requirements File
|
||||
- ModelsPackageContractTests
|
||||
- UrlContractTests
|
||||
- system.py
|
||||
|
||||
## God Nodes (most connected - your core abstractions)
|
||||
1. `StoreRoom` - 25 edges
|
||||
2. `ProductInStore` - 23 edges
|
||||
3. `User` - 21 edges
|
||||
4. `Action` - 20 edges
|
||||
5. `Product` - 18 edges
|
||||
6. `StoreTransferRequest` - 15 edges
|
||||
7. `Article` - 15 edges
|
||||
8. `ProductCategory` - 14 edges
|
||||
9. `Customer` - 13 edges
|
||||
10. `approve_transfer_request()` - 12 edges
|
||||
1. `Rip'n Fast. Fewer Tokens. Better Code.` - 23 edges
|
||||
2. `User` - 22 edges
|
||||
3. `create_transfer_request()` - 22 edges
|
||||
4. `Reference guide` - 18 edges
|
||||
5. `current_stock()` - 16 edges
|
||||
6. `make_user()` - 15 edges
|
||||
7. `stock_in()` - 15 edges
|
||||
8. `Customer ID Backup/New Factor Template` - 15 edges
|
||||
9. `approve_transfer_request()` - 13 edges
|
||||
10. `make_product()` - 13 edges
|
||||
|
||||
## Surprising Connections (you probably didn't know these)
|
||||
- `check_financial_discrepancy()` --uses--> `Customer` [INFERRED]
|
||||
check_finance.py → admin/models.py
|
||||
- `run_360_audit()` --uses--> `Customer` [INFERRED]
|
||||
ghost_hunter.py → admin/models.py
|
||||
- ``nest=` is a max, so it cannot tell a long function from a tangled one` --references--> `index()` [INFERRED]
|
||||
ripwire - README.md → admin/views.py
|
||||
- `VisitQueue` --uses--> `User` [INFERRED]
|
||||
admin_panel/models.py → admin/models.py
|
||||
- `Factor` --uses--> `User` [INFERRED]
|
||||
admin_panel/models.py → admin/models.py
|
||||
admin_panel/models/actions.py → admin/models.py
|
||||
- `StoreRoom` --uses--> `User` [INFERRED]
|
||||
admin_panel/models.py → admin/models.py
|
||||
- `StoreRoomOwner` --uses--> `User` [INFERRED]
|
||||
admin_panel/models.py → admin/models.py
|
||||
- `StoreTransferRequest` --uses--> `User` [INFERRED]
|
||||
admin_panel/models.py → admin/models.py
|
||||
admin_panel/models/store.py → admin/models.py
|
||||
|
||||
## Import Cycles
|
||||
- None detected.
|
||||
@@ -110,102 +129,160 @@
|
||||
- **Customer Satisfaction & Collection** — admin_panel_templates_admin_panel_report_rezayat, admin_panel_templates_admin_panel_report_debtors_no_vosol, admin_panel_customer_customernazarinc [EXTRACTED 0.90]
|
||||
- **Customer Management Flow** — admin_panel_customer_base_new, admin_panel_customer_action_inc, admin_panel_customer_id_factor, admin_panel_customer_hesabdari [EXTRACTED 1.00]
|
||||
- **Financial & Debt Management** — admin_panel_templates_admin_panel_financial_dashboard, admin_panel_templates_admin_panel_hesabdari [EXTRACTED 1.00]
|
||||
- **Graphify Output Artifacts** — graphify-out_graph_json, graphify-out_wiki_index_md, graphify-out_graph_report_md [EXTRACTED 1.00]
|
||||
- **Graphify Subcommands** — graphify_query, graphify_path, graphify_explain [EXTRACTED 1.00]
|
||||
- **Inventory Management System** — admin_panel_templates_admin_panel_store, admin_panel_templates_admin_panel_store_transfers, admin_panel_templates_admin_panel_report_inventory_movements, admin_panel_templates_admin_panel_operationsbk [EXTRACTED 1.00]
|
||||
- **Bedehi Report Navigation** — django_urls_admin_panel_bedehi, django_urls_admin_panel_bedehikhadamat, django_urls_admin_panel_bedehinasb [EXTRACTED]
|
||||
- **Login Form** — admin_panel_templates_admin_panel_login, django_url_admin_panel_p_login, django_csrf_token, form_field_username, form_field_password [EXTRACTED]
|
||||
- **New Store Form** — admin_panel_templates_admin_panel_storebk1, django_url_admin_panel_newstore, django_csrf_token, form_field_storename [EXTRACTED]
|
||||
- **Store Inventory Display** — admin_panel_templates_admin_panel_storebk1, template_variable_productsum, product_property_product_name, product_property_mojodi [EXTRACTED]
|
||||
- **Customer List Page** — admin_panel_templates_admin_panel_entezar_html, concept_customer_data, concept_url_admin_panel_customers [INFERRED 0.80]
|
||||
- **Customer Lifecycle Management** — admin_panel_templates_admin_panel_customers [INFERRED 0.85]
|
||||
- **Customer Interaction Lifecycle** — admin_panel_templates_admin_panel_customers, admin_panel_templates_admin_panel_customerNew, admin_panel_templates_admin_panel_customerRezayat [INFERRED 0.85]
|
||||
- **New Factor Creation Flow** — admin_panel_templates_admin_panel_customernewbackup_html, admin_panel_urls_newfactor, concept_client_side_form_logic, concept_customer, concept_category [INFERRED 0.85]
|
||||
- **Customer Management UI Flow** — admin_panel_templates_admin_panel_customersbk_html, admin_panel_templates_admin_panel_customersbk_html_createcs, admin_panel_templates_admin_panel_customersbk_html_openrow, concept_customer_modal_ui [INFERRED 0.90]
|
||||
- **Financial & Debt Management Flow** — admin_panel_templates_admin_panel_customerSarresid, admin_panel_templates_admin_panel_hesabdari, admin_panel_templates_admin_panel_financial_dashboard [INFERRED 0.90]
|
||||
- **New Factor Item Creation Flow** — admin_panel_templates_admin_panel_customeridfactorbackup_html_js, panel_urls_productmojodi, concept_product, concept_productcategory [INFERRED 0.90]
|
||||
- **Settle Due Payment Flow** — admin_panel_templates_admin_panel_hesabdaricheckpassfail_html, admin_panel_url_passcheck, admin_panel_url_pardakhtnesiye [INFERRED 0.90]
|
||||
- **Accounting and Billing Flow** — admin_panel_templates_admin_panel_basehesabdari, admin_panel_templates_admin_panel_bedehi, admin_panel_templates_admin_panel_hesabdariheckpass, concept_url_admin_panel_hesabdari, concept_url_admin_panel_bedehi, concept_url_admin_panel_pardakhtnesiye [INFERRED]
|
||||
- **Check Clearing Flow** — admin_panel_templates_admin_panel_hesabdariresidnashode, admin_panel_views_passcheck [INFERRED]
|
||||
- **Customer Management Flow** — admin_panel_templates_admin_panel_customerbase, admin_panel_templates_admin_panel_customerbasenew, admin_panel_templates_admin_panel_customeraction, admin_panel_templates_admin_panel_customerid, admin_panel_templates_admin_panel_customeridfactorbackup, concept_url_admin_panel_customers, concept_url_admin_panel_customeredit, concept_url_admin_panel_newfactor [INFERRED]
|
||||
- **Accounting and Billing Flow** — admin_panel_templates_admin_panel_basehesabdari, concept_url_admin_panel_hesabdari, concept_url_admin_panel_bedehi [INFERRED]
|
||||
- **Bedehi Template Data Contract** — admin_panel_templates_admin_panel_bedehi_html, django_context_variable_mandehesabkp, django_context_variable_articls [INFERRED]
|
||||
- **Customer Action Creation Flow** — admin_panel_templates_admin_panel_customeraction, admin_panel_urls_customernewaction, concept_action_model [INFERRED]
|
||||
- **Customer Management UI** — admin_panel_templates_admin_panel_customeridbackup_html, concept_customer, django_url_admin_panel_customeredit, api_endpoint_panel_customer_del, django_url_admin_panel_customerfactors [INFERRED]
|
||||
- **Client-side Payment Processing Flow** — admin_panel_templates_admin_panel_hesabdaricheckpass_html, admin_panel_urls_passcheck, admin_panel_urls_pardakhtnesiye [INFERRED]
|
||||
- **Nesiye Payment Flow** — admin_panel_templates_admin_panel_hesabdariresidnashode, admin_panel_urls_pardakhtnesiye [INFERRED]
|
||||
- **Store and Inventory Management Flow** — admin_panel_templates_admin_panel_basestore, concept_url_admin_panel_stores, concept_url_admin_panel_productadd, concept_url_admin_panel_operations, concept_url_admin_panel_store_transfers, concept_url_admin_panel_productsjson, concept_url_panel_productmojodi [INFERRED]
|
||||
- **New Factor Creation Flow** — admin_panel_templates_admin_panel_customeridbackup_html, django_url_admin_panel_productsjson, api_endpoint_panel_productmojodi, admin_panel_templates_admin_panel_customeridbackup_html_orderlist, admin_panel_templates_admin_panel_customeridbackup_html_buildorderlist, django_url_admin_panel_newfactor [INFERRED]
|
||||
- **Store and Inventory Management Flow** — admin_panel_templates_admin_panel_basestore, concept_url_admin_panel_stores, concept_url_admin_panel_productadd, concept_url_admin_panel_operations, concept_url_admin_panel_store_transfers [INFERRED]
|
||||
|
||||
## Communities (80 total, 66 thin omitted)
|
||||
## Communities (93 total, 67 thin omitted)
|
||||
|
||||
### Community 0 - "User Action Views"
|
||||
Cohesion: 0.05
|
||||
Nodes (57): User, Action, ProductCategory, ActionJson(), ActionList(), ActionListAll(), ActionListToday(), ActionListUser() (+49 more)
|
||||
|
||||
### Community 1 - "Inventory and Product Models"
|
||||
### Community 0 - "views/store.py"
|
||||
Cohesion: 0.06
|
||||
Nodes (30): ArticleType, CacheTable, PhoneNumber, Product, Meta, ProductInStore, Meta, MovementType (+22 more)
|
||||
Nodes (28): StoreRoom, Meta, StoreRoomOwner, StoreTransferRequest, Meta, Status, approve_transfer_request(), cancel_transfer_request() (+20 more)
|
||||
|
||||
### Community 2 - "Daily Reporting Command"
|
||||
Cohesion: 0.08
|
||||
Nodes (15): Command, Article, Meta, Customer, Meta, Sarresid, toJalali(), remove_query_param() (+7 more)
|
||||
### Community 1 - "project_tree.py"
|
||||
Cohesion: 0.05
|
||||
Nodes (18): remove_query_param(), check_financial_discrepancy(), entrypoint.sh script, run_360_audit(), main(), Counts, format_header(), generate_snapshot() (+10 more)
|
||||
|
||||
### Community 3 - "Customer Admin Templates"
|
||||
Cohesion: 0.08
|
||||
Nodes (22): admin_panel/base.html, admin_panel/templates/admin_panel/customerActionInc.html, admin_panel/templates/admin_panel/customerBaseNew.html, Django Custom Tags: custom_tags, URL: admin_panel:bedehi, URL: admin_panel:bedehiKhadamat, URL: admin_panel:bedehiNasb, URL: admin_panel:customer (+14 more)
|
||||
### Community 2 - "sqlite_db_backup.py"
|
||||
Cohesion: 0.06
|
||||
Nodes (3): Command, Command, Command
|
||||
|
||||
### Community 4 - "Database Schema Migrations"
|
||||
### Community 3 - "django_db"
|
||||
Cohesion: 0.06
|
||||
Nodes (16): Migration, Migration, Migration, Migration, Migration, Migration, Migration, Migration (+8 more)
|
||||
|
||||
### Community 5 - "Admin Panel URLs"
|
||||
Cohesion: 0.06
|
||||
Nodes (30): URL: admin_panel:ActionListUserToday, URL: admin_panel:entezar, URL: admin_panel:hesabdari, URL: admin_panel:hesabdari_residnashone, URL: admin_panel:index, URL: admin_panel:p_logout, URL: admin_panel:rezayat, URL: admin_panel:stores (+22 more)
|
||||
### Community 4 - "Customer ID Backup/New Factor Template"
|
||||
Cohesion: 0.07
|
||||
Nodes (17): Customer ID Backup/New Factor Template, all_products (JS variable), buildOrderList (JS function), orderList (JS variable), Client-side script in customerIDFactorBackup.html, URL: admin_panel:customerEdit, admin_panel:newFactor URL, Category Data (+9 more)
|
||||
|
||||
### Community 7 - "Initial Database Migrations"
|
||||
Cohesion: 0.09
|
||||
Nodes (11): Migration, Migration, Migration, Migration, Migration, Migration, Migration, Migration (+3 more)
|
||||
### Community 5 - "views/customers.py"
|
||||
Cohesion: 0.05
|
||||
Nodes (22): Customer, Meta, PhoneNumber, customer(), customerDel(), customerEdit(), customerFactor(), customerFactors() (+14 more)
|
||||
|
||||
### Community 8 - "User Relation Migrations"
|
||||
Cohesion: 0.09
|
||||
Nodes (11): Migration, Migration, Migration, Migration, Migration, Migration, Migration, Migration (+3 more)
|
||||
|
||||
### Community 9 - "Financial and Accounting Views"
|
||||
Cohesion: 0.10
|
||||
Nodes (20): Customer Data Object, Factor Data Object, Sarresid Data Object, admin_panel.ViewReportMali, base.html, toCurrency, admin_panel:customerNew URL, admin_panel:pardakhtNesiye URL (+12 more)
|
||||
|
||||
### Community 10 - "Invoice Management Logic"
|
||||
### Community 6 - "cheque_inquiry.py"
|
||||
Cohesion: 0.17
|
||||
Nodes (8): Factor, Meta, FactorIteam, InventoryException, newFactor(), newFactor2(), _parse_jalali_date(), _parse_timestamp()
|
||||
Nodes (6): api_check_sabt_inquiry(), api_check_status_inquiry(), parse_cheque_result_html(), run_bpi_cheque_inquiry_logic(), run_cbi_sayad_inquiry_logic(), solve_base64_captcha()
|
||||
|
||||
### Community 11 - "Django Admin Configuration"
|
||||
### Community 7 - "admin_panel/templates/admin_panel/baseHesabdari.html"
|
||||
Cohesion: 0.08
|
||||
Nodes (22): admin_panel/base.html, Django Custom Tags: custom_tags, URL: admin_panel:ActionListUserToday, URL: admin_panel:bedehi, URL: admin_panel:customers, URL: admin_panel:entezar, URL: admin_panel:hesabdari, URL: admin_panel:hesabdari_residnashone (+14 more)
|
||||
|
||||
### Community 8 - "django_db_models_deletion"
|
||||
Cohesion: 0.08
|
||||
Nodes (12): Migration, Migration, Migration, Migration, Migration, Migration, Migration, Migration (+4 more)
|
||||
|
||||
### Community 9 - "django_conf"
|
||||
Cohesion: 0.09
|
||||
Nodes (9): Migration, Migration, Migration, Migration, Migration, Migration, Migration, Migration (+1 more)
|
||||
|
||||
### Community 10 - "admin_panel/storeBK1.html"
|
||||
Cohesion: 0.11
|
||||
Nodes (17): Django CSRF Token, Django Custom Tags, Django Static Files, Django URL: admin_panel:newStore, Django URL: admin_panel:operations, Django URL: admin_panel:p_login, Django URL: admin_panel:store, Form Field: password (+9 more)
|
||||
|
||||
### Community 12 - "admin_panel/templates/admin_panel/customerAction.html"
|
||||
Cohesion: 0.16
|
||||
Nodes (10): admin_panel/templates/admin_panel/customerActionInc.html, admin_panel:customerNewAction URL, Action Data Model, DataTables Library, jQuery, User Data Model, Django 'url' template tag, Bulma CSS (+2 more)
|
||||
|
||||
### Community 13 - "User"
|
||||
Cohesion: 0.15
|
||||
Nodes (19): User, Action, VisitQueue, Factor, Meta, FactorIteam, UserProfile, ActionList() (+11 more)
|
||||
|
||||
### Community 14 - "admin_panel/templates/admin_panel/bedehi.html"
|
||||
Cohesion: 0.22
|
||||
Nodes (8): Articl Model, Customer Model, articls, toJalali, admin_panel:bedehi, admin_panel:bedehiKhadamat, admin_panel:bedehiNasb, admin_panel:customer
|
||||
|
||||
### Community 15 - "django_contrib"
|
||||
Cohesion: 0.22
|
||||
Nodes (3): StoreRoomAdmin, StoreRoomOwnerInline, StoreTransferRequestAdmin
|
||||
|
||||
### Community 12 - "Core Application Models"
|
||||
Cohesion: 0.20
|
||||
Nodes (9): Customer, Factor, FactorIteam, PhoneNumber, Product, ProductCategory, ProductInStore, StoreRoom (+1 more)
|
||||
### Community 16 - "models/__init__.py"
|
||||
Cohesion: 0.06
|
||||
Nodes (22): Customer, Factor, FactorIteam, PhoneNumber, Product, ProductCategory, ProductInStore, StoreRoom (+14 more)
|
||||
|
||||
### Community 15 - "Timezone-related Migrations"
|
||||
### Community 18 - "hesabdariCheckPassFail.html"
|
||||
Cohesion: 0.20
|
||||
Nodes (9): Customer Data Object, Factor Data Object, Sarresid Data Object, admin_panel:customerNew URL, admin_panel:pardakhtNesiye URL, /panel/passCheck/ URL, admin_panel.ViewReportMali Permission, jQuery JS Library (+1 more)
|
||||
|
||||
### Community 19 - "Reference guide"
|
||||
Cohesion: 0.07
|
||||
Nodes (27): 10. Quality evaluation, 11. Agent integration, 12. Supported languages and formats, 13. Performance, 14. Known limits, 15. Documentation, 16. Improvement, 17. License (+19 more)
|
||||
|
||||
### Community 20 - "ProductInStore"
|
||||
Cohesion: 0.14
|
||||
Nodes (8): Product, Meta, ProductCategory, ProductInStore, Meta, MovementType, ProductStockHistory, Meta
|
||||
|
||||
### Community 21 - "graphify"
|
||||
Cohesion: 0.22
|
||||
Nodes (8): Copilot Chat, graphify, graphify-out/graph.json, graphify-out/GRAPH_REPORT.md, graphify-out/wiki/index.md, graphify explain, graphify path, graphify query
|
||||
|
||||
### Community 22 - "hesabdariResidNashode.html"
|
||||
Cohesion: 0.21
|
||||
Nodes (11): admin_panel:customerNew, admin_panel:pardakhtNesiye, /panel/passCheck/, all_sarresid, mandehesabKP, admin_panel.ViewReportMali, jQuery, pDatepicker JS Library (+3 more)
|
||||
|
||||
### Community 25 - "0002_auto_20220114_1616.py"
|
||||
Cohesion: 0.29
|
||||
Nodes (3): Migration, Migration, Migration
|
||||
|
||||
### Community 19 - "Graphify Tooling"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): graphify tool, graphify explain, graphify path, graphify query, graphify
|
||||
### Community 27 - "Rip'n Fast. Fewer Tokens. Better Code."
|
||||
Cohesion: 0.05
|
||||
Nodes (42): index(), 1. Codebase Navigation & Analysis (Ripwire vs. Graphify), 2. Dynamic Help, Self-Correction & Staleness, 3. Git Operations & Safety, 4. Development Server Lifecycle, 5. Environment, Frontend & PWA Safety, 6. End-to-End Browser Tests (Playwright), Project Operating & Agent Guidelines (+34 more)
|
||||
|
||||
### Community 29 - "admin_panel/entezar.html"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): Customer Data Structure, kamaDatepicker Library, custom_tags, toJalali filter
|
||||
|
||||
### Community 30 - "views/finance.py"
|
||||
Cohesion: 0.14
|
||||
Nodes (21): Migration, Article, Meta, ArticleType, Sarresid, addSarresid(), bedehi(), bedehiKhadamat() (+13 more)
|
||||
|
||||
### Community 31 - "Customer Modals UI"
|
||||
Cohesion: 0.67
|
||||
Nodes (3): createCS click handler, openRow click handler, Customer Modals UI
|
||||
|
||||
## Ambiguous Edges - Review These
|
||||
- `admin_panel/base.html` → `admin_panel/templates/admin_panel/baseHesabdari.html` [AMBIGUOUS]
|
||||
admin_panel/templates/admin_panel/baseHesabdari.html · relation: conceptually_related_to
|
||||
|
||||
## Knowledge Gaps
|
||||
- **84 isolated node(s):** `base.html`, `toCurrency`, `Customer Data Object`, `Factor Data Object`, `admin_panel:customerNew URL` (+79 more)
|
||||
These have ≤1 connection - possible missing edges or undocumented components. (Counts symbols only; 276 node(s) total have ≤1 connection when file, concept and rationale nodes are included.)
|
||||
- **66 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
||||
- **110 isolated node(s):** `DataTables Library`, `Customer Data Object`, `Factor Data Object`, `admin_panel:pardakhtNesiye URL`, `/panel/passCheck/ URL` (+105 more)
|
||||
These have ≤1 connection - possible missing edges or undocumented components. (Counts symbols only; 398 node(s) total have ≤1 connection when file, concept and rationale nodes are included.)
|
||||
- **67 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
||||
|
||||
## Suggested Questions
|
||||
_Questions this graph is uniquely positioned to answer:_
|
||||
|
||||
- **Why does `passCheck()` connect `Financial and Accounting Views` to `User Action Views`, `Daily Reporting Command`?**
|
||||
_High betweenness centrality (0.208) - this node is a cross-community bridge._
|
||||
- **Are the 20 inferred relationships involving `StoreRoom` (e.g. with `User` and `approve_transfer_request()`) actually correct?**
|
||||
_`StoreRoom` has 20 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **What connects `base.html`, `toCurrency`, `Customer Data Object` to the rest of the system?**
|
||||
_84 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
- **Should `User Action Views` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.053728949478749 - nodes in this community are weakly interconnected._
|
||||
- **Why does `admin_panel/base.html` connect `Customer Admin Templates` to `Financial and Accounting Views`, `Admin Panel URLs`?**
|
||||
_High betweenness centrality (0.194) - this node is a cross-community bridge._
|
||||
- **Are the 14 inferred relationships involving `ProductInStore` (e.g. with `Command` and `approve_transfer_request()`) actually correct?**
|
||||
_`ProductInStore` has 14 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **Why does `Rip'n Fast. Fewer Tokens. Better Code.` connect `Rip'n Fast. Fewer Tokens. Better Code.` to `Reference guide`?**
|
||||
_High betweenness centrality (0.117) - this node is a cross-community bridge._
|
||||
- **Are the 21 inferred relationships involving `User` (e.g. with `Action` and `VisitQueue`) actually correct?**
|
||||
_`User` has 21 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **What connects `DataTables Library`, `Customer Data Object`, `Factor Data Object` to the rest of the system?**
|
||||
_110 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
- **Should `views/store.py` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.06247086247086247 - nodes in this community are weakly interconnected._
|
||||
- **Why does `index()` connect `Rip'n Fast. Fewer Tokens. Better Code.` to `views/customers.py`?**
|
||||
_High betweenness centrality (0.116) - this node is a cross-community bridge._
|
||||
- **Should `project_tree.py` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.052600818234950324 - nodes in this community are weakly interconnected._
|
||||
|
||||
### Low-confidence Hints
|
||||
_AMBIGUOUS edges — the extractor was unsure. Verify before acting on these._
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -63,12 +63,12 @@
|
||||
</div>
|
||||
<div id="legend"></div>
|
||||
</div>
|
||||
<div id="stats">548 nodes · 887 edges · 80 communities</div>
|
||||
<div id="stats">821 nodes · 1291 edges · 93 communities</div>
|
||||
</div>
|
||||
<script>
|
||||
const RAW_NODES = [{"id": "admin_models_user", "label": "User", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 15.5, "font": {"size": 12, "color": "#ffffff"}, "title": "User", "community": 0, "community_name": "User Action Views", "source_file": "admin/models.py", "file_type": "code", "degree": 21}, {"id": "admin_panel_models_action", "label": "Action", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 15.2, "font": {"size": 12, "color": "#ffffff"}, "title": "Action", "community": 0, "community_name": "User Action Views", "source_file": "admin_panel/models.py", "file_type": "code", "degree": 20}, {"id": "admin_panel_models_productcategory", "label": "ProductCategory", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 13.7, "font": {"size": 0, "color": "#ffffff"}, "title": "ProductCategory", "community": 0, "community_name": "User Action Views", "source_file": "admin_panel/models.py", "file_type": "code", "degree": 14}, {"id": "admin_panel_models_articletype", "label": "ArticleType", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "ArticleType", "community": 1, "community_name": "Inventory and Product Models", "source_file": "admin_panel/models.py", "file_type": "code", "degree": 1}, {"id": "admin_panel_models_cachetable", "label": "CacheTable", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 10.5, "font": {"size": 0, "color": "#ffffff"}, "title": "CacheTable", "community": 1, "community_name": "Inventory and Product Models", "source_file": "admin_panel/models.py", "file_type": "code", "degree": 2}, {"id": "admin_panel_models_phonenumber", "label": "PhoneNumber", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 10.5, "font": {"size": 0, "color": "#ffffff"}, "title": "PhoneNumber", "community": 1, "community_name": "Inventory and Product Models", "source_file": "admin_panel/models.py", "file_type": "code", "degree": 2}, {"id": "admin_panel_models_productstockhistory", "label": "ProductStockHistory", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 10.8, "font": {"size": 0, "color": "#ffffff"}, "title": "ProductStockHistory", "community": 1, "community_name": "Inventory and Product Models", "source_file": "admin_panel/models.py", "file_type": "code", "degree": 3}, {"id": "admin_panel_models_productstockhistory_meta", "label": "Meta", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 10.3, "font": {"size": 0, "color": "#ffffff"}, "title": "Meta", "community": 1, "community_name": "Inventory and Product Models", "source_file": "admin_panel/models.py", "file_type": "code", "degree": 1}, {"id": "admin_panel_models_storeroomowner", "label": "StoreRoomOwner", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 10.8, "font": {"size": 0, "color": "#ffffff"}, "title": "StoreRoomOwner", "community": 1, "community_name": "Inventory and Product Models", "source_file": "admin_panel/models.py", "file_type": "code", "degree": 3}, {"id": "admin_panel_models_userprofile", "label": "UserProfile", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 10.8, "font": {"size": 0, "color": "#ffffff"}, "title": "UserProfile", "community": 1, "community_name": "Inventory and Product Models", "source_file": "admin_panel/models.py", "file_type": "code", "degree": 3}, {"id": "admin_panel_models_visitqueue", "label": "VisitQueue", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 10.5, "font": {"size": 0, "color": "#ffffff"}, "title": "VisitQueue", "community": 1, "community_name": "Inventory and Product Models", "source_file": "admin_panel/models.py", "file_type": "code", "degree": 2}, {"id": "admin_panel_models_product", "label": "Product", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 14.7, "font": {"size": 12, "color": "#ffffff"}, "title": "Product", "community": 1, "community_name": "Inventory and Product Models", "source_file": "admin_panel/models.py", "file_type": "code", "degree": 18}, {"id": "admin_panel_models_product_meta", "label": "Meta", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 10.3, "Line truncated
|
||||
const RAW_EDGES = [{"from": "admin_models", "to": "admin_models_user", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_models_action", "to": "admin_models_user", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "admin_panel_models_factor", "to": "admin_models_user", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "admin_panel_models_storeroom", "to": "admin_models_user", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "admin_panel_models_storeroomowner", "to": "admin_models_user", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "admin_panel_models_storetransferrequest", "to": "admin_models_user", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "admin_panel_models_userprofile", "to": "admin_models_user", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "admin_panel_models_visitqueue", "to": "admin_models_user", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "admin_panel_views_actionlist", "to": "admin_models_user", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "admin_panel_views_actionlistall", "to": "admin_models_user", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "admin_panel_views_actionlisttoday", "to": "admin_models_user", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "admin_panel_views_actionlistuser", "to": "admin_models_user", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "admin_panel_views_actionlistusertoday", "to": "admin_models_user", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "admin_panel_views_customeraction", "to": "admin_models_user", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "admin_panel_views_customereditaction", "to": "admin_models_user", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "admin_panel_views_customernazar", "to": "admin_models_user", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "admin_panel_views_customernewaction", "to": "admin_models_user", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "admin_panel_views_inventory_movements_report", "to": "admin_models_user", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "admin_panel_views_newfactor", "to": "admin_models_user", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "admin_panel_views_report", "to": "admin_models_user", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "admin_panel_views_rezayat_dashboard", "to": "admin_models_user", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "admin_panel_management_commands_send_daily_report", "to": "admin_panel_models_action", "label": "imports", "title": "imports [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_management_commands_send_daily_report_command", "to": "admin_panel_models_action", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "admin_panel_models", "to": "admin_panel_models_action", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_views", "to": "admin_panel_models_action", "label": "imports", "title": "imports [EXTRACTED]", "dashes":Line truncated
|
||||
const LEGEND = [{"cid": 0, "color": "#4E79A7", "label": "User Action Views", "count": 87}, {"cid": 1, "color": "#F28E2B", "label": "Inventory and Product Models", "count": 60}, {"cid": 2, "color": "#E15759", "label": "Daily Reporting Command", "count": 45}, {"cid": 3, "color": "#76B7B2", "label": "Customer Admin Templates", "count": 34}, {"cid": 4, "color": "#59A14F", "label": "Database Schema Migrations", "count": 33}, {"cid": 5, "color": "#EDC948", "label": "Admin Panel URLs", "count": 33}, {"cid": 6, "color": "#B07AA1", "label": "Database Backup Command", "count": 27}, {"cid": 7, "color": "#FF9DA7", "label": "Initial Database Migrations", "count": 23}, {"cid": 8, "color": "#9C755F", "label": "User Relation Migrations", "count": 23}, {"cid": 9, "color": "#BAB0AC", "label": "Financial and Accounting Views", "count": 22}, {"cid": 10, "color": "#4E79A7", "label": "Invoice Management Logic", "count": 13}, {"cid": 11, "color": "#F28E2B", "label": "Django Admin Configuration", "count": 10}, {"cid": 12, "color": "#E15759", "label": "Core Application Models", "count": 10}, {"cid": 13, "color": "#76B7B2", "label": "Resource Cleanup Command", "count": 10}, {"cid": 14, "color": "#59A14F", "label": "Django App Configurations", "count": 7}, {"cid": 15, "color": "#EDC948", "label": "Timezone-related Migrations", "count": 7}, {"cid": 16, "color": "#B07AA1", "label": "Application Entrypoint Scripts", "count": 7}, {"cid": 17, "color": "#FF9DA7", "label": "Core Admin App", "count": 6}, {"cid": 18, "color": "#9C755F", "label": "Project URL Configuration", "count": 6}, {"cid": 19, "color": "#BAB0AC", "label": "Graphify Tooling", "count": 6}, {"cid": 20, "color": "#4E79A7", "label": "Data Validation Migration", "count": 3}, {"cid": 21, "color": "#F28E2B", "label": "Application Tests", "count": 3}, {"cid": 22, "color": "#E15759", "label": "Database Schema Migration", "count": 2}, {"cid": 23, "color": "#76B7B2", "label": "Database Schema Migration", "count": 2}, {"cid": 24, "color": "#59A14F", "label": "Remove StoreRoom User Relation", "count": 2}, {"cid": 25, "color": "#EDC948", "label": "Database Schema Migration", "count": 2}, {"cid": 26, "color": "#B07AA1", "label": "Add Action Type Field", "count": 2}, {"cid": 27, "color": "#FF9DA7", "label": "Add Action Grade Field", "count": 2}, {"cid": 28, "color": "#9C755F", "label": "Database Schema Migration", "count": 2}, {"cid": 29, "color": "#BAB0AC", "label": "Add Article Payment Status", "count": 2}, {"cid": 30, "color": "#4E79A7", "label": "Add Article Payment Method", "count": 2}, {"cid": 31, "color": "#F28E2B", "label": "Add Customer Last Visit", "count": 2}, {"cid": 32, "color": "#E15759", "label": "Database Schema Migration", "count": 2}, {"cid": 33, "color": "#76B7B2", "label": "Add Sarresid Description", "count": 2}, {"cid": 34, "color": "#59A14F", "label": "Update ProductInStore Options", "count": 2}, {"cid": 35, "color": "#EDC948", "label": "Update ProductInStore Options", "count": 2}, {"cid": 36, "color": "#B07AA1", "label": "AI Tooling Documentation", "count": 2}, {"cid": 37, "color": "#FF9DA7", "label": "Admin Migrations Package", "count": 1}, {"cid": 38, "color": "#9C755F", "label": "Customer Action Partial", "count": 1}, {"cid": 39, "color": "#BAB0AC", "label": "New Customer Base Template", "count": 1}, {"cid": 40, "color": "#4E79A7", "label": "Customer Feedback Partial", "count": 1}, {"cid": 41, "color": "#F28E2B", "label": "Customer Invoice Detail", "count": 1}, {"cid": 42, "color": "#E15759", "label": "Customer Accounting Template", "count": 1}, {"cid": 43, "color": "#76B7B2", "label": "Customer Invoice List", "count": 1}, {"cid": 44, "color": "#59A14F", "label": "Customer Feedback Page", "count": 1}, {"cid": 45, "color": "#EDC948", "label": "Management Commands Package", "count": 1}, {"cid": 46, "color": "#B07AA1", "label": "Management Package", "count": 1}, {"cid": 47, "color": "#FF9DA7", "label": "Admin Panel Migrations Package", "count": 1}, {"cid": 48, "color": "#9C755F", "label": "Action List Template", "count": 1}, {"cid": 49, "color": "#BAB0AC", "label": "New Customer Invoice Page", "count": 1}, {"cid": 50, "color": "#4E79A7", "label": "New Customer Invoice V2", "count": 1}, {"cid": 51, "color": "#F28E2B", "label": "Customer Satisfaction List", "count": 1}, {"cid": 52, "color": "#E15759", "label": "Customer Satisfaction Partial", "count": 1}, {"cid": 53, "color": "#76B7B2", "label": "Customer Debt Management", "count": 1}, {"cid": 54, "color": "#59A14F", "label": "Customer List Page", "count": 1}, {"cid": 55, "color": "#EDC948", "label": "Customer List Backup Template", "count": 1}, {"cid": 56, "color": "#B07AA1", "label": "Waiting List Backup Template", "count": 1}, {"cid": 57, "color": "#FF9DA7", "label": "Financial Dashboard", "count": 1}, {"cid": 58, "color": "#9C755F", "label": "Accounting Main View", "count": 1}, {"cid": 59, "color": "#BAB0AC", "label": "Admin Panel Index Page", "count": 1}, {"cid": 60, "color": "#4E79A7", "label": "New Inventory Line truncated
|
||||
const RAW_NODES = [{"id": "admin_panel_models_store_storeroom", "label": "StoreRoom", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 12.1, "font": {"size": 0, "color": "#ffffff"}, "title": "StoreRoom", "community": 0, "community_name": "views/store.py", "source_file": "admin_panel/models/store.py", "file_type": "code", "degree": 5}, {"id": "admin_panel_models_store_storeroom_meta", "label": "Meta", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 10.4, "font": {"size": 0, "color": "#ffffff"}, "title": "Meta", "community": 0, "community_name": "views/store.py", "source_file": "admin_panel/models/store.py", "file_type": "code", "degree": 1}, {"id": "admin_panel_models_store_storeroomowner", "label": "StoreRoomOwner", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 11.3, "font": {"size": 0, "color": "#ffffff"}, "title": "StoreRoomOwner", "community": 0, "community_name": "views/store.py", "source_file": "admin_panel/models/store.py", "file_type": "code", "degree": 3}, {"id": "admin_panel_models_store_storetransferrequest", "label": "StoreTransferRequest", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 14.2, "font": {"size": 0, "color": "#ffffff"}, "title": "StoreTransferRequest", "community": 0, "community_name": "views/store.py", "source_file": "admin_panel/models/store.py", "file_type": "code", "degree": 10}, {"id": "admin_panel_models_store_storetransferrequest_meta", "label": "Meta", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 10.4, "font": {"size": 0, "color": "#ffffff"}, "title": "Meta", "community": 0, "community_name": "views/store.py", "source_file": "admin_panel/models/store.py", "file_type": "code", "degree": 1}, {"id": "admin_panel_models_store_storetransferrequest_status", "label": "Status", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 10.4, "font": {"size": 0, "color": "#ffffff"}, "title": "Status", "community": 0, "community_name": "views/store.py", "source_file": "admin_panel/models/store.py", "file_type": "code", "degree": 1}, {"id": "tests_test_inventory_transferworkflowtests", "label": "TransferWorkflowTests", "color": {"background": "#4E79A7", "border": "#4E79A7", "highlight": {"background": "#ffffff", "border": "#4E79A7"}}, "size": 15.5, "font": {"size": 12, "color": "#ffffff"}, "title": "TransferWorkflowTests", "community": 0, "community_name": "views/store.py", "source_file": "tests/test_inventory.py", "file_type": "code", "degree": 13}, {"id": "project_tree_counts", "label": "Counts", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 12.5, "font": {"size": 0, "color": "#ffffff"}, "title": "Counts", "community": 1, "community_name": "project_tree.py", "source_file": "project_tree.py", "file_type": "code", "degree": 6}, {"id": "tests_test_urls_contract_urlcontracttests", "label": "UrlContractTests", "color": {"background": "#E15759", "border": "#E15759", "highlight": {"background": "#ffffff", "border": "#E15759"}}, "size": 12.5, "font": {"size": 0, "color": "#ffffff"}, "title": "UrlContractTests", "community": 102, "community_name": "UrlContractTests", "source_file": "tests/test_urls_contract.py", "file_type": "code", "degree": 6}, {"id": "admin_panel_models_system_cachetable", "label": "CacheTable", "color": {"background": "#59A14F", "border": "#59A14F", "highlight": {"background": "#ffffff", "border": "#59A14F"}}, "size": 10.8, "font": {"size": 0, "color": "#ffffff"}, "title": "CacheTable", "community": 104, "community_name": "system.py", "source_file": "admin_panel/models/system.py", "file_type": "code", "degree": 2}, {"id": "tests_test_view_smoke_viewsmoketests", "label": "ViewSmokeTests", "color": {"background": "#F28E2B", "border": "#F28E2B", "highlight": {"background": "#ffffff", "border": "#F28E2B"}}, "size": 13.8, "font": {"size": 0, "color": "#ffffff"}, "title": "ViewSmokeTests", "community": 11, "community_name": "ViewSmokeTests", "source_file": "tests/test_view_smoke.py", "file_type": "code", "degree": 9}, {"id": "admin_models_user", "label": "User", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": 19.3, "font": {"size": 12, "color": "#ffffff"}, "title": "User", "community": 13, "community_name": "User", "source_file": "admin/models.py", "file_type": "code", "degree": 22}, {"id": "admin_panel_models_actions_action", "label": "Action", "color": {"background": "#76B7B2", "border": "#76B7B2", "highlight": {"background": "#ffffff", "border": "#76B7B2"}}, "size": Line truncated
|
||||
const RAW_EDGES = [{"from": "admin_panel_models_store", "to": "admin_panel_models_store_storeroom", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_models_store_storeroom", "to": "admin_panel_models_store_storeroom_meta", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_models_store_storeroom", "to": "admin_panel_models_store_storeroom_str", "label": "method", "title": "method [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_views_store_newstore", "to": "admin_panel_models_store_storeroom", "label": "calls", "title": "calls [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "admin_panel_models_store_storeroom", "to": "admin_models_user", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "admin_panel_models_store", "to": "admin_panel_models_store_storeroomowner", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_models_store_rationale_34", "to": "admin_panel_models_store_storeroomowner", "label": "rationale_for", "title": "rationale_for [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_models_store_storeroomowner", "to": "admin_models_user", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "admin_panel_services_approve_transfer_request", "to": "admin_panel_models_store_storetransferrequest", "label": "references", "title": "references [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_services_cancel_transfer_request", "to": "admin_panel_models_store_storetransferrequest", "label": "references", "title": "references [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_services_create_transfer_request", "to": "admin_panel_models_store_storetransferrequest", "label": "references", "title": "references [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_services_reject_transfer_request", "to": "admin_panel_models_store_storetransferrequest", "label": "references", "title": "references [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_models_store", "to": "admin_panel_models_store_storetransferrequest", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_models_store_storetransferrequest", "to": "admin_panel_models_store_storetransferrequest_meta", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_models_store_storetransferrequest", "to": "admin_panel_models_store_storetransferrequest_status", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_models_store_storetransferrequest", "to": "admin_panel_models_store_storetransferrequest_str", "label": "method", "title": "method [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_models_store_storetransferrequest", "to": "admin_panel_models_store_storetransferrequest_clean", "label": "method", "title": "method [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "admin_panel_models_store_storetransferrequest", "to": "admin_models_user", "label": "uses", "title": "uses [INFERRED]", "dashes": true, "width": 1, "color": {"opacity": 0.35}, "confidence": "INFERRED"}, {"from": "tests_test_inventory", "to": "tests_test_inventory_transferworkflowtests", "label": "contains", "title": "contains [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "tests_test_inventory_transferworkflowtests", "to": "tests_test_inventory_py_testcase", "label": "inherits", "title": "inherits [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "tests_test_inventory_transferworkflowtests", "to": "tests_test_inventory_transferworkflowtests_setup", "label": "method", "title": "method [EXTRACTED]", "dashes": false, "width": 2, "color": {"opacity": 0.7}, "confidence": "EXTRACTED"}, {"from": "tests_Line truncated
|
||||
const LEGEND = [{"cid": 0, "color": "#4E79A7", "label": "views/store.py", "count": 66}, {"cid": 1, "color": "#F28E2B", "label": "project_tree.py", "count": 59}, {"cid": 2, "color": "#E15759", "label": "sqlite_db_backup.py", "count": 43}, {"cid": 3, "color": "#76B7B2", "label": "django_db", "count": 33}, {"cid": 4, "color": "#59A14F", "label": "Customer ID Backup/New Factor Template", "count": 31}, {"cid": 5, "color": "#EDC948", "label": "views/customers.py", "count": 62}, {"cid": 6, "color": "#B07AA1", "label": "cheque_inquiry.py", "count": 20}, {"cid": 7, "color": "#FF9DA7", "label": "admin_panel/templates/admin_panel/baseHesabdari.html", "count": 25}, {"cid": 8, "color": "#9C755F", "label": "django_db_models_deletion", "count": 25}, {"cid": 9, "color": "#BAB0AC", "label": "django_conf", "count": 24}, {"cid": 10, "color": "#4E79A7", "label": "admin_panel/storeBK1.html", "count": 19}, {"cid": 11, "color": "#F28E2B", "label": "ViewSmokeTests", "count": 8}, {"cid": 12, "color": "#E15759", "label": "admin_panel/templates/admin_panel/customerAction.html", "count": 14}, {"cid": 13, "color": "#76B7B2", "label": "User", "count": 27}, {"cid": 14, "color": "#59A14F", "label": "admin_panel/templates/admin_panel/bedehi.html", "count": 9}, {"cid": 15, "color": "#EDC948", "label": "django_contrib", "count": 10}, {"cid": 16, "color": "#B07AA1", "label": "models/__init__.py", "count": 72}, {"cid": 17, "color": "#FF9DA7", "label": "admin_panel/templates/admin_panel/customerBase.html", "count": 8}, {"cid": 18, "color": "#9C755F", "label": "hesabdariCheckPassFail.html", "count": 10}, {"cid": 19, "color": "#BAB0AC", "label": "Reference guide", "count": 27}, {"cid": 20, "color": "#4E79A7", "label": "ProductInStore", "count": 15}, {"cid": 21, "color": "#F28E2B", "label": "graphify", "count": 9}, {"cid": 22, "color": "#E15759", "label": "hesabdariResidNashode.html", "count": 13}, {"cid": 23, "color": "#76B7B2", "label": "0017_factoriteam_garantyesh.py", "count": 2}, {"cid": 24, "color": "#59A14F", "label": "django_apps", "count": 7}, {"cid": 25, "color": "#EDC948", "label": "0002_auto_20220114_1616.py", "count": 7}, {"cid": 26, "color": "#B07AA1", "label": "0021_alter_article_type.py", "count": 2}, {"cid": 27, "color": "#FF9DA7", "label": "Rip'n Fast. Fewer Tokens. Better Code.", "count": 44}, {"cid": 28, "color": "#9C755F", "label": "0027_alter_action_nazar.py", "count": 2}, {"cid": 29, "color": "#BAB0AC", "label": "admin_panel/entezar.html", "count": 5}, {"cid": 30, "color": "#4E79A7", "label": "views/finance.py", "count": 31}, {"cid": 31, "color": "#F28E2B", "label": "Customer Modals UI", "count": 3}, {"cid": 32, "color": "#E15759", "label": "0029_alter_article_options.py", "count": 2}, {"cid": 33, "color": "#76B7B2", "label": "0034_alter_article_sarresid.py", "count": 2}, {"cid": 34, "color": "#59A14F", "label": "0005_auto_20220121_2054.py", "count": 2}, {"cid": 35, "color": "#EDC948", "label": "0035_alter_article_sarresid.py", "count": 2}, {"cid": 36, "color": "#B07AA1", "label": "0009_remove_storeroom_user.py", "count": 2}, {"cid": 37, "color": "#FF9DA7", "label": "0010_auto_20220122_1546.py", "count": 2}, {"cid": 38, "color": "#9C755F", "label": "0040_customer_laghv_bazdid.py", "count": 2}, {"cid": 39, "color": "#BAB0AC", "label": "0043_sarresid_user.py", "count": 2}, {"cid": 40, "color": "#4E79A7", "label": "0048_alter_productinstore_options.py", "count": 2}, {"cid": 41, "color": "#F28E2B", "label": "0032_article_paystatus.py", "count": 2}, {"cid": 42, "color": "#E15759", "label": "0033_article_nahvepardakht.py", "count": 2}, {"cid": 45, "color": "#EDC948", "label": "0044_sarresid_desc.py", "count": 2}, {"cid": 48, "color": "#9C755F", "label": "GEMINI.md", "count": 2}, {"cid": 49, "color": "#BAB0AC", "label": "admin/migrations/__init__.py", "count": 1}, {"cid": 50, "color": "#4E79A7", "label": "Customer Action Include", "count": 1}, {"cid": 51, "color": "#F28E2B", "label": "New Customer Base Template", "count": 1}, {"cid": 52, "color": "#E15759", "label": "Customer Feedback Partial", "count": 1}, {"cid": 53, "color": "#76B7B2", "label": "Customer Factor Detail", "count": 1}, {"cid": 54, "color": "#59A14F", "label": "Customer Accounting Template", "count": 1}, {"cid": 55, "color": "#EDC948", "label": "Customer Factor List", "count": 1}, {"cid": 56, "color": "#B07AA1", "label": "Customer Feedback Page", "count": 1}, {"cid": 57, "color": "#FF9DA7", "label": "commands/__init__.py", "count": 1}, {"cid": 58, "color": "#9C755F", "label": "management/__init__.py", "count": 1}, {"cid": 59, "color": "#BAB0AC", "label": "admin_panel/migrations/__init__.py", "count": 1}, {"cid": 60, "color": "#4E79A7", "label": "Action List Template", "count": 1}, {"cid": 61, "color": "#F28E2B", "label": "base.html", "count": 1}, {"cid": 62, "color": "#E15759", "label": "Customer New Factor", "count": 1}, {"cid": 63, "color": "#76B7B2", "label": "Customer New Factor V2", "count": 1}, {"cid": 64, "color": "#59A14F", "label": "Customer SatisfactLine truncated
|
||||
|
||||
// HTML-escape helper — prevents XSS when injecting graph data into innerHTML
|
||||
function esc(s) {
|
||||
@@ -293,7 +293,7 @@ LEGEND.forEach(c => {
|
||||
</script>
|
||||
<script>
|
||||
// Render hyperedges as shaded regions
|
||||
const hyperedges = [{"id": "customer_retention_flow", "label": "Customer Satisfaction & Collection", "nodes": ["admin_panel_templates_admin_panel_report_rezayat", "admin_panel_templates_admin_panel_report_debtors_no_vosol", "admin_panel_customer_customernazarinc"], "relation": "participate_in", "confidence": "EXTRACTED", "confidence_score": 0.9}, {"id": "customer_management_flow", "label": "Customer Management Flow", "nodes": ["admin_panel_customer_base_new", "admin_panel_customer_action_inc", "admin_panel_customer_id_factor", "admin_panel_customer_hesabdari"], "relation": "participate_in", "confidence": "EXTRACTED", "confidence_score": 1.0}, {"id": "financial_management_flow", "label": "Financial & Debt Management", "nodes": ["admin_panel_templates_admin_panel_financial_dashboard", "admin_panel_templates_admin_panel_hesabdari"], "relation": "participate_in", "confidence": "EXTRACTED", "confidence_score": 1.0}, {"id": "inventory_management_flow", "label": "Inventory Management System", "nodes": ["admin_panel_templates_admin_panel_store", "admin_panel_templates_admin_panel_store_transfers", "admin_panel_templates_admin_panel_report_inventory_movements", "admin_panel_templates_admin_panel_operationsbk"], "relation": "participate_in", "confidence": "EXTRACTED", "confidence_score": 1.0}, {"id": "login_form", "label": "Login Form", "nodes": ["admin_panel_templates_admin_panel_login", "django_url_admin_panel_p_login", "django_csrf_token", "form_field_username", "form_field_password"], "relation": "form", "confidence": "EXTRACTED", "source_file": "admin_panel/templates/admin_panel/login.html"}, {"id": "new_store_form", "label": "New Store Form", "nodes": ["admin_panel_templates_admin_panel_storebk1", "django_url_admin_panel_newstore", "django_csrf_token", "form_field_storename"], "relation": "form", "confidence": "EXTRACTED", "source_file": "admin_panel/templates/admin_panel/storeBK1.html"}, {"id": "store_inventory_display", "label": "Store Inventory Display", "nodes": ["admin_panel_templates_admin_panel_storebk1", "template_variable_productsum", "product_property_product_name", "product_property_mojodi"], "relation": "participate_in", "confidence": "EXTRACTED", "source_file": "admin_panel/templates/admin_panel/storeBK1.html"}, {"id": "customer_interaction_flow", "label": "Customer Lifecycle Management", "nodes": ["admin_panel_templates_admin_panel_customers"], "relation": "participate_in", "confidence": "INFERRED", "confidence_score": 0.85}, {"id": "customer_lifecycle_management", "label": "Customer Interaction Lifecycle", "nodes": ["admin_panel_templates_admin_panel_customers", "admin_panel_templates_admin_panel_customerNew", "admin_panel_templates_admin_panel_customerRezayat"], "relation": "form", "confidence": "INFERRED", "confidence_score": 0.85, "source_file": "admin_panel/templates/admin_panel/customers.html"}, {"id": "financial_management_flow", "label": "Financial & Debt Management Flow", "nodes": ["admin_panel_templates_admin_panel_customerSarresid", "admin_panel_templates_admin_panel_hesabdari", "admin_panel_templates_admin_panel_financial_dashboard"], "relation": "participate_in", "confidence": "INFERRED", "confidence_score": 0.9, "source_file": "admin_panel/templates/admin_panel/financial_dashboard.html"}, {"id": "settle_due_payment_flow", "label": "Settle Due Payment Flow", "nodes": ["admin_panel_templates_admin_panel_hesabdaricheckpassfail_html", "admin_panel_url_passcheck", "admin_panel_url_pardakhtnesiye"], "relation": "participate_in", "confidence": "INFERRED", "confidence_score": 0.9, "source_file": "admin_panel/templates/admin_panel/hesabdariCheckPassFail.html"}, {"id": "accounting_flow", "label": "Accounting and Billing Flow", "nodes": ["admin_panel_templates_admin_panel_basehesabdari", "admin_panel_templates_admin_panel_bedehi", "admin_panel_templates_admin_panel_hesabdariheckpass", "concept_url_admin_panel_hesabdari", "concept_url_admin_panel_bedehi", "concept_url_admin_panel_pardakhtnesiye"], "relation": "participate_in", "confidence": "INFERRED", "source_file": "admin_panel/templates/admin_panel/baseHesabdari.html"}, {"id": "check_clearing_flow", "label": "Check Clearing Flow", "nodes": ["admin_panel_templates_admin_panel_hesabdariresidnashode", "admin_panel_views_passcheck"], "relation": "participate_in", "confidence": "INFERRED", "source_file": "admin_panel/templates/admin_panel/hesabdariResidNashode.html"}, {"id": "customer_management_flow", "label": "Customer Management Flow", "nodes": ["admin_panel_templates_admin_panel_customerbase", "admin_panel_templates_admin_panel_customerbasenew", "admin_panel_templates_admin_panel_customeraction", "admin_panel_templates_admin_panel_customerid", "admin_panel_templates_admin_panel_customeridfactorbackup", "concept_url_admin_panel_customers", "concept_url_admin_panel_customeredit", "concept_url_admin_panel_newfactor"], "relation": "participate_in", "confidence": "INFERRED", "source_file": "admin_panel/templates/admin_panel/customerBase.html"}, {"Line truncated
|
||||
const hyperedges = [{"id": "customer_retention_flow", "label": "Customer Satisfaction & Collection", "nodes": ["admin_panel_templates_admin_panel_report_rezayat", "admin_panel_templates_admin_panel_report_debtors_no_vosol", "admin_panel_customer_customernazarinc"], "relation": "participate_in", "confidence": "EXTRACTED", "confidence_score": 0.9}, {"id": "customer_management_flow", "label": "Customer Management Flow", "nodes": ["admin_panel_customer_base_new", "admin_panel_customer_action_inc", "admin_panel_customer_id_factor", "admin_panel_customer_hesabdari"], "relation": "participate_in", "confidence": "EXTRACTED", "confidence_score": 1.0}, {"id": "financial_management_flow", "label": "Financial & Debt Management", "nodes": ["admin_panel_templates_admin_panel_financial_dashboard", "admin_panel_templates_admin_panel_hesabdari"], "relation": "participate_in", "confidence": "EXTRACTED", "confidence_score": 1.0}, {"id": "graphify_output_artifacts", "label": "Graphify Output Artifacts", "nodes": ["graphify-out_graph_json", "graphify-out_wiki_index_md", "graphify-out_graph_report_md"], "relation": "form", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": ".github/copilot-instructions.md"}, {"id": "graphify_subcommands", "label": "Graphify Subcommands", "nodes": ["graphify_query", "graphify_path", "graphify_explain"], "relation": "form", "confidence": "EXTRACTED", "confidence_score": 1.0, "source_file": ".github/copilot-instructions.md"}, {"id": "inventory_management_flow", "label": "Inventory Management System", "nodes": ["admin_panel_templates_admin_panel_store", "admin_panel_templates_admin_panel_store_transfers", "admin_panel_templates_admin_panel_report_inventory_movements", "admin_panel_templates_admin_panel_operationsbk"], "relation": "participate_in", "confidence": "EXTRACTED", "confidence_score": 1.0}, {"id": "bedehi_report_navigation", "label": "Bedehi Report Navigation", "nodes": ["django_urls_admin_panel_bedehi", "django_urls_admin_panel_bedehikhadamat", "django_urls_admin_panel_bedehinasb"], "relation": "participate_in", "confidence": "EXTRACTED", "source_file": "admin_panel/templates/admin_panel/bedehi.html"}, {"id": "login_form", "label": "Login Form", "nodes": ["admin_panel_templates_admin_panel_login", "django_url_admin_panel_p_login", "django_csrf_token", "form_field_username", "form_field_password"], "relation": "form", "confidence": "EXTRACTED", "source_file": "admin_panel/templates/admin_panel/login.html"}, {"id": "new_store_form", "label": "New Store Form", "nodes": ["admin_panel_templates_admin_panel_storebk1", "django_url_admin_panel_newstore", "django_csrf_token", "form_field_storename"], "relation": "form", "confidence": "EXTRACTED", "source_file": "admin_panel/templates/admin_panel/storeBK1.html"}, {"id": "store_inventory_display", "label": "Store Inventory Display", "nodes": ["admin_panel_templates_admin_panel_storebk1", "template_variable_productsum", "product_property_product_name", "product_property_mojodi"], "relation": "participate_in", "confidence": "EXTRACTED", "source_file": "admin_panel/templates/admin_panel/storeBK1.html"}, {"id": "customer_list_page", "label": "Customer List Page", "nodes": ["admin_panel_templates_admin_panel_entezar_html", "concept_customer_data", "concept_url_admin_panel_customers"], "relation": "form", "confidence": "INFERRED", "confidence_score": 0.8, "source_file": "admin_panel/templates/admin_panel/entezar.html"}, {"id": "customer_interaction_flow", "label": "Customer Lifecycle Management", "nodes": ["admin_panel_templates_admin_panel_customers"], "relation": "participate_in", "confidence": "INFERRED", "confidence_score": 0.85}, {"id": "customer_lifecycle_management", "label": "Customer Interaction Lifecycle", "nodes": ["admin_panel_templates_admin_panel_customers", "admin_panel_templates_admin_panel_customerNew", "admin_panel_templates_admin_panel_customerRezayat"], "relation": "form", "confidence": "INFERRED", "confidence_score": 0.85, "source_file": "admin_panel/templates/admin_panel/customers.html"}, {"id": "new_factor_creation_flow", "label": "New Factor Creation Flow", "nodes": ["admin_panel_templates_admin_panel_customernewbackup_html", "admin_panel_urls_newfactor", "concept_client_side_form_logic", "concept_customer", "concept_category"], "relation": "participate_in", "confidence": "INFERRED", "confidence_score": 0.85, "source_file": "admin_panel/templates/admin_panel/customerNewBackup.html"}, {"id": "customer_management_ui_flow", "label": "Customer Management UI Flow", "nodes": ["admin_panel_templates_admin_panel_customersbk_html", "admin_panel_templates_admin_panel_customersbk_html_createcs", "admin_panel_templates_admin_panel_customersbk_html_openrow", "concept_customer_modal_ui"], "relation": "participate_in", "confidence": "INFERRED", "confidence_score": 0.9, "source_file": "admin_panel/templates/admin_panel/customersBK.html"}, {"id": "financial_management_flow", "label": "Financial & Debt Management Flow", "nodes": ["adLine truncated
|
||||
// afterDrawing passes ctx already transformed to network coordinate space.
|
||||
// Draw node positions raw — no manual pan/zoom/DPR math needed.
|
||||
|
||||
|
||||
+14253
-7258
File diff suppressed because it is too large.
Load diff
+1
-890
@@ -1,890 +1 @@
|
||||
{
|
||||
".gemini/settings.json": {
|
||||
"mtime": 1791551505.4245954,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "5381fe51a877fa6d2641ad3a87ae7ee1",
|
||||
"semantic_hash": "5381fe51a877fa6d2641ad3a87ae7ee1"
|
||||
},
|
||||
".opencode/opencode.json": {
|
||||
"mtime": 1791551506.0350895,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "f2ae21510d07365eb2bce8159272c7a0",
|
||||
"semantic_hash": "f2ae21510d07365eb2bce8159272c7a0"
|
||||
},
|
||||
"admin/__init__.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "d41d8cd98f00b204e9800998ecf8427e",
|
||||
"semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"
|
||||
},
|
||||
"admin/admin.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "ef4c241c5311eee11a93e9366f492a72",
|
||||
"semantic_hash": "ef4c241c5311eee11a93e9366f492a72"
|
||||
},
|
||||
"admin/apps.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "8b3d9eda5e9ce46986255783cb994a6c",
|
||||
"semantic_hash": "8b3d9eda5e9ce46986255783cb994a6c"
|
||||
},
|
||||
"admin/migrations/__init__.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "d41d8cd98f00b204e9800998ecf8427e",
|
||||
"semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"
|
||||
},
|
||||
"admin/models.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "335dbbbd9bec5b785bb826dbf6ccc8c9",
|
||||
"semantic_hash": "335dbbbd9bec5b785bb826dbf6ccc8c9"
|
||||
},
|
||||
"admin/tests.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "e08f0582500f6562bf0a931ef9503b39",
|
||||
"semantic_hash": "e08f0582500f6562bf0a931ef9503b39"
|
||||
},
|
||||
"admin/urls.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "9fab312930c17e4fb8e2293ef4cf1f1b",
|
||||
"semantic_hash": "9fab312930c17e4fb8e2293ef4cf1f1b"
|
||||
},
|
||||
"admin/views.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "65e892f735b878955bb7face4f3a15f7",
|
||||
"semantic_hash": "65e892f735b878955bb7face4f3a15f7"
|
||||
},
|
||||
"admin_panel/__init__.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "d41d8cd98f00b204e9800998ecf8427e",
|
||||
"semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"
|
||||
},
|
||||
"admin_panel/admin.py": {
|
||||
"mtime": 1783247518.769521,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "9bf27d273cdeaf40f7f82d818318fa28",
|
||||
"semantic_hash": "9bf27d273cdeaf40f7f82d818318fa28"
|
||||
},
|
||||
"admin_panel/apps.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "7b45752661a9c60826cd3b3cb648f7b9",
|
||||
"semantic_hash": "7b45752661a9c60826cd3b3cb648f7b9"
|
||||
},
|
||||
"admin_panel/context_processor.py": {
|
||||
"mtime": 1785506207.060478,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "a9bc61278ecd5950347b24fd8438583b",
|
||||
"semantic_hash": "a9bc61278ecd5950347b24fd8438583b"
|
||||
},
|
||||
"admin_panel/management/__init__.py": {
|
||||
"mtime": 1782242229.7079034,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "d41d8cd98f00b204e9800998ecf8427e",
|
||||
"semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"
|
||||
},
|
||||
"admin_panel/management/commands/__init__.py": {
|
||||
"mtime": 1782242229.7079034,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "d41d8cd98f00b204e9800998ecf8427e",
|
||||
"semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"
|
||||
},
|
||||
"admin_panel/management/commands/gt_esh.py": {
|
||||
"mtime": 1782311882.9799201,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "a93adc1bf01ae69281bc8736f254f7cc",
|
||||
"semantic_hash": "a93adc1bf01ae69281bc8736f254f7cc"
|
||||
},
|
||||
"admin_panel/management/commands/send_daily_report.py": {
|
||||
"mtime": 1787309060.0288901,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "acb098dbb2d4f61659f7d291b8ababbb",
|
||||
"semantic_hash": "acb098dbb2d4f61659f7d291b8ababbb"
|
||||
},
|
||||
"admin_panel/management/commands/sqlite_db_backup.py": {
|
||||
"mtime": 1782242044.4963484,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "03685ea937f847e6665a40f6cf482121",
|
||||
"semantic_hash": "03685ea937f847e6665a40f6cf482121"
|
||||
},
|
||||
"admin_panel/migrations/0001_initial.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "5ee5874f5ba128bb012c8b3eb7a00915",
|
||||
"semantic_hash": "5ee5874f5ba128bb012c8b3eb7a00915"
|
||||
},
|
||||
"admin_panel/migrations/0002_auto_20220114_1616.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "5086374174d36b7a4a14b6d75f51bb33",
|
||||
"semantic_hash": "5086374174d36b7a4a14b6d75f51bb33"
|
||||
},
|
||||
"admin_panel/migrations/0003_auto_20220115_1401.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "0574a9ab80ed8668f0254c3bf7d84d02",
|
||||
"semantic_hash": "0574a9ab80ed8668f0254c3bf7d84d02"
|
||||
},
|
||||
"admin_panel/migrations/0004_factor_paytype.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "34e4ef38eb7384fef97781003c6bdb0b",
|
||||
"semantic_hash": "34e4ef38eb7384fef97781003c6bdb0b"
|
||||
},
|
||||
"admin_panel/migrations/0005_auto_20220121_2054.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "bb1cbeecc3832ba9940dd171c794739f",
|
||||
"semantic_hash": "bb1cbeecc3832ba9940dd171c794739f"
|
||||
},
|
||||
"admin_panel/migrations/0006_auto_20220121_2100.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "486a3056059cd8cb5fe3dae35d3f2369",
|
||||
"semantic_hash": "486a3056059cd8cb5fe3dae35d3f2369"
|
||||
},
|
||||
"admin_panel/migrations/0007_productinstore_factor.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "f3fec01eca2e30f20bb5048788e0dfe9",
|
||||
"semantic_hash": "f3fec01eca2e30f20bb5048788e0dfe9"
|
||||
},
|
||||
"admin_panel/migrations/0008_storeroom_user.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "d0f91fb7d0fda45e7b55c2c518e94d5a",
|
||||
"semantic_hash": "d0f91fb7d0fda45e7b55c2c518e94d5a"
|
||||
},
|
||||
"admin_panel/migrations/0009_remove_storeroom_user.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "4c22c87bea29138e1b1c1a9718743af3",
|
||||
"semantic_hash": "4c22c87bea29138e1b1c1a9718743af3"
|
||||
},
|
||||
"admin_panel/migrations/0010_auto_20220122_1546.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "5b0929503f30d4bbc07681a98b218883",
|
||||
"semantic_hash": "5b0929503f30d4bbc07681a98b218883"
|
||||
},
|
||||
"admin_panel/migrations/0011_storeroom_user.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "80fa2b4783e9eb67d94fad0cc7f7a3e0",
|
||||
"semantic_hash": "80fa2b4783e9eb67d94fad0cc7f7a3e0"
|
||||
},
|
||||
"admin_panel/migrations/0012_visitqueue_user.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "063c36f0c675ca74d6c4ab47fc3a2460",
|
||||
"semantic_hash": "063c36f0c675ca74d6c4ab47fc3a2460"
|
||||
},
|
||||
"admin_panel/migrations/0013_alter_factor_options.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "a1a9cbb07d67c2a85e2f3bb3321ffcb6",
|
||||
"semantic_hash": "a1a9cbb07d67c2a85e2f3bb3321ffcb6"
|
||||
},
|
||||
"admin_panel/migrations/0014_auto_20220123_1348.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "5f562d5f34b9a225af1bc212060598b4",
|
||||
"semantic_hash": "5f562d5f34b9a225af1bc212060598b4"
|
||||
},
|
||||
"admin_panel/migrations/0015_auto_20220124_0858.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "7969fac0b9f3bcfe374252f63162a044",
|
||||
"semantic_hash": "7969fac0b9f3bcfe374252f63162a044"
|
||||
},
|
||||
"admin_panel/migrations/0016_factor_user.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "ea959dd744b9d8b2386b8ea254365213",
|
||||
"semantic_hash": "ea959dd744b9d8b2386b8ea254365213"
|
||||
},
|
||||
"admin_panel/migrations/0017_factoriteam_garantyesh.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "03245dcb79b9acb25c6942241342e167",
|
||||
"semantic_hash": "03245dcb79b9acb25c6942241342e167"
|
||||
},
|
||||
"admin_panel/migrations/0018_alter_factoriteam_product.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "af05fba320620794d19ce1435b0cb94f",
|
||||
"semantic_hash": "af05fba320620794d19ce1435b0cb94f"
|
||||
},
|
||||
"admin_panel/migrations/0019_article_articletype.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "bffe3cbec0759137d8b9659ef0ac7917",
|
||||
"semantic_hash": "bffe3cbec0759137d8b9659ef0ac7917"
|
||||
},
|
||||
"admin_panel/migrations/0020_auto_20220126_1532.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "de0476b4f325083fb85f72e0b2b07f6e",
|
||||
"semantic_hash": "de0476b4f325083fb85f72e0b2b07f6e"
|
||||
},
|
||||
"admin_panel/migrations/0021_alter_article_type.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "7def7b4625409c8afadb454d59f711b1",
|
||||
"semantic_hash": "7def7b4625409c8afadb454d59f711b1"
|
||||
},
|
||||
"admin_panel/migrations/0022_article_typepy.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "534616af969f58abb7a691a42bb5d89c",
|
||||
"semantic_hash": "534616af969f58abb7a691a42bb5d89c"
|
||||
},
|
||||
"admin_panel/migrations/0023_alter_article_typepy.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "bf5f29a0c95c00db2f327bcfd59ef3e2",
|
||||
"semantic_hash": "bf5f29a0c95c00db2f327bcfd59ef3e2"
|
||||
},
|
||||
"admin_panel/migrations/0024_action.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "bdf1314341fe7f3fdc5f30a2a814a85b",
|
||||
"semantic_hash": "bdf1314341fe7f3fdc5f30a2a814a85b"
|
||||
},
|
||||
"admin_panel/migrations/0025_action_actiontype.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "4c0e586b80b058330e63e484d0030806",
|
||||
"semantic_hash": "4c0e586b80b058330e63e484d0030806"
|
||||
},
|
||||
"admin_panel/migrations/0026_auto_20220209_1924.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "cdebac520a0ca63caa1f96a50f23eb8b",
|
||||
"semantic_hash": "cdebac520a0ca63caa1f96a50f23eb8b"
|
||||
},
|
||||
"admin_panel/migrations/0027_alter_action_nazar.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "a2b02bc693f8f79bbcc8550de42821a4",
|
||||
"semantic_hash": "a2b02bc693f8f79bbcc8550de42821a4"
|
||||
},
|
||||
"admin_panel/migrations/0028_action_grade.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "e415b68bd27ef19db2c95d7cde690023",
|
||||
"semantic_hash": "e415b68bd27ef19db2c95d7cde690023"
|
||||
},
|
||||
"admin_panel/migrations/0029_alter_article_options.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "85e0849c5e61b965e1828fd77996690f",
|
||||
"semantic_hash": "85e0849c5e61b965e1828fd77996690f"
|
||||
},
|
||||
"admin_panel/migrations/0030_auto_20220414_1047.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "e4a28feae47beb437db5275ca1003c51",
|
||||
"semantic_hash": "e4a28feae47beb437db5275ca1003c51"
|
||||
},
|
||||
"admin_panel/migrations/0031_article_sarresid.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "a036c6a51657a842e0e309db26cf5c30",
|
||||
"semantic_hash": "a036c6a51657a842e0e309db26cf5c30"
|
||||
},
|
||||
"admin_panel/migrations/0032_article_paystatus.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "1e9cc1508976669c79784d9ecdacaa5c",
|
||||
"semantic_hash": "1e9cc1508976669c79784d9ecdacaa5c"
|
||||
},
|
||||
"admin_panel/migrations/0033_article_nahvepardakht.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "a171b5d3d41e024eec96060a7553023c",
|
||||
"semantic_hash": "a171b5d3d41e024eec96060a7553023c"
|
||||
},
|
||||
"admin_panel/migrations/0034_alter_article_sarresid.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "f8b4c19246ffd432671dc645b5f7a27e",
|
||||
"semantic_hash": "f8b4c19246ffd432671dc645b5f7a27e"
|
||||
},
|
||||
"admin_panel/migrations/0035_alter_article_sarresid.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "5fc7aaa82f013ed17435c8f712433aeb",
|
||||
"semantic_hash": "5fc7aaa82f013ed17435c8f712433aeb"
|
||||
},
|
||||
"admin_panel/migrations/0036_sarresid.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "41190a4c3a8a42368b9e68be15e59da9",
|
||||
"semantic_hash": "41190a4c3a8a42368b9e68be15e59da9"
|
||||
},
|
||||
"admin_panel/migrations/0037_sarresid_sarresiddt.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "74ce86a53b5317423def533f08e40041",
|
||||
"semantic_hash": "74ce86a53b5317423def533f08e40041"
|
||||
},
|
||||
"admin_panel/migrations/0038_auto_20240213_1735.py": {
|
||||
"mtime": 1707833116.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "1f436c242cd8233cf36b46a344712893",
|
||||
"semantic_hash": "1f436c242cd8233cf36b46a344712893"
|
||||
},
|
||||
"admin_panel/migrations/0039_customer_last_bazdid_dt.py": {
|
||||
"mtime": 1708021379.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "cdbf72bdc29027c67ec6f579dbfd87a5",
|
||||
"semantic_hash": "cdbf72bdc29027c67ec6f579dbfd87a5"
|
||||
},
|
||||
"admin_panel/migrations/0040_customer_laghv_bazdid.py": {
|
||||
"mtime": 1708086290.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "f240e539790c8266e8e6436de3f7ba8e",
|
||||
"semantic_hash": "f240e539790c8266e8e6436de3f7ba8e"
|
||||
},
|
||||
"admin_panel/migrations/0041_auto_20240216_1607.py": {
|
||||
"mtime": 1708087066.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "0e18ee1383cce3042f8d951170a4206c",
|
||||
"semantic_hash": "0e18ee1383cce3042f8d951170a4206c"
|
||||
},
|
||||
"admin_panel/migrations/0042_article_user.py": {
|
||||
"mtime": 1708097636.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "9c1401cdcc3e57cd7fd94bc02b4f5712",
|
||||
"semantic_hash": "9c1401cdcc3e57cd7fd94bc02b4f5712"
|
||||
},
|
||||
"admin_panel/migrations/0043_sarresid_user.py": {
|
||||
"mtime": 1712324765.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "854ae72ac9682f257391fddaafd8df18",
|
||||
"semantic_hash": "854ae72ac9682f257391fddaafd8df18"
|
||||
},
|
||||
"admin_panel/migrations/0044_sarresid_desc.py": {
|
||||
"mtime": 1731443780.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "a016eaa6fc679bf509d831b1a970b625",
|
||||
"semantic_hash": "a016eaa6fc679bf509d831b1a970b625"
|
||||
},
|
||||
"admin_panel/migrations/0045_auto_20260610_1421.py": {
|
||||
"mtime": 1781088666.153017,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "4ef65d5c188e0cdfe6ada14be13a5289",
|
||||
"semantic_hash": "4ef65d5c188e0cdfe6ada14be13a5289"
|
||||
},
|
||||
"admin_panel/migrations/0046_auto_20260624_1818.py": {
|
||||
"mtime": 1782312515.2476697,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "5ca2702b20a1a595e8f70d0febce437b",
|
||||
"semantic_hash": "5ca2702b20a1a595e8f70d0febce437b"
|
||||
},
|
||||
"admin_panel/migrations/0047_alter_productinstore_movement_type.py": {
|
||||
"mtime": 1782400614.7357888,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "dc43c8e6b4e1e070f12746a3ce7b48eb",
|
||||
"semantic_hash": "dc43c8e6b4e1e070f12746a3ce7b48eb"
|
||||
},
|
||||
"admin_panel/migrations/0048_alter_productinstore_options.py": {
|
||||
"mtime": 1783073642.7854881,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "1ef983920121630add36d9d681b13aee",
|
||||
"semantic_hash": "1ef983920121630add36d9d681b13aee"
|
||||
},
|
||||
"admin_panel/migrations/0049_alter_productinstore_options.py": {
|
||||
"mtime": 1783075090.874336,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "6535f13daa18a07d6bbf04101b371d9c",
|
||||
"semantic_hash": "6535f13daa18a07d6bbf04101b371d9c"
|
||||
},
|
||||
"admin_panel/migrations/0050_storetransferrequest.py": {
|
||||
"mtime": 1783081448.2086937,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "479c29e8beb5388215e2dc24751686d1",
|
||||
"semantic_hash": "479c29e8beb5388215e2dc24751686d1"
|
||||
},
|
||||
"admin_panel/migrations/0051_auto_20260705_1356.py": {
|
||||
"mtime": 1783247162.7750995,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "ce9e0ca56be568fae0613e79298666f2",
|
||||
"semantic_hash": "ce9e0ca56be568fae0613e79298666f2"
|
||||
},
|
||||
"admin_panel/migrations/0052_storeroomowner.py": {
|
||||
"mtime": 1783247323.7533815,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "cd1a8e4784e4f21b50c5200da2677199",
|
||||
"semantic_hash": "cd1a8e4784e4f21b50c5200da2677199"
|
||||
},
|
||||
"admin_panel/migrations/0053_storeroom_owners.py": {
|
||||
"mtime": 1783247371.5792572,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "4015f9c7449f85884f89b31d3af39894",
|
||||
"semantic_hash": "4015f9c7449f85884f89b31d3af39894"
|
||||
},
|
||||
"admin_panel/migrations/0054_auto_20260715_2250.py": {
|
||||
"mtime": 1784143234.375771,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "9ee31b1cc86ea3ac5db8e3bb7ad38aad",
|
||||
"semantic_hash": "9ee31b1cc86ea3ac5db8e3bb7ad38aad"
|
||||
},
|
||||
"admin_panel/migrations/0055_action_nextnazardt.py": {
|
||||
"mtime": 1784916401.6852713,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "6dfed65de7902632cb291defd05914ee",
|
||||
"semantic_hash": "6dfed65de7902632cb291defd05914ee"
|
||||
},
|
||||
"admin_panel/migrations/0056_auto_20260731_1824.py": {
|
||||
"mtime": 1785509644.1099384,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "c08ef1beaff253c2e43eccb981073c1f",
|
||||
"semantic_hash": "c08ef1beaff253c2e43eccb981073c1f"
|
||||
},
|
||||
"admin_panel/migrations/__init__.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "d41d8cd98f00b204e9800998ecf8427e",
|
||||
"semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"
|
||||
},
|
||||
"admin_panel/models.py": {
|
||||
"mtime": 1785509631.4338894,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "1b4254486bf126b24e73d7a1fc930007",
|
||||
"semantic_hash": "1b4254486bf126b24e73d7a1fc930007"
|
||||
},
|
||||
"admin_panel/services.py": {
|
||||
"mtime": 1786864057.7585652,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "164f88ca79dff2643a1c38f5974126be",
|
||||
"semantic_hash": "164f88ca79dff2643a1c38f5974126be"
|
||||
},
|
||||
"admin_panel/templatetags/__init__.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "d41d8cd98f00b204e9800998ecf8427e",
|
||||
"semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"
|
||||
},
|
||||
"admin_panel/templatetags/custom_tags.py": {
|
||||
"mtime": 1783251836.4148617,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "8d8c826e168cec5bb68a21c8457380ba",
|
||||
"semantic_hash": "8d8c826e168cec5bb68a21c8457380ba"
|
||||
},
|
||||
"admin_panel/templatetags/remove_q_p.py": {
|
||||
"mtime": 1683307703.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "6ac789d61f86b1a27bc215d2a77761c6",
|
||||
"semantic_hash": "6ac789d61f86b1a27bc215d2a77761c6"
|
||||
},
|
||||
"admin_panel/tests.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "e08f0582500f6562bf0a931ef9503b39",
|
||||
"semantic_hash": "e08f0582500f6562bf0a931ef9503b39"
|
||||
},
|
||||
"admin_panel/urls.py": {
|
||||
"mtime": 1787302652.4618616,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "d550a6f4bbfd8ec74f8ef482ee7f1cff",
|
||||
"semantic_hash": "d550a6f4bbfd8ec74f8ef482ee7f1cff"
|
||||
},
|
||||
"admin_panel/views.py": {
|
||||
"mtime": 1787304201.392812,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "40c436a0d14e9ca9dd798fd6ffc2ba7b",
|
||||
"semantic_hash": "40c436a0d14e9ca9dd798fd6ffc2ba7b"
|
||||
},
|
||||
"check_finance.py": {
|
||||
"mtime": 1786878923.269553,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "75f1e60011aa238abef8ba028761d97f",
|
||||
"semantic_hash": "75f1e60011aa238abef8ba028761d97f"
|
||||
},
|
||||
"entrypoint.sh": {
|
||||
"mtime": 1782246266.1269526,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "f06f9179baa5071b374895438cc05176",
|
||||
"semantic_hash": "f06f9179baa5071b374895438cc05176"
|
||||
},
|
||||
"ghost_hunter.py": {
|
||||
"mtime": 1786881510.522436,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "51f0a2e2323df5d9e444aad5a2883ee5",
|
||||
"semantic_hash": "51f0a2e2323df5d9e444aad5a2883ee5"
|
||||
},
|
||||
"khatibi/__init__.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "d41d8cd98f00b204e9800998ecf8427e",
|
||||
"semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"
|
||||
},
|
||||
"khatibi/asgi.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "b4e49af22c6a30c16299cb82ab55df6d",
|
||||
"semantic_hash": "b4e49af22c6a30c16299cb82ab55df6d"
|
||||
},
|
||||
"khatibi/production_settings.py": {
|
||||
"mtime": 1782245208.734714,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "73a68e0d30f37dc4fbdad316c02b5fc4",
|
||||
"semantic_hash": "73a68e0d30f37dc4fbdad316c02b5fc4"
|
||||
},
|
||||
"khatibi/settings.py": {
|
||||
"mtime": 1782309009.7845018,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "bf91e375449195bc3d958a6c51266d7f",
|
||||
"semantic_hash": "bf91e375449195bc3d958a6c51266d7f"
|
||||
},
|
||||
"khatibi/urls.py": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "c84a36287ab919a7c7af7665f7f74fe7",
|
||||
"semantic_hash": "c84a36287ab919a7c7af7665f7f74fe7"
|
||||
},
|
||||
"khatibi/wsgi.py": {
|
||||
"mtime": 1708013917.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "3201b17eab76f3e248ee368647070e4c",
|
||||
"semantic_hash": "3201b17eab76f3e248ee368647070e4c"
|
||||
},
|
||||
"manage.py": {
|
||||
"mtime": 1708013913.0,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "f2880415f68590c74305913ff0f025f3",
|
||||
"semantic_hash": "f2880415f68590c74305913ff0f025f3"
|
||||
},
|
||||
"reconciliation_tool.py": {
|
||||
"mtime": 1786880527.1578598,
|
||||
"seen": 1791552045.91651,
|
||||
"ast_hash": "29c3e16d73d00a9f3f09692194be3b18",
|
||||
"semantic_hash": "29c3e16d73d00a9f3f09692194be3b18"
|
||||
},
|
||||
".github/copilot-instructions.md": {
|
||||
"mtime": 1791551555.5196745,
|
||||
"seen": 1791553187.9024515,
|
||||
"ast_hash": "2b23503628bf2ec71d067a85ca8948ab",
|
||||
"semantic_hash": "2b23503628bf2ec71d067a85ca8948ab"
|
||||
},
|
||||
"GEMINI.md": {
|
||||
"mtime": 1791551505.4181063,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "82efb97a359f5c7290bf2513d14686be",
|
||||
"semantic_hash": "82efb97a359f5c7290bf2513d14686be"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/ActionList.html": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "4ba55e04b37e585d96f0d628c695d228",
|
||||
"semantic_hash": "4ba55e04b37e585d96f0d628c695d228"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/base.html": {
|
||||
"mtime": 1787305605.0913658,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "3062a4756b2649b3b24a52e5972163d2",
|
||||
"semantic_hash": "3062a4756b2649b3b24a52e5972163d2"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/baseHesabdari.html": {
|
||||
"mtime": 1703841590.0,
|
||||
"seen": 1791553187.9024515,
|
||||
"ast_hash": "47b9ea8bc69fc1731d0dbe267f1ed433",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"admin_panel/templates/admin_panel/baseStore.html": {
|
||||
"mtime": 1783083554.0144844,
|
||||
"seen": 1791553187.9024515,
|
||||
"ast_hash": "d2fe6f58a7303c1874ebe08af1e50564",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"admin_panel/templates/admin_panel/bedehi.html": {
|
||||
"mtime": 1703843184.0,
|
||||
"seen": 1791553187.9024515,
|
||||
"ast_hash": "642f82c9f9c217c83acb79185b9439da",
|
||||
"semantic_hash": "642f82c9f9c217c83acb79185b9439da"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/bedehiBK.html": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791553187.9024515,
|
||||
"ast_hash": "9f714287989d56c19bc1fc28a08cd89d",
|
||||
"semantic_hash": "9f714287989d56c19bc1fc28a08cd89d"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/customerAction.html": {
|
||||
"mtime": 1784913239.8701046,
|
||||
"seen": 1791553187.9024515,
|
||||
"ast_hash": "bb1af41ef956a5e2f30b1266c70a30ee",
|
||||
"semantic_hash": "bb1af41ef956a5e2f30b1266c70a30ee"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/customerActionInc.html": {
|
||||
"mtime": 1784913846.5075295,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "3232cbf82efe94504f69735791ca5cf4",
|
||||
"semantic_hash": "3232cbf82efe94504f69735791ca5cf4"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/customerBase.html": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791553187.9024515,
|
||||
"ast_hash": "2724fa49985d8c8fa76ddf5c31b64054",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"admin_panel/templates/admin_panel/customerBaseNew.html": {
|
||||
"mtime": 1786558511.2232144,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "1684b269805012f620bcd90f90572203",
|
||||
"semantic_hash": "1684b269805012f620bcd90f90572203"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/customerFactor.html": {
|
||||
"mtime": 1782399903.8791773,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "23cc139c4d246a9640220cc1f42a1bd1",
|
||||
"semantic_hash": "23cc139c4d246a9640220cc1f42a1bd1"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/customerFactorBackup.html": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791553187.9034579,
|
||||
"ast_hash": "ff7615faae4df99564cf85d3212a93da",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"admin_panel/templates/admin_panel/customerHesabDari.html": {
|
||||
"mtime": 1708533891.0,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "2d7c82b08780f0915a1dc145db522cd5",
|
||||
"semantic_hash": "2d7c82b08780f0915a1dc145db522cd5"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/customerID.html": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791553187.9034579,
|
||||
"ast_hash": "52739e2559c60cad69fb7efaf65e2dd3",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"admin_panel/templates/admin_panel/customerIDBackup.html": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791553187.9034579,
|
||||
"ast_hash": "c6646ddb883c9f85033d9e9d3fb54af2",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"admin_panel/templates/admin_panel/customerIDFactor.html": {
|
||||
"mtime": 1784392254.4440935,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "2c0c3fb300c440860d3f1dd8a6181af1",
|
||||
"semantic_hash": "2c0c3fb300c440860d3f1dd8a6181af1"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/customerIDFactorBackup.html": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791553187.9034579,
|
||||
"ast_hash": "d04aff45b81af659d92173536cbc224f",
|
||||
"semantic_hash": "d04aff45b81af659d92173536cbc224f"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/customerNew.html": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "6eec9291fd4f6bce96e553f9e7352e44",
|
||||
"semantic_hash": "6eec9291fd4f6bce96e553f9e7352e44"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/customerNew2.html": {
|
||||
"mtime": 1712320283.0,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "47fa6edfc3228d414f22e1a8f3ae4a7a",
|
||||
"semantic_hash": "47fa6edfc3228d414f22e1a8f3ae4a7a"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/customerNewBackup.html": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791553187.9034579,
|
||||
"ast_hash": "6f281fc25edb4c2822b6a058e4049655",
|
||||
"semantic_hash": "6f281fc25edb4c2822b6a058e4049655"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/customerRezayat.html": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "4e954f6dd0f4ed95656c457fd4399cdd",
|
||||
"semantic_hash": "4e954f6dd0f4ed95656c457fd4399cdd"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/customerRezayatinc.html": {
|
||||
"mtime": 1784916750.5503716,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "033adb9aa1873c64ac7bf495cdef7cf5",
|
||||
"semantic_hash": "033adb9aa1873c64ac7bf495cdef7cf5"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/customerSarresid.html": {
|
||||
"mtime": 1786557950.072763,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "dd92347d0f721008570850177989b927",
|
||||
"semantic_hash": "dd92347d0f721008570850177989b927"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/customer/customerNazarInc.html": {
|
||||
"mtime": 1707672379.0,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "7687f61651a191ba38a3a7fbc0fa4eea",
|
||||
"semantic_hash": "7687f61651a191ba38a3a7fbc0fa4eea"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/customer/nazar.html": {
|
||||
"mtime": 1707669675.0,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "1cb1e5086a0f291b86437f03af835407",
|
||||
"semantic_hash": "1cb1e5086a0f291b86437f03af835407"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/customers.html": {
|
||||
"mtime": 1784142973.2160225,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "77809af7284de3bdacce2e6bb27dccf5",
|
||||
"semantic_hash": "77809af7284de3bdacce2e6bb27dccf5"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/customersBK.html": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791553187.904458,
|
||||
"ast_hash": "c3912ead765eb7b5ada5634b70ebc644",
|
||||
"semantic_hash": "c3912ead765eb7b5ada5634b70ebc644"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/entezar.html": {
|
||||
"mtime": 1708088349.0,
|
||||
"seen": 1791553187.904458,
|
||||
"ast_hash": "808fd766dd0d0abec11bfcccd57dcee2",
|
||||
"semantic_hash": "808fd766dd0d0abec11bfcccd57dcee2"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/entezarBK.html": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791553187.904458,
|
||||
"ast_hash": "09796b3916f635c6bc2a5aa3d2df108f",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"admin_panel/templates/admin_panel/financial_dashboard.html": {
|
||||
"mtime": 1786882219.7287638,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "d0782d6533e6b105ff1cd837d3b73108",
|
||||
"semantic_hash": "d0782d6533e6b105ff1cd837d3b73108"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/hesabdari.html": {
|
||||
"mtime": 1786553421.551038,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "9a44f015176e20e333a42321701b3575",
|
||||
"semantic_hash": "9a44f015176e20e333a42321701b3575"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/hesabdariCheckPass.html": {
|
||||
"mtime": 1707470201.0,
|
||||
"seen": 1791553187.905458,
|
||||
"ast_hash": "4f41af8d09e60f2eb1b4db77c5a2ec11",
|
||||
"semantic_hash": "4f41af8d09e60f2eb1b4db77c5a2ec11"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/hesabdariCheckPassFail.html": {
|
||||
"mtime": 1707470341.0,
|
||||
"seen": 1791553187.905458,
|
||||
"ast_hash": "c83c6a1ab4af819947dda04360ba42b4",
|
||||
"semantic_hash": "c83c6a1ab4af819947dda04360ba42b4"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/hesabdariResidNashode.html": {
|
||||
"mtime": 1703840323.0,
|
||||
"seen": 1791553187.905458,
|
||||
"ast_hash": "fec700cfae2a2f99fc20d8e1d4f20aab",
|
||||
"semantic_hash": "fec700cfae2a2f99fc20d8e1d4f20aab"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/index.html": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791553187.905458,
|
||||
"ast_hash": "ead4632d75560f5fc6ab13c6fe0bd33c",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"admin_panel/templates/admin_panel/login.html": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791553187.905458,
|
||||
"ast_hash": "297f4c4a04f98fa223796ca4482dfbbe",
|
||||
"semantic_hash": "297f4c4a04f98fa223796ca4482dfbbe"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/newOperation.html": {
|
||||
"mtime": 1783251875.7455926,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "9fd1d5148c0121c47d07e4e1346a32ff",
|
||||
"semantic_hash": "9fd1d5148c0121c47d07e4e1346a32ff"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/operations.html": {
|
||||
"mtime": 1782127647.8900125,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "3199582c3342413eeb8b8a5cf5fb879a",
|
||||
"semantic_hash": "3199582c3342413eeb8b8a5cf5fb879a"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/operationsBK.html": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "fa2ab834231c3ff3f7500431b74978f4",
|
||||
"semantic_hash": "fa2ab834231c3ff3f7500431b74978f4"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/report.html": {
|
||||
"mtime": 1712328717.0,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "cc8bbc00a3ec2bf85b67b42d8d48a6aa",
|
||||
"semantic_hash": "cc8bbc00a3ec2bf85b67b42d8d48a6aa"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/reportCheckNesiye.html": {
|
||||
"mtime": 1712328596.0,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "213c083816f68261bbc926124e6c67ad",
|
||||
"semantic_hash": "213c083816f68261bbc926124e6c67ad"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/report_debtors_no_vosol.html": {
|
||||
"mtime": 1787304395.4691105,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "fef97825343ad1b8acda81df48a9540e",
|
||||
"semantic_hash": "fef97825343ad1b8acda81df48a9540e"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/report_inventory_movements.html": {
|
||||
"mtime": 1782459887.7241113,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "6b1591c8e2d96ca73e38c55be001cbd4",
|
||||
"semantic_hash": "6b1591c8e2d96ca73e38c55be001cbd4"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/report_rezayat.html": {
|
||||
"mtime": 1784918413.9920633,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "96e967c44debbabf2ee8c630cd371a23",
|
||||
"semantic_hash": "96e967c44debbabf2ee8c630cd371a23"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/rezayat.html": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552476.826179,
|
||||
"ast_hash": "df2df73cb519f7915d7f656e46ced406",
|
||||
"semantic_hash": "df2df73cb519f7915d7f656e46ced406"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/store.html": {
|
||||
"mtime": 1783252370.4796727,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "95e5186068796e24b2635441089dacb5",
|
||||
"semantic_hash": "95e5186068796e24b2635441089dacb5"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/storeBK1.html": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791553187.906458,
|
||||
"ast_hash": "79e507d05c12c39715fc90cf0d8b891d",
|
||||
"semantic_hash": "79e507d05c12c39715fc90cf0d8b891d"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/store_transfers.html": {
|
||||
"mtime": 1783083492.176341,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "9a02fb0648ea6d8cc7ff73647c173a46",
|
||||
"semantic_hash": "9a02fb0648ea6d8cc7ff73647c173a46"
|
||||
},
|
||||
"admin_panel/templates/admin_panel/stores.html": {
|
||||
"mtime": 1650778147.0,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "40dcac5b8cdbaf25efa63e4a9680d4ad",
|
||||
"semantic_hash": "40dcac5b8cdbaf25efa63e4a9680d4ad"
|
||||
},
|
||||
"requirements.txt": {
|
||||
"mtime": 1787308505.3543158,
|
||||
"seen": 1791552251.327148,
|
||||
"ast_hash": "4988f4d0e1372076211a9b5ae632ab2d",
|
||||
"semantic_hash": "4988f4d0e1372076211a9b5ae632ab2d"
|
||||
},
|
||||
"requirements2.txt": {
|
||||
"mtime": 1654964543.0,
|
||||
"seen": 1791552476.826179,
|
||||
"ast_hash": "b68cf8783499ded7f16a7da13f974f7b",
|
||||
"semantic_hash": "b68cf8783499ded7f16a7da13f974f7b"
|
||||
}
|
||||
}
|
||||
{".gemini/settings.json": {"mtime": 1791551505.4245954, "seen": 1791552045.91651, "ast_hash": "5381fe51a877fa6d2641ad3a87ae7ee1", "semantic_hash": "5381fe51a877fa6d2641ad3a87ae7ee1"}, ".opencode/opencode.json": {"mtime": 1791551506.0350895, "seen": 1791552045.91651, "ast_hash": "f2ae21510d07365eb2bce8159272c7a0", "semantic_hash": "f2ae21510d07365eb2bce8159272c7a0"}, "admin/__init__.py": {"mtime": 1650778147.0, "seen": 1791552045.91651, "ast_hash": "d41d8cd98f00b204e9800998ecf8427e", "semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"}, "admin/admin.py": {"mtime": 1650778147.0, "seen": 1791552045.91651, "ast_hash": "ef4c241c5311eee11a93e9366f492a72", "semantic_hash": "ef4c241c5311eee11a93e9366f492a72"}, "admin/apps.py": {"mtime": 1650778147.0, "seen": 1791552045.91651, "ast_hash": "8b3d9eda5e9ce46986255783cb994a6c", "semantic_hash": "8b3d9eda5e9ce46986255783cb994a6c"}, "admin/migrations/__init__.py": {"mtime": 1650778147.0, "seen": 1791552045.91651, "ast_hash": "d41d8cd98f00b204e9800998ecf8427e", "semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"}, "admin/models.py": {"mtime": 1650778147.0, "seen": 1791552045.91651, "ast_hash": "335dbbbd9bec5b785bb826dbf6ccc8c9", "semantic_hash": "335dbbbd9bec5b785bb826dbf6ccc8c9"}, "admin/tests.py": {"mtime": 1650778147.0, "seen": 1791552045.91651, "ast_hash": "e08f0582500f6562bf0a931ef9503b39", "semantic_hash": "e08f0582500f6562bf0a931ef9503b39"}, "admin/urls.py": {"mtime": 1650778147.0, "seen": 1791552045.91651, "ast_hash": "9fab312930c17e4fb8e2293ef4cf1f1b", "semantic_hash": "9fab312930c17e4fb8e2293ef4cf1f1b"}, "admin/views.py": {"mtime": 1650778147.0, "seen": 1791552045.91651, "ast_hash": "65e892f735b878955bb7face4f3a15f7", "semantic_hash": "65e892f735b878955bb7face4f3a15f7"}, "admin_panel/__init__.py": {"mtime": 1650778147.0, "seen": 1791552045.91651, "ast_hash": "d41d8cd98f00b204e9800998ecf8427e", "semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"}, "admin_panel/admin.py": {"mtime": 1783247518.769521, "seen": 1791552045.91651, "ast_hash": "9bf27d273cdeaf40f7f82d818318fa28", "semantic_hash": "9bf27d273cdeaf40f7f82d818318fa28"}, "admin_panel/apps.py": {"mtime": 1650778147.0, "seen": 1791552045.91651, "ast_hash": "7b45752661a9c60826cd3b3cb648f7b9", "semantic_hash": "7b45752661a9c60826cd3b3cb648f7b9"}, "admin_panel/context_processor.py": {"mtime": 1785506207.060478, "seen": 1791552045.91651, "ast_hash": "a9bc61278ecd5950347b24fd8438583b", "semantic_hash": "a9bc61278ecd5950347b24fd8438583b"}, "admin_panel/management/__init__.py": {"mtime": 1782242229.7079034, "seen": 1791552045.91651, "ast_hash": "d41d8cd98f00b204e9800998ecf8427e", "semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"}, "admin_panel/management/commands/__init__.py": {"mtime": 1782242229.7079034, "seen": 1791552045.91651, "ast_hash": "d41d8cd98f00b204e9800998ecf8427e", "semantic_hash": "d41d8cd98f00b204e9800998ecf8427e"}, "admin_panel/management/commands/gt_esh.py": {"mtime": 1782311882.9799201, "seen": 1791552045.91651, "ast_hash": "a93adc1bf01ae69281bc8736f254f7cc", "semantic_hash": "a93adc1bf01ae69281bc8736f254f7cc"}, "admin_panel/management/commands/send_daily_report.py": {"mtime": 1787309060.0288901, "seen": 1791552045.91651, "ast_hash": "acb098dbb2d4f61659f7d291b8ababbb", "semantic_hash": "acb098dbb2d4f61659f7d291b8ababbb"}, "admin_panel/management/commands/sqlite_db_backup.py": {"mtime": 1782242044.4963484, "seen": 1791552045.91651, "ast_hash": "03685ea937f847e6665a40f6cf482121", "semantic_hash": "03685ea937f847e6665a40f6cf482121"}, "admin_panel/migrations/0001_initial.py": {"mtime": 1650778147.0, "seen": 1791552045.91651, "ast_hash": "5ee5874f5ba128bb012c8b3eb7a00915", "semantic_hash": "5ee5874f5ba128bb012c8b3eb7a00915"}, "admin_panel/migrations/0002_auto_20220114_1616.py": {"mtime": 1650778147.0, "seen": 1791552045.91651, "ast_hash": "5086374174d36b7a4a14b6d75f51bb33", "semantic_hash": "5086374174d36b7a4a14b6d75f51bb33"}, "admin_panel/migrations/0003_auto_20220115_1401.py": {"mtime": 1650778147.0, "seen": 1791552045.91651, "ast_hash": "0574a9ab80ed8668f0254c3bf7d84d02", "semantic_hash": "0574a9ab80ed8668f0254c3bf7d84d02"}, "admin_panel/migrations/0004_factor_paytype.py": {"mtime": 1650778147.0, "seen": 1791552045.91651, "ast_hash": "34e4ef38eb7384fef97781003c6bdb0b", "semantic_hash": "34e4ef38eb7384fef97781003c6bdb0b"}, "admin_panel/migrations/0005_auto_20220121_2054.py": {"mtime": 1650778147.0, "seen": 1791552045.91651, "ast_hash": "bb1cbeecc3832ba9940dd171c794739f", "semantic_hash": "bb1cbeecc3832ba9940dd171c794739f"}, "admin_panel/migrations/0006_auto_20220121_2100.py": {"mtime": 1650778147.0, "seen": 1791552045.91651, "ast_hash": "486a3056059cd8cb5fe3dae35d3f2369", "semantic_hash": "486a3056059cd8cb5fe3dae35d3f2369"}, "admin_panel/migrations/0007_productinstore_factor.py": {"mtime": 1650778147.0, "seen": 1791552045.91651, "ast_hash": "f3fec01eca2e30f20bb5048788e0dfe9", "semantic_hash": "f3fec01eca2e30f20bb5048788e0dfe9"}, "admin_panel/migrations/0008_storeroom_user.py": {"mtime": 1650778147.0,Line truncated
|
||||
Whitespace-only changes.
@@ -0,0 +1,107 @@
|
||||
"""
|
||||
کارخانههای ساخت داده آزمایشی برای تستهای پروژه.
|
||||
|
||||
این توابع فقط روی دیتابیس تستی Django اجرا میشوند و هیچگاه به
|
||||
`data9/kt.sqlite3` (دیتابیس تولیدی) دست نمیزنند.
|
||||
"""
|
||||
import itertools
|
||||
|
||||
from django.contrib.auth.models import Permission, User
|
||||
|
||||
from admin_panel.models import (Customer, PhoneNumber, Product, ProductCategory,
|
||||
ProductInStore, StoreRoom, StoreRoomOwner)
|
||||
|
||||
_counter = itertools.count(1)
|
||||
|
||||
|
||||
def _next(prefix):
|
||||
return f"{prefix}{next(_counter)}"
|
||||
|
||||
|
||||
def make_user(username=None, *, password='TestPass!2345', superuser=False,
|
||||
first_name='', last_name='', perms=()):
|
||||
"""
|
||||
ساخت کاربر. با `superuser=True` تمام بررسیهای دسترسی عبور داده میشوند.
|
||||
`perms` لیستی از کدنام پرمیشنهاست (مثلاً ['OnlyAnbar', 'ViewStore']).
|
||||
"""
|
||||
username = username or _next('user')
|
||||
if superuser:
|
||||
user = User.objects.create_superuser(username=username, email='', password=password)
|
||||
else:
|
||||
user = User.objects.create_user(username=username, password=password)
|
||||
|
||||
if first_name or last_name:
|
||||
user.first_name = first_name
|
||||
user.last_name = last_name
|
||||
user.save(update_fields=['first_name', 'last_name'])
|
||||
|
||||
if perms:
|
||||
user.user_permissions.add(*Permission.objects.filter(codename__in=list(perms)))
|
||||
# رفرش کش پرمیشنها تا بلافاصله در دسترس باشند
|
||||
user = User.objects.get(pk=user.pk)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def make_store(name=None, *, viewers=(), owner=None):
|
||||
"""
|
||||
ساخت انبار.
|
||||
- `viewers` به M2M `user` اضافه میشوند (دسترسی مشاهده/تایید انتقال).
|
||||
- `owner` به M2M `owners` (از طریق StoreRoomOwner) اضافه میشود
|
||||
(همان چیزی که `StoreRoom.objects.get(owners=user)` انتظار دارد).
|
||||
"""
|
||||
store = StoreRoom.objects.create(store_name=name or _next('انبار'))
|
||||
|
||||
for v in viewers:
|
||||
store.user.add(v)
|
||||
|
||||
if owner is not None:
|
||||
StoreRoomOwner.objects.create(storeroom=store, user=owner)
|
||||
store.user.add(owner)
|
||||
|
||||
return store
|
||||
|
||||
|
||||
def make_category(name=None):
|
||||
return ProductCategory.objects.create(category_name=name or _next('دسته'))
|
||||
|
||||
|
||||
def make_product(name=None, *, price=1000, category=None, year=1400):
|
||||
return Product.objects.create(
|
||||
product_name=name or _next('کالا'),
|
||||
product_price=price,
|
||||
ProductCategory=category or make_category(),
|
||||
year=year,
|
||||
)
|
||||
|
||||
|
||||
def stock_in(store, product, quantity, *, year=1400):
|
||||
"""ثبت ورود موجودی اولیه به انبار (از مسیر امن ProductInStore.save)."""
|
||||
return ProductInStore.objects.create(
|
||||
StoreRoom=store,
|
||||
Product=product,
|
||||
s_in=quantity,
|
||||
s_out=0,
|
||||
year=year,
|
||||
desc=f"موجودی اولیه: {quantity}",
|
||||
movement_type=ProductInStore.MovementType.INITIAL,
|
||||
)
|
||||
|
||||
|
||||
def current_stock(store, product, *, year=1400):
|
||||
"""موجودی لحظهای کالا در انبار (s_in - s_out)."""
|
||||
total = 0
|
||||
for row in ProductInStore.objects.filter(StoreRoom=store, Product=product, year=year):
|
||||
total += (row.s_in or 0) - (row.s_out or 0)
|
||||
return total
|
||||
|
||||
|
||||
def make_customer(name=None, *, address='آدرس تست', phones=(), is_active=True):
|
||||
customer = Customer.objects.create(
|
||||
name_family=name or _next('مشتری'),
|
||||
address=address,
|
||||
is_active=is_active,
|
||||
)
|
||||
for i, phone in enumerate(phones):
|
||||
PhoneNumber.objects.create(customer=customer, title=f'تلفن {i + 1}', phone_number=phone)
|
||||
return customer
|
||||
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
تستهای قرارداد مشتری: افزودن، ویرایش و نگهبان حذف (عدم حذف مشتری دارای سابقه).
|
||||
"""
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from admin_panel.models import Article, Customer, PhoneNumber
|
||||
|
||||
from .factories import make_customer, make_user
|
||||
|
||||
|
||||
class CustomerCrudTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.user = make_user('csUser', superuser=True)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_add_customer_creates_phones(self):
|
||||
url = reverse('admin_panel:customersAdd')
|
||||
response = self.client.post(url, {
|
||||
'an_name': 'علی رضایی',
|
||||
'an_address': 'تهران',
|
||||
'installDate': '1400/01/01',
|
||||
'idnCode': '1234567890',
|
||||
'phone_titles': ['موبایل', 'ثابت', ''],
|
||||
'phones': ['09120000000', '02112345678', ''],
|
||||
})
|
||||
|
||||
self.assertIn(response.status_code, (200, 302))
|
||||
customer = Customer.objects.get(name_family='علی رضایی')
|
||||
self.assertEqual(customer.phone1, '09120000000')
|
||||
self.assertEqual(customer.phone2, '02112345678')
|
||||
# ردیف خالی نباید ذخیره شود
|
||||
self.assertEqual(PhoneNumber.objects.filter(customer=customer).count(), 2)
|
||||
|
||||
def test_edit_customer_replaces_phones(self):
|
||||
customer = make_customer('مشتری ویرایش', phones=['09120000001'])
|
||||
url = reverse('admin_panel:customerEdit', kwargs={'c_id': customer.id})
|
||||
|
||||
response = self.client.post(url, {
|
||||
'an_name': 'مشتری ویرایش شده',
|
||||
'an_address': 'شیراز',
|
||||
'installDate': '1401/02/03',
|
||||
'idnCode': '999',
|
||||
'laghv_bazdid': 'true',
|
||||
'phone_titles': ['موبایل جدید'],
|
||||
'phones': ['09120009999'],
|
||||
})
|
||||
self.assertIn(response.status_code, (200, 302))
|
||||
|
||||
customer.refresh_from_db()
|
||||
self.assertEqual(customer.name_family, 'مشتری ویرایش شده')
|
||||
self.assertTrue(customer.laghv_bazdid)
|
||||
phones = list(PhoneNumber.objects.filter(customer=customer))
|
||||
self.assertEqual(len(phones), 1)
|
||||
self.assertEqual(phones[0].phone_number, '09120009999')
|
||||
|
||||
|
||||
class CustomerDeleteGuardTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.user = make_user('delUser', superuser=True)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_customer_without_history_is_deactivated(self):
|
||||
customer = make_customer('مشتری خام')
|
||||
url = reverse('admin_panel:customerDel', kwargs={'c_id': customer.id})
|
||||
|
||||
self.client.get(url)
|
||||
|
||||
customer.refresh_from_db()
|
||||
# حذف نرم: رکورد باقی میماند ولی غیرفعال میشود
|
||||
self.assertFalse(customer.is_active)
|
||||
|
||||
def test_customer_with_financial_history_is_never_deleted(self):
|
||||
customer = make_customer('مشتری دارای سابقه')
|
||||
Article.objects.create(customer=customer, debit=1000, credit=0, desc='سابقه مالی')
|
||||
|
||||
url = reverse('admin_panel:customerDel', kwargs={'c_id': customer.id})
|
||||
self.client.get(url)
|
||||
|
||||
customer.refresh_from_db()
|
||||
self.assertTrue(customer.is_active, msg="مشتری دارای سابقه مالی نباید حذف/غیرفعال شود.")
|
||||
self.assertTrue(Customer.objects.filter(id=customer.id).exists())
|
||||
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
تستهای قرارداد فاکتور: حذف نرم، محافظت از اسناد انبار و جریان ساخت فاکتور از ویو.
|
||||
"""
|
||||
import json
|
||||
|
||||
from django.db.models import ProtectedError
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from admin_panel.models import (Article, Factor, FactorIteam, ProductInStore)
|
||||
|
||||
from .factories import current_stock, make_customer, make_product, make_store, make_user, stock_in
|
||||
|
||||
|
||||
class FactorSoftDeleteTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.customer = make_customer('مشتری فاکتور')
|
||||
self.factor = Factor.objects.create(
|
||||
customer=self.customer, amount=250_000, pay_status='unpaid',
|
||||
FactorType='Nasb', PayType='naghd',
|
||||
)
|
||||
|
||||
def test_delete_is_soft(self):
|
||||
factor_id = self.factor.id
|
||||
self.factor.delete()
|
||||
|
||||
# رکورد باید باقی بماند اما باطل شده باشد
|
||||
still_there = Factor.objects.get(id=factor_id)
|
||||
self.assertFalse(still_there.is_active)
|
||||
self.assertEqual(still_there.pay_status, 'cancell')
|
||||
|
||||
def test_instore_transaction_protects_its_factor(self):
|
||||
store = make_store('انبار فاکتور')
|
||||
product = make_product('کالای فاکتور')
|
||||
ProductInStore.objects.create(
|
||||
StoreRoom=store, Product=product, factor=self.factor,
|
||||
s_in=0, s_out=1, desc='خروج فاکتور',
|
||||
movement_type=ProductInStore.MovementType.SALE,
|
||||
)
|
||||
|
||||
with self.assertRaises(ProtectedError):
|
||||
Factor.objects.filter(id=self.factor.id).delete()
|
||||
|
||||
|
||||
class NewFactor2ViewTests(TestCase):
|
||||
"""
|
||||
تست جریان اصلی ویو `newFactor2` (مسیر استاندارد ساخت فاکتور) که پس از
|
||||
تقسیم views.py به ماژول factors منتقل شده است.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.user = make_user('factorUser', superuser=True)
|
||||
self.store = make_store('انبار فاکتور', owner=self.user)
|
||||
self.customer = make_customer('مشتری فاکتور')
|
||||
self.product = make_product('کالای فروش', price=1000)
|
||||
stock_in(self.store, self.product, 20)
|
||||
|
||||
def test_new_factor2_creates_factor_article_and_stock_out(self):
|
||||
self.client.force_login(self.user)
|
||||
url = reverse('admin_panel:newFactor2', kwargs={'c_id': self.customer.id})
|
||||
|
||||
payload = {
|
||||
'prdsList': json.dumps([
|
||||
{'prd_id': self.product.id, 'prd_count': 3, 'price': 1000,
|
||||
'garanty_Esh': '', 'product_name': self.product.product_name},
|
||||
]),
|
||||
'PardakhtHa': json.dumps({'l_naghd': 3000}),
|
||||
'FactorType': 'Nasb',
|
||||
'FactorDesc': 'تست ساخت فاکتور',
|
||||
'sarResidDate': '',
|
||||
}
|
||||
response = self.client.post(url, payload)
|
||||
|
||||
self.assertIn(response.status_code, (200, 302), msg=f"status={response.status_code}")
|
||||
|
||||
factor = Factor.objects.filter(customer=self.customer).order_by('-id').first()
|
||||
self.assertIsNotNone(factor, msg="فاکتور ساخته نشد.")
|
||||
self.assertEqual(factor.amount, 3000)
|
||||
self.assertEqual(FactorIteam.objects.filter(factor=factor).count(), 1)
|
||||
|
||||
# از انبار به اندازه ۳ عدد کسر شده باشد
|
||||
self.assertEqual(current_stock(self.store, self.product), 17)
|
||||
|
||||
# سند بدهی بابت فاکتور ثبت شده باشد
|
||||
self.assertTrue(Article.objects.filter(factor=factor, debit=3000).exists())
|
||||
|
||||
def test_new_factor2_rejects_oversell_without_creating_factor(self):
|
||||
self.client.force_login(self.user)
|
||||
url = reverse('admin_panel:newFactor2', kwargs={'c_id': self.customer.id})
|
||||
|
||||
payload = {
|
||||
'prdsList': json.dumps([
|
||||
{'prd_id': self.product.id, 'prd_count': 999, 'price': 1000,
|
||||
'garanty_Esh': '', 'product_name': self.product.product_name},
|
||||
]),
|
||||
'PardakhtHa': json.dumps({}),
|
||||
'FactorType': 'Nasb',
|
||||
'FactorDesc': '',
|
||||
'sarResidDate': '',
|
||||
}
|
||||
response = self.client.post(url, payload)
|
||||
|
||||
# ویو نباید خطای سرور بدهد و نباید فاکتور ناقص ثبت شود
|
||||
self.assertNotEqual(response.status_code, 500)
|
||||
self.assertEqual(Factor.objects.filter(customer=self.customer).count(), 0)
|
||||
self.assertEqual(current_stock(self.store, self.product), 20)
|
||||
|
||||
def test_new_factor2_rejects_get(self):
|
||||
self.client.force_login(self.user)
|
||||
url = reverse('admin_panel:newFactor2', kwargs={'c_id': self.customer.id})
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
@@ -0,0 +1,201 @@
|
||||
"""
|
||||
تستهای اطمینان انبار: ریاضی موجودی، تاریخچه گردش کالا و چرخهٔ کامل انتقال بین انبارها.
|
||||
"""
|
||||
from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from django.test import TestCase
|
||||
|
||||
from admin_panel.models import (ProductInStore, ProductStockHistory,
|
||||
StoreTransferRequest)
|
||||
from admin_panel.services import (approve_transfer_request,
|
||||
cancel_transfer_request,
|
||||
create_transfer_request,
|
||||
get_product_current_stock,
|
||||
reject_transfer_request)
|
||||
|
||||
from .factories import (current_stock, make_product, make_store, make_user,
|
||||
stock_in)
|
||||
|
||||
|
||||
class ProductInStoreMathTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.store = make_store('انبار مرکزی')
|
||||
self.product = make_product('کالای تست', price=500)
|
||||
|
||||
def test_initial_in_records_history_and_stock(self):
|
||||
row = stock_in(self.store, self.product, 10)
|
||||
|
||||
row.refresh_from_db()
|
||||
self.assertEqual(row.current_stock_at_time, 10)
|
||||
self.assertEqual(current_stock(self.store, self.product), 10)
|
||||
|
||||
history = ProductStockHistory.objects.filter(store_room=self.store, product=self.product)
|
||||
self.assertEqual(history.count(), 1)
|
||||
self.assertEqual(history.first().quantity_changed, 10)
|
||||
self.assertEqual(history.first().current_stock, 10)
|
||||
|
||||
def test_multiple_movements_accumulate_correctly(self):
|
||||
stock_in(self.store, self.product, 10)
|
||||
ProductInStore.objects.create(
|
||||
StoreRoom=self.store, Product=self.product, s_in=0, s_out=4,
|
||||
desc='خروج تست', movement_type=ProductInStore.MovementType.DIRECT_OUT,
|
||||
)
|
||||
ProductInStore.objects.create(
|
||||
StoreRoom=self.store, Product=self.product, s_in=7, s_out=0,
|
||||
desc='ورود تست', movement_type=ProductInStore.MovementType.DIRECT_IN,
|
||||
)
|
||||
|
||||
self.assertEqual(current_stock(self.store, self.product), 13)
|
||||
self.assertEqual(get_product_current_stock(self.store, self.product), 13)
|
||||
self.assertEqual(
|
||||
ProductStockHistory.objects.filter(store_room=self.store, product=self.product).count(),
|
||||
3,
|
||||
)
|
||||
|
||||
def test_editing_a_row_updates_stock_and_history(self):
|
||||
row = stock_in(self.store, self.product, 10)
|
||||
|
||||
row.s_in = 25
|
||||
row.save()
|
||||
|
||||
row.refresh_from_db()
|
||||
self.assertEqual(row.current_stock_at_time, 25)
|
||||
self.assertEqual(current_stock(self.store, self.product), 25)
|
||||
|
||||
# آخرین رکورد تاریخچه باید تغییر (+15) را نشان دهد
|
||||
last = ProductStockHistory.objects.filter(
|
||||
store_room=self.store, product=self.product
|
||||
).order_by('-id').first()
|
||||
self.assertEqual(last.quantity_changed, 15)
|
||||
self.assertEqual(last.current_stock, 25)
|
||||
|
||||
def test_deleting_a_row_creates_history_and_reduces_stock(self):
|
||||
stock_in(self.store, self.product, 10)
|
||||
out_row = ProductInStore.objects.create(
|
||||
StoreRoom=self.store, Product=self.product, s_in=0, s_out=3,
|
||||
desc='خروج', movement_type=ProductInStore.MovementType.DIRECT_OUT,
|
||||
)
|
||||
self.assertEqual(current_stock(self.store, self.product), 7)
|
||||
|
||||
out_row.delete()
|
||||
|
||||
self.assertEqual(current_stock(self.store, self.product), 10)
|
||||
last = ProductStockHistory.objects.filter(
|
||||
store_room=self.store, product=self.product
|
||||
).order_by('-id').first()
|
||||
self.assertEqual(last.quantity_changed, 3)
|
||||
self.assertEqual(last.current_stock, 10)
|
||||
|
||||
def test_garbage_numeric_values_are_coerced_to_zero(self):
|
||||
row = ProductInStore.objects.create(
|
||||
StoreRoom=self.store, Product=self.product,
|
||||
s_in='abc', s_out=None, desc='مقدار خراب',
|
||||
)
|
||||
row.refresh_from_db()
|
||||
self.assertEqual(row.s_in, 0)
|
||||
self.assertEqual(row.s_out, 0)
|
||||
|
||||
|
||||
class TransferWorkflowTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.boss = make_user('boss', superuser=True)
|
||||
self.store_a = make_store('مبدا', viewers=[self.boss])
|
||||
self.store_b = make_store('مقصد', viewers=[self.boss])
|
||||
self.product = make_product('کالای انتقالی', price=1000)
|
||||
stock_in(self.store_a, self.product, 10)
|
||||
|
||||
def test_create_transfer_request_pending(self):
|
||||
transfer = create_transfer_request(
|
||||
source_store=self.store_a,
|
||||
target_store=self.store_b,
|
||||
product=self.product,
|
||||
quantity=4,
|
||||
requester=self.boss,
|
||||
)
|
||||
self.assertEqual(transfer.status, StoreTransferRequest.Status.PENDING)
|
||||
# تا زمان تایید، موجودی هیچ انباری تغییر نمیکند
|
||||
self.assertEqual(current_stock(self.store_a, self.product), 10)
|
||||
self.assertEqual(current_stock(self.store_b, self.product), 0)
|
||||
|
||||
def test_create_rejects_insufficient_stock(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
create_transfer_request(
|
||||
source_store=self.store_a, target_store=self.store_b,
|
||||
product=self.product, quantity=999, requester=self.boss,
|
||||
)
|
||||
|
||||
def test_create_rejects_same_store(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
create_transfer_request(
|
||||
source_store=self.store_a, target_store=self.store_a,
|
||||
product=self.product, quantity=1, requester=self.boss,
|
||||
)
|
||||
|
||||
def test_create_rejects_non_positive_quantity(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
create_transfer_request(
|
||||
source_store=self.store_a, target_store=self.store_b,
|
||||
product=self.product, quantity=0, requester=self.boss,
|
||||
)
|
||||
|
||||
def test_approve_moves_stock_and_links_transactions(self):
|
||||
transfer = create_transfer_request(
|
||||
source_store=self.store_a, target_store=self.store_b,
|
||||
product=self.product, quantity=4, requester=self.boss,
|
||||
)
|
||||
|
||||
approved = approve_transfer_request(request_id=transfer.id, approver=self.boss)
|
||||
|
||||
self.assertEqual(approved.status, StoreTransferRequest.Status.APPROVED)
|
||||
self.assertIsNotNone(approved.out_transaction_id)
|
||||
self.assertIsNotNone(approved.in_transaction_id)
|
||||
self.assertEqual(current_stock(self.store_a, self.product), 6)
|
||||
self.assertEqual(current_stock(self.store_b, self.product), 4)
|
||||
|
||||
def test_double_approval_is_rejected(self):
|
||||
transfer = create_transfer_request(
|
||||
source_store=self.store_a, target_store=self.store_b,
|
||||
product=self.product, quantity=4, requester=self.boss,
|
||||
)
|
||||
approve_transfer_request(request_id=transfer.id, approver=self.boss)
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
approve_transfer_request(request_id=transfer.id, approver=self.boss)
|
||||
|
||||
def test_reject_keeps_stock_untouched(self):
|
||||
transfer = create_transfer_request(
|
||||
source_store=self.store_a, target_store=self.store_b,
|
||||
product=self.product, quantity=4, requester=self.boss,
|
||||
)
|
||||
rejected = reject_transfer_request(request_id=transfer.id, rejector=self.boss, reason='کالا خراب')
|
||||
|
||||
self.assertEqual(rejected.status, StoreTransferRequest.Status.REJECTED)
|
||||
self.assertEqual(rejected.rejection_reason, 'کالا خراب')
|
||||
self.assertEqual(current_stock(self.store_a, self.product), 10)
|
||||
self.assertEqual(current_stock(self.store_b, self.product), 0)
|
||||
|
||||
def test_cancel_by_requester(self):
|
||||
transfer = create_transfer_request(
|
||||
source_store=self.store_a, target_store=self.store_b,
|
||||
product=self.product, quantity=4, requester=self.boss,
|
||||
)
|
||||
canceled = cancel_transfer_request(request_id=transfer.id, user=self.boss)
|
||||
self.assertEqual(canceled.status, StoreTransferRequest.Status.CANCELED)
|
||||
|
||||
def test_permission_denied_for_outsider(self):
|
||||
outsider = make_user('outsider')
|
||||
with self.assertRaises(PermissionDenied):
|
||||
create_transfer_request(
|
||||
source_store=self.store_a, target_store=self.store_b,
|
||||
product=self.product, quantity=1, requester=outsider,
|
||||
)
|
||||
|
||||
def test_approval_permission_denied_for_non_target_member(self):
|
||||
transfer = create_transfer_request(
|
||||
source_store=self.store_a, target_store=self.store_b,
|
||||
product=self.product, quantity=4, requester=self.boss,
|
||||
)
|
||||
outsider = make_user('outsider2')
|
||||
with self.assertRaises(PermissionDenied):
|
||||
approve_transfer_request(request_id=transfer.id, approver=outsider)
|
||||
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
قرارداد (Contract) پکیج مدلها پس از بازنویسی.
|
||||
|
||||
هدف: تضمین اینکه تقسیم `admin_panel/models.py` به پکیج `admin_panel/models/`
|
||||
هیچ نامی را حذف نکرده، `app_label`ها ثابت ماندهاند و نام جدولهای دیتابیس
|
||||
دقیقاً همان مقادیر پیشین هستند (شرط لازم برای «No changes detected» در مایگریشنها).
|
||||
"""
|
||||
from django.apps import apps
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
import admin_panel.models as models_pkg
|
||||
|
||||
# ۱۸ مدل اصلی فایل قدیمی models.py (پیش از بازنویسی)
|
||||
EXPECTED_MODELS = [
|
||||
'UserProfile',
|
||||
'Customer',
|
||||
'PhoneNumber',
|
||||
'StoreRoom',
|
||||
'StoreRoomOwner',
|
||||
'ProductCategory',
|
||||
'Product',
|
||||
'Factor',
|
||||
'ProductInStore',
|
||||
'ProductStockHistory',
|
||||
'StoreTransferRequest',
|
||||
'FactorIteam',
|
||||
'VisitQueue',
|
||||
'ArticleType',
|
||||
'Article',
|
||||
'Action',
|
||||
'Sarresid',
|
||||
'CacheTable',
|
||||
]
|
||||
|
||||
# نامهایی که کدهای دیگر از طریق star-import به آنها تکیه دارند
|
||||
# (مثلاً context_processor.py از `User` استفاده میکند که از همین نشتی میآید).
|
||||
LEAKED_NAMES = ['models', 'User', 'Sum', 'transaction', 'connection', 'ValidationError']
|
||||
|
||||
|
||||
class ModelsPackageContractTests(SimpleTestCase):
|
||||
|
||||
def test_all_models_importable_from_package(self):
|
||||
for name in EXPECTED_MODELS:
|
||||
self.assertTrue(
|
||||
hasattr(models_pkg, name),
|
||||
msg=f"مدل «{name}» از admin_panel.models قابل import نیست.",
|
||||
)
|
||||
|
||||
def test_models_are_registered_under_admin_panel_app(self):
|
||||
for name in EXPECTED_MODELS:
|
||||
model = getattr(models_pkg, name)
|
||||
self.assertEqual(
|
||||
model._meta.app_label, 'admin_panel',
|
||||
msg=f"app_label مدل {name} تغییر کرده است: {model._meta.app_label}",
|
||||
)
|
||||
|
||||
def test_db_table_names_are_unchanged_defaults(self):
|
||||
for name in EXPECTED_MODELS:
|
||||
model = getattr(models_pkg, name)
|
||||
expected = f"admin_panel_{name.lower()}"
|
||||
self.assertEqual(
|
||||
model._meta.db_table, expected,
|
||||
msg=f"نام جدول مدل {name} تغییر کرده است: {model._meta.db_table} != {expected}",
|
||||
)
|
||||
|
||||
def test_star_import_leak_names_still_present(self):
|
||||
"""
|
||||
رفتار star-import باید دقیقاً مانند فایل تکپارچهی قبلی حفظ شود؛ چراکه
|
||||
context_processor.py و چند ماژول دیگر به نشتی نامهایی مثل `User` و `models`
|
||||
وابستهاند.
|
||||
"""
|
||||
for name in LEAKED_NAMES:
|
||||
self.assertTrue(
|
||||
hasattr(models_pkg, name),
|
||||
msg=f"نام «{name}» که از طریق star-import منتشر میشد، حذف شده است.",
|
||||
)
|
||||
|
||||
def test_models_package_has_no_duplicate_registration(self):
|
||||
registered = [m for m in apps.get_app_config('admin_panel').get_models()]
|
||||
labels = [m.__name__ for m in registered]
|
||||
self.assertEqual(len(labels), len(set(labels)), msg=f"نام تکراری مدل: {labels}")
|
||||
@@ -0,0 +1,169 @@
|
||||
"""
|
||||
قرارداد URLها و ویوها پس از تقسیم `views.py` به پکیج `views/`.
|
||||
|
||||
نگهبان حذف تصادفی مسیر/ویو: مجموعهی نامهای URL با یک snapshot ثابت مقایسه میشود
|
||||
و هر نام باید با ``reverse()`` قابل بازیابی باشد.
|
||||
"""
|
||||
from django.test import SimpleTestCase
|
||||
from django.urls import reverse
|
||||
|
||||
# snapshot قطعی نامهای مسیرهای اپ admin_panel (۶۲ مسیر)
|
||||
EXPECTED_URL_NAMES = {
|
||||
'index',
|
||||
'p_login',
|
||||
'p_logout',
|
||||
'bedehi',
|
||||
'report',
|
||||
'inventory_movements_report',
|
||||
'financial_dashboard',
|
||||
'report_debtors_no_vosol',
|
||||
'rezayat_dashboard',
|
||||
'hesabdari',
|
||||
'hesabdari_residnashone',
|
||||
'hesabdari_check_pass',
|
||||
'hesabdari_check_pass_fail',
|
||||
'bedehiKhadamat',
|
||||
'bedehiNasb',
|
||||
'customers',
|
||||
'customersSearch',
|
||||
'customersAdd',
|
||||
'customer',
|
||||
'api_check_status_inquiry',
|
||||
'api_check_sabt_inquiry',
|
||||
'customer2',
|
||||
'customerNew',
|
||||
'customerAction',
|
||||
'customerNazar',
|
||||
'customerNewAction',
|
||||
'customerEditAction',
|
||||
'customerDel',
|
||||
'customerFactors',
|
||||
'customerFactor',
|
||||
'FactorDel',
|
||||
'newFactor',
|
||||
'newFactor2',
|
||||
'customerHesabdari',
|
||||
'customerSarresidh',
|
||||
'customerHesabdariAdd',
|
||||
'stores',
|
||||
'productsJson',
|
||||
'ActionJson',
|
||||
'CustomerJson',
|
||||
'productMojodi',
|
||||
'store',
|
||||
'operations',
|
||||
'operationProduct',
|
||||
'newOperation',
|
||||
'newStore',
|
||||
'store_transfers',
|
||||
'process_transfer_action',
|
||||
'productAdd',
|
||||
'productEdit',
|
||||
'operation',
|
||||
'productDel',
|
||||
'customerEdit',
|
||||
'rezayat',
|
||||
'rezayatEdit',
|
||||
'entezar',
|
||||
'ActionList',
|
||||
'ActionListToday',
|
||||
'ActionListUserToday',
|
||||
'ActionListAll',
|
||||
'passCheck',
|
||||
'update_check_info',
|
||||
'addSarresid',
|
||||
'pardakhtNesiye',
|
||||
}
|
||||
|
||||
# مسیرهایی که آرگومان میگیرند: نام -> kwargs نمونه
|
||||
PARAMETERIZED = {
|
||||
'customersSearch': {'searchVal': 'ali', 'searchOn': 'name'},
|
||||
'customer': {'c_id': 1},
|
||||
'customer2': {'c_id': 1},
|
||||
'customerNew': {'c_id': 1},
|
||||
'customerAction': {'c_id': 1},
|
||||
'customerNazar': {'c_id': 1},
|
||||
'customerNewAction': {'c_id': 1},
|
||||
'customerEditAction': {'c_id': 1, 'action_id': 1},
|
||||
'customerDel': {'c_id': 1},
|
||||
'customerFactors': {'c_id': 1},
|
||||
'customerFactor': {'c_id': 1, 'f_id': 1},
|
||||
'FactorDel': {'c_id': 1, 'f_id': 1},
|
||||
'newFactor': {'c_id': 1},
|
||||
'newFactor2': {'c_id': 1},
|
||||
'customerHesabdari': {'c_id': 1},
|
||||
'customerSarresidh': {'c_id': 1},
|
||||
'customerHesabdariAdd': {'c_id': 1},
|
||||
'ActionJson': {'action_id': 1},
|
||||
'CustomerJson': {'c_id': 1},
|
||||
'productMojodi': {'p_id': 1},
|
||||
'store': {'s_id': 1},
|
||||
'operations': {'s_id': 1},
|
||||
'operationProduct': {'s_id': 1, 'p_id': 1},
|
||||
'newOperation': {'s_id': 1, 'p_id': 1},
|
||||
'store_transfers': {'s_id': 1},
|
||||
'process_transfer_action': {'transfer_id': 1},
|
||||
'productDel': {'p_id': 1, 's_id': 1},
|
||||
'customerEdit': {'c_id': 1},
|
||||
'rezayatEdit': {'ac_id': 1},
|
||||
'ActionListUserToday': {'u_id': 1},
|
||||
'passCheck': {'articl_id': 1},
|
||||
'update_check_info': {'sarresid_id': 1},
|
||||
'addSarresid': {'cs_id': 1},
|
||||
'pardakhtNesiye': {'cs_id': 1},
|
||||
}
|
||||
|
||||
|
||||
class UrlContractTests(SimpleTestCase):
|
||||
|
||||
def _all_names(self):
|
||||
from admin_panel.urls import urlpatterns
|
||||
return {p.name for p in urlpatterns if p.name}
|
||||
|
||||
def test_url_name_snapshot_unchanged(self):
|
||||
names = self._all_names()
|
||||
missing = EXPECTED_URL_NAMES - names
|
||||
added = names - EXPECTED_URL_NAMES
|
||||
self.assertEqual(
|
||||
missing, set(),
|
||||
msg=f"این نامهای URL پس از refactor گم شدهاند: {sorted(missing)}",
|
||||
)
|
||||
self.assertEqual(
|
||||
added, set(),
|
||||
msg=f"نامهای URL جدید و پیشبینینشده اضافه شدهاند: {sorted(added)}",
|
||||
)
|
||||
|
||||
def test_every_url_reverses(self):
|
||||
for name in sorted(EXPECTED_URL_NAMES):
|
||||
kwargs = PARAMETERIZED.get(name, {})
|
||||
with self.subTest(url=name):
|
||||
try:
|
||||
reverse(f'admin_panel:{name}', kwargs=kwargs)
|
||||
except Exception as exc: # noqa: BLE001 - هدف: گزارش دقیق نام مشکلدار
|
||||
self.fail(f"reverse('admin_panel:{name}') شکست خورد: {exc}")
|
||||
|
||||
def test_all_view_callables_are_importable(self):
|
||||
"""هر `views.X` در urls.py باید در پکیج views موجود و قابل فراخوانی باشد."""
|
||||
from admin_panel import views
|
||||
from admin_panel.urls import urlpatterns
|
||||
|
||||
for pattern in urlpatterns:
|
||||
if pattern.name not in EXPECTED_URL_NAMES:
|
||||
# مسیرهای سرو استاتیک (از static()) ویوی اپ نیستند
|
||||
continue
|
||||
callback = getattr(pattern, 'callback', None)
|
||||
if callback is None:
|
||||
continue
|
||||
self.assertTrue(
|
||||
callable(callback),
|
||||
msg=f"ویو مسیر «{pattern.name}» قابل فراخوانی نیست.",
|
||||
)
|
||||
self.assertTrue(
|
||||
callback.__module__.startswith('admin_panel.views'),
|
||||
msg=(
|
||||
f"ویو مسیر «{pattern.name}» بجای پکیج admin_panel.views از "
|
||||
f"ماژول {callback.__module__} میآید."
|
||||
),
|
||||
)
|
||||
# اطمینان از اینکه از دسترسپذیری نامها استفاده میکنیم
|
||||
self.assertTrue(hasattr(views, 'p_login'))
|
||||
@@ -0,0 +1,229 @@
|
||||
"""
|
||||
تست دودی (Smoke Test) تمام اندپوینتهای اپ admin_panel.
|
||||
|
||||
چرا این تست حیاتی است؟
|
||||
`python manage.py check` خطاهای `NameError`/`ImportError` را که داخل *بدنهٔ توابع*
|
||||
ویو رخ میدهند نمیبیند. پس از تقسیم `views.py` به پکیج `views/`، تنها راه مطمئن
|
||||
برای کشف importهای جاافتاده، فراخوانی واقعی هر ویو است. این ماژول دقیقاً همان کار
|
||||
را انجام میدهد: هر مسیر urls.py با یک کاربر مجاز (superuser) فراخوانی میشود و
|
||||
هیچ پاسخی نباید کد ۵۰۰ داشته باشد.
|
||||
"""
|
||||
import json
|
||||
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from admin_panel.models import (Action, Article, Factor, Sarresid,
|
||||
StoreTransferRequest)
|
||||
|
||||
from .factories import (make_category, make_customer, make_product, make_store,
|
||||
make_user, stock_in)
|
||||
|
||||
# کدهای وضعیتی که «نتیجهٔ قابلقبول» تلقی میشوند (نشانهٔ اجرای سالم ویو)
|
||||
ACCEPTABLE = {200, 201, 302, 400, 403, 404, 405}
|
||||
|
||||
GET = 'get'
|
||||
POST = 'post'
|
||||
|
||||
|
||||
class ViewSmokeTests(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.user = make_user('smokeAdmin', superuser=True)
|
||||
cls.other_user = make_user('smokeOther')
|
||||
# انبار اصلی که کاربر مالک آن است (owners) و دسترسی مشاهده دارد
|
||||
cls.store = make_store('انبار اصلی', owner=cls.user)
|
||||
# نکته: کاربر عمداً به انبار دوم اضافه نمیشود؛ چون چند ویو از
|
||||
# `StoreRoom.objects.get(user__id=...)` استفاده میکنند که با دو انبار
|
||||
# خطای MultipleObjectsReturned میدهد (رفتار پیشین کد، نه ناشی از refactor).
|
||||
cls.store2 = make_store('انبار دوم')
|
||||
|
||||
cls.category = make_category('دسته اصلی')
|
||||
cls.product = make_product('کالای اصلی', price=2000, category=cls.category)
|
||||
cls.spare_product = make_product('کالای حذفی', price=1500, category=cls.category)
|
||||
stock_in(cls.store, cls.product, 100)
|
||||
stock_in(cls.store, cls.spare_product, 5)
|
||||
|
||||
cls.customer = make_customer('مشتری اصلی')
|
||||
cls.spare_customer = make_customer('مشتری خام')
|
||||
# یک سند مالی تا ویو «بدهی» مسیر موفق را طی کند (با دیتابیس کاملاً خالی،
|
||||
# این ویو به دلیل باگ ازپیشموجود UnboundLocalError خطای ۵۰۰ میدهد).
|
||||
Article.objects.create(customer=cls.customer, debit=1000, credit=0, desc='سند آغازین دودی')
|
||||
cls.factor = Factor.objects.create(customer=cls.customer, amount=1000, pay_status='unpaid')
|
||||
cls.spare_factor = Factor.objects.create(customer=cls.customer, amount=1000, pay_status='unpaid')
|
||||
|
||||
cls.action = Action.objects.create(
|
||||
customer=cls.customer, forUser=cls.user, actionType='bazdid', status='waite',
|
||||
actionDateTime=timezone.now(),
|
||||
)
|
||||
cls.sarresid = Sarresid.objects.create(
|
||||
customer=cls.customer, price=5000, payStatus='false', type='check',
|
||||
sarresidDT=timezone.now(),
|
||||
)
|
||||
cls.transfer = StoreTransferRequest.objects.create(
|
||||
source_store=cls.store, target_store=cls.store2,
|
||||
product=cls.product, quantity=1, requester=cls.user,
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
self.client.force_login(self.user)
|
||||
# در صورت خطای سرور، استثنا را نگه میداریم تا کد وضعیت ۵۰۰ را ببینیم
|
||||
self.client.raise_request_exception = False
|
||||
|
||||
# ------------------------------------------------------------------ helpers
|
||||
|
||||
def _now_ms(self):
|
||||
return int(timezone.now().timestamp() * 1000)
|
||||
|
||||
def _url(self, name, **kwargs):
|
||||
return reverse(f'admin_panel:{name}', kwargs=kwargs)
|
||||
|
||||
def _endpoints(self):
|
||||
"""(نام مسیر, kwargs, متد, دادهٔ ارسالی)"""
|
||||
ms = self._now_ms()
|
||||
c = self.customer
|
||||
return [
|
||||
# --- داشبوردها و گزارشها ---
|
||||
('index', {}, GET, None),
|
||||
('p_login', {}, GET, None),
|
||||
('bedehi', {}, GET, None),
|
||||
('report', {}, GET, None),
|
||||
('inventory_movements_report', {}, GET, None),
|
||||
('financial_dashboard', {}, GET, None),
|
||||
('report_debtors_no_vosol', {}, GET, None),
|
||||
('rezayat_dashboard', {}, GET, None),
|
||||
('hesabdari', {}, GET, None),
|
||||
('hesabdari_residnashone', {}, GET, None),
|
||||
('hesabdari_check_pass', {}, GET, None),
|
||||
('hesabdari_check_pass_fail', {}, GET, None),
|
||||
('bedehiKhadamat', {}, GET, None),
|
||||
('bedehiNasb', {}, GET, None),
|
||||
# --- مشتریان ---
|
||||
('customers', {}, GET, None),
|
||||
# مسیر از converter نوع <slug> استفاده میکند، پس فقط کاراکترهای مجاز slug
|
||||
('customersSearch', {'searchVal': 'ali', 'searchOn': 'name'}, GET, None),
|
||||
('customersAdd', {}, POST, {
|
||||
'an_name': 'مشتری دودی', 'an_address': 'تهران',
|
||||
'installDate': '1400/01/01', 'idnCode': '111',
|
||||
'phone_titles': ['موبایل'], 'phones': ['09120000000'],
|
||||
}),
|
||||
('customer', {'c_id': c.id}, GET, None),
|
||||
('customer2', {'c_id': c.id}, GET, None),
|
||||
('customerNew', {'c_id': c.id}, GET, None),
|
||||
('customerAction', {'c_id': c.id}, GET, None),
|
||||
('customerNazar', {'c_id': c.id}, GET, None),
|
||||
('customerNewAction', {'c_id': c.id}, POST, {
|
||||
'actionType': 'bazdid', 'dateTimeAnjam': ms,
|
||||
'userID': self.user.id, 'desc': 'اقدام دودی',
|
||||
}),
|
||||
('customerEditAction', {'c_id': c.id, 'action_id': self.action.id}, POST, {
|
||||
'actionType': 'bazdid', 'dateTimeAnjam': ms,
|
||||
'userID': self.user.id, 'desc': 'ویرایش دودی',
|
||||
}),
|
||||
('customerDel', {'c_id': self.spare_customer.id}, GET, None),
|
||||
('customerFactors', {'c_id': c.id}, GET, None),
|
||||
('customerFactor', {'c_id': c.id, 'f_id': self.factor.id}, GET, None),
|
||||
('FactorDel', {'c_id': c.id, 'f_id': self.spare_factor.id}, POST, {
|
||||
'cancel_reason': 'error', 'payment_action': 'keep',
|
||||
}),
|
||||
('newFactor', {'c_id': c.id}, POST, {
|
||||
'prdsList': json.dumps([]), 'payType': 'naghd',
|
||||
'FactorType': 'Nasb', 'FactorDesc': 'دودی', 'sarResidDate': '',
|
||||
}),
|
||||
('newFactor2', {'c_id': c.id}, POST, {
|
||||
'prdsList': json.dumps([]), 'PardakhtHa': json.dumps({}),
|
||||
'FactorType': 'Nasb', 'FactorDesc': 'دودی', 'sarResidDate': '',
|
||||
}),
|
||||
('customerHesabdari', {'c_id': c.id}, GET, None),
|
||||
('customerSarresidh', {'c_id': c.id}, GET, None),
|
||||
('customerHesabdariAdd', {'c_id': c.id}, POST, {
|
||||
'price': '1000', 'type': 'debit', 'typePy': 'nasb',
|
||||
'desc': 'سند دودی', 'nahvePardakhat': 'نقد',
|
||||
}),
|
||||
# --- JSON ---
|
||||
('productsJson', {}, GET, None),
|
||||
('ActionJson', {'action_id': self.action.id}, GET, None),
|
||||
('CustomerJson', {'c_id': c.id}, GET, None),
|
||||
('productMojodi', {'p_id': self.product.id}, GET, None),
|
||||
# --- انبار ---
|
||||
('stores', {}, GET, None),
|
||||
('store', {'s_id': self.store.id}, GET, None),
|
||||
('operations', {'s_id': self.store.id}, GET, None),
|
||||
('operationProduct', {'s_id': self.store.id, 'p_id': self.product.id}, GET, None),
|
||||
('newOperation', {'s_id': self.store.id, 'p_id': self.product.id}, GET, None),
|
||||
('newStore', {}, POST, {'storeName': 'انبار دودی'}),
|
||||
('store_transfers', {'s_id': self.store.id}, GET, None),
|
||||
('process_transfer_action', {'transfer_id': self.transfer.id}, POST, {'action': 'invalid'}),
|
||||
('productAdd', {}, POST, {
|
||||
'namePrd': 'کالای دودی', 'category': self.category.id,
|
||||
'price': '1000', 'mojodiAval': '5', 'storeroom': self.store.id,
|
||||
}),
|
||||
('productEdit', {}, POST, {
|
||||
'prd_code': self.product.id, 'namePrd': 'کالای ویرایششده',
|
||||
'category': self.category.id, 'price': '2500',
|
||||
}),
|
||||
('operation', {}, POST, {
|
||||
'prd_id': self.product.id, 'actionType': 'vorod',
|
||||
'tedad': '3', 'azanbar': self.store.id, 'desc': 'ورود دودی',
|
||||
}),
|
||||
('productDel', {'p_id': self.spare_product.id, 's_id': self.store.id}, GET, None),
|
||||
('customerEdit', {'c_id': c.id}, POST, {
|
||||
'an_name': 'مشتری ویرایشدودی', 'an_address': 'کرج',
|
||||
'installDate': '1400/02/02', 'idnCode': '222',
|
||||
'phone_titles': ['موبایل'], 'phones': ['09120000001'],
|
||||
}),
|
||||
# --- اقدامات و نظرسنجی ---
|
||||
('rezayat', {}, GET, None),
|
||||
('rezayatEdit', {'ac_id': self.action.id}, POST, {'csGrade': '20', 'csDesc': 'عالی'}),
|
||||
('entezar', {}, GET, None),
|
||||
('ActionList', {}, GET, None),
|
||||
('ActionListToday', {}, GET, None),
|
||||
('ActionListUserToday', {'u_id': self.user.id}, GET, None),
|
||||
('ActionListAll', {}, GET, None),
|
||||
# --- چک و سررسید ---
|
||||
('passCheck', {'articl_id': self.sarresid.id}, POST, {'message': 'وصول شد'}),
|
||||
('update_check_info', {'sarresid_id': self.sarresid.id}, POST, {
|
||||
'check_row_number': '1', 'check_sayyad_id': '1234567890123456',
|
||||
'check_owner_name': 'مالک', 'check_owner_phone': '09120000000',
|
||||
}),
|
||||
('addSarresid', {'cs_id': c.id}, POST, {
|
||||
'n_sarresidDateTime': ms, 'n_sarresidPrice': '1000',
|
||||
'n_sarresidType': 'nesiye',
|
||||
}),
|
||||
('pardakhtNesiye', {'cs_id': c.id}, POST, {
|
||||
'next_sarresidDateTime': ms, 'next_sarresidPrice': '100',
|
||||
'sr_articlID': self.sarresid.id, 'nahvePardakht': 'naghd',
|
||||
}),
|
||||
('api_check_status_inquiry', {}, GET, None),
|
||||
('api_check_sabt_inquiry', {}, GET, None),
|
||||
# در انتهای لیست: خروج از حساب، چون بعد از آن تمام درخواستها بیاعتبار میشوند
|
||||
('p_logout', {}, GET, None),
|
||||
]
|
||||
|
||||
# ------------------------------------------------------------------ tests
|
||||
|
||||
def test_every_endpoint_responds_without_server_error(self):
|
||||
failures = []
|
||||
for name, kwargs, method, data in self._endpoints():
|
||||
url = self._url(name, **kwargs)
|
||||
with self.subTest(endpoint=name):
|
||||
# HTTP_REFERER لازم است چون چند ویو با redirect(request.META['HTTP_REFERER'])
|
||||
# بازمیگردند و مرورگر واقعی همیشه آن را میفرستد.
|
||||
headers = {'HTTP_REFERER': '/panel/'}
|
||||
if method == GET:
|
||||
response = self.client.get(url, **headers)
|
||||
else:
|
||||
response = self.client.post(url, data or {}, **headers)
|
||||
|
||||
if response.status_code not in ACCEPTABLE:
|
||||
failures.append((name, response.status_code, url))
|
||||
|
||||
self.assertEqual(
|
||||
failures, [],
|
||||
msg=(
|
||||
"این اندپوینتها پاسخ غیرمنتظره (احتمالاً خطای ۵۰۰ ناشی از import جاافتاده) "
|
||||
f"دادهاند: {failures}"
|
||||
),
|
||||
)
|
||||
Reference in new issue
Block a user