This commit is contained in:
javad committed 2026-10-09 20:02:36 +03:30
1 parent 6f36db63a1
commit 250cdbab7f
42 files changed
+96363 -11699

No files matched your search

+2
View File
@@ -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/
+2
View File
@@ -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
View File
@@ -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
-515
View File
@@ -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}'
+19
View File
@@ -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
+38
View File
@@ -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)
+42
View File
@@ -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}"
+58
View File
@@ -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)
+71
View File
@@ -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="تلفن مالک چک"
)
+148
View File
@@ -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
)
+148
View File
@@ -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("تعداد انتقال باید بیشتر از صفر باشد.")
+9
View File
@@ -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}'
+18
View File
@@ -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})"
-2896
View File
File diff suppressed because it is too large. Load diff
+23
View File
@@ -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
+49
View File
@@ -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
+263
View File
@@ -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('لطفا همه فیلد ها را پر کنید')
+29
View File
@@ -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/")
+273
View File
@@ -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": "خطای سرور در برقراری ارتباط با سامانه بانک."})
+463
View File
@@ -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')
+465
View File
@@ -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)
+974
View File
@@ -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)
+458
View File
@@ -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))
+95 -1
View File
@@ -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
View File
@@ -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
View File
@@ -1 +1 @@
D:\Projects\khatibi
.
+200 -123
View File
@@ -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
+5 -5
View File
@@ -63,12 +63,12 @@
</div>
<div id="legend"></div>
</div>
<div id="stats">548 nodes &middot; 887 edges &middot; 80 communities</div>
<div id="stats">821 nodes &middot; 1291 edges &middot; 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&#x27;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
View File
File diff suppressed because it is too large. Load diff
+1 -890
View File
@@ -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
View File
Whitespace-only changes.
+107
View File
@@ -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
+84
View File
@@ -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())
+113
View File
@@ -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)
+201
View File
@@ -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)
+81
View File
@@ -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}")
+169
View File
@@ -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'))
+229
View File
@@ -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}"
),
)