refactor
This commit is contained in:
1 parent
6f36db63a1
commit
250cdbab7f
42 files changed
+96363
-11699
No files matched your search
@@ -1,515 +0,0 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import Sum
|
||||
# from django.db.models.signals import post_save, post_delete
|
||||
from django.dispatch import receiver
|
||||
from django.db import transaction, connection # اصلاح فیکس خطای تراکنش
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.validators import RegexValidator
|
||||
|
||||
userModel = get_user_model()
|
||||
|
||||
|
||||
# اصلاح مدل کاربر: تبدیل به پروفایل برای حفظ امنیت و اتصال به سیستم پیشفرض جنگو
|
||||
class UserProfile(models.Model):
|
||||
UserTypeList = [
|
||||
('admin', 'admin'),
|
||||
('office', 'office'),
|
||||
('operator', 'operator'),
|
||||
]
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
|
||||
user_type = models.CharField(max_length=10, choices=UserTypeList)
|
||||
name_family = models.CharField(max_length=200)
|
||||
create_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username} ({self.user_type})"
|
||||
|
||||
|
||||
class Customer(models.Model):
|
||||
class Meta:
|
||||
permissions = [
|
||||
("AddCustomer", "AddCustomer"),
|
||||
("DelCustomer", "DelCustomer"),
|
||||
("EditCustomer", "EditCustomer"),
|
||||
("ViewCustomer", "ViewCustomer")
|
||||
]
|
||||
name_family = models.CharField(max_length=200)
|
||||
address = models.CharField(max_length=200)
|
||||
phone1 = models.CharField(max_length=20, default='')
|
||||
phone2 = models.CharField(max_length=20, default='', null=True, blank=True)
|
||||
phone3 = models.CharField(max_length=20, default='', null=True, blank=True)
|
||||
idCode = models.CharField(max_length=20, default='', null=True, blank=True)
|
||||
installDate = models.DateField(auto_now_add=True)
|
||||
create_date = models.DateTimeField(auto_now_add=True)
|
||||
last_bazdid_dt = models.DateTimeField(null=True, blank=True)
|
||||
laghv_bazdid = models.BooleanField(null=True, blank=True, default=False)
|
||||
|
||||
# فیلد جدید برای حذف نرم (بدون تداخل با دادههای قبلی)
|
||||
is_active = models.BooleanField(default=True, null=True, blank=True, help_text="وضعیت فعالیت مشتری")
|
||||
|
||||
def __str__(self):
|
||||
return self.name_family
|
||||
|
||||
# بازنویسی متد حذف: جلوگیری از فاجعه در دیتابیس
|
||||
def delete(self, *args, **kwargs):
|
||||
# به جای حذف فیزیکی که انبار و فاکتورها را نابود میکند، مشتری را غیرفعال میکنیم
|
||||
self.is_active = False
|
||||
self.save()
|
||||
|
||||
class PhoneNumber(models.Model):
|
||||
customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name='phones')
|
||||
title = models.CharField(max_length=50, default='تلفن', blank=True, null=True, help_text="عنوان شماره (مثل: موبایل پدر)")
|
||||
phone_number = models.CharField(max_length=20)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.title}: {self.phone_number}"
|
||||
|
||||
class StoreRoom(models.Model):
|
||||
class Meta:
|
||||
permissions = [
|
||||
("ViewStore", "ViewStore"),
|
||||
("AddStore", "AddStore"),
|
||||
("EditStore", "EditStore"),
|
||||
("DelStore", "DelStore")
|
||||
]
|
||||
store_name = models.CharField(max_length=200)
|
||||
user = models.ManyToManyField(User,
|
||||
blank=True,
|
||||
verbose_name="دسترسی مشاهده انبار احتمالا",
|
||||
help_text="از این برای دسترسی مشاهده آن اتبار استفاده می شود"
|
||||
)
|
||||
owners = models.ManyToManyField(
|
||||
User,
|
||||
through='StoreRoomOwner', # کلید حل مشکل بدون شکستن کدهای قبلی
|
||||
blank=True,
|
||||
related_name="owned_storerooms",
|
||||
verbose_name="مالکین انبار در فاکتور و موجودی و...",
|
||||
help_text="این کاربران اگر فاکتور بزنند از این انبار کسر میگردد"
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.store_name
|
||||
|
||||
|
||||
class StoreRoomOwner(models.Model):
|
||||
"""
|
||||
این جدول واسط، پشت صحنه قرار میگیرد تا فقط قانون مالکیت را مدیریت کند.
|
||||
کدهای دیگر پروژه اصلاً نیازی ندارند مستقیم با این جدول درگیر شوند.
|
||||
"""
|
||||
storeroom = models.ForeignKey(StoreRoom, on_delete=models.CASCADE)
|
||||
|
||||
# OneToOneField تضمین میکند یک کاربر نمیتواند در این جدول (و هیچ انبار دیگری) تکرار شود
|
||||
user = models.OneToOneField(
|
||||
User,
|
||||
on_delete=models.CASCADE,
|
||||
error_messages={
|
||||
'unique': 'این کاربر در حال حاضر مالک انبار دیگری است.'
|
||||
}
|
||||
)
|
||||
|
||||
class ProductCategory(models.Model):
|
||||
category_name = models.CharField(max_length=50)
|
||||
|
||||
def __str__(self):
|
||||
return self.category_name
|
||||
|
||||
|
||||
class Product(models.Model):
|
||||
class Meta:
|
||||
permissions = [
|
||||
("ViewProduct", "ViewProduct"),
|
||||
]
|
||||
ProductCategory = models.ForeignKey(ProductCategory, on_delete=models.CASCADE)
|
||||
product_name = models.CharField(max_length=50)
|
||||
product_price = models.IntegerField()
|
||||
year = models.IntegerField(default=1400)
|
||||
createdDT = models.DateTimeField(auto_now_add=True)
|
||||
changeDT = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.product_name
|
||||
|
||||
|
||||
class Factor(models.Model):
|
||||
class Meta:
|
||||
permissions = [
|
||||
("ViewFactor", "ViewFactor"),
|
||||
("DelFactor", "DelFactor"),
|
||||
("EditFactor", "EditFactor"),
|
||||
("AddFactor", "AddFactor")
|
||||
]
|
||||
customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
|
||||
create_date = models.DateTimeField(auto_now_add=True)
|
||||
amount = models.IntegerField()
|
||||
discount = models.IntegerField(null=True, blank=True)
|
||||
deadline = models.DateTimeField(null=True, blank=True)
|
||||
desc = models.CharField(max_length=100, default='')
|
||||
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
statusList = [
|
||||
('unpaid', 'unpaid'),
|
||||
('paid', 'paid'),
|
||||
('cancell', 'cancell'),
|
||||
]
|
||||
pay_status = models.CharField(max_length=10, choices=statusList, default='')
|
||||
FactorTypeList = [
|
||||
('Nasb', 'Nasb'),
|
||||
('Khadamat', 'Khadamat'),
|
||||
('Garranty', 'Garranty'),
|
||||
]
|
||||
FactorType = models.CharField(max_length=10, choices=FactorTypeList, default='')
|
||||
PayTypeList = [
|
||||
('naghd', 'naghd'),
|
||||
('cardToCard', 'cardToCard'),
|
||||
('kartKhan', 'kartKhan'),
|
||||
('nesiye', 'nesiye'),
|
||||
('check', 'check'),
|
||||
]
|
||||
PayType = models.CharField(max_length=30, choices=PayTypeList, default='')
|
||||
|
||||
# فیلد جدید برای حذف نرم
|
||||
is_active = models.BooleanField(default=True, null=True, blank=True)
|
||||
|
||||
# بازنویسی متد حذف: ابطال فاکتور به جای حذف فیزیکی
|
||||
def delete(self, *args, **kwargs):
|
||||
# فاکتور باطل میشود اما رکوردهای انبار دستنخورده باقی میمانند!
|
||||
self.is_active = False
|
||||
self.pay_status = 'cancell'
|
||||
self.save()
|
||||
|
||||
class ProductInStore(models.Model):
|
||||
class Meta:
|
||||
permissions = [
|
||||
("ViewProductInStore", "ViewProductInStore"),
|
||||
("EditProductInStore", "EditProductInStore"),
|
||||
("OnlyAnbar", "OnlyAnbar"),
|
||||
]
|
||||
class MovementType(models.TextChoices):
|
||||
SALE = 'SALE', 'فروش عادی'
|
||||
WARRANTY = 'WARRANTY', 'گارانتی'
|
||||
PROMOTION = 'PROMOTION', 'اشانتیون'
|
||||
TRANSFER = 'TRANSFER', 'انتقال'
|
||||
INITIAL = 'INITIAL', 'موجودی اولیه'
|
||||
DIRECT_IN = 'DIRECT_IN', 'ورود دستی'
|
||||
DIRECT_OUT = 'DIRECT_OUT', 'خروج دستی'
|
||||
RETURN = 'RETURN', 'برگشت از فروش'
|
||||
ERROR_CORRECTION = 'ERROR_CORRECTION', 'اصلاح خطای ثبت'
|
||||
|
||||
StoreRoom = models.ForeignKey(StoreRoom, on_delete=models.CASCADE)
|
||||
Product = models.ForeignKey(Product, on_delete=models.CASCADE)
|
||||
s_in = models.IntegerField(default=0)
|
||||
s_out = models.IntegerField(default=0)
|
||||
current_stock_at_time = models.IntegerField(default=0, help_text="موجودی انبار در لحظه این تراکنش")
|
||||
desc = models.CharField(max_length=50, default='')
|
||||
year = models.IntegerField(default=1400)
|
||||
createdDT = models.DateTimeField(auto_now_add=True)
|
||||
changeDT = models.DateTimeField(auto_now=True)
|
||||
|
||||
# تغییر بسیار مهم استراتژیک: تبدیل CASCADE به PROTECT
|
||||
# هیچکس نمیتواند فاکتوری که کالا از انبار خارج کرده را از دیتابیس پاک کند!
|
||||
factor = models.ForeignKey(Factor, on_delete=models.PROTECT, null=True, blank=True)
|
||||
|
||||
movement_type = models.CharField(
|
||||
max_length=20,
|
||||
choices=MovementType.choices,
|
||||
db_index=True,
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.Product.product_name} - In: {self.s_in} Out: {self.s_out}"
|
||||
|
||||
# متدهای save و delete دقیقاً همان کدهای بینقص و پرسرعتی هستند که در پیام قبل تایید کردیم
|
||||
def save(self, *args, **kwargs):
|
||||
try:
|
||||
self.s_in = int(self.s_in or 0)
|
||||
self.s_out = int(self.s_out or 0)
|
||||
except (ValueError, TypeError):
|
||||
self.s_in, self.s_out = 0, 0
|
||||
|
||||
is_created = self.pk is None
|
||||
|
||||
with transaction.atomic():
|
||||
qs_lock = ProductInStore.objects.filter(StoreRoom=self.StoreRoom, Product=self.Product)
|
||||
if connection.features.has_select_for_update:
|
||||
qs_lock = qs_lock.select_for_update()
|
||||
list(qs_lock)
|
||||
|
||||
if not is_created:
|
||||
old_instance = ProductInStore.objects.get(pk=self.pk)
|
||||
old_balance = old_instance.s_in - old_instance.s_out
|
||||
new_balance = self.s_in - self.s_out
|
||||
change_amount = new_balance - old_balance
|
||||
description = self.desc or f"ویرایش تراکنش: {self.desc}"
|
||||
else:
|
||||
change_amount = self.s_in - self.s_out
|
||||
description = self.desc or "ثبت تراکنش جدید انبار"
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
totals = ProductInStore.objects.filter(StoreRoom=self.StoreRoom, Product=self.Product).aggregate(
|
||||
total_in=Sum('s_in'),
|
||||
total_out=Sum('s_out')
|
||||
)
|
||||
total_in = totals['total_in'] or 0
|
||||
total_out = totals['total_out'] or 0
|
||||
current_stock = total_in - total_out
|
||||
|
||||
ProductInStore.objects.filter(pk=self.pk).update(current_stock_at_time=current_stock)
|
||||
self.current_stock_at_time = current_stock
|
||||
|
||||
ProductStockHistory.objects.create(
|
||||
store_room=self.StoreRoom,
|
||||
product=self.Product,
|
||||
quantity_changed=change_amount,
|
||||
current_stock=current_stock,
|
||||
desc=description
|
||||
)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
store_room = self.StoreRoom
|
||||
product = self.Product
|
||||
change_amount = -(int(self.s_in or 0) - int(self.s_out or 0))
|
||||
description = f"حذف دستی تراکنش انبار: {self.desc}"
|
||||
|
||||
with transaction.atomic():
|
||||
qs_lock = ProductInStore.objects.filter(StoreRoom=store_room, Product=product)
|
||||
if connection.features.has_select_for_update:
|
||||
qs_lock = qs_lock.select_for_update()
|
||||
list(qs_lock)
|
||||
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
totals = ProductInStore.objects.filter(StoreRoom=store_room, Product=product).aggregate(
|
||||
total_in=Sum('s_in'),
|
||||
total_out=Sum('s_out')
|
||||
)
|
||||
current_stock = (totals['total_in'] or 0) - (totals['total_out'] or 0)
|
||||
|
||||
ProductStockHistory.objects.create(
|
||||
store_room=store_room,
|
||||
product=product,
|
||||
quantity_changed=change_amount,
|
||||
current_stock=current_stock,
|
||||
desc=description
|
||||
)
|
||||
|
||||
|
||||
class ProductStockHistory(models.Model):
|
||||
store_room = models.ForeignKey(StoreRoom, on_delete=models.CASCADE, related_name='stock_histories')
|
||||
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='stock_histories')
|
||||
quantity_changed = models.IntegerField(help_text="مقدار تغییر یافته (مثبت برای ورود، منفی برای خروج)")
|
||||
current_stock = models.IntegerField(help_text="موجودی کل انبار در این لحظه")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
desc = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.product.product_name} in {self.store_room.store_name} - Stock: {self.current_stock}"
|
||||
|
||||
|
||||
class StoreTransferRequest(models.Model):
|
||||
class Status(models.TextChoices):
|
||||
PENDING = 'PENDING', 'در انتظار تایید انبار مقصد'
|
||||
APPROVED = 'APPROVED', 'تایید شده و منتقل شده'
|
||||
REJECTED = 'REJECTED', 'رد شده توسط انبار مقصد'
|
||||
CANCELED = 'CANCELED', 'انصراف توسط مبدا'
|
||||
|
||||
source_store = models.ForeignKey(
|
||||
'StoreRoom',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='outgoing_transfers',
|
||||
verbose_name="انبار مبدا"
|
||||
)
|
||||
target_store = models.ForeignKey(
|
||||
'StoreRoom',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='incoming_transfers',
|
||||
verbose_name="انبار مقصد"
|
||||
)
|
||||
product = models.ForeignKey(
|
||||
'Product',
|
||||
on_delete=models.PROTECT,
|
||||
verbose_name="کالا"
|
||||
)
|
||||
quantity = models.PositiveIntegerField(verbose_name="تعداد انتقال")
|
||||
|
||||
status = models.CharField(
|
||||
max_length=15,
|
||||
choices=Status.choices,
|
||||
default=Status.PENDING,
|
||||
db_index=True,
|
||||
verbose_name="وضعیت"
|
||||
)
|
||||
desc = models.CharField(max_length=500, blank=True, null=True, verbose_name="توضیحات درخواست")
|
||||
rejection_reason = models.CharField(max_length=500, blank=True, null=True, verbose_name="دلیل رد درخواست")
|
||||
|
||||
requester = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='requested_transfers',
|
||||
verbose_name="کاربر درخواستدهنده"
|
||||
)
|
||||
approver = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='processed_transfers',
|
||||
verbose_name="کاربر تعیینتکلیفکننده"
|
||||
)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True, db_index=True, verbose_name="زمان ثبت درخواست")
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="زمان آخرین بهروزرسانی")
|
||||
action_date = models.DateTimeField(null=True, blank=True, db_index=True, verbose_name="زمان تعیین تکلیف")
|
||||
|
||||
out_transaction = models.ForeignKey(
|
||||
'ProductInStore',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='transfer_out_ref',
|
||||
verbose_name="سند خروج انبار مبدا"
|
||||
)
|
||||
in_transaction = models.ForeignKey(
|
||||
'ProductInStore',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='transfer_in_ref',
|
||||
verbose_name="سند ورود انبار مقصد"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"[{self.get_status_display()}] {self.quantity} عدد {self.product.product_name} از {self.source_store.store_name} به {self.target_store.store_name}"
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if self.source_store_id == self.target_store_id:
|
||||
raise ValidationError("انبار مبدا و مقصد نمیتوانند یکسان باشند.")
|
||||
if self.quantity and self.quantity <= 0:
|
||||
raise ValidationError("تعداد انتقال باید بیشتر از صفر باشد.")
|
||||
|
||||
class FactorIteam(models.Model): # پیشنهاد: در آینده املای نام این کلاس را به FactorItem اصلاح کنید
|
||||
factor = models.ForeignKey(Factor, on_delete=models.CASCADE)
|
||||
product = models.ForeignKey(Product, on_delete=models.CASCADE, null=True, blank=True)
|
||||
product_name = models.CharField(max_length=200, default='')
|
||||
num = models.IntegerField()
|
||||
amount = models.IntegerField()
|
||||
garantyEsh = models.CharField(max_length=100, default='', null=True, blank=True)
|
||||
|
||||
|
||||
class VisitQueue(models.Model):
|
||||
customer = models.ForeignKey(Customer, on_delete=models.CASCADE)
|
||||
dateTime = models.DateTimeField()
|
||||
statusList = [
|
||||
('waiting', 'waiting'),
|
||||
('finish', 'finish'),
|
||||
('cancell', 'cancell'),
|
||||
]
|
||||
status = models.CharField(max_length=10, choices=statusList, default='waiting')
|
||||
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
GradeList = [
|
||||
('3', '3'),
|
||||
('2', '2'),
|
||||
('1', '1'),
|
||||
]
|
||||
grade = models.CharField(max_length=1, choices=GradeList)
|
||||
grade_desc = models.CharField(max_length=200)
|
||||
|
||||
|
||||
class ArticleType(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
|
||||
|
||||
class Article(models.Model):
|
||||
class Meta:
|
||||
permissions = [
|
||||
("ViewReportMali", "ViewReportMali")
|
||||
]
|
||||
debit = models.IntegerField(null=True, blank=True)
|
||||
credit = models.IntegerField(null=True, blank=True)
|
||||
customer = models.ForeignKey(Customer, on_delete=models.CASCADE, null=True, blank=True)
|
||||
factor = models.ForeignKey(Factor, on_delete=models.CASCADE, null=True, blank=True)
|
||||
type = models.CharField(max_length=50, null=True, blank=True)
|
||||
typePy = models.CharField(max_length=50, null=True, blank=True)
|
||||
checkNum = models.IntegerField(null=True, blank=True)
|
||||
nahvePardakht = models.CharField(max_length=50, null=True, blank=True)
|
||||
sarresid = models.DateTimeField(null=True, blank=True)
|
||||
payStatus = models.CharField(max_length=50, null=True, blank=True)
|
||||
createdDT = models.DateTimeField(auto_now_add=True)
|
||||
changeDT = models.DateTimeField(auto_now=True)
|
||||
desc = models.CharField(max_length=200)
|
||||
user = models.ForeignKey(userModel, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
|
||||
|
||||
class Action(models.Model):
|
||||
actionType = models.CharField(max_length=50, null=True, blank=True)
|
||||
forUser = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
customer = models.ForeignKey(Customer, related_name='actionrlt', on_delete=models.CASCADE, null=True, blank=True)
|
||||
createdDT = models.DateTimeField(auto_now_add=True)
|
||||
changeDT = models.DateTimeField(auto_now=True)
|
||||
actionDateTime = models.DateTimeField(null=True, blank=True)
|
||||
finishDateTime = models.DateTimeField(null=True, blank=True)
|
||||
desc = models.CharField(max_length=500, null=True, blank=True)
|
||||
result = models.CharField(max_length=500, null=True, blank=True)
|
||||
status = models.CharField(max_length=50, null=True, blank=True)
|
||||
nazar = models.CharField(max_length=50, null=True, blank=True, default=None)
|
||||
nextNazarDT = models.DateTimeField(null=True, blank=True, default=None)
|
||||
nazarDT = models.DateTimeField(null=True, blank=True, default=None)
|
||||
grade = models.IntegerField(null=True, blank=True, default=None)
|
||||
|
||||
|
||||
class Sarresid(models.Model):
|
||||
customer = models.ForeignKey('Customer', on_delete=models.CASCADE, null=True, blank=True)
|
||||
createdDT = models.DateTimeField(auto_now_add=True)
|
||||
changeDT = models.DateTimeField(auto_now=True)
|
||||
price = models.IntegerField(null=False, blank=False)
|
||||
payStatus = models.CharField(max_length=50, null=True, blank=True)
|
||||
type = models.CharField(max_length=50, null=True, blank=True)
|
||||
factor = models.ForeignKey('Factor', on_delete=models.CASCADE, null=True, blank=True)
|
||||
sarresidDT = models.DateTimeField(null=True, blank=True)
|
||||
user = models.ForeignKey(userModel, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
desc = models.TextField(null=True, blank=True)
|
||||
|
||||
# ---------------- فیلدهای استاندارد و ایمنشده چک ----------------
|
||||
check_row_number = models.CharField(
|
||||
max_length=50,
|
||||
null=True, blank=True,
|
||||
verbose_name="شماره ردیف چک"
|
||||
)
|
||||
check_sayyad_id = models.CharField(
|
||||
max_length=16,
|
||||
validators=[
|
||||
RegexValidator(regex=r'^\d{16}$', message='شناسه صیادی باید دقیقاً ۱۶ رقم عدد باشد.')
|
||||
],
|
||||
null=True, blank=True,
|
||||
verbose_name="شناسه صیادی"
|
||||
)
|
||||
check_owner_name = models.CharField(
|
||||
max_length=100,
|
||||
null=True, blank=True,
|
||||
verbose_name="نام مالک چک"
|
||||
)
|
||||
check_owner_phone = models.CharField(
|
||||
max_length=11,
|
||||
validators=[
|
||||
RegexValidator(regex=r'^\d{11}$', message='شماره تلفن باید ۱۱ رقم و فقط شامل اعداد باشد.')
|
||||
],
|
||||
null=True, blank=True,
|
||||
verbose_name="تلفن مالک چک"
|
||||
)
|
||||
|
||||
class CacheTable(models.Model):
|
||||
key = models.CharField(max_length=50, unique=True)
|
||||
value = models.CharField(max_length=400)
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.key} # {self.value}'
|
||||
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
پکیج مدلهای admin_panel.
|
||||
|
||||
این پکیج جایگزین فایل تکپارچهٔ models.py است و آن را به ماژولهای کوچکتر
|
||||
دامنهای تقسیم میکند. همهٔ نامها اینجا re-export میشوند تا تمام
|
||||
مصرفکنندههای فعلی (`from .models import *`, `from admin_panel.models import X`)
|
||||
بدون هیچ تغییری کار کنند و app_label و مایگریشنها ثابت بمانند.
|
||||
|
||||
نکته: عمداً __all__ تعریف نشده تا رفتار star-import دقیقاً مانند فایل قبلی باشد
|
||||
(نامهایی مثل `models`, `User`, `Sum` نیز مثل قبل منتشر میشوند).
|
||||
"""
|
||||
from .users import * # noqa: F401,F403
|
||||
from .customers import * # noqa: F401,F403
|
||||
from .products import * # noqa: F401,F403
|
||||
from .store import * # noqa: F401,F403
|
||||
from .factors import * # noqa: F401,F403
|
||||
from .finance import * # noqa: F401,F403
|
||||
from .actions import * # noqa: F401,F403
|
||||
from .system import * # noqa: F401,F403
|
||||
@@ -0,0 +1,38 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
class VisitQueue(models.Model):
|
||||
customer = models.ForeignKey('Customer', on_delete=models.CASCADE)
|
||||
dateTime = models.DateTimeField()
|
||||
statusList = [
|
||||
('waiting', 'waiting'),
|
||||
('finish', 'finish'),
|
||||
('cancell', 'cancell'),
|
||||
]
|
||||
status = models.CharField(max_length=10, choices=statusList, default='waiting')
|
||||
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
GradeList = [
|
||||
('3', '3'),
|
||||
('2', '2'),
|
||||
('1', '1'),
|
||||
]
|
||||
grade = models.CharField(max_length=1, choices=GradeList)
|
||||
grade_desc = models.CharField(max_length=200)
|
||||
|
||||
|
||||
class Action(models.Model):
|
||||
actionType = models.CharField(max_length=50, null=True, blank=True)
|
||||
forUser = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
customer = models.ForeignKey('Customer', related_name='actionrlt', on_delete=models.CASCADE, null=True, blank=True)
|
||||
createdDT = models.DateTimeField(auto_now_add=True)
|
||||
changeDT = models.DateTimeField(auto_now=True)
|
||||
actionDateTime = models.DateTimeField(null=True, blank=True)
|
||||
finishDateTime = models.DateTimeField(null=True, blank=True)
|
||||
desc = models.CharField(max_length=500, null=True, blank=True)
|
||||
result = models.CharField(max_length=500, null=True, blank=True)
|
||||
status = models.CharField(max_length=50, null=True, blank=True)
|
||||
nazar = models.CharField(max_length=50, null=True, blank=True, default=None)
|
||||
nextNazarDT = models.DateTimeField(null=True, blank=True, default=None)
|
||||
nazarDT = models.DateTimeField(null=True, blank=True, default=None)
|
||||
grade = models.IntegerField(null=True, blank=True, default=None)
|
||||
@@ -0,0 +1,42 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Customer(models.Model):
|
||||
class Meta:
|
||||
permissions = [
|
||||
("AddCustomer", "AddCustomer"),
|
||||
("DelCustomer", "DelCustomer"),
|
||||
("EditCustomer", "EditCustomer"),
|
||||
("ViewCustomer", "ViewCustomer")
|
||||
]
|
||||
name_family = models.CharField(max_length=200)
|
||||
address = models.CharField(max_length=200)
|
||||
phone1 = models.CharField(max_length=20, default='')
|
||||
phone2 = models.CharField(max_length=20, default='', null=True, blank=True)
|
||||
phone3 = models.CharField(max_length=20, default='', null=True, blank=True)
|
||||
idCode = models.CharField(max_length=20, default='', null=True, blank=True)
|
||||
installDate = models.DateField(auto_now_add=True)
|
||||
create_date = models.DateTimeField(auto_now_add=True)
|
||||
last_bazdid_dt = models.DateTimeField(null=True, blank=True)
|
||||
laghv_bazdid = models.BooleanField(null=True, blank=True, default=False)
|
||||
|
||||
# فیلد جدید برای حذف نرم (بدون تداخل با دادههای قبلی)
|
||||
is_active = models.BooleanField(default=True, null=True, blank=True, help_text="وضعیت فعالیت مشتری")
|
||||
|
||||
def __str__(self):
|
||||
return self.name_family
|
||||
|
||||
# بازنویسی متد حذف: جلوگیری از فاجعه در دیتابیس
|
||||
def delete(self, *args, **kwargs):
|
||||
# به جای حذف فیزیکی که انبار و فاکتورها را نابود میکند، مشتری را غیرفعال میکنیم
|
||||
self.is_active = False
|
||||
self.save()
|
||||
|
||||
|
||||
class PhoneNumber(models.Model):
|
||||
customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name='phones')
|
||||
title = models.CharField(max_length=50, default='تلفن', blank=True, null=True, help_text="عنوان شماره (مثل: موبایل پدر)")
|
||||
phone_number = models.CharField(max_length=20)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.title}: {self.phone_number}"
|
||||
@@ -0,0 +1,58 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
class Factor(models.Model):
|
||||
class Meta:
|
||||
permissions = [
|
||||
("ViewFactor", "ViewFactor"),
|
||||
("DelFactor", "DelFactor"),
|
||||
("EditFactor", "EditFactor"),
|
||||
("AddFactor", "AddFactor")
|
||||
]
|
||||
customer = models.ForeignKey('Customer', on_delete=models.CASCADE)
|
||||
create_date = models.DateTimeField(auto_now_add=True)
|
||||
amount = models.IntegerField()
|
||||
discount = models.IntegerField(null=True, blank=True)
|
||||
deadline = models.DateTimeField(null=True, blank=True)
|
||||
desc = models.CharField(max_length=100, default='')
|
||||
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
statusList = [
|
||||
('unpaid', 'unpaid'),
|
||||
('paid', 'paid'),
|
||||
('cancell', 'cancell'),
|
||||
]
|
||||
pay_status = models.CharField(max_length=10, choices=statusList, default='')
|
||||
FactorTypeList = [
|
||||
('Nasb', 'Nasb'),
|
||||
('Khadamat', 'Khadamat'),
|
||||
('Garranty', 'Garranty'),
|
||||
]
|
||||
FactorType = models.CharField(max_length=10, choices=FactorTypeList, default='')
|
||||
PayTypeList = [
|
||||
('naghd', 'naghd'),
|
||||
('cardToCard', 'cardToCard'),
|
||||
('kartKhan', 'kartKhan'),
|
||||
('nesiye', 'nesiye'),
|
||||
('check', 'check'),
|
||||
]
|
||||
PayType = models.CharField(max_length=30, choices=PayTypeList, default='')
|
||||
|
||||
# فیلد جدید برای حذف نرم
|
||||
is_active = models.BooleanField(default=True, null=True, blank=True)
|
||||
|
||||
# بازنویسی متد حذف: ابطال فاکتور به جای حذف فیزیکی
|
||||
def delete(self, *args, **kwargs):
|
||||
# فاکتور باطل میشود اما رکوردهای انبار دستنخورده باقی میمانند!
|
||||
self.is_active = False
|
||||
self.pay_status = 'cancell'
|
||||
self.save()
|
||||
|
||||
|
||||
class FactorIteam(models.Model): # پیشنهاد: در آینده املای نام این کلاس را به FactorItem اصلاح کنید
|
||||
factor = models.ForeignKey(Factor, on_delete=models.CASCADE)
|
||||
product = models.ForeignKey('Product', on_delete=models.CASCADE, null=True, blank=True)
|
||||
product_name = models.CharField(max_length=200, default='')
|
||||
num = models.IntegerField()
|
||||
amount = models.IntegerField()
|
||||
garantyEsh = models.CharField(max_length=100, default='', null=True, blank=True)
|
||||
@@ -0,0 +1,71 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.validators import RegexValidator
|
||||
|
||||
userModel = get_user_model()
|
||||
|
||||
|
||||
class ArticleType(models.Model):
|
||||
name = models.CharField(max_length=50)
|
||||
|
||||
|
||||
class Article(models.Model):
|
||||
class Meta:
|
||||
permissions = [
|
||||
("ViewReportMali", "ViewReportMali")
|
||||
]
|
||||
debit = models.IntegerField(null=True, blank=True)
|
||||
credit = models.IntegerField(null=True, blank=True)
|
||||
customer = models.ForeignKey('Customer', on_delete=models.CASCADE, null=True, blank=True)
|
||||
factor = models.ForeignKey('Factor', on_delete=models.CASCADE, null=True, blank=True)
|
||||
type = models.CharField(max_length=50, null=True, blank=True)
|
||||
typePy = models.CharField(max_length=50, null=True, blank=True)
|
||||
checkNum = models.IntegerField(null=True, blank=True)
|
||||
nahvePardakht = models.CharField(max_length=50, null=True, blank=True)
|
||||
sarresid = models.DateTimeField(null=True, blank=True)
|
||||
payStatus = models.CharField(max_length=50, null=True, blank=True)
|
||||
createdDT = models.DateTimeField(auto_now_add=True)
|
||||
changeDT = models.DateTimeField(auto_now=True)
|
||||
desc = models.CharField(max_length=200)
|
||||
user = models.ForeignKey(userModel, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
|
||||
|
||||
class Sarresid(models.Model):
|
||||
customer = models.ForeignKey('Customer', on_delete=models.CASCADE, null=True, blank=True)
|
||||
createdDT = models.DateTimeField(auto_now_add=True)
|
||||
changeDT = models.DateTimeField(auto_now=True)
|
||||
price = models.IntegerField(null=False, blank=False)
|
||||
payStatus = models.CharField(max_length=50, null=True, blank=True)
|
||||
type = models.CharField(max_length=50, null=True, blank=True)
|
||||
factor = models.ForeignKey('Factor', on_delete=models.CASCADE, null=True, blank=True)
|
||||
sarresidDT = models.DateTimeField(null=True, blank=True)
|
||||
user = models.ForeignKey(userModel, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
desc = models.TextField(null=True, blank=True)
|
||||
|
||||
# ---------------- فیلدهای استاندارد و ایمنشده چک ----------------
|
||||
check_row_number = models.CharField(
|
||||
max_length=50,
|
||||
null=True, blank=True,
|
||||
verbose_name="شماره ردیف چک"
|
||||
)
|
||||
check_sayyad_id = models.CharField(
|
||||
max_length=16,
|
||||
validators=[
|
||||
RegexValidator(regex=r'^\d{16}$', message='شناسه صیادی باید دقیقاً ۱۶ رقم عدد باشد.')
|
||||
],
|
||||
null=True, blank=True,
|
||||
verbose_name="شناسه صیادی"
|
||||
)
|
||||
check_owner_name = models.CharField(
|
||||
max_length=100,
|
||||
null=True, blank=True,
|
||||
verbose_name="نام مالک چک"
|
||||
)
|
||||
check_owner_phone = models.CharField(
|
||||
max_length=11,
|
||||
validators=[
|
||||
RegexValidator(regex=r'^\d{11}$', message='شماره تلفن باید ۱۱ رقم و فقط شامل اعداد باشد.')
|
||||
],
|
||||
null=True, blank=True,
|
||||
verbose_name="تلفن مالک چک"
|
||||
)
|
||||
@@ -0,0 +1,148 @@
|
||||
from django.db import models, transaction, connection
|
||||
from django.db.models import Sum
|
||||
|
||||
|
||||
class ProductCategory(models.Model):
|
||||
category_name = models.CharField(max_length=50)
|
||||
|
||||
def __str__(self):
|
||||
return self.category_name
|
||||
|
||||
|
||||
class Product(models.Model):
|
||||
class Meta:
|
||||
permissions = [
|
||||
("ViewProduct", "ViewProduct"),
|
||||
]
|
||||
ProductCategory = models.ForeignKey(ProductCategory, on_delete=models.CASCADE)
|
||||
product_name = models.CharField(max_length=50)
|
||||
product_price = models.IntegerField()
|
||||
year = models.IntegerField(default=1400)
|
||||
createdDT = models.DateTimeField(auto_now_add=True)
|
||||
changeDT = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.product_name
|
||||
|
||||
|
||||
class ProductInStore(models.Model):
|
||||
class Meta:
|
||||
permissions = [
|
||||
("ViewProductInStore", "ViewProductInStore"),
|
||||
("EditProductInStore", "EditProductInStore"),
|
||||
("OnlyAnbar", "OnlyAnbar"),
|
||||
]
|
||||
class MovementType(models.TextChoices):
|
||||
SALE = 'SALE', 'فروش عادی'
|
||||
WARRANTY = 'WARRANTY', 'گارانتی'
|
||||
PROMOTION = 'PROMOTION', 'اشانتیون'
|
||||
TRANSFER = 'TRANSFER', 'انتقال'
|
||||
INITIAL = 'INITIAL', 'موجودی اولیه'
|
||||
DIRECT_IN = 'DIRECT_IN', 'ورود دستی'
|
||||
DIRECT_OUT = 'DIRECT_OUT', 'خروج دستی'
|
||||
RETURN = 'RETURN', 'برگشت از فروش'
|
||||
ERROR_CORRECTION = 'ERROR_CORRECTION', 'اصلاح خطای ثبت'
|
||||
|
||||
StoreRoom = models.ForeignKey('StoreRoom', on_delete=models.CASCADE)
|
||||
Product = models.ForeignKey(Product, on_delete=models.CASCADE)
|
||||
s_in = models.IntegerField(default=0)
|
||||
s_out = models.IntegerField(default=0)
|
||||
current_stock_at_time = models.IntegerField(default=0, help_text="موجودی انبار در لحظه این تراکنش")
|
||||
desc = models.CharField(max_length=50, default='')
|
||||
year = models.IntegerField(default=1400)
|
||||
createdDT = models.DateTimeField(auto_now_add=True)
|
||||
changeDT = models.DateTimeField(auto_now=True)
|
||||
|
||||
# تغییر بسیار مهم استراتژیک: تبدیل CASCADE به PROTECT
|
||||
# هیچکس نمیتواند فاکتوری که کالا از انبار خارج کرده را از دیتابیس پاک کند!
|
||||
factor = models.ForeignKey('Factor', on_delete=models.PROTECT, null=True, blank=True)
|
||||
|
||||
movement_type = models.CharField(
|
||||
max_length=20,
|
||||
choices=MovementType.choices,
|
||||
db_index=True,
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.Product.product_name} - In: {self.s_in} Out: {self.s_out}"
|
||||
|
||||
# متدهای save و delete دقیقاً همان کدهای بینقص و پرسرعتی هستند که در پیام قبل تایید کردیم
|
||||
def save(self, *args, **kwargs):
|
||||
from .store import ProductStockHistory
|
||||
|
||||
try:
|
||||
self.s_in = int(self.s_in or 0)
|
||||
self.s_out = int(self.s_out or 0)
|
||||
except (ValueError, TypeError):
|
||||
self.s_in, self.s_out = 0, 0
|
||||
|
||||
is_created = self.pk is None
|
||||
|
||||
with transaction.atomic():
|
||||
qs_lock = ProductInStore.objects.filter(StoreRoom=self.StoreRoom, Product=self.Product)
|
||||
if connection.features.has_select_for_update:
|
||||
qs_lock = qs_lock.select_for_update()
|
||||
list(qs_lock)
|
||||
|
||||
if not is_created:
|
||||
old_instance = ProductInStore.objects.get(pk=self.pk)
|
||||
old_balance = old_instance.s_in - old_instance.s_out
|
||||
new_balance = self.s_in - self.s_out
|
||||
change_amount = new_balance - old_balance
|
||||
description = self.desc or f"ویرایش تراکنش: {self.desc}"
|
||||
else:
|
||||
change_amount = self.s_in - self.s_out
|
||||
description = self.desc or "ثبت تراکنش جدید انبار"
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
totals = ProductInStore.objects.filter(StoreRoom=self.StoreRoom, Product=self.Product).aggregate(
|
||||
total_in=Sum('s_in'),
|
||||
total_out=Sum('s_out')
|
||||
)
|
||||
total_in = totals['total_in'] or 0
|
||||
total_out = totals['total_out'] or 0
|
||||
current_stock = total_in - total_out
|
||||
|
||||
ProductInStore.objects.filter(pk=self.pk).update(current_stock_at_time=current_stock)
|
||||
self.current_stock_at_time = current_stock
|
||||
|
||||
ProductStockHistory.objects.create(
|
||||
store_room=self.StoreRoom,
|
||||
product=self.Product,
|
||||
quantity_changed=change_amount,
|
||||
current_stock=current_stock,
|
||||
desc=description
|
||||
)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
from .store import ProductStockHistory
|
||||
|
||||
store_room = self.StoreRoom
|
||||
product = self.Product
|
||||
change_amount = -(int(self.s_in or 0) - int(self.s_out or 0))
|
||||
description = f"حذف دستی تراکنش انبار: {self.desc}"
|
||||
|
||||
with transaction.atomic():
|
||||
qs_lock = ProductInStore.objects.filter(StoreRoom=store_room, Product=product)
|
||||
if connection.features.has_select_for_update:
|
||||
qs_lock = qs_lock.select_for_update()
|
||||
list(qs_lock)
|
||||
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
totals = ProductInStore.objects.filter(StoreRoom=store_room, Product=product).aggregate(
|
||||
total_in=Sum('s_in'),
|
||||
total_out=Sum('s_out')
|
||||
)
|
||||
current_stock = (totals['total_in'] or 0) - (totals['total_out'] or 0)
|
||||
|
||||
ProductStockHistory.objects.create(
|
||||
store_room=store_room,
|
||||
product=product,
|
||||
quantity_changed=change_amount,
|
||||
current_stock=current_stock,
|
||||
desc=description
|
||||
)
|
||||
@@ -0,0 +1,148 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
class StoreRoom(models.Model):
|
||||
class Meta:
|
||||
permissions = [
|
||||
("ViewStore", "ViewStore"),
|
||||
("AddStore", "AddStore"),
|
||||
("EditStore", "EditStore"),
|
||||
("DelStore", "DelStore")
|
||||
]
|
||||
store_name = models.CharField(max_length=200)
|
||||
user = models.ManyToManyField(User,
|
||||
blank=True,
|
||||
verbose_name="دسترسی مشاهده انبار احتمالا",
|
||||
help_text="از این برای دسترسی مشاهده آن اتبار استفاده می شود"
|
||||
)
|
||||
owners = models.ManyToManyField(
|
||||
User,
|
||||
through='StoreRoomOwner', # کلید حل مشکل بدون شکستن کدهای قبلی
|
||||
blank=True,
|
||||
related_name="owned_storerooms",
|
||||
verbose_name="مالکین انبار در فاکتور و موجودی و...",
|
||||
help_text="این کاربران اگر فاکتور بزنند از این انبار کسر میگردد"
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.store_name
|
||||
|
||||
|
||||
class StoreRoomOwner(models.Model):
|
||||
"""
|
||||
این جدول واسط، پشت صحنه قرار میگیرد تا فقط قانون مالکیت را مدیریت کند.
|
||||
کدهای دیگر پروژه اصلاً نیازی ندارند مستقیم با این جدول درگیر شوند.
|
||||
"""
|
||||
storeroom = models.ForeignKey(StoreRoom, on_delete=models.CASCADE)
|
||||
|
||||
# OneToOneField تضمین میکند یک کاربر نمیتواند در این جدول (و هیچ انبار دیگری) تکرار شود
|
||||
user = models.OneToOneField(
|
||||
User,
|
||||
on_delete=models.CASCADE,
|
||||
error_messages={
|
||||
'unique': 'این کاربر در حال حاضر مالک انبار دیگری است.'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ProductStockHistory(models.Model):
|
||||
store_room = models.ForeignKey(StoreRoom, on_delete=models.CASCADE, related_name='stock_histories')
|
||||
product = models.ForeignKey('Product', on_delete=models.CASCADE, related_name='stock_histories')
|
||||
quantity_changed = models.IntegerField(help_text="مقدار تغییر یافته (مثبت برای ورود، منفی برای خروج)")
|
||||
current_stock = models.IntegerField(help_text="موجودی کل انبار در این لحظه")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
desc = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.product.product_name} in {self.store_room.store_name} - Stock: {self.current_stock}"
|
||||
|
||||
|
||||
class StoreTransferRequest(models.Model):
|
||||
class Status(models.TextChoices):
|
||||
PENDING = 'PENDING', 'در انتظار تایید انبار مقصد'
|
||||
APPROVED = 'APPROVED', 'تایید شده و منتقل شده'
|
||||
REJECTED = 'REJECTED', 'رد شده توسط انبار مقصد'
|
||||
CANCELED = 'CANCELED', 'انصراف توسط مبدا'
|
||||
|
||||
source_store = models.ForeignKey(
|
||||
'StoreRoom',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='outgoing_transfers',
|
||||
verbose_name="انبار مبدا"
|
||||
)
|
||||
target_store = models.ForeignKey(
|
||||
'StoreRoom',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='incoming_transfers',
|
||||
verbose_name="انبار مقصد"
|
||||
)
|
||||
product = models.ForeignKey(
|
||||
'Product',
|
||||
on_delete=models.PROTECT,
|
||||
verbose_name="کالا"
|
||||
)
|
||||
quantity = models.PositiveIntegerField(verbose_name="تعداد انتقال")
|
||||
|
||||
status = models.CharField(
|
||||
max_length=15,
|
||||
choices=Status.choices,
|
||||
default=Status.PENDING,
|
||||
db_index=True,
|
||||
verbose_name="وضعیت"
|
||||
)
|
||||
desc = models.CharField(max_length=500, blank=True, null=True, verbose_name="توضیحات درخواست")
|
||||
rejection_reason = models.CharField(max_length=500, blank=True, null=True, verbose_name="دلیل رد درخواست")
|
||||
|
||||
requester = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='requested_transfers',
|
||||
verbose_name="کاربر درخواستدهنده"
|
||||
)
|
||||
approver = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='processed_transfers',
|
||||
verbose_name="کاربر تعیینتکلیفکننده"
|
||||
)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True, db_index=True, verbose_name="زمان ثبت درخواست")
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="زمان آخرین بهروزرسانی")
|
||||
action_date = models.DateTimeField(null=True, blank=True, db_index=True, verbose_name="زمان تعیین تکلیف")
|
||||
|
||||
out_transaction = models.ForeignKey(
|
||||
'ProductInStore',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='transfer_out_ref',
|
||||
verbose_name="سند خروج انبار مبدا"
|
||||
)
|
||||
in_transaction = models.ForeignKey(
|
||||
'ProductInStore',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='transfer_in_ref',
|
||||
verbose_name="سند ورود انبار مقصد"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"[{self.get_status_display()}] {self.quantity} عدد {self.product.product_name} از {self.source_store.store_name} به {self.target_store.store_name}"
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if self.source_store_id == self.target_store_id:
|
||||
raise ValidationError("انبار مبدا و مقصد نمیتوانند یکسان باشند.")
|
||||
if self.quantity and self.quantity <= 0:
|
||||
raise ValidationError("تعداد انتقال باید بیشتر از صفر باشد.")
|
||||
@@ -0,0 +1,9 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class CacheTable(models.Model):
|
||||
key = models.CharField(max_length=50, unique=True)
|
||||
value = models.CharField(max_length=400)
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.key} # {self.value}'
|
||||
@@ -0,0 +1,18 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
# اصلاح مدل کاربر: تبدیل به پروفایل برای حفظ امنیت و اتصال به سیستم پیشفرض جنگو
|
||||
class UserProfile(models.Model):
|
||||
UserTypeList = [
|
||||
('admin', 'admin'),
|
||||
('office', 'office'),
|
||||
('operator', 'operator'),
|
||||
]
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
|
||||
user_type = models.CharField(max_length=10, choices=UserTypeList)
|
||||
name_family = models.CharField(max_length=200)
|
||||
create_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username} ({self.user_type})"
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
پکیج ویوهای اپ admin_panel.
|
||||
|
||||
این پکیج جایگزین فایل تکپارچهٔ سابق `admin_panel/views.py` شده است. هر ویو در ماژول
|
||||
دامنهای مربوط به خود قرار گرفته و در اینجا همهٔ آنها re-export میشوند تا مصرفکنندهٔ
|
||||
فعلی (`from . import views` در urls.py) بدون هیچ تغییری به `views.X` دسترسی داشته باشد.
|
||||
"""
|
||||
|
||||
from ._helpers import ( # noqa: F401
|
||||
logger,
|
||||
InventoryException,
|
||||
_safe_int,
|
||||
_parse_jalali_date,
|
||||
_parse_timestamp,
|
||||
)
|
||||
|
||||
from .auth import * # noqa: F401,F403
|
||||
from .customers import * # noqa: F401,F403
|
||||
from .factors import * # noqa: F401,F403
|
||||
from .store import * # noqa: F401,F403
|
||||
from .finance import * # noqa: F401,F403
|
||||
from .actions import * # noqa: F401,F403
|
||||
from .cheque_inquiry import * # noqa: F401,F403
|
||||
@@ -0,0 +1,49 @@
|
||||
"""توابع و ابزارهای مشترک لایهٔ ویوها."""
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from django.utils import timezone
|
||||
from persiantools.jdatetime import JalaliDate
|
||||
from persiantools import digits
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InventoryException(Exception):
|
||||
"""خطای اختصاصی برای کنترل موجودی انبار"""
|
||||
pass
|
||||
|
||||
|
||||
def _safe_int(value, default=0):
|
||||
"""تابع امن برای تبدیل مقادیر دریافتی از جاوااسکریپت به عدد"""
|
||||
if value is None:
|
||||
return default
|
||||
try:
|
||||
clean_val = str(value).strip()
|
||||
return int(clean_val) if clean_val else default
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
|
||||
|
||||
def _parse_jalali_date(date_str):
|
||||
"""تبدیل امن تاریخ ارسالی از pDatepicker به میلادی"""
|
||||
if not date_str or not str(date_str).strip():
|
||||
return None
|
||||
try:
|
||||
clean_date = digits.fa_to_en(digits.ar_to_fa(str(date_str).strip()))
|
||||
y, m, d = map(int, clean_date.split("/"))
|
||||
return JalaliDate(y, m, d).to_gregorian()
|
||||
except Exception as e:
|
||||
logger.warning(f"Error parsing Jalali date '{date_str}': {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _parse_timestamp(ts_value):
|
||||
"""تبدیل یونیکس تایماستمپهای ارسالی از جاوااسکریپت"""
|
||||
if not ts_value:
|
||||
return None
|
||||
try:
|
||||
# تبدیل میلیثانیه جاوااسکریپت به ثانیه پایتون
|
||||
return datetime.datetime.fromtimestamp(int(ts_value) / 1000.0, tz=timezone.get_current_timezone())
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
@@ -0,0 +1,263 @@
|
||||
import datetime
|
||||
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.http import HttpResponse
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
from ..models import Action, Customer
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customerAction(request, c_id):
|
||||
# استفاده از get_object_or_404 برای امنیت بیشتر
|
||||
customer = get_object_or_404(Customer, id=c_id)
|
||||
|
||||
# دریافت پارامترهای فیلتر از URL (پیشفرض: all)
|
||||
filter_type = request.GET.get('actionType', 'all')
|
||||
filter_status = request.GET.get('status', 'all')
|
||||
|
||||
# کوئری پایه برای اقدامات این مشتری
|
||||
actions = Action.objects.filter(customer=customer)
|
||||
|
||||
# اعمال فیلتر بر اساس نوع اقدام (بازدید، نصب و ...)
|
||||
if filter_type != 'all':
|
||||
actions = actions.filter(actionType=filter_type)
|
||||
|
||||
# اعمال فیلتر بر اساس وضعیت (انجام شده، در انتظار و ...)
|
||||
if filter_status != 'all':
|
||||
actions = actions.filter(status=filter_status)
|
||||
|
||||
# مرتبسازی نزولی بر اساس تاریخ ایجاد
|
||||
actions = actions.order_by("-createdDT")
|
||||
|
||||
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
|
||||
|
||||
data = {
|
||||
'customer': customer,
|
||||
'actions': actions,
|
||||
'allUsers': allUsers,
|
||||
# ارسال مقادیر فیلتر به قالب برای حفظ وضعیت انتخابشده در دراپداونها
|
||||
'filter_type': filter_type,
|
||||
'filter_status': filter_status
|
||||
}
|
||||
|
||||
return render(request, 'admin_panel/customerAction.html', data)
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customerNazar(request ,c_id):
|
||||
customer = Customer.objects.get(id=c_id)
|
||||
actions = Action.objects.filter(customer=customer, nazarDT__isnull=False).order_by("-createdDT")
|
||||
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
|
||||
data = {'customer': customer, 'actions': actions, 'allUsers': allUsers}
|
||||
return render(request, 'admin_panel/customer/nazar.html', data)
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customerNewAction(request ,c_id):
|
||||
actionType = request.POST.get('actionType', None)
|
||||
dateTimeAnjam = request.POST.get('dateTimeAnjam', None)
|
||||
#return HttpResponse(float(dateTimeAnjam+".0"))
|
||||
try:
|
||||
dateTimeAnjam = datetime.datetime.fromtimestamp(int(dateTimeAnjam)/1e3)
|
||||
except:
|
||||
pass
|
||||
desc = request.POST.get('desc', None)
|
||||
userID = request.POST.get('userID', None)
|
||||
is_done = request.POST.get('is_done', None)
|
||||
result = request.POST.get('result', None)
|
||||
FinishDateTime = request.POST.get('FinishDateTime', None)
|
||||
try:
|
||||
FinishDateTime = datetime.datetime.fromtimestamp(int(FinishDateTime)/1e3)
|
||||
except:
|
||||
pass
|
||||
|
||||
customer_obj = Customer.objects.get(id=int(c_id))
|
||||
newAction = Action()
|
||||
newAction.actionType = actionType
|
||||
newAction.actionDateTime = dateTimeAnjam
|
||||
newAction.desc = desc
|
||||
newAction.forUser = User.objects.get(id=int(userID))
|
||||
newAction.customer = customer_obj
|
||||
if is_done:
|
||||
newAction.status = 'finish'
|
||||
newAction.finishDateTime = FinishDateTime
|
||||
newAction.result = result
|
||||
else:
|
||||
newAction.status = 'waite'
|
||||
newAction.save()
|
||||
|
||||
if newAction.status == 'finish' and newAction.actionType == 'bazdid':
|
||||
customer_obj.last_bazdid_dt = newAction.finishDateTime
|
||||
customer_obj.save()
|
||||
|
||||
return redirect('/panel/customer/'+str(c_id)+'/action')
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def ActionList(request):
|
||||
actions = Action.objects.filter(forUser__id=request.user.id, status='waite').order_by("-createdDT")
|
||||
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
|
||||
data = {'actions': actions, 'allUsers': allUsers}
|
||||
return render(request, 'admin_panel/ActionList.html', data)
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def ActionListUser(request, u_id):
|
||||
actions = Action.objects.filter(forUser__id=u_id, status='waite').order_by("-createdDT")
|
||||
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
|
||||
data = {'actions': actions, 'allUsers': allUsers}
|
||||
return render(request, 'admin_panel/ActionList.html', data)
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customerEditAction(request ,c_id, action_id):
|
||||
|
||||
actionType = request.POST.get('actionType', None)
|
||||
dateTimeAnjam = request.POST.get('dateTimeAnjam', None)
|
||||
print("ok")
|
||||
#return HttpResponse(float(dateTimeAnjam+".0"))
|
||||
try:
|
||||
dateTimeAnjam = datetime.datetime.fromtimestamp(int(dateTimeAnjam)/1e3)
|
||||
except:
|
||||
pass
|
||||
desc = request.POST.get('desc', None)
|
||||
userID = request.POST.get('userID', None)
|
||||
is_done = request.POST.get('is_done', None)
|
||||
result = request.POST.get('result', None)
|
||||
FinishDateTime = request.POST.get('FinishDateTime', None)
|
||||
try:
|
||||
FinishDateTime = datetime.datetime.fromtimestamp(int(FinishDateTime)/1e3)
|
||||
except:
|
||||
pass
|
||||
|
||||
customer_obj = Customer.objects.get(id=int(c_id))
|
||||
newAction = Action.objects.get(id=action_id)
|
||||
newAction.actionType = actionType
|
||||
newAction.actionDateTime = dateTimeAnjam
|
||||
newAction.desc = desc
|
||||
newAction.forUser = User.objects.get(id=int(userID))
|
||||
newAction.customer = customer_obj
|
||||
if is_done:
|
||||
newAction.status = 'finish'
|
||||
newAction.finishDateTime = FinishDateTime
|
||||
newAction.result = result
|
||||
else:
|
||||
newAction.status = 'waite'
|
||||
newAction.save()
|
||||
|
||||
if newAction.status == 'finish' and newAction.actionType == 'bazdid':
|
||||
customer_obj.last_bazdid_dt = newAction.finishDateTime
|
||||
customer_obj.save()
|
||||
#return redirect('/panel/customer/'+str(c_id)+'/action')
|
||||
return redirect(request.META.get('HTTP_REFERER', 'redirect_if_referer_not_found'))
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def ActionListAll(request):
|
||||
actions = Action.objects.filter().order_by("-id")
|
||||
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
|
||||
data = {'actions': actions, 'allUsers': allUsers}
|
||||
return render(request, 'admin_panel/ActionList.html', data)
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def ActionListToday(request):
|
||||
startTodayDT= datetime.datetime.today().strftime('%Y-%m-%d 00:00:00')
|
||||
endTodayDT= datetime.datetime.today().strftime('%Y-%m-%d 23:59:59')
|
||||
#startTodayDT = datetime.datetime.strptime(startTodayDT, '%Y-%m-%d-00:00:00')
|
||||
#endTodayDT = datetime.datetime.strptime(endTodayDT, '%Y-%m-%d-23:59:59')
|
||||
#return HttpResponse(todayDT)
|
||||
actions = Action.objects.filter(actionDateTime__gte=startTodayDT,actionDateTime__lte=endTodayDT, forUser__id=request.user.id, status='waite').order_by("-actionDateTime")[:50]
|
||||
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
|
||||
data = {'actions': actions, 'allUsers': allUsers}
|
||||
return render(request, 'admin_panel/ActionList.html', data)
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def ActionListUserToday(request, u_id):
|
||||
dateTimeAction = request.GET.get('dateTime', None)
|
||||
status = request.GET.get('status', None)
|
||||
unixADT = dateTimeAction
|
||||
if dateTimeAction:
|
||||
dateTimeAction = datetime.datetime.fromtimestamp(int(dateTimeAction)/1e3)
|
||||
startTodayDT = dateTimeAction.strftime('%Y-%m-%d 00:00:00')
|
||||
endTodayDT= dateTimeAction.strftime('%Y-%m-%d 23:59:59')
|
||||
else:
|
||||
startTodayDT= datetime.datetime.today().strftime('%Y-%m-%d 00:00:00')
|
||||
endTodayDT= datetime.datetime.today().strftime('%Y-%m-%d 23:59:59')
|
||||
#startTodayDT = datetime.datetime.strptime(startTodayDT, '%Y-%m-%d-00:00:00')
|
||||
#endTodayDT = datetime.datetime.strptime(endTodayDT, '%Y-%m-%d-23:59:59')
|
||||
#return HttpResponse(todayDT)
|
||||
if status == 'waite' or status == None or status == '' or status == ' ':
|
||||
status = 'waite'
|
||||
actions = Action.objects.filter(actionDateTime__gte=startTodayDT,actionDateTime__lte=endTodayDT, forUser__id=u_id, status=status).order_by("-actionDateTime")[:50]
|
||||
actionPr = Action.objects.filter(actionDateTime__lt=startTodayDT, forUser__id=u_id, status=status).order_by("-actionDateTime")[:200]
|
||||
elif status == 'finish':
|
||||
actionPr = None
|
||||
actions = Action.objects.filter(actionDateTime__gte=startTodayDT,actionDateTime__lte=endTodayDT, forUser__id=u_id, status=status).order_by("-actionDateTime")[:50]
|
||||
|
||||
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
|
||||
data = {'actions': actions, 'allUsers': allUsers, 'u_id': u_id, "unixADT": unixADT, "status": status, 'actionPr': actionPr}
|
||||
return render(request, 'admin_panel/ActionList.html', data)
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def rezayat(request):
|
||||
statusRezayat = request.GET.get('status', 'waite')
|
||||
now = datetime.datetime.now()
|
||||
|
||||
# محاسبه بازههای زمانی ۳۰ روز و ۶۰ روز قبل
|
||||
dt_30_days_ago = now - datetime.timedelta(days=30)
|
||||
dt_60_days_ago = now - datetime.timedelta(days=60)
|
||||
|
||||
if statusRezayat == 'finish':
|
||||
# نمایش نظرسنجیهای تعیینتکلیف شده (ثبت شده یا کنسل شده)
|
||||
actions = Action.objects.exclude(actionType='vosol').filter(
|
||||
status='finish',
|
||||
nazarDT__isnull=False
|
||||
).order_by("-nazarDT")[:500]
|
||||
else:
|
||||
statusRezayat = 'waite'
|
||||
# نمایش نظرسنجیهای در انتظار با اعمال شرط عدم انقضای زمان
|
||||
actions = Action.objects.exclude(actionType='vosol').filter(
|
||||
status='finish',
|
||||
nazarDT__isnull=True, # نظرسنجی هنوز نهایی نشده
|
||||
createdDT__gte=dt_60_days_ago # حداکثر ۶۰ روز از ایجاد گذشته باشد
|
||||
).filter(
|
||||
Q(finishDateTime__isnull=True) | Q(finishDateTime__gte=dt_30_days_ago) # حداکثر ۳۰ روز از اتمام گذشته باشد
|
||||
).filter(
|
||||
Q(nextNazarDT__isnull=True) | Q(nextNazarDT__lte=now) # موعد پیگیری رسیده یا تعویقی نداشته باشد
|
||||
).order_by("-id")[:500]
|
||||
|
||||
data = {'actions': actions, "status": statusRezayat}
|
||||
return render(request, 'admin_panel/customerRezayat.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def rezayatEdit(request, ac_id):
|
||||
csGrade = request.POST.get('csGrade', None)
|
||||
csDesc = request.POST.get('csDesc', None)
|
||||
|
||||
if csDesc and csGrade:
|
||||
action = get_object_or_404(Action, id=ac_id)
|
||||
|
||||
if csGrade == '-1':
|
||||
# عدم موفقیت: تنظیم nextNazarDT برای ۳ روز بعد (nazarDT پر نمیشود تا نهایی نشود)
|
||||
action.nazar = csDesc
|
||||
action.grade = int(csGrade)
|
||||
action.nextNazarDT = datetime.datetime.now() + datetime.timedelta(days=3)
|
||||
|
||||
elif csGrade == '-2':
|
||||
# کنسل کردن: پر کردن nazarDT برای خروج کامل از لیست کارهای در انتظار
|
||||
action.nazar = csDesc
|
||||
action.grade = int(csGrade)
|
||||
action.nazarDT = datetime.datetime.now()
|
||||
action.nextNazarDT = None
|
||||
|
||||
else:
|
||||
# ثبت امتیاز عادی
|
||||
action.nazar = csDesc
|
||||
action.grade = int(csGrade)
|
||||
action.nazarDT = datetime.datetime.now()
|
||||
action.nextNazarDT = None
|
||||
|
||||
action.save()
|
||||
return redirect('/panel/rezayat/')
|
||||
else:
|
||||
return HttpResponse('لطفا همه فیلد ها را پر کنید')
|
||||
@@ -0,0 +1,29 @@
|
||||
from django.shortcuts import redirect, render
|
||||
from django.contrib.auth import authenticate, login, logout
|
||||
|
||||
|
||||
def p_login(request):
|
||||
username = request.POST.get('username', None )
|
||||
password = request.POST.get('password', None )
|
||||
if username != None and password != None:
|
||||
if not request.user.is_authenticated:
|
||||
user = authenticate(request, username=username, password=password)
|
||||
if user is not None:
|
||||
login(request, user)
|
||||
return redirect("/panel/ActionList/"+str(request.user.id)+"/today")
|
||||
else:
|
||||
data = {'message': 'یوزنیم یا پسورد اشتباه است'}
|
||||
return render(request, 'admin_panel/login.html', data)
|
||||
else:
|
||||
return redirect("/panel/")
|
||||
else:
|
||||
if request.user.is_authenticated:
|
||||
return redirect("/panel/")
|
||||
else:
|
||||
return render(request, 'admin_panel/login.html')
|
||||
#return logout(request)
|
||||
|
||||
|
||||
def p_logout(request):
|
||||
logout(request)
|
||||
return redirect("/panel/p_login/")
|
||||
@@ -0,0 +1,273 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
import cv2
|
||||
import ddddocr
|
||||
import numpy as np
|
||||
from asgiref.sync import async_to_sync
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.http import JsonResponse
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
from ._helpers import logger, _safe_int
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# توابع کمکی و منطق استعلام چک (برگرفته از اسکریپتهای شما)
|
||||
# =====================================================================
|
||||
|
||||
def solve_base64_captcha(base64_string: str) -> str:
|
||||
try:
|
||||
if ',' in base64_string:
|
||||
base64_string = base64_string.split(',')[1]
|
||||
|
||||
image_bytes = base64.b64decode(base64_string)
|
||||
np_arr = np.frombuffer(image_bytes, np.uint8)
|
||||
img = cv2.imdecode(np_arr, cv2.IMREAD_COLOR)
|
||||
|
||||
if img is None:
|
||||
return ""
|
||||
|
||||
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
||||
blur = cv2.medianBlur(gray, 3)
|
||||
_, thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
|
||||
|
||||
kernel = np.ones((2, 2), np.uint8)
|
||||
cleaned_img = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=1)
|
||||
final_img = cv2.bitwise_not(cleaned_img)
|
||||
|
||||
success, encoded_image = cv2.imencode('.png', final_img)
|
||||
if not success:
|
||||
return ""
|
||||
|
||||
processed_bytes = encoded_image.tobytes()
|
||||
ocr = ddddocr.DdddOcr(show_ad=False)
|
||||
extracted_text = ocr.classification(processed_bytes)
|
||||
|
||||
numbers = re.findall(r'\d+', extracted_text)
|
||||
if len(numbers) >= 2:
|
||||
num1, num2 = int(numbers[0]), int(numbers[1])
|
||||
if '+' in extracted_text:
|
||||
ans = num1 + num2
|
||||
elif '-' in extracted_text:
|
||||
ans = num1 - num2
|
||||
elif '*' in extracted_text or 'x' in extracted_text.lower():
|
||||
ans = num1 * num2
|
||||
else:
|
||||
ans = num1 + num2
|
||||
|
||||
return str(ans)
|
||||
return ""
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def parse_cheque_result_html(html_content: str) -> dict:
|
||||
sayad_id = re.search(r'شناسه\s*<b>?\s*(\d+)\s*</b>?', html_content)
|
||||
owner_name = re.search(r'متعلق به\s*<b>?\s*(.*?)\s*</b>?\s*می باشد', html_content)
|
||||
validity_date = re.search(r'تا تاریخ\s*<b>?\s*([\d/]+)\s*</b>?', html_content)
|
||||
status = re.search(r'در وضعیت\s*<b>?\s*(.*?)\s*</b>?\s*در پایگاه', html_content)
|
||||
|
||||
clean_full_text = re.sub(r'<[^>]+>', '', html_content).strip()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"sayad_id": sayad_id.group(1) if sayad_id else "",
|
||||
"owner_name": owner_name.group(1).strip() if owner_name else "",
|
||||
"validity_date": validity_date.group(1) if validity_date else "",
|
||||
"status": status.group(1).strip() if status else "",
|
||||
"raw_message": clean_full_text
|
||||
},
|
||||
"error": None
|
||||
}
|
||||
|
||||
async def run_cbi_sayad_inquiry_logic(sayad_id: str, max_retries: int = 7) -> str:
|
||||
lightpanda_url = os.environ.get("LIGHTPANDA_URL") or "ws://127.0.0.1:9222"
|
||||
async with async_playwright() as p:
|
||||
try:
|
||||
browser = await p.chromium.connect_over_cdp(lightpanda_url)
|
||||
except Exception as e:
|
||||
return json.dumps({"success": False, "data": None, "error": f"عدم اتصال به Lightpanda: {str(e)}"})
|
||||
|
||||
for attempt in range(1, max_retries + 1):
|
||||
context = await browser.new_context(ignore_https_errors=True, bypass_csp=True)
|
||||
page = await context.new_page()
|
||||
|
||||
try:
|
||||
url = "https://cbi.ir/EstelamSayad/19689.aspx"
|
||||
await page.goto(url, wait_until="domcontentloaded", timeout=20000)
|
||||
|
||||
captcha_selector = "#ctl00_ucBody_ucContent_ctl00_imgcpatcha"
|
||||
await page.wait_for_selector(captcha_selector, state="visible", timeout=15000)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
base64_src = await page.locator(captcha_selector).get_attribute("src")
|
||||
viewstate = await page.locator("#__VIEWSTATE").get_attribute("value") or ""
|
||||
viewstate_gen = await page.locator("#__VIEWSTATEGENERATOR").get_attribute("value") or "453B48D8"
|
||||
|
||||
if not base64_src or not viewstate:
|
||||
await context.close()
|
||||
continue
|
||||
|
||||
captcha_answer = solve_base64_captcha(base64_src)
|
||||
if not captcha_answer:
|
||||
await context.close()
|
||||
continue
|
||||
|
||||
ajax_raw_response = await page.evaluate("""
|
||||
async ({ sayadId, captchaAns, viewState, viewStateGen }) => {
|
||||
try {
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('ctl00$ucBody$ucContent$ctl00$ScriptManager1', 'ctl00$ucBody$ucContent$ctl00$UpdatePanel1|ctl00$ucBody$ucContent$ctl00$btnSearch');
|
||||
formData.append('__EVENTTARGET', '');
|
||||
formData.append('__EVENTARGUMENT', '');
|
||||
formData.append('__VIEWSTATE', viewState);
|
||||
formData.append('__VIEWSTATEGENERATOR', viewStateGen);
|
||||
formData.append('__VIEWSTATEENCRYPTED', '');
|
||||
formData.append('ctl00$ucHeader$txtQuickSearch', '');
|
||||
formData.append('ctl00$ucBody$ucContent$ctl00$txtChequeNumber', sayadId);
|
||||
formData.append('ctl00$ucBody$ucContent$ctl00$txtCaptchaInput', captchaAns);
|
||||
formData.append('__ASYNCPOST', 'true');
|
||||
formData.append('ctl00$ucBody$ucContent$ctl00$btnSearch', 'استعلام چک صیادی');
|
||||
|
||||
const response = await fetch(window.location.href, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Cache-Control': 'no-cache'
|
||||
},
|
||||
body: formData.toString()
|
||||
});
|
||||
return await response.text();
|
||||
} catch (err) {
|
||||
return 'FETCH_ERROR: ' + err.message;
|
||||
}
|
||||
}
|
||||
""", {'sayadId': sayad_id, 'captchaAns': captcha_answer, 'viewState': viewstate, 'viewStateGen': viewstate_gen})
|
||||
|
||||
if "FETCH_ERROR:" in ajax_raw_response:
|
||||
await context.close()
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
|
||||
error_match = re.search(r'id="ctl00_ucBody_ucContent_ctl00_lblError"[^>]*>(.*?)</span>', ajax_raw_response, re.DOTALL)
|
||||
if error_match and error_match.group(1).strip():
|
||||
clean_error = re.sub(r'<[^>]+>', '', error_match.group(1)).strip()
|
||||
if "عبارت محاسباتی" in clean_error:
|
||||
await context.close()
|
||||
continue
|
||||
else:
|
||||
await context.close()
|
||||
await browser.close()
|
||||
return json.dumps({"success": False, "data": None, "error": clean_error})
|
||||
|
||||
result_match = re.search(r'id="ctl00_ucBody_ucContent_ctl00_lblChequeNumberResult"[^>]*>(.*?)</span>', ajax_raw_response, re.DOTALL)
|
||||
if result_match:
|
||||
raw_html = result_match.group(1)
|
||||
parsed_result = parse_cheque_result_html(raw_html)
|
||||
await context.close()
|
||||
await browser.close()
|
||||
return json.dumps(parsed_result)
|
||||
|
||||
await context.close()
|
||||
except Exception:
|
||||
await context.close()
|
||||
await asyncio.sleep(1)
|
||||
|
||||
await browser.close()
|
||||
return json.dumps({"success": False, "data": None, "error": "استعلام ناموفق بود (خطای شبکه یا تشخیص کپچا)"})
|
||||
|
||||
|
||||
async def run_bpi_cheque_inquiry_logic(national_id: str, sayad_id: str, id_type: int = 1):
|
||||
lightpanda_url = os.environ.get("LIGHTPANDA_URL") or "ws://127.0.0.1:9222"
|
||||
async with async_playwright() as p:
|
||||
try:
|
||||
browser = await p.chromium.connect_over_cdp(lightpanda_url)
|
||||
except Exception as e:
|
||||
return {"success": False, "error": f"عدم اتصال به مرورگر Lightpanda: {str(e)}"}
|
||||
|
||||
context = await browser.new_context()
|
||||
page = await context.new_page()
|
||||
|
||||
try:
|
||||
await page.goto("https://vbank.bpi.ir/public/inquiries/inquiry-bounced-cheques", wait_until="domcontentloaded", timeout=20000)
|
||||
api_url = f"https://sec.bpi.ir/prls/api/v1/inquiry/chequeStatus?IdCode={national_id}&IdType={id_type}&SayadId={sayad_id}"
|
||||
|
||||
response = await context.request.get(
|
||||
api_url,
|
||||
headers={
|
||||
"Referer": "https://vbank.bpi.ir/public/inquiries/inquiry-bounced-cheques",
|
||||
"Accept": "application/json, text/plain, */*"
|
||||
}
|
||||
)
|
||||
|
||||
if response.ok:
|
||||
data = await response.json()
|
||||
return {"success": True, "data": data}
|
||||
else:
|
||||
error_text = await response.text()
|
||||
return {"success": False, "error": f"سرور خطا برگرداند (کد {response.status}): {error_text}"}
|
||||
|
||||
except Exception as e:
|
||||
return {"success": False, "error": f"خطایی در فرآیند استعلام رخ داد: {str(e)}"}
|
||||
finally:
|
||||
await context.close()
|
||||
await browser.close()
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# View های مربوط به استعلام (API)
|
||||
# =====================================================================
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewStore', raise_exception=True)
|
||||
def api_check_status_inquiry(request):
|
||||
"""
|
||||
استعلام وضعیت چک از بانک مرکزی
|
||||
پارامتر ورودی: sayad_id (GET)
|
||||
"""
|
||||
sayad_id = request.GET.get('sayad_id', '').strip()
|
||||
|
||||
if not sayad_id or not sayad_id.isdigit() or len(sayad_id) != 16:
|
||||
return JsonResponse({"success": False, "error": "شناسه صیادی باید دقیقاً ۱۶ رقم باشد."})
|
||||
|
||||
try:
|
||||
# اجرای تابع Async به صورت Sync
|
||||
result_json_str = async_to_sync(run_cbi_sayad_inquiry_logic)(sayad_id)
|
||||
# تبدیل رشته جیسون به دیکشنری برای ارسال مجدد با JsonResponse
|
||||
result_data = json.loads(result_json_str)
|
||||
return JsonResponse(result_data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in CBI Check Status Inquiry: {str(e)}", exc_info=True)
|
||||
return JsonResponse({"success": False, "error": "خطای سرور در برقراری ارتباط با بانک مرکزی."})
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewStore', raise_exception=True)
|
||||
def api_check_sabt_inquiry(request):
|
||||
"""
|
||||
استعلام ثبت چک از بانک پاسارگاد
|
||||
پارامترهای ورودی: sayad_id, national_id, id_type (GET)
|
||||
"""
|
||||
sayad_id = request.GET.get('sayad_id', '').strip()
|
||||
national_id = request.GET.get('national_id', '').strip()
|
||||
id_type = _safe_int(request.GET.get('id_type', 1)) # عدد 1 حقیقی، 2 حقوقی
|
||||
|
||||
# اگر کدملی از فرانتاند خالی ارسال شد، از یک کدملی پیشفرض استفاده میکنیم
|
||||
if not national_id:
|
||||
national_id = "1219968218"
|
||||
|
||||
if not sayad_id:
|
||||
return JsonResponse({"success": False, "error": "شناسه صیادی برای استعلام ثبت الزامی است."})
|
||||
|
||||
try:
|
||||
# اجرای تابع Async به صورت Sync
|
||||
result_data = async_to_sync(run_bpi_cheque_inquiry_logic)(national_id, sayad_id, id_type)
|
||||
return JsonResponse(result_data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in BPI Check Registration Inquiry: {str(e)}", exc_info=True)
|
||||
return JsonResponse({"success": False, "error": "خطای سرور در برقراری ارتباط با سامانه بانک."})
|
||||
@@ -0,0 +1,463 @@
|
||||
import datetime
|
||||
import os
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Sum, Q
|
||||
from django.shortcuts import redirect, render, get_object_or_404
|
||||
from django.core.paginator import Paginator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from persiantools.jdatetime import JalaliDate
|
||||
|
||||
from ..models import (Customer, PhoneNumber, ProductCategory, Product, Factor,
|
||||
FactorIteam, Article, Sarresid, Action, CacheTable)
|
||||
from ._helpers import logger, _parse_jalali_date
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customers(request):
|
||||
searchVal = request.GET.get('searchVal', '')
|
||||
searchOn = request.GET.get('searchOn', '')
|
||||
searchStatus = True
|
||||
full_path = request.get_full_path()
|
||||
|
||||
if searchOn == 'code':
|
||||
all_customers = Customer.objects.filter(idCode__contains=searchVal).order_by('-create_date')
|
||||
elif searchOn == 'phone':
|
||||
all_customers = Customer.objects.filter(Q(phone1__contains=searchVal) | Q(phone2__contains=searchVal) | Q(phone3__contains=searchVal) ).order_by('-create_date')
|
||||
elif searchOn == 'name':
|
||||
all_customers = Customer.objects.filter(name_family__contains=searchVal).order_by('-create_date')
|
||||
elif searchOn == 'address':
|
||||
all_customers = Customer.objects.filter(address__contains=searchVal).order_by('-create_date')
|
||||
elif searchOn == 'laghv':
|
||||
all_customers = Customer.objects.filter(laghv_bazdid=True).order_by('-create_date')
|
||||
else:
|
||||
searchStatus = False
|
||||
all_customers = Customer.objects.filter(is_active=True).order_by('-create_date')
|
||||
|
||||
paginator = Paginator(all_customers, 25) # Show 25 contacts per page.
|
||||
page_number = request.GET.get('page', None)
|
||||
all_customers = paginator.get_page(page_number)
|
||||
|
||||
|
||||
data = {'customersD': all_customers, 'searchStatus': searchStatus, 'full_path': full_path, 'searchVal': searchVal, 'searchOn':searchOn}
|
||||
return render(request, 'admin_panel/customers.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customersSearch(request, searchVal,searchOn):
|
||||
searchStatus = True
|
||||
|
||||
if searchOn == 'code':
|
||||
all_customers = Customer.objects.filter(idCode__contains=searchVal).order_by('-create_date')
|
||||
elif searchOn == 'phone':
|
||||
all_customers = Customer.objects.filter(phone1__contains=searchVal).order_by('-create_date')
|
||||
elif searchOn == 'name':
|
||||
all_customers = Customer.objects.filter(name_family__contains=searchVal).order_by('-create_date')
|
||||
elif searchOn == 'address':
|
||||
all_customers = Customer.objects.filter(address__contains=searchVal).order_by('-create_date')
|
||||
else:
|
||||
searchStatus = False
|
||||
all_customers = Customer.objects.all().order_by('-create_date')
|
||||
|
||||
paginator = Paginator(all_customers, 25) # Show 25 contacts per page.
|
||||
page_number = request.GET.get('page', None)
|
||||
all_customers = paginator.get_page(page_number)
|
||||
|
||||
|
||||
data = {'customersD': all_customers, 'searchStatus': searchStatus}
|
||||
return render(request, 'admin_panel/customers.html', data)
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customer(request ,c_id):
|
||||
customer = Customer.objects.get(id=c_id)
|
||||
categorys = ProductCategory.objects.all()
|
||||
factorha = Factor.objects.filter(customer__id = c_id)
|
||||
#all_prd = serializers.serialize("json", all_prda)
|
||||
#return JsonResponse(all_prd,safe = False)
|
||||
dateNow = datetime.datetime.now().date
|
||||
data = {'customer': customer, 'categorys': categorys, 'dateNow': dateNow, 'factorha': factorha}
|
||||
return render(request, 'admin_panel/customerID.html', data)
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customerNew(request ,c_id):
|
||||
customer = Customer.objects.get(id=c_id)
|
||||
categorys = ProductCategory.objects.all()
|
||||
factorha = Factor.objects.filter(customer__id = c_id)
|
||||
#all_prd = serializers.serialize("json", all_prda)
|
||||
#return JsonResponse(all_prd,safe = False)
|
||||
dateNow = datetime.datetime.now().date
|
||||
data = {'customer': customer, 'categorys': categorys, 'dateNow': dateNow, 'factorha': factorha}
|
||||
return render(request, 'admin_panel/customerNew.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customerNew2(request ,c_id):
|
||||
customer = get_object_or_404(Customer, id=c_id, is_active=True)
|
||||
categorys = ProductCategory.objects.all()
|
||||
factorha = Factor.objects.filter(customer__id = c_id)
|
||||
|
||||
debit = Article.objects.filter(customer=customer).aggregate(Sum('debit'))['debit__sum']
|
||||
credit = Article.objects.filter(customer=customer).aggregate(Sum('credit'))['credit__sum']
|
||||
mandehesabCs = 0
|
||||
try:
|
||||
if (debit - credit) < 0:
|
||||
mandehesabCs = abs(debit - credit)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
#all_prd = serializers.serialize("json", all_prda)
|
||||
#return JsonResponse(all_prd,safe = False)
|
||||
dateNow = datetime.datetime.now().date
|
||||
data = {'customer': customer, 'categorys': categorys, 'dateNow': dateNow, 'factorha': factorha, 'mandehesabCs':mandehesabCs}
|
||||
return render(request, 'admin_panel/customerNew2.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customerFactors(request, c_id):
|
||||
customer = get_object_or_404(Customer, id=c_id)
|
||||
categorys = ProductCategory.objects.all()
|
||||
|
||||
# کوئری پایه برای دریافت همه فاکتورهای این مشتری
|
||||
factor_qs = Factor.objects.filter(customer=customer)
|
||||
|
||||
# دریافت پارامترهای فیلتر و جستجو از آدرس URL
|
||||
search_query = request.GET.get('search_factor', '').strip()
|
||||
filter_status = request.GET.get('status', 'all')
|
||||
filter_type = request.GET.get('type', 'all')
|
||||
az_tarikh = request.GET.get('az_tarikh', '').strip()
|
||||
ta_tarikh = request.GET.get('ta_tarikh', '').strip()
|
||||
|
||||
# ۱. اعمال فیلتر جستجو (شناسه فاکتور یا متن توضیحات)
|
||||
if search_query:
|
||||
if search_query.isdigit():
|
||||
factor_qs = factor_qs.filter(models.Q(id=int(search_query)) | models.Q(desc__icontains=search_query))
|
||||
else:
|
||||
factor_qs = factor_qs.filter(models.Q(desc__icontains=search_query))
|
||||
|
||||
# ۲. اعمال فیلتر وضعیت پرداخت و حذف نرم
|
||||
if filter_status != 'all':
|
||||
if filter_status == 'deleted':
|
||||
factor_qs = factor_qs.filter(is_active=False)
|
||||
else:
|
||||
factor_qs = factor_qs.filter(pay_status=filter_status, is_active=True)
|
||||
|
||||
# ۳. اعمال فیلتر نوع فاکتور (نصب، خدمات، گارانتی)
|
||||
if filter_type != 'all':
|
||||
factor_qs = factor_qs.filter(FactorType=filter_type)
|
||||
|
||||
# ۴. اعمال فیلتر تاریخ شروع (از تاریخ)
|
||||
if az_tarikh:
|
||||
az_gregorian = _parse_jalali_date(az_tarikh)
|
||||
if az_gregorian:
|
||||
az_datetime = datetime.datetime.combine(az_gregorian, datetime.time.min)
|
||||
factor_qs = factor_qs.filter(create_date__gte=az_datetime)
|
||||
|
||||
# ۵. اعمال فیلتر تاریخ پایان (تا تاریخ)
|
||||
if ta_tarikh:
|
||||
ta_gregorian = _parse_jalali_date(ta_tarikh)
|
||||
if ta_gregorian:
|
||||
ta_datetime = datetime.datetime.combine(ta_gregorian, datetime.time.max)
|
||||
factor_qs = factor_qs.filter(create_date__lte=ta_datetime)
|
||||
|
||||
# مرتبسازی بر اساس آخرین تاریخ ایجاد
|
||||
factorha = factor_qs.order_by('-create_date')
|
||||
|
||||
dateNow = datetime.datetime.now().date
|
||||
|
||||
data = {
|
||||
'customer': customer,
|
||||
'categorys': categorys,
|
||||
'dateNow': dateNow,
|
||||
'factorha': factorha,
|
||||
'search_query': search_query,
|
||||
'filter_status': filter_status,
|
||||
'filter_type': filter_type,
|
||||
'az_tarikh': az_tarikh,
|
||||
'ta_tarikh': ta_tarikh
|
||||
}
|
||||
return render(request, 'admin_panel/customerIDFactor.html', data)
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customerFactor(request ,c_id, f_id):
|
||||
customer = Customer.objects.get(id=c_id)
|
||||
categorys = ProductCategory.objects.all()
|
||||
factorO = Factor.objects.get(customer=customer, id=f_id)
|
||||
fiteam = FactorIteam.objects.filter(factor=factorO)
|
||||
#all_prd = serializers.serialize("json", all_prda)
|
||||
#return JsonResponse(all_prd,safe = False)
|
||||
dateNow = datetime.datetime.now().date
|
||||
data = {'customer': customer, 'categorys': categorys, 'dateNow': dateNow, 'factorO': factorO, 'fiteam': fiteam}
|
||||
return render(request, 'admin_panel/customerFactor.html', data)
|
||||
|
||||
|
||||
def productsJson(request):
|
||||
from django.core import serializers
|
||||
from django.http import JsonResponse
|
||||
all_prd = Product.objects.all()
|
||||
all_prd = serializers.serialize("json", all_prd)
|
||||
return JsonResponse(all_prd,safe = False)
|
||||
|
||||
def ActionJson(request, action_id):
|
||||
from django.core import serializers
|
||||
from django.http import JsonResponse
|
||||
AcJson = Action.objects.filter(id=action_id)
|
||||
AcJson = serializers.serialize("json", AcJson)
|
||||
return JsonResponse(AcJson,safe = False)
|
||||
|
||||
def CustomerJson(request, c_id):
|
||||
from django.core import serializers
|
||||
from django.http import JsonResponse
|
||||
customerJson = Customer.objects.filter(id=c_id)
|
||||
customerJson = serializers.serialize("json", customerJson)
|
||||
return JsonResponse(customerJson,safe = False)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customersAdd(request):
|
||||
an_name = request.POST.get('an_name', '')
|
||||
an_address = request.POST.get('an_address', '')
|
||||
installDate = request.POST.get('installDate', '')
|
||||
idnCode = request.POST.get('idnCode', '')
|
||||
|
||||
# دریافت آرایههای عنوان و شماره تلفن
|
||||
titles_list = request.POST.getlist('phone_titles')
|
||||
phones_list = request.POST.getlist('phones')
|
||||
|
||||
# ترکیب عنوانها و شمارهها و حذف ردیفهای خالی
|
||||
valid_phones = []
|
||||
for title, phone in zip(titles_list, phones_list):
|
||||
if phone.strip():
|
||||
valid_phones.append({
|
||||
'title': title.strip() if title.strip() else 'تلفن',
|
||||
'phone': phone.strip()
|
||||
})
|
||||
|
||||
year, month, day = map(int, installDate.split('/'))
|
||||
installDate_gregorian = JalaliDate(year, month, day).to_gregorian()
|
||||
|
||||
newCs = Customer()
|
||||
newCs.name_family = an_name
|
||||
newCs.address = an_address
|
||||
newCs.installDate = installDate_gregorian
|
||||
newCs.idCode = idnCode
|
||||
|
||||
# حفظ سازگاری با کدهای قدیمی (ریختن ۳ شماره اول در فیلدهای قدیمی)
|
||||
newCs.phone1 = valid_phones[0]['phone'] if len(valid_phones) > 0 else ''
|
||||
newCs.phone2 = valid_phones[1]['phone'] if len(valid_phones) > 1 else ''
|
||||
newCs.phone3 = valid_phones[2]['phone'] if len(valid_phones) > 2 else ''
|
||||
newCs.save()
|
||||
|
||||
# ذخیره شمارهها به صورت نامحدود در جدول PhoneNumber
|
||||
for item in valid_phones:
|
||||
PhoneNumber.objects.create(customer=newCs, title=item['title'], phone_number=item['phone'])
|
||||
|
||||
return redirect('/panel/customers/')
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customerEdit(request, c_id):
|
||||
an_name = request.POST.get('an_name', '')
|
||||
an_address = request.POST.get('an_address', '')
|
||||
installDate = request.POST.get('installDate', '')
|
||||
idnCode = request.POST.get('idnCode', '')
|
||||
laghv_bazdid = request.POST.get('laghv_bazdid', False)
|
||||
|
||||
# دریافت آرایههای عنوان و شماره تلفن
|
||||
titles_list = request.POST.getlist('phone_titles')
|
||||
phones_list = request.POST.getlist('phones')
|
||||
|
||||
valid_phones = []
|
||||
for title, phone in zip(titles_list, phones_list):
|
||||
if phone.strip():
|
||||
valid_phones.append({
|
||||
'title': title.strip() if title.strip() else 'تلفن',
|
||||
'phone': phone.strip()
|
||||
})
|
||||
|
||||
year, month, day = map(int, installDate.split('/'))
|
||||
installDate_gregorian = JalaliDate(year, month, day).to_gregorian()
|
||||
|
||||
newCs = get_object_or_404(Customer, id=c_id)
|
||||
newCs.name_family = an_name
|
||||
newCs.address = an_address
|
||||
newCs.installDate = installDate_gregorian
|
||||
newCs.idCode = idnCode
|
||||
newCs.laghv_bazdid = True if laghv_bazdid == 'true' else False
|
||||
|
||||
# آپدیت فیلدهای قدیمی جهت حفظ سازگاری
|
||||
newCs.phone1 = valid_phones[0]['phone'] if len(valid_phones) > 0 else ''
|
||||
newCs.phone2 = valid_phones[1]['phone'] if len(valid_phones) > 1 else ''
|
||||
newCs.phone3 = valid_phones[2]['phone'] if len(valid_phones) > 2 else ''
|
||||
newCs.save()
|
||||
|
||||
# بروزرسانی جدول بینهایت (حذف قبلیها و درج مجدد)
|
||||
PhoneNumber.objects.filter(customer=newCs).delete()
|
||||
for item in valid_phones:
|
||||
PhoneNumber.objects.create(customer=newCs, title=item['title'], phone_number=item['phone'])
|
||||
|
||||
return redirect('/panel/customer/'+str(c_id))
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def entezar(request):
|
||||
a = datetime.datetime.now()
|
||||
|
||||
searchVal = request.GET.get('searchVal', '')
|
||||
searchOn = request.GET.get('searchOn', '')
|
||||
searchStatus = True
|
||||
full_path = request.get_full_path()
|
||||
|
||||
from django.db.models import Q
|
||||
|
||||
bazdid = os.environ.get('bazdid', 9)
|
||||
|
||||
bazdid = int ( int(bazdid) * 4.3 )
|
||||
|
||||
dateTime6MonthAgo = a - datetime.timedelta(weeks=bazdid)
|
||||
''' customers = Customer.objects.filter(name_family__isnull=False)
|
||||
m6mbazdid = []
|
||||
for customerObj in customers:
|
||||
try:
|
||||
actionAa = Action.objects.filter(customer=customerObj, actionType='bazdid', status='finish').last()
|
||||
if actionAa.status == 'finish':
|
||||
customerObj.last_bazdid_dt = actionAa.actionDateTime
|
||||
customerObj.save()
|
||||
if actionAa.actionDateTime.date() < dateTime6MonthAgo.date():
|
||||
m6mbazdid.append(actionAa)
|
||||
except:
|
||||
pass '''
|
||||
|
||||
if searchOn == 'address':
|
||||
print("addressaddressaddressaddressaddress")
|
||||
customers_objj = Customer.objects.filter(Q(last_bazdid_dt__lte=dateTime6MonthAgo) | Q(last_bazdid_dt__isnull=True),
|
||||
installDate__lte=dateTime6MonthAgo,
|
||||
laghv_bazdid=False, name_family__isnull=False, address__contains=searchVal).order_by('last_bazdid_dt')
|
||||
else:
|
||||
searchStatus = False
|
||||
customers_objj = Customer.objects.filter(Q(last_bazdid_dt__lte=dateTime6MonthAgo) | Q(last_bazdid_dt__isnull=True),
|
||||
installDate__lte=dateTime6MonthAgo,
|
||||
laghv_bazdid=False, name_family__isnull=False).order_by('last_bazdid_dt')
|
||||
|
||||
#customers_objj = Customer.objects.filter(name_family__isnull=False, last_bazdid_dt__lte=dateTime6MonthAgo)
|
||||
|
||||
try:
|
||||
entezar_bazdid = CacheTable.objects.get(key='9mahbazdid')
|
||||
|
||||
entezar_bazdid.value = Customer.objects.filter(
|
||||
Q(last_bazdid_dt__lte=dateTime6MonthAgo) | Q(last_bazdid_dt__isnull=True),
|
||||
installDate__lte=dateTime6MonthAgo,
|
||||
laghv_bazdid=False, name_family__isnull=False).count()
|
||||
|
||||
entezar_bazdid.save()
|
||||
except:
|
||||
pass
|
||||
|
||||
paginator = Paginator(customers_objj, 20) # Show 25 contacts per page.
|
||||
page_number = request.GET.get('page', None)
|
||||
customerHa = paginator.get_page(page_number)
|
||||
|
||||
return render(request, 'admin_panel/entezar.html', {'customers': customerHa, 'searchStatus': searchStatus, 'full_path': full_path, 'searchVal': searchVal, 'searchOn':searchOn})
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.DelCustomer', raise_exception=True)
|
||||
def customerDel(request, c_id):
|
||||
customer_obj = get_object_or_404(Customer, id=c_id)
|
||||
|
||||
try:
|
||||
# ۱. بررسی وجود هرگونه سابقه مالی/حسابداری (حتی با مانده صفر)
|
||||
has_articles = Article.objects.filter(customer=customer_obj).exists()
|
||||
|
||||
# ۲. بررسی وجود هرگونه سابقه چک یا نسیه (حتی در صورت پاس شدن و تسویه کامل)
|
||||
has_sarresids = Sarresid.objects.filter(customer=customer_obj).exists()
|
||||
|
||||
# ۳. بررسی وجود هرگونه فاکتور ثبت شده برای این مشتری
|
||||
has_factors = Factor.objects.filter(customer=customer_obj).exists()
|
||||
|
||||
# ۴. بررسی وجود هرگونه سابقه اقدام، بازدید یا نظرسنجی
|
||||
has_actions = Action.objects.filter(customer=customer_obj).exists()
|
||||
|
||||
# ۵. اگر هر کدام از این سوابق وجود داشته باشد، به هیچ عنوان اجازه غیرفعالسازی/حذف نمیدهیم
|
||||
if has_articles or has_sarresids or has_factors or has_actions:
|
||||
error_msg = f"عملیات ناموفق: مشتری '{customer_obj.name_family}' به دلیل داشتن سابقه در سیستم قابل حذف نیست! دلایل: "
|
||||
reasons = []
|
||||
|
||||
if has_articles:
|
||||
reasons.append("داشتن تراکنش مالی یا گردش حسابداری")
|
||||
if has_sarresids:
|
||||
reasons.append("داشتن سابقه چک یا نسیه (حتی تسویه شده)")
|
||||
if has_factors:
|
||||
reasons.append("داشتن فاکتور ثبتشده")
|
||||
if has_actions:
|
||||
reasons.append("داشتن سابقه اقدامات، بازدید یا نظرسنجی")
|
||||
|
||||
error_msg += "، ".join(reasons) + "."
|
||||
|
||||
# ارسال پیام خطا به تمپلیت و بازگرداندن کاربر به صفحه قبلی
|
||||
messages.error(request, error_msg)
|
||||
return redirect(request.META.get('HTTP_REFERER', '/panel/customers/'))
|
||||
|
||||
# ۶. در صورتی که مشتری کاملاً خام بود و هیچ سابقهای در هیچ بخشی نداشت، حذف میشود
|
||||
customer_obj.delete()
|
||||
messages.success(request, f"مشتری '{customer_obj.name_family}' که فاقد هرگونه سابقهای بود، با موفقیت از سیستم حذف شد.")
|
||||
return redirect('/panel/customers/')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in deleting customer {c_id}: {str(e)}", exc_info=True)
|
||||
messages.error(request, "خطای سیستمی در بررسی سوابق و حذف مشتری رخ داد.")
|
||||
return redirect(request.META.get('HTTP_REFERER', '/panel/customers/'))
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customerHesabdari(request ,c_id):
|
||||
customer = Customer.objects.get(id=c_id)
|
||||
try:
|
||||
debit = Article.objects.filter(customer=customer).aggregate(Sum('debit'))['debit__sum']
|
||||
credit = Article.objects.filter(customer=customer).aggregate(Sum('credit'))['credit__sum']
|
||||
mandehesabCs = debit - credit
|
||||
articls = Article.objects.filter(customer=customer).order_by('-createdDT')
|
||||
except:
|
||||
articls =None
|
||||
mandehesabCs = 0
|
||||
|
||||
dateNow = datetime.datetime.now().date
|
||||
data = {'customer': customer, 'mandehesabCs': mandehesabCs, 'articls': articls}
|
||||
return render(request, 'admin_panel/customerHesabDari.html', data)
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def sarresidh(request ,c_id):
|
||||
customer = Customer.objects.get(id=c_id)
|
||||
try:
|
||||
all_sarresid = Sarresid.objects.filter(customer=customer).order_by('-sarresidDT')
|
||||
except:
|
||||
all_sarresid = None
|
||||
|
||||
data = {'customer': customer, 'all_sarresid': all_sarresid}
|
||||
return render(request, 'admin_panel/customerSarresid.html', data)
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customerHesabdariAdd(request ,c_id):
|
||||
user_op = request.user
|
||||
price = request.POST.get('price', None)
|
||||
type = request.POST.get('type', None)
|
||||
typePy = request.POST.get('typePy', None)
|
||||
desc = request.POST.get('desc', None)
|
||||
nahvePardakhat = request.POST.get('nahvePardakhat', None)
|
||||
|
||||
newArticl = Article()
|
||||
newArticl.customer = Customer.objects.get(id=c_id)
|
||||
newArticl.desc = desc
|
||||
newArticl.typePy = typePy
|
||||
if type == 'credit':
|
||||
newArticl.credit = price
|
||||
newArticl.debit = 0
|
||||
elif type == 'debit':
|
||||
newArticl.debit = price
|
||||
newArticl.credit = 0
|
||||
newArticl.user = user_op
|
||||
newArticl.nahvePardakht = nahvePardakhat
|
||||
newArticl.save()
|
||||
return redirect('/panel/customer/'+str(c_id)+'/hesabdari')
|
||||
@@ -0,0 +1,465 @@
|
||||
import json
|
||||
|
||||
from django.db import transaction, connection
|
||||
from django.db.models import Sum, Q
|
||||
from django.shortcuts import redirect, get_object_or_404
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from persiantools.jdatetime import JalaliDate
|
||||
from persiantools import digits
|
||||
|
||||
from ..models import (Customer, StoreRoom, Factor, FactorIteam, Product,
|
||||
ProductInStore, Article, Sarresid)
|
||||
from ._helpers import logger, _safe_int, _parse_jalali_date, _parse_timestamp, InventoryException
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def newFactor(request, c_id):
|
||||
prdsList = request.POST.get('prdsList', '')
|
||||
sarResidDate = request.POST.get('sarResidDate', None)
|
||||
pardakhti = request.POST.get('pardakhti', None)
|
||||
#return HttpResponse(pardakhti)
|
||||
if pardakhti:
|
||||
pardakhti=int(pardakhti)
|
||||
if pardakhti>=1:
|
||||
pass
|
||||
else:
|
||||
pardakhti = None
|
||||
#return HttpResponse(sarResidDate)
|
||||
try:
|
||||
sarResidDate = digits.fa_to_en(digits.ar_to_fa(sarResidDate))
|
||||
#return HttpResponse(sarResidDate)
|
||||
sarResidDate = sarResidDate.split("/")
|
||||
#return HttpResponse(sarResidDate[2])
|
||||
sarResidDate = JalaliDate(int(sarResidDate[0]), int(sarResidDate[1]), int(sarResidDate[2])).to_gregorian()
|
||||
#return HttpResponse(sarResidDate)
|
||||
except:
|
||||
sarResidDate = None
|
||||
|
||||
#return HttpResponse(sarResidDate[1])
|
||||
#sarResidDate = JalaliDate(sarResidDate[0], sarResidDate[1], sarResidDate[2]).to_gregorian()
|
||||
|
||||
payType = request.POST.get('payType', '')
|
||||
FactorDate = request.POST.get('FactorDate', '')
|
||||
FactorType = request.POST.get('FactorType', '')
|
||||
FactorDesc = request.POST.get('FactorDesc', '')
|
||||
Factorfail = False
|
||||
customerObj=Customer.objects.get(id=c_id)
|
||||
storeUser = StoreRoom.objects.get(user__id=request.user.id)
|
||||
import json
|
||||
prdsList = json.loads(prdsList)
|
||||
sum_price = 0
|
||||
for prd in prdsList:
|
||||
if prd['garanty_Esh'] == ' ' or prd['garanty_Esh'] == '' or prd['garanty_Esh'] == None:
|
||||
sum_price += prd['prd_count'] * prd['price']
|
||||
|
||||
#return HttpResponse(prd_code)
|
||||
try:
|
||||
newFactor = Factor()
|
||||
newFactor.customer= customerObj
|
||||
newFactor.amount = sum_price
|
||||
if payType in ('naghd', 'kartKhan', 'cardToCard'):
|
||||
newFactor.pay_status = 'paid'
|
||||
else:
|
||||
newFactor.pay_status = 'unpaid'
|
||||
newFactor.FactorType = FactorType
|
||||
newFactor.PayType = payType
|
||||
newFactor.desc = FactorDesc
|
||||
if sarResidDate != None:
|
||||
newFactor.deadline = sarResidDate
|
||||
newFactor.user = User.objects.get(id=request.user.id)
|
||||
newFactor.save()
|
||||
except:
|
||||
try:
|
||||
Factorfail=True
|
||||
newFactor.delete()
|
||||
except:
|
||||
pass
|
||||
|
||||
if Factorfail != True:
|
||||
#accFactor To article
|
||||
is_naghd = False
|
||||
articleHesabDari = Article()
|
||||
articleHesabDari.debit = newFactor.amount
|
||||
articleHesabDari.credit = 0
|
||||
articleHesabDari.customer = customerObj
|
||||
articleHesabDari.factor = newFactor
|
||||
|
||||
if payType in ('naghd', 'kartKhan', 'cardToCard'):
|
||||
is_naghd = True
|
||||
|
||||
articleHesabDari.type = payType
|
||||
articleHesabDari.desc = str(FactorType) + " " + str(customerObj.name_family) + " طی فاکتور شماره: " + str(newFactor.id)
|
||||
articleHesabDari.save()
|
||||
|
||||
if is_naghd:
|
||||
articleHesabDari2 = Article()
|
||||
articleHesabDari2.debit = 0
|
||||
articleHesabDari2.credit = newFactor.amount
|
||||
articleHesabDari2.customer = customerObj
|
||||
articleHesabDari2.type = payType
|
||||
articleHesabDari2.factor = newFactor
|
||||
articleHesabDari2.desc = str(FactorType) + " " + str(customerObj.name_family) + " تسویه نقدی فاکتور شماره: " + str(newFactor.id)
|
||||
articleHesabDari2.save()
|
||||
elif pardakhti != None and pardakhti!='' and pardakhti != ' ':
|
||||
articleHesabDari2 = Article()
|
||||
articleHesabDari2.debit = 0
|
||||
articleHesabDari2.credit = pardakhti
|
||||
articleHesabDari2.customer = customerObj
|
||||
articleHesabDari2.type = payType
|
||||
articleHesabDari2.factor = newFactor
|
||||
articleHesabDari2.desc = str(customerObj.name_family) + " پرداخت نقد در محل برا فاکتور " + str(newFactor.id)
|
||||
articleHesabDari2.save()
|
||||
|
||||
for prd in prdsList:
|
||||
try:
|
||||
fIteams = FactorIteam()
|
||||
fIteams.factor = newFactor
|
||||
if prd['prd_id'] == 9999:
|
||||
Gproduct = None
|
||||
fIteams.product = Gproduct
|
||||
fIteams.product_name = prd['product_name']
|
||||
fIteams.num = prd['prd_count']
|
||||
fIteams.amount = int(prd['priceKol'])
|
||||
else:
|
||||
Gproduct = Product.objects.get(id=prd['prd_id'])
|
||||
fIteams.product = Gproduct
|
||||
fIteams.product_name = Gproduct.product_name
|
||||
fIteams.num = prd['prd_count']
|
||||
fIteams.amount = prd['prd_count'] * Gproduct.product_price
|
||||
fIteams.garantyEsh = prd['garanty_Esh']
|
||||
fIteams.save()
|
||||
except:
|
||||
Factorfail = True
|
||||
newFactor.delete()
|
||||
break
|
||||
|
||||
if Factorfail != True:
|
||||
for prd in prdsList:
|
||||
if prd['prd_id'] != 9999:
|
||||
pInStore = ProductInStore()
|
||||
sRoom = storeUser
|
||||
productO = Product.objects.get(id=prd['prd_id'])
|
||||
pInStore.StoreRoom = sRoom
|
||||
pInStore.Product = productO
|
||||
pInStore.factor = newFactor
|
||||
productS_in = ProductInStore.objects.filter(Product=productO, StoreRoom=sRoom, year=1400).aggregate(Sum('s_in'))['s_in__sum']
|
||||
productS_out = ProductInStore.objects.filter(Product=productO, StoreRoom=sRoom, year=1400).aggregate(Sum('s_out'))['s_out__sum']
|
||||
mojodi_prd = productS_in - productS_out
|
||||
if mojodi_prd >= prd['prd_count']:
|
||||
pInStore.s_out = prd['prd_count']
|
||||
if prd['garanty_Esh'] != ' ' and prd['garanty_Esh'] != '' and prd['garanty_Esh'] != None:
|
||||
pInStore.desc = "خروج: " + str(prd['prd_count']) + "#"+str(prd['garanty_Esh'])+ "#" + str(customerObj.name_family) + " * " + str(request.user.first_name) + " " + str(request.user.last_name)
|
||||
else:
|
||||
pInStore.desc = "خروج: " + str(prd['prd_count']) + "#فروش" + "#" + str(customerObj.name_family) + " * " + str(request.user.first_name) + " " + str(request.user.last_name)
|
||||
pInStore.save()
|
||||
else:
|
||||
return HttpResponse("موجودی کافی نیست، موجودی فعلی: "+ str(mojodi_prd))
|
||||
|
||||
return redirect('/panel/customer/'+str(c_id)+'/factors/')
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def newFactor2(request, c_id):
|
||||
if request.method != 'POST':
|
||||
return HttpResponseBadRequest("متد ارسالی باید POST باشد.")
|
||||
|
||||
user_op = request.user
|
||||
|
||||
try:
|
||||
prds_list = json.loads(request.POST.get('prdsList') or '[]')
|
||||
json_pardakht_ha = json.loads(request.POST.get('PardakhtHa') or '{}')
|
||||
except json.JSONDecodeError:
|
||||
return HttpResponseBadRequest("فرمت دادههای ارسالی JSON نامعتبر است.")
|
||||
|
||||
factor_type = request.POST.get('FactorType', '')
|
||||
factor_desc = request.POST.get('FactorDesc', '')
|
||||
sar_resid_date = _parse_jalali_date(request.POST.get('sarResidDate'))
|
||||
|
||||
customer_obj = get_object_or_404(Customer, id=c_id)
|
||||
store_room = get_object_or_404(StoreRoom, owners=user_op)
|
||||
|
||||
# رفع مشکل متنی/عددی در استخراج شناسه کالاها
|
||||
product_ids = []
|
||||
for item in prds_list:
|
||||
p_id = item.get('prd_id')
|
||||
if p_id and _safe_int(p_id) != 9999:
|
||||
product_ids.append(_safe_int(p_id))
|
||||
|
||||
products_by_id = {p.id: p for p in Product.objects.filter(id__in=product_ids)}
|
||||
|
||||
sum_price = sum(
|
||||
_safe_int(item.get('prd_count')) * _safe_int(item.get('price'))
|
||||
for item in prds_list
|
||||
if not str(item.get('garanty_Esh', '')).strip()
|
||||
)
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
|
||||
new_factor = Factor.objects.create(
|
||||
customer=customer_obj,
|
||||
amount=sum_price,
|
||||
FactorType=factor_type,
|
||||
desc=factor_desc,
|
||||
deadline=sar_resid_date,
|
||||
user=user_op
|
||||
)
|
||||
|
||||
Article.objects.create(
|
||||
debit=sum_price,
|
||||
credit=0,
|
||||
customer=customer_obj,
|
||||
factor=new_factor,
|
||||
desc=f"{customer_obj.name_family} فاکتور شماره: {new_factor.id}",
|
||||
user=user_op
|
||||
)
|
||||
|
||||
payment_map = {
|
||||
'l_naghd': 'نقد',
|
||||
'l_kartbekart': 'کارت به کارت',
|
||||
'l_kartKhan': 'کارت خوان',
|
||||
'l_takhfif': 'تخفیف'
|
||||
}
|
||||
|
||||
for key, amount_raw in json_pardakht_ha.items():
|
||||
amount = _safe_int(amount_raw)
|
||||
if amount <= 0:
|
||||
continue
|
||||
|
||||
if key in payment_map:
|
||||
nahve_pardakht = payment_map[key]
|
||||
Article.objects.create(
|
||||
debit=0,
|
||||
credit=amount,
|
||||
customer=customer_obj,
|
||||
factor=new_factor,
|
||||
desc=f"{nahve_pardakht} {customer_obj.name_family} فاکتور شماره: {new_factor.id}",
|
||||
nahvePardakht=nahve_pardakht,
|
||||
payStatus="true",
|
||||
user=user_op
|
||||
)
|
||||
|
||||
elif key in ['l_nesiye', 'l_checkPrice']:
|
||||
is_nesiye = (key == 'l_nesiye')
|
||||
type_str = 'nesiye' if is_nesiye else 'check'
|
||||
ts_key = 'l_nesiyeSarResid' if is_nesiye else 'l_checkPriceSarResid'
|
||||
|
||||
sar_resid_dt = _parse_timestamp(json_pardakht_ha.get(ts_key))
|
||||
|
||||
Sarresid.objects.create(
|
||||
customer=customer_obj,
|
||||
price=amount,
|
||||
payStatus="false",
|
||||
type=type_str,
|
||||
user=user_op,
|
||||
factor=new_factor,
|
||||
sarresidDT=sar_resid_dt
|
||||
)
|
||||
|
||||
factor_items_pool = []
|
||||
user_fullname = f"{user_op.first_name} {user_op.last_name}".strip() or user_op.username
|
||||
|
||||
for item in prds_list:
|
||||
# تبدیل امن شناسه به عدد
|
||||
prd_id = _safe_int(item.get('prd_id'))
|
||||
prd_count = _safe_int(item.get('prd_count'))
|
||||
garanty_esh = item.get('garanty_Esh', '')
|
||||
|
||||
if prd_id == 9999:
|
||||
factor_items_pool.append(FactorIteam(
|
||||
factor=new_factor,
|
||||
product=None,
|
||||
product_name=item.get('product_name', ''),
|
||||
num=prd_count,
|
||||
amount=_safe_int(item.get('priceKol')),
|
||||
garantyEsh=garanty_esh
|
||||
))
|
||||
else:
|
||||
product_obj = products_by_id.get(prd_id)
|
||||
|
||||
if not product_obj:
|
||||
raise InventoryException(f"خطا در دریافت محصول: محصولی با شناسه {prd_id} در سیستم وجود ندارد.")
|
||||
|
||||
factor_items_pool.append(FactorIteam(
|
||||
factor=new_factor,
|
||||
product=product_obj,
|
||||
product_name=product_obj.product_name,
|
||||
num=prd_count,
|
||||
amount=prd_count * _safe_int(product_obj.product_price),
|
||||
garantyEsh=garanty_esh
|
||||
))
|
||||
|
||||
FactorIteam.objects.bulk_create(factor_items_pool, batch_size=500)
|
||||
|
||||
# فرآیند کسر از انبار (Enterprise Level)
|
||||
for item in prds_list:
|
||||
prd_id = _safe_int(item.get('prd_id'))
|
||||
if prd_id == 9999:
|
||||
continue
|
||||
|
||||
prd_count = _safe_int(item.get('prd_count'))
|
||||
garanty_esh = item.get('garanty_Esh', '')
|
||||
product_obj = products_by_id.get(prd_id)
|
||||
|
||||
if not product_obj:
|
||||
continue
|
||||
|
||||
# ۱. کوئری پایه
|
||||
qs = ProductInStore.objects.filter(
|
||||
Product=product_obj,
|
||||
StoreRoom=store_room,
|
||||
year=1400
|
||||
)
|
||||
|
||||
# ۲. قفل دیتابیس هوشمند و سازگار با تمام پایگاههای داده
|
||||
if connection.features.has_select_for_update:
|
||||
qs = qs.select_for_update()
|
||||
|
||||
# ۳. بارگذاری امن در رم برای جلوگیری از خطای Aggregate در دیتابیس قفلشده
|
||||
locked_records = list(qs)
|
||||
|
||||
# ۴. محاسبه دقیق و پرسرعت در پایتون
|
||||
total_in = sum(rec.s_in for rec in locked_records if rec.s_in)
|
||||
total_out = sum(rec.s_out for rec in locked_records if rec.s_out)
|
||||
mojodi_prd = total_in - total_out
|
||||
|
||||
if mojodi_prd < prd_count:
|
||||
# استفاده از متن دقیق خطای قبلی جهت حفظ سازگاری با فرانتاند
|
||||
raise InventoryException(f"موجودی کافی نیست، موجودی فعلی {product_obj.product_name}: {mojodi_prd}")
|
||||
|
||||
# ۵. تشخیص نوع تراکنش برای سیستم گزارشگیری جدید انبار
|
||||
if garanty_esh and str(garanty_esh).strip():
|
||||
desc_text = f"خروج: {prd_count}#{garanty_esh}#{customer_obj.name_family} * {user_fullname}"
|
||||
|
||||
if "اشانتیون" in str(garanty_esh):
|
||||
m_type = ProductInStore.MovementType.PROMOTION
|
||||
else:
|
||||
m_type = ProductInStore.MovementType.WARRANTY
|
||||
else:
|
||||
desc_text = f"خروج: {prd_count}#فروش#{customer_obj.name_family} * {user_fullname}"
|
||||
m_type = ProductInStore.MovementType.SALE
|
||||
|
||||
# ۶. ثبت تراکنش خروجی انبار
|
||||
ProductInStore.objects.create(
|
||||
StoreRoom=store_room,
|
||||
Product=product_obj,
|
||||
factor=new_factor,
|
||||
s_out=prd_count,
|
||||
desc=desc_text,
|
||||
movement_type=m_type, # ثبت ساختاریافته نوع خروج برای سرعت در گزارشگیری
|
||||
year=1400
|
||||
)
|
||||
|
||||
except InventoryException as e:
|
||||
return HttpResponse(str(e), status=400)
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating factor for customer {c_id}: {str(e)}", exc_info=True)
|
||||
return HttpResponse("خطایی غیرمنتظره در سرور رخ داد. اطلاعات ذخیره نشد.", status=500)
|
||||
|
||||
return redirect(f'/panel/customer/{c_id}/factors/')
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.DelFactor', raise_exception=True)
|
||||
@require_POST
|
||||
def FactorDel(request, c_id, f_id):
|
||||
customer_obj = get_object_or_404(Customer, id=c_id)
|
||||
factor_initial = get_object_or_404(Factor, id=f_id, customer=customer_obj)
|
||||
|
||||
if factor_initial.pay_status == 'cancell' or not factor_initial.is_active:
|
||||
return HttpResponseBadRequest("این فاکتور قبلاً باطل یا برگشت داده شده است.")
|
||||
|
||||
user_fullname = f"{request.user.first_name} {request.user.last_name}".strip() or request.user.username
|
||||
cancel_reason = request.POST.get('cancel_reason', 'error')
|
||||
payment_action = request.POST.get('payment_action', 'keep')
|
||||
|
||||
if cancel_reason == 'return':
|
||||
reason_text = "برگشت از فروش (مرجوعی مشتری)"
|
||||
inv_movement = ProductInStore.MovementType.RETURN
|
||||
else:
|
||||
reason_text = "اصلاح خطای کاربری"
|
||||
inv_movement = ProductInStore.MovementType.ERROR_CORRECTION
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
if connection.features.has_select_for_update:
|
||||
factor = Factor.objects.select_for_update().get(pk=factor_initial.pk)
|
||||
else:
|
||||
factor = Factor.objects.get(pk=factor_initial.pk)
|
||||
|
||||
if factor.pay_status == 'cancell' or not factor.is_active:
|
||||
return HttpResponseBadRequest("خطای همزمانی: این فاکتور در لحظه فرستادن درخواست باطل شده بود.")
|
||||
|
||||
factor.is_active = False
|
||||
factor.pay_status = 'cancell'
|
||||
factor.save(update_fields=['is_active', 'pay_status'])
|
||||
|
||||
# واکشی اسناد پرداخت قبل از ایجاد سند جدید (جلوگیری از باگ تکرار سند)
|
||||
payments_to_reverse = []
|
||||
if payment_action == 'reverse' or cancel_reason == 'error':
|
||||
payments_to_reverse = list(Article.objects.filter(
|
||||
Q(debit=0) | Q(debit__isnull=True),
|
||||
factor=factor,
|
||||
credit__gt=0
|
||||
))
|
||||
|
||||
# ۱. صدور سند معکوس مالی برای کل مبلغ فاکتور
|
||||
factor_amount = int(factor.amount or 0)
|
||||
if factor_amount > 0:
|
||||
Article.objects.create(
|
||||
debit=0,
|
||||
credit=factor_amount,
|
||||
customer=customer_obj,
|
||||
factor=factor,
|
||||
type=factor.PayType,
|
||||
desc=f"برگشت از فروش و ابطال فاکتور شماره {factor.id} (مرجوعی کالا توسط مشتری) - کاربر: {user_fullname}" if cancel_reason == 'return' else f"اصلاح خطای سیستم: ابطال فاکتور اشتباه شماره {factor.id} (بدون جابجایی واقعی کالا) - کاربر: {user_fullname}",
|
||||
user=request.user,
|
||||
payStatus="true"
|
||||
)
|
||||
|
||||
# ۲. خنثی کردن پرداختهای اولیه فاکتور
|
||||
for p in payments_to_reverse:
|
||||
Article.objects.create(
|
||||
debit=p.credit,
|
||||
credit=0,
|
||||
customer=customer_obj,
|
||||
factor=factor,
|
||||
type=p.type,
|
||||
nahvePardakht=p.nahvePardakht,
|
||||
desc=f"عودت وجه پرداخت شده / ابطال رسید فاکتور {factor.id} (به دلیل مرجوعی کالا) - کاربر: {user_fullname}" if cancel_reason == 'return' else f"اصلاح خطای سیستم: ابطال رسید پرداخت فاکتور اشتباه شماره {factor.id} - کاربر: {user_fullname}",
|
||||
user=request.user,
|
||||
payStatus="true"
|
||||
)
|
||||
|
||||
# ۳. 🛠️ اصلاح انبار (حل باگ عدم تغییر موجودی سیستم)
|
||||
stock_movements = list(ProductInStore.objects.filter(factor=factor, s_out__gt=0))
|
||||
|
||||
for movement in stock_movements:
|
||||
pInStore = ProductInStore(
|
||||
StoreRoom=movement.StoreRoom,
|
||||
Product=movement.Product,
|
||||
factor=factor,
|
||||
s_in=movement.s_out, # برگرداندن تعداد کسر شده به ستون ورودی
|
||||
s_out=0,
|
||||
year=movement.year, # 💡 فیکس قطعی: استفاده دقیق از سال مالی تراکنش اصلی (۱۴۰۰) جهت همخوانی با توابع سیستم
|
||||
desc=f"ورود به انبار بابت برگشت از فروش کالا (فاکتور {factor.id}) - کاربر: {user_fullname}" if cancel_reason == 'return' else f"ورود به انبار بابت اصلاح خطای ثبت فاکتور {factor.id} - کاربر: {user_fullname}",
|
||||
movement_type=inv_movement
|
||||
)
|
||||
pInStore.save() # فراخوانی متد save مدل شما که محاسبات تاریخچه و موجودی لحظهای را انجام میدهد
|
||||
|
||||
# ۴. ابطال اقساط و سررسیدهای پاس نشده
|
||||
Sarresid.objects.filter(factor=factor, payStatus="false").update(
|
||||
payStatus="cancell",
|
||||
desc=f"ابطال بابت {reason_text} فاکتور {factor.id}"
|
||||
)
|
||||
|
||||
logger.info(f"Factor {factor.id} successfully cancelled. Reason: {cancel_reason}, Payment: {payment_action}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Critical error in FactorDel for factor {f_id}: {str(e)}", exc_info=True)
|
||||
return HttpResponse("خطای سیستمی در فرآیند ابطال و صدور اسناد اصلاحی رخ داد.", status=500)
|
||||
|
||||
return redirect('admin_panel:customerFactors', c_id=c_id)
|
||||
@@ -0,0 +1,974 @@
|
||||
import datetime
|
||||
import json
|
||||
|
||||
from django.db import transaction
|
||||
from django.db.models import (Sum, Q, Count, Avg, Min, F, Subquery, OuterRef,
|
||||
IntegerField, Value, Case, When)
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.shortcuts import redirect, render, get_object_or_404
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from persiantools.jdatetime import JalaliDate
|
||||
|
||||
from ..models import (Article, Sarresid, Customer, Factor, ProductInStore,
|
||||
StoreRoom, Action)
|
||||
from ._helpers import logger, _safe_int
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||||
def report(request):
|
||||
|
||||
nahvePardakhat = [
|
||||
{'fa': 'تخفیف', 'en': 'تخفیف'},
|
||||
{'fa': 'نسیه', 'en': 'نسیه'},
|
||||
{'fa': 'چک', 'en': 'چک'},
|
||||
{'fa': 'کارتخوان', 'en': 'کارت خوان'},
|
||||
{'fa': 'کارت به کارت', 'en': 'کارت به کارت'},
|
||||
{'fa': 'نقدی', 'en': 'نقد'},
|
||||
]
|
||||
|
||||
userHa = User.objects.filter()
|
||||
|
||||
if request.method == "GET":
|
||||
nahvePardakht = request.GET.get("nahvepardakht", None)
|
||||
user = request.GET.get("user", None)
|
||||
azTarikh = request.GET.get("azTarikh", None)
|
||||
taTarikh = request.GET.get("taTarikh", None)
|
||||
pagenumber = request.GET.get("page", None)
|
||||
|
||||
|
||||
if azTarikh and nahvePardakht:
|
||||
|
||||
query_data = {
|
||||
'nahvePardakht': nahvePardakht,
|
||||
'user': user,
|
||||
'azTarikh': azTarikh,
|
||||
'taTarikh': taTarikh,
|
||||
}
|
||||
|
||||
azDateTime = datetime.datetime.fromtimestamp(int(azTarikh) / 1e3)
|
||||
azDateTime = azDateTime.strftime('%Y-%m-%d 00:00:00')
|
||||
|
||||
taDateTime = datetime.datetime.fromtimestamp(int(taTarikh) / 1e3)
|
||||
taDateTime = taDateTime.strftime('%Y-%m-%d 23:59:59')
|
||||
print("b")
|
||||
user_a = User.objects.get(username=user)
|
||||
print(user_a)
|
||||
|
||||
if nahvePardakht == "نسیه" or nahvePardakht == "چک":
|
||||
if nahvePardakht == 'نسیه':
|
||||
typeSarresid = 'nesiye'
|
||||
elif nahvePardakht == 'چک':
|
||||
typeSarresid = 'check'
|
||||
|
||||
|
||||
sumSarresidha = Sarresid.objects.filter(
|
||||
type=typeSarresid,
|
||||
user=user_a,
|
||||
createdDT__lte=taDateTime,
|
||||
createdDT__gte=azDateTime,
|
||||
payStatus='false'
|
||||
).aggregate(Sum('price'))
|
||||
|
||||
sarresidha = Sarresid.objects.filter(
|
||||
type=typeSarresid,
|
||||
user=user_a,
|
||||
createdDT__lte=taDateTime,
|
||||
createdDT__gte=azDateTime,
|
||||
payStatus='false'
|
||||
)
|
||||
|
||||
paginator = Paginator(sarresidha, 25) # Show 25 contacts per page.
|
||||
page_number = request.GET.get('page', None)
|
||||
all_sarresidha = paginator.get_page(pagenumber)
|
||||
data = {'sarresidha': all_sarresidha ,'sumSarresidha': sumSarresidha, 'nahvePardakhat': nahvePardakhat, 'userHa':userHa, 'query_data': query_data}
|
||||
return render(request, 'admin_panel/reportCheckNesiye.html', data)
|
||||
|
||||
else:
|
||||
|
||||
sumAricles = Article.objects.filter(
|
||||
nahvePardakht=nahvePardakht,
|
||||
user=user_a,
|
||||
createdDT__lte=taDateTime,
|
||||
createdDT__gte=azDateTime,
|
||||
).aggregate(Sum('credit'))
|
||||
|
||||
aricles1 = Article.objects.filter(
|
||||
nahvePardakht=nahvePardakht,
|
||||
user=user_a,
|
||||
createdDT__lte=taDateTime,
|
||||
createdDT__gte=azDateTime,
|
||||
)
|
||||
|
||||
paginator = Paginator(aricles1, 25) # Show 25 contacts per page.
|
||||
page_number = request.GET.get('page', None)
|
||||
all_aricles = paginator.get_page(page_number)
|
||||
data = {'aricles': all_aricles,'sum_credit': sumAricles, 'nahvePardakhat': nahvePardakhat, 'userHa':userHa, 'query_data': query_data}
|
||||
return render(request, 'admin_panel/report.html', data)
|
||||
else:
|
||||
data = {'nahvePardakhat': nahvePardakhat, 'userHa':userHa}
|
||||
return render(request, 'admin_panel/report.html', data)
|
||||
|
||||
|
||||
data = {'nahvePardakhat': nahvePardakhat, 'userHa':userHa}
|
||||
return render(request, 'admin_panel/report.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||||
def report_debtors_no_vosol(request):
|
||||
now = datetime.datetime.now()
|
||||
|
||||
# --- ۱. دریافت و اعتبارسنجی پارامترهای فیلتر از فرانتاند ---
|
||||
try:
|
||||
days_threshold = int(request.GET.get('days', 20))
|
||||
if days_threshold < 0:
|
||||
days_threshold = 20
|
||||
except ValueError:
|
||||
days_threshold = 20
|
||||
|
||||
filter_type = request.GET.get('filter_type', 'all') # all, never, expired
|
||||
sort_field = request.GET.get('sort', 'amount') # amount, oldest_date, last_action
|
||||
sort_dir = request.GET.get('dir', 'desc') # asc, desc
|
||||
|
||||
threshold_date = now - datetime.timedelta(days=days_threshold)
|
||||
|
||||
# --- ۲. ساخت Subquery های فوقسریع و ایمن ---
|
||||
sq_debit = Article.objects.filter(customer=OuterRef('pk')).values('customer').annotate(s=Sum('debit')).values('s')
|
||||
sq_credit = Article.objects.filter(customer=OuterRef('pk')).values('customer').annotate(s=Sum('credit')).values('s')
|
||||
|
||||
sq_overdue_amt = Sarresid.objects.filter(
|
||||
customer=OuterRef('pk'), payStatus='false', sarresidDT__lte=now
|
||||
).values('customer').annotate(s=Sum('price')).values('s')
|
||||
|
||||
sq_oldest_date = Sarresid.objects.filter(
|
||||
customer=OuterRef('pk'), payStatus='false', sarresidDT__lte=now
|
||||
).values('customer').annotate(min_dt=Min('sarresidDT')).values('min_dt')
|
||||
|
||||
sq_overdue_count = Sarresid.objects.filter(
|
||||
customer=OuterRef('pk'), payStatus='false', sarresidDT__lte=now
|
||||
).values('customer').annotate(c=Count('id')).values('c')
|
||||
|
||||
# استخراج آخرین سابقه اکشن وصول
|
||||
latest_vosol_qs = Action.objects.filter(
|
||||
customer=OuterRef('pk'), actionType='vosol'
|
||||
).order_by('-createdDT')
|
||||
|
||||
# --- ۳. ساخت کوئری پایه (فقط مشتریان فعال و دارای معوقه) ---
|
||||
base_qs = Customer.objects.filter(is_active=True).annotate(
|
||||
t_debit=Coalesce(Subquery(sq_debit, output_field=IntegerField()), Value(0)),
|
||||
t_credit=Coalesce(Subquery(sq_credit, output_field=IntegerField()), Value(0)),
|
||||
overdue_amount=Coalesce(Subquery(sq_overdue_amt, output_field=IntegerField()), Value(0)),
|
||||
overdue_count=Coalesce(Subquery(sq_overdue_count, output_field=IntegerField()), Value(0)),
|
||||
oldest_overdue_date=Subquery(sq_oldest_date),
|
||||
last_vosol_date=Subquery(latest_vosol_qs.values('createdDT')[:1]),
|
||||
last_vosol_desc=Subquery(latest_vosol_qs.values('desc')[:1])
|
||||
).annotate(
|
||||
net_debt=F('t_debit') - F('t_credit')
|
||||
).filter(
|
||||
overdue_amount__gt=0 # شرط اساسی: فقط کسانی که معوقه سررسید گذشته دارند
|
||||
)
|
||||
|
||||
# استخراج مشتریانی که نیازمند پیگیری هستند (هرگز پیگیری نشدهاند یا موعد پیگیریشان گذشته است)
|
||||
action_needed_qs = base_qs.filter(
|
||||
Q(last_vosol_date__isnull=True) | Q(last_vosol_date__lt=threshold_date)
|
||||
)
|
||||
|
||||
# --- ۴. محاسبه آمار دقیق (بدون خطا) برای داشبورد ---
|
||||
# استفاده از Case/When برای جلوگیری کامل از خطای None در Aggregate
|
||||
stats = action_needed_qs.aggregate(
|
||||
never_count=Coalesce(Sum(Case(When(last_vosol_date__isnull=True, then=1), default=Value(0), output_field=IntegerField())), Value(0)),
|
||||
never_amount=Coalesce(Sum(Case(When(last_vosol_date__isnull=True, then=F('overdue_amount')), default=Value(0), output_field=IntegerField())), Value(0)),
|
||||
expired_count=Coalesce(Sum(Case(When(last_vosol_date__isnull=False, then=1), default=Value(0), output_field=IntegerField())), Value(0)),
|
||||
expired_amount=Coalesce(Sum(Case(When(last_vosol_date__isnull=False, then=F('overdue_amount')), default=Value(0), output_field=IntegerField())), Value(0)),
|
||||
)
|
||||
|
||||
# --- ۵. اعمال فیلتر کشویی (Dropdown) ---
|
||||
if filter_type == 'never':
|
||||
final_qs = action_needed_qs.filter(last_vosol_date__isnull=True)
|
||||
elif filter_type == 'expired':
|
||||
final_qs = action_needed_qs.filter(last_vosol_date__isnull=False)
|
||||
else: # all
|
||||
final_qs = action_needed_qs
|
||||
|
||||
# --- ۶. اعمال مرتبسازی (Sorting) پویا ---
|
||||
if sort_field == 'amount':
|
||||
order_expr = F('overdue_amount').desc() if sort_dir == 'desc' else F('overdue_amount').asc()
|
||||
elif sort_field == 'oldest_date':
|
||||
order_expr = F('oldest_overdue_date').desc(nulls_last=True) if sort_dir == 'desc' else F('oldest_overdue_date').asc(nulls_last=True)
|
||||
elif sort_field == 'last_action':
|
||||
order_expr = F('last_vosol_date').desc(nulls_last=True) if sort_dir == 'desc' else F('last_vosol_date').asc(nulls_first=True)
|
||||
else:
|
||||
order_expr = F('overdue_amount').desc()
|
||||
|
||||
final_qs = final_qs.order_by(order_expr)
|
||||
|
||||
# --- ۷. صفحهبندی ایمن ---
|
||||
paginator = Paginator(final_qs, 50)
|
||||
page_number = request.GET.get('page', 1)
|
||||
try:
|
||||
page_obj = paginator.page(page_number)
|
||||
except (PageNotAnInteger, EmptyPage):
|
||||
page_obj = paginator.page(1)
|
||||
|
||||
context = {
|
||||
'page_obj': page_obj,
|
||||
'page_name': 'debtors_no_vosol',
|
||||
'stats': stats,
|
||||
# پارامترهای کنترل پنل
|
||||
'current_days': days_threshold,
|
||||
'current_filter': filter_type,
|
||||
'current_sort': sort_field,
|
||||
'current_dir': sort_dir,
|
||||
}
|
||||
|
||||
return render(request, 'admin_panel/report_debtors_no_vosol.html', context)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||||
def inventory_movements_report(request):
|
||||
"""
|
||||
گزارش جامع گردش و خروج کالا از انبار به همراه فیلتر متنی نام کالا،
|
||||
نوع تراکنش، کاربر، انبار و بازه زمانی شمسی.
|
||||
"""
|
||||
all_users = User.objects.all().order_by('first_name')
|
||||
all_stores = StoreRoom.objects.all()
|
||||
movement_choices = ProductInStore.MovementType.choices
|
||||
|
||||
# دریافت پارامترهای فیلتر از فرانتاند
|
||||
selected_type = request.GET.get('movement_type', '')
|
||||
selected_user = request.GET.get('user_id', '')
|
||||
selected_store = request.GET.get('store_id', '')
|
||||
selected_product_name = request.GET.get('product_name', '').strip() # فیلد جدید جستجوی نام کالا
|
||||
az_tarikh = request.GET.get('azTarikh', '')
|
||||
ta_tarikh = request.GET.get('taTarikh', '')
|
||||
|
||||
# ۱. کوئری پایه بهینه
|
||||
movements_list = ProductInStore.objects.select_related(
|
||||
'Product', 'StoreRoom', 'factor__customer', 'factor__user'
|
||||
).filter(year=1400).order_by('-createdDT')
|
||||
|
||||
# ۲. اعمال فیلترهای پویا
|
||||
if selected_type:
|
||||
movements_list = movements_list.filter(movement_type=selected_type)
|
||||
|
||||
if selected_user:
|
||||
movements_list = movements_list.filter(factor__user__id=_safe_int(selected_user))
|
||||
|
||||
if selected_store:
|
||||
movements_list = movements_list.filter(StoreRoom__id=_safe_int(selected_store))
|
||||
|
||||
if selected_product_name:
|
||||
# فیلتر متنی حساس نبودن به حروف بزرگ و کوچک (Case-Insensitive Contains)
|
||||
movements_list = movements_list.filter(Product__product_name__icontains=selected_product_name)
|
||||
|
||||
# ۳. پردازش بازه زمانی
|
||||
if az_tarikh:
|
||||
try:
|
||||
az_dt = datetime.datetime.fromtimestamp(int(az_tarikh) / 1000.0)
|
||||
movements_list = movements_list.filter(createdDT__gte=az_dt.strftime('%Y-%m-%d 00:00:00'))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
if ta_tarikh:
|
||||
try:
|
||||
ta_dt = datetime.datetime.fromtimestamp(int(ta_tarikh) / 1000.0)
|
||||
movements_list = movements_list.filter(createdDT__lte=ta_dt.strftime('%Y-%m-%d 23:59:59'))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# ۴. محاسبه آماری سریع دیتابیس روی نتایج فیلتر شده
|
||||
summary_stats = movements_list.aggregate(
|
||||
total_out_qty=Sum('s_out'),
|
||||
total_in_qty=Sum('s_in')
|
||||
)
|
||||
|
||||
# ۵. صفحهبندی نتایج
|
||||
paginator = Paginator(movements_list, 50)
|
||||
page_number = request.GET.get('page')
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
context = {
|
||||
'page_obj': page_obj,
|
||||
'all_users': all_users,
|
||||
'all_stores': all_stores,
|
||||
'movement_choices': movement_choices,
|
||||
'summary_stats': summary_stats,
|
||||
'filters': {
|
||||
'movement_type': selected_type,
|
||||
'user_id': _safe_int(selected_user) if selected_user else '',
|
||||
'store_id': _safe_int(selected_store) if selected_store else '',
|
||||
'product_name': selected_product_name, # ارسال فیلد جدید به قالب برای حفظ مقدار ورودی
|
||||
'azTarikh': az_tarikh,
|
||||
'taTarikh': ta_tarikh,
|
||||
}
|
||||
}
|
||||
|
||||
return render(request, 'admin_panel/report_inventory_movements.html', context)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||||
def financial_dashboard(request):
|
||||
"""
|
||||
داشبورد هوش مالی و مدیریت نقدینگی (BI Dashboard)
|
||||
مجهز به هسته حسابرسی ۳۶۰ درجه برای کشف تمامی مغایرتها (بدهی رو هوا، چکهای الکی، پیشدریافتها)
|
||||
"""
|
||||
az_tarikh = request.GET.get('azTarikh', '')
|
||||
ta_tarikh = request.GET.get('taTarikh', '')
|
||||
|
||||
az_dt = None
|
||||
ta_dt = None
|
||||
|
||||
if az_tarikh:
|
||||
try:
|
||||
az_dt = datetime.datetime.fromtimestamp(int(az_tarikh) / 1000.0)
|
||||
az_dt = az_dt.replace(hour=0, minute=0, second=0)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
if ta_tarikh:
|
||||
try:
|
||||
ta_dt = datetime.datetime.fromtimestamp(int(ta_tarikh) / 1000.0)
|
||||
ta_dt = ta_dt.replace(hour=23, minute=59, second=59)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
anchor_dt = ta_dt if ta_dt else datetime.datetime.now()
|
||||
|
||||
# =========================================================================
|
||||
# محاسبه روند ۱۲ ماهه غلتان (TTM)
|
||||
# =========================================================================
|
||||
jalali_months = ['فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور', 'مهر', 'آبان', 'آذر', 'دی', 'بهمن', 'اسفند']
|
||||
trend_labels = []
|
||||
trend_data = []
|
||||
|
||||
anchor_jd = JalaliDate(anchor_dt.date())
|
||||
curr_y = int(anchor_jd.year)
|
||||
curr_m = int(anchor_jd.month)
|
||||
|
||||
for _ in range(12):
|
||||
if curr_m < 1:
|
||||
curr_m = 12
|
||||
curr_y -= 1
|
||||
|
||||
label = f"{jalali_months[curr_m - 1]} {curr_y}"
|
||||
trend_labels.insert(0, label)
|
||||
|
||||
last_day = JalaliDate.days_in_month(curr_m, curr_y)
|
||||
end_of_month_jd = JalaliDate(curr_y, curr_m, last_day)
|
||||
end_of_month_gd = end_of_month_jd.to_gregorian()
|
||||
end_dt_str = end_of_month_gd.strftime('%Y-%m-%d 23:59:59')
|
||||
|
||||
totals = Article.objects.filter(createdDT__lte=end_dt_str).aggregate(
|
||||
t_deb=Sum('debit'),
|
||||
t_cred=Sum('credit')
|
||||
)
|
||||
t_deb = totals['t_deb'] or 0
|
||||
t_cred = totals['t_cred'] or 0
|
||||
month_total_debt = t_deb - t_cred
|
||||
|
||||
trend_data.insert(0, month_total_debt)
|
||||
curr_m -= 1
|
||||
|
||||
# =========================================================================
|
||||
# فیلترهای پایه (دادههای زماندار و داشبورد)
|
||||
# =========================================================================
|
||||
sarresid_qs = Sarresid.objects.filter(factor__productinstore__year=1400).distinct()
|
||||
factor_qs = Factor.objects.filter(productinstore__year=1400, is_active=True).distinct()
|
||||
article_qs = Article.objects.filter(factor__productinstore__year=1400).distinct()
|
||||
store_qs = ProductInStore.objects.filter(year=1400)
|
||||
|
||||
if az_dt:
|
||||
sarresid_qs = sarresid_qs.filter(createdDT__gte=az_dt)
|
||||
factor_qs = factor_qs.filter(create_date__gte=az_dt)
|
||||
article_qs = article_qs.filter(createdDT__gte=az_dt)
|
||||
store_qs = store_qs.filter(createdDT__gte=az_dt)
|
||||
|
||||
if ta_dt:
|
||||
sarresid_qs = sarresid_qs.filter(createdDT__lte=ta_dt)
|
||||
factor_qs = factor_qs.filter(create_date__lte=ta_dt)
|
||||
article_qs = article_qs.filter(createdDT__lte=ta_dt)
|
||||
store_qs = store_qs.filter(createdDT__lte=ta_dt)
|
||||
|
||||
# کارتهای شاخص عملکرد عمومی
|
||||
total_unpaid_debts = sarresid_qs.filter(payStatus="false").aggregate(Sum('price'))['price__sum'] or 0
|
||||
total_sales_period = factor_qs.aggregate(Sum('amount'))['amount__sum'] or 0
|
||||
overdue_checks_count = sarresid_qs.filter(payStatus="false", type="check", sarresidDT__lte=anchor_dt).count()
|
||||
|
||||
# =========================================================================
|
||||
# 👑 هسته حسابرسی ۳۶۰ درجه (برگرفته از Ghost Hunter / Deep Audit)
|
||||
# =========================================================================
|
||||
|
||||
# الف) تراز کل ریاضی سیستم
|
||||
g_art = Article.objects.aggregate(d=Sum('debit'), c=Sum('credit'))
|
||||
global_mande = (g_art['d'] or 0) - (g_art['c'] or 0)
|
||||
g_sar = Sarresid.objects.filter(payStatus="false").aggregate(s=Sum('price'))['s'] or 0
|
||||
global_diff = global_mande - g_sar
|
||||
|
||||
# ب) محاسبات Subquery فوقسریع برای تمام مشتریان
|
||||
sq_debit = Article.objects.filter(customer=OuterRef('pk')).values('customer').annotate(s=Sum('debit')).values('s')
|
||||
sq_credit = Article.objects.filter(customer=OuterRef('pk')).values('customer').annotate(s=Sum('credit')).values('s')
|
||||
sq_sarresid = Sarresid.objects.filter(customer=OuterRef('pk'), payStatus="false").values('customer').annotate(s=Sum('price')).values('s')
|
||||
|
||||
base_customers = Customer.objects.filter(is_active=True).annotate(
|
||||
t_debit=Coalesce(Subquery(sq_debit, output_field=IntegerField()), 0),
|
||||
t_credit=Coalesce(Subquery(sq_credit, output_field=IntegerField()), 0),
|
||||
t_sarresid=Coalesce(Subquery(sq_sarresid, output_field=IntegerField()), 0),
|
||||
).annotate(
|
||||
net_debt=F('t_debit') - F('t_credit') # تراز نهایی
|
||||
).annotate(
|
||||
diff=F('net_debt') - F('t_sarresid') # میزان کسر/اضافه سند
|
||||
)
|
||||
|
||||
# 🔴 گروه 1: بدهکارانی که کسر سند/چک دارند (بدهی خطرناک روی هوا) [diff > 0]
|
||||
group1_qs = base_customers.filter(diff__gt=0).order_by('-diff')
|
||||
group1_count = group1_qs.count()
|
||||
group1_total = sum(group1_qs.values_list('diff', flat=True)) or 0
|
||||
group1_sample = group1_qs[:20]
|
||||
|
||||
# 🟡 گروه 2: چک پاسنشده در سیستم بیشتر از بدهی است (خطای ثبتی اپراتور) [diff < 0 AND net_debt >= 0]
|
||||
group2_qs = base_customers.filter(diff__lt=0, net_debt__gte=0).annotate(
|
||||
fake_amount=F('t_sarresid') - F('net_debt')
|
||||
).order_by('-fake_amount')
|
||||
group2_count = group2_qs.count()
|
||||
group2_total = sum(group2_qs.values_list('fake_amount', flat=True)) or 0
|
||||
group2_sample = group2_qs[:20]
|
||||
|
||||
# 🟢 گروه 3: پیشدریافت خالص (بستانکار است و هیچ چکی ندارد) [diff < 0 AND net_debt < 0 AND t_sarresid = 0]
|
||||
group3_qs = base_customers.filter(diff__lt=0, net_debt__lt=0, t_sarresid=0).order_by('net_debt') # net_debt منفی است
|
||||
group3_count = group3_qs.count()
|
||||
group3_total = sum(group3_qs.values_list('net_debt', flat=True)) or 0
|
||||
group3_sample = group3_qs[:20]
|
||||
|
||||
# 🟣 گروه 4: عجیبترین حالت! مشتری بستانکار است، اما چک پاسنشده هم در سیستم دارد! [diff < 0 AND net_debt < 0 AND t_sarresid > 0]
|
||||
group4_qs = base_customers.filter(diff__lt=0, net_debt__lt=0, t_sarresid__gt=0).order_by('net_debt')
|
||||
group4_count = group4_qs.count()
|
||||
group4_total = sum(group4_qs.values_list('diff', flat=True)) or 0
|
||||
group4_sample = group4_qs[:20]
|
||||
|
||||
# =========================================================================
|
||||
# نمودارها و آمار تکمیلی داشبورد
|
||||
# =========================================================================
|
||||
debt_future = sarresid_qs.filter(payStatus="false", sarresidDT__gt=anchor_dt).aggregate(Sum('price'))['price__sum'] or 0
|
||||
dt_30_days_ago = anchor_dt - datetime.timedelta(days=30)
|
||||
debt_0_30 = sarresid_qs.filter(payStatus="false", sarresidDT__lte=anchor_dt, sarresidDT__gte=dt_30_days_ago).aggregate(Sum('price'))['price__sum'] or 0
|
||||
dt_90_days_ago = anchor_dt - datetime.timedelta(days=90)
|
||||
debt_30_90 = sarresid_qs.filter(payStatus="false", sarresidDT__lt=dt_30_days_ago, sarresidDT__gte=dt_90_days_ago).aggregate(Sum('price'))['price__sum'] or 0
|
||||
debt_above_90 = sarresid_qs.filter(payStatus="false", sarresidDT__lt=dt_90_days_ago).aggregate(Sum('price'))['price__sum'] or 0
|
||||
|
||||
payment_articles = article_qs.filter(credit__gt=0).values('nahvePardakht').annotate(total_amount=Sum('credit')).order_by('-total_amount')
|
||||
pay_labels = []
|
||||
pay_data = []
|
||||
for item in payment_articles:
|
||||
label = item['nahvePardakht'] or 'سایر/نامشخص'
|
||||
pay_labels.append(label)
|
||||
pay_data.append(item['total_amount'])
|
||||
|
||||
store_leaks = store_qs.filter(
|
||||
movement_type__in=[ProductInStore.MovementType.WARRANTY, ProductInStore.MovementType.PROMOTION]
|
||||
).values('movement_type').annotate(total_qty=Sum('s_out'))
|
||||
|
||||
leak_data = {'WARRANTY': 0, 'PROMOTION': 0}
|
||||
for leak in store_leaks:
|
||||
leak_data[leak['movement_type']] = leak['total_qty']
|
||||
|
||||
context = {
|
||||
'kpi_total_unpaid': total_unpaid_debts,
|
||||
'kpi_total_sales': total_sales_period,
|
||||
'kpi_overdue_checks': overdue_checks_count,
|
||||
|
||||
# آمار کلان ریاضیاتی
|
||||
'global_mande': global_mande,
|
||||
'g_sar': g_sar,
|
||||
'global_diff': global_diff,
|
||||
|
||||
# متغیرهای ۴ گروه حسابرسی
|
||||
'group1_count': group1_count, 'group1_total': group1_total, 'group1_sample': group1_sample,
|
||||
'group2_count': group2_count, 'group2_total': group2_total, 'group2_sample': group2_sample,
|
||||
'group3_count': group3_count, 'group3_total': group3_total, 'group3_sample': group3_sample,
|
||||
'group4_count': group4_count, 'group4_total': group4_total, 'group4_sample': group4_sample,
|
||||
|
||||
'trend_labels': trend_labels,
|
||||
'trend_data': trend_data,
|
||||
|
||||
'aging_data': [debt_future, debt_0_30, debt_30_90, debt_above_90],
|
||||
'pay_labels': pay_labels,
|
||||
'pay_data': pay_data,
|
||||
'warranty_qty': leak_data['WARRANTY'],
|
||||
'promotion_qty': leak_data['PROMOTION'],
|
||||
|
||||
'filters': {
|
||||
'azTarikh': az_tarikh,
|
||||
'taTarikh': ta_tarikh,
|
||||
}
|
||||
}
|
||||
|
||||
return render(request, 'admin_panel/financial_dashboard.html', context)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||||
def bedehi(request):
|
||||
try:
|
||||
debit = Article.objects.filter().aggregate(Sum('debit'))['debit__sum']
|
||||
credit = Article.objects.filter().aggregate(Sum('credit'))['credit__sum']
|
||||
mandehesabCs = debit - credit
|
||||
articls = Article.objects.filter().order_by('-createdDT')
|
||||
articlsPaginator = Paginator(articls, 10)
|
||||
pageNumber = request.GET.get("page", 1)
|
||||
articl_objs = articlsPaginator.get_page(pageNumber)
|
||||
|
||||
except:
|
||||
articls =None
|
||||
mandehesabCs = 0
|
||||
|
||||
data = {'articls': articl_objs, 'mandehesabKP': mandehesabCs}
|
||||
return render(request, 'admin_panel/bedehi.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||||
def hesabdari(request):
|
||||
page_name = 'sarresidShodeha'
|
||||
anchor_dt = datetime.datetime.now()
|
||||
|
||||
# تعریف محدودههای زمانی ماتریس ریسک (سن مطالبات)
|
||||
dt_30_days_ago = anchor_dt - datetime.timedelta(days=30)
|
||||
dt_90_days_ago = anchor_dt - datetime.timedelta(days=90)
|
||||
|
||||
# کوئری پایه: تمام سررسیدهای پرداختنشده که از موعد آنها گذشته است
|
||||
base_qs = Sarresid.objects.filter(payStatus="false", sarresidDT__lte=anchor_dt)
|
||||
|
||||
# محاسبه پرسرعت مجموع مبالغ در هر دسته (توسط موتور دیتابیس)
|
||||
matrix_totals = base_qs.aggregate(
|
||||
minor_delay=Sum('price', filter=Q(sarresidDT__gte=dt_30_days_ago)),
|
||||
high_risk=Sum('price', filter=Q(sarresidDT__lt=dt_30_days_ago, sarresidDT__gte=dt_90_days_ago)),
|
||||
critical=Sum('price', filter=Q(sarresidDT__lt=dt_90_days_ago)),
|
||||
total_sum=Sum('price')
|
||||
)
|
||||
|
||||
# دریافت فیلتر انتخابی کاربر از URL
|
||||
risk_filter = request.GET.get('risk_level', 'all')
|
||||
|
||||
# اعمال فیلتر روی لیست نمایش داده شده
|
||||
if risk_filter == 'minor':
|
||||
filtered_qs = base_qs.filter(sarresidDT__gte=dt_30_days_ago)
|
||||
elif risk_filter == 'high':
|
||||
filtered_qs = base_qs.filter(sarresidDT__lt=dt_30_days_ago, sarresidDT__gte=dt_90_days_ago)
|
||||
elif risk_filter == 'critical':
|
||||
filtered_qs = base_qs.filter(sarresidDT__lt=dt_90_days_ago)
|
||||
else:
|
||||
filtered_qs = base_qs
|
||||
|
||||
# مرتبسازی: قدیمیترینها (خطرناکترینها) در صدر جدول قرار میگیرند
|
||||
filtered_qs = filtered_qs.order_by('-sarresidDT')
|
||||
|
||||
page_number = request.GET.get('page', 1)
|
||||
paginator = Paginator(filtered_qs, per_page=50)
|
||||
all_sarresid_pgi = paginator.get_page(page_number)
|
||||
|
||||
data = {
|
||||
'page_name': page_name,
|
||||
'dateNow': anchor_dt.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'all_sarresid': all_sarresid_pgi,
|
||||
'current_filter': risk_filter,
|
||||
# متغیرهای ماتریس ریسک
|
||||
'sum_sarresid': matrix_totals['total_sum'] or 0,
|
||||
'minor_sum': matrix_totals['minor_delay'] or 0,
|
||||
'high_sum': matrix_totals['high_risk'] or 0,
|
||||
'critical_sum': matrix_totals['critical'] or 0,
|
||||
}
|
||||
|
||||
return render(request, 'admin_panel/hesabdari.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||||
def hesabdari_checkPass(request):
|
||||
|
||||
page_name = 'checkPass'
|
||||
|
||||
dateNow = datetime.datetime.today().strftime('%Y-%m-%d 23:59:59')
|
||||
all_sarresid = Sarresid.objects.filter(payStatus="true", type='check').order_by('-sarresidDT')
|
||||
|
||||
page_number = request.GET.get('page', None)
|
||||
paginator = Paginator(all_sarresid, per_page=50)
|
||||
|
||||
all_sarresid_pgi = paginator.get_page(page_number)
|
||||
|
||||
#paginator = Paginator(all_sarresid, per_page=200)
|
||||
|
||||
data = {'all_sarresid': all_sarresid_pgi, "dateNow":dateNow, "page_name":page_name}
|
||||
return render(request, 'admin_panel/hesabdari.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||||
def hesabdari_checkPassFail(request):
|
||||
|
||||
page_name = 'checkPassFaild'
|
||||
|
||||
dateNow = datetime.datetime.today().strftime('%Y-%m-%d 23:59:59')
|
||||
all_sarresid = Sarresid.objects.filter(payStatus="false", type='check').order_by('sarresidDT')
|
||||
|
||||
count_check_passfail = all_sarresid.count()
|
||||
|
||||
sum_check_passfail = all_sarresid.aggregate(Sum('price'))['price__sum']
|
||||
|
||||
page_number = request.GET.get('page', None)
|
||||
paginator = Paginator(all_sarresid, per_page=50)
|
||||
|
||||
all_sarresid_pgi = paginator.get_page(page_number)
|
||||
|
||||
#paginator = Paginator(all_sarresid, per_page=200)
|
||||
|
||||
data = {'all_sarresid': all_sarresid_pgi, 'sum_check_passfail':sum_check_passfail, 'count_check_passfail': count_check_passfail, "dateNow":dateNow, "page_name":page_name}
|
||||
return render(request, 'admin_panel/hesabdari.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||||
def hesabdari_residnashone(request):
|
||||
|
||||
page_name = 'sarresidNaShodeha'
|
||||
|
||||
dateNow = datetime.datetime.today().strftime('%Y-%m-%d 23:59:59')
|
||||
all_sarresid = Sarresid.objects.filter(payStatus="false", sarresidDT__gte=dateNow).order_by('sarresidDT')
|
||||
|
||||
sarresidNashode = all_sarresid.aggregate(Sum('price'))['price__sum']
|
||||
page_number = request.GET.get('page', None)
|
||||
paginator = Paginator(all_sarresid, per_page=50)
|
||||
|
||||
all_sarresid_pgi = paginator.get_page(page_number)
|
||||
|
||||
#paginator = Paginator(all_sarresid, per_page=200)
|
||||
|
||||
data = {'sarresidNashode': sarresidNashode,'all_sarresid': all_sarresid_pgi, "dateNow":dateNow, "page_name":page_name}
|
||||
return render(request, 'admin_panel/hesabdari.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||||
def bedehiKhadamat(request):
|
||||
#bedehiKhadamat
|
||||
try:
|
||||
debit = Article.objects.filter(typePy='khadamat').aggregate(Sum('debit'))['debit__sum']
|
||||
credit = Article.objects.filter(typePy='khadamat').aggregate(Sum('credit'))['credit__sum']
|
||||
mandehesabCs = debit - credit
|
||||
articls = Article.objects.filter(typePy='khadamat').order_by('-createdDT')[:100]
|
||||
except:
|
||||
articls =None
|
||||
mandehesabCs = 0
|
||||
|
||||
data = {'articls': articls, 'mandehesabKP': mandehesabCs}
|
||||
return render(request, 'admin_panel/bedehi.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||||
def bedehiNasb(request):
|
||||
try:
|
||||
debit = Article.objects.filter(typePy='nasb').aggregate(Sum('debit'))['debit__sum']
|
||||
credit = Article.objects.filter(typePy='nasb').aggregate(Sum('credit'))['credit__sum']
|
||||
mandehesabCs = debit - credit
|
||||
articls = Article.objects.filter(typePy='nasb').order_by('-createdDT')[:100]
|
||||
except:
|
||||
articls =None
|
||||
mandehesabCs = 0
|
||||
|
||||
data = {'articls': articls, 'mandehesabKP': mandehesabCs}
|
||||
return render(request, 'admin_panel/bedehi.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewStore', raise_exception=True)
|
||||
def passCheck(request, articl_id):
|
||||
|
||||
sarresidINC = Sarresid.objects.get(id=articl_id)
|
||||
user_op = request.user
|
||||
|
||||
desc = request.POST.get('message')
|
||||
|
||||
articleHesabDari2 = Article()
|
||||
articleHesabDari2.debit = 0
|
||||
articleHesabDari2.credit = sarresidINC.price
|
||||
articleHesabDari2.customer = sarresidINC.customer
|
||||
articleHesabDari2.factor = sarresidINC.factor
|
||||
articleHesabDari2.desc = 'وصول چک' + " " + str(sarresidINC.customer.name_family)
|
||||
if sarresidINC.factor:
|
||||
articleHesabDari2.desc += " فاکتور شماره: " + str(sarresidINC.factor.id)
|
||||
articleHesabDari2.nahvePardakht = 'وصول چک'
|
||||
articleHesabDari2.payStatus = "true"
|
||||
articleHesabDari2.user = user_op
|
||||
articleHesabDari2.save()
|
||||
|
||||
sarresidINC.payStatus = "true"
|
||||
sarresidINC.desc = desc
|
||||
sarresidINC.save()
|
||||
|
||||
return HttpResponse("ok")
|
||||
#return redirect(request.META.get('HTTP_REFERER'))
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewStore', raise_exception=True)
|
||||
@require_POST
|
||||
def update_check_info(request, sarresid_id):
|
||||
# دریافت رکورد مورد نظر
|
||||
sarresid = get_object_or_404(Sarresid, id=sarresid_id)
|
||||
|
||||
# اطمینان از اینکه آیتم حتما چک است
|
||||
if sarresid.type != 'check':
|
||||
messages.error(request, "عملیات نامعتبر: این سند چک نیست.")
|
||||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||||
|
||||
# دریافت مقادیر فرم با حذف فاصلههای اضافی
|
||||
check_row_number = request.POST.get('check_row_number', '').strip()
|
||||
check_sayyad_id = request.POST.get('check_sayyad_id', '').strip()
|
||||
check_owner_name = request.POST.get('check_owner_name', '').strip()
|
||||
check_owner_phone = request.POST.get('check_owner_phone', '').strip()
|
||||
|
||||
# اعتبارسنجی فیلدهای اجباری
|
||||
if not check_row_number or not check_sayyad_id or not check_owner_name:
|
||||
messages.error(request, "شماره ردیف، شناسه صیادی و نام مالک اجباری است.")
|
||||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||||
|
||||
# اعتبارسنجی شناسه صیادی (باید دقیقا ۱۶ رقم عدد باشد)
|
||||
if not check_sayyad_id.isdigit() or len(check_sayyad_id) != 16:
|
||||
messages.error(request, "شناسه صیادی باید دقیقاً ۱۶ رقم عدد باشد.")
|
||||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||||
|
||||
# اعتبارسنجی شماره تلفن (در صورت وارد شدن باید فقط عدد باشد)
|
||||
if check_owner_phone and not check_owner_phone.isdigit():
|
||||
messages.error(request, "شماره تلفن مالک چک باید فقط شامل اعداد باشد.")
|
||||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||||
|
||||
try:
|
||||
# بروزرسانی اطلاعات
|
||||
sarresid.check_row_number = check_row_number
|
||||
sarresid.check_sayyad_id = check_sayyad_id
|
||||
sarresid.check_owner_name = check_owner_name
|
||||
sarresid.check_owner_phone = check_owner_phone
|
||||
# آپدیت فیلد توضیحات با اطلاعات جدید
|
||||
sarresid.desc = f"صیادی: {check_sayyad_id} | مالک: {check_owner_name} | تلفن: {check_owner_phone or 'ندارد'}"
|
||||
|
||||
sarresid.save()
|
||||
messages.success(request, "اطلاعات چک با موفقیت ثبت/بروزرسانی شد.")
|
||||
except Exception as e:
|
||||
messages.error(request, "خطای سیستمی در بروزرسانی اطلاعات رخ داد.")
|
||||
|
||||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewStore', raise_exception=True)
|
||||
def addSarresid(request, cs_id):
|
||||
if request.method != 'POST':
|
||||
return HttpResponseBadRequest("متد ارسالی نامعتبر است.")
|
||||
|
||||
try:
|
||||
customerObj = get_object_or_404(Customer, id=cs_id)
|
||||
|
||||
# دریافت فیلدهای اصلی
|
||||
sarrresidUNIX = request.POST.get('n_sarresidDateTime')
|
||||
sarresidPrice = request.POST.get('n_sarresidPrice')
|
||||
sarresidType = request.POST.get('n_sarresidType')
|
||||
|
||||
# دریافت و پاکسازی فیلدهای چک (جلوگیری از باگ فاصلههای خالی)
|
||||
check_row_number = request.POST.get('check_row_number', '').strip()
|
||||
check_sayyad_id = request.POST.get('check_sayyad_id', '').strip()
|
||||
check_owner_name = request.POST.get('check_owner_name', '').strip()
|
||||
check_owner_phone = request.POST.get('check_owner_phone', '').strip()
|
||||
|
||||
# --- لایه اول: اعتبارسنجی فیلدهای پایه ---
|
||||
if not sarrresidUNIX or not sarresidPrice or not sarresidType:
|
||||
messages.error(request, "لطفا فیلدهای اصلی (مبلغ، نوع و تاریخ) را پر کنید.")
|
||||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||||
|
||||
# --- لایه دوم: اعتبارسنجی دقیق و امنیتی فیلدهای چک ---
|
||||
if sarresidType == "check":
|
||||
if not check_row_number or not check_sayyad_id or not check_owner_name:
|
||||
messages.error(request, "برای ثبت چک، وارد کردن شماره ردیف، شناسه صیادی و نام مالک اجباری است.")
|
||||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||||
|
||||
# بررسی ۱۶ رقمی و عددی بودن شناسه صیادی
|
||||
if not check_sayyad_id.isdigit() or len(check_sayyad_id) != 16:
|
||||
messages.error(request, "شناسه صیادی باید دقیقاً ۱۶ رقم عدد باشد.")
|
||||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||||
|
||||
# بررسی عددی بودن شماره تلفن (در صورت وارد شدن)
|
||||
if check_owner_phone and not check_owner_phone.isdigit():
|
||||
messages.error(request, "شماره تلفن مالک چک باید فقط شامل اعداد باشد.")
|
||||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||||
|
||||
# تبدیل نوع دادهها با مدیریت خطا (جلوگیری از کرش سرور 500)
|
||||
try:
|
||||
sarResidDateTime01 = datetime.datetime.fromtimestamp(int(sarrresidUNIX) / 1000.0)
|
||||
price_int = int(sarresidPrice.replace(',', ''))
|
||||
except ValueError:
|
||||
messages.error(request, "مقادیر تاریخ یا مبلغ نامعتبر است.")
|
||||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||||
|
||||
# --- لایه سوم: ذخیرهسازی اتمیک (تضمین جامعیت دادهها) ---
|
||||
with transaction.atomic():
|
||||
# ۱. ثبت سند حسابداری
|
||||
articleHesabDariObj = Article()
|
||||
articleHesabDariObj.debit = price_int
|
||||
articleHesabDariObj.credit = 0
|
||||
articleHesabDariObj.customer = customerObj
|
||||
articleHesabDariObj.user = request.user
|
||||
|
||||
if sarresidType == "check":
|
||||
articleHesabDariObj.desc = f"بدهی بابت ثبت چک دستی (صیادی: {check_sayyad_id})"
|
||||
elif sarresidType == "nesiye":
|
||||
articleHesabDariObj.desc = "بدهی بابت ثبت نسیه دستی"
|
||||
|
||||
articleHesabDariObj.save()
|
||||
|
||||
# ۲. ثبت سررسید / چک
|
||||
sarresidINC = Sarresid()
|
||||
sarresidINC.customer = customerObj
|
||||
sarresidINC.price = price_int
|
||||
sarresidINC.payStatus = "false"
|
||||
sarresidINC.type = sarresidType
|
||||
sarresidINC.user = request.user
|
||||
sarresidINC.sarresidDT = sarResidDateTime01
|
||||
|
||||
if sarresidType == "check":
|
||||
sarresidINC.check_row_number = check_row_number
|
||||
sarresidINC.check_sayyad_id = check_sayyad_id
|
||||
sarresidINC.check_owner_name = check_owner_name
|
||||
sarresidINC.check_owner_phone = check_owner_phone
|
||||
sarresidINC.desc = f"صیادی: {check_sayyad_id} | مالک: {check_owner_name} | تلفن: {check_owner_phone or 'ندارد'}"
|
||||
|
||||
sarresidINC.save()
|
||||
|
||||
messages.success(request, "اطلاعات با موفقیت ثبت شد.")
|
||||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in addSarresid: {str(e)}", exc_info=True)
|
||||
messages.error(request, "خطای سیستمی رخ داد. لطفا مجددا تلاش کنید.")
|
||||
return redirect(request.META.get('HTTP_REFERER', '/'))
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewStore', raise_exception=True)
|
||||
def pardakhtNesiye(request, cs_id):
|
||||
try:
|
||||
customerObj = Customer.objects.get(id=cs_id)
|
||||
user_op = request.user
|
||||
|
||||
sarrresidUNIX = request.POST.get('next_sarresidDateTime', None)
|
||||
sarresidPrice = int(request.POST.get('next_sarresidPrice', None))
|
||||
sr_articlID = request.POST.get('sr_articlID', None)
|
||||
nahvePardakht = request.POST.get('nahvePardakht', None)
|
||||
desc_sar = request.POST.get('desc_sar', None)
|
||||
|
||||
sarResidDateTime01 = datetime.datetime.fromtimestamp(int(sarrresidUNIX)/1000.0)
|
||||
|
||||
SarresidObj = Sarresid.objects.get(id=sr_articlID)
|
||||
#return HttpResponse(sarresidPrice)
|
||||
debitNow = SarresidObj.price - sarresidPrice
|
||||
#return HttpResponse(debitNow)
|
||||
|
||||
if debitNow==0:
|
||||
#توسیه بدهی بخورد
|
||||
SarresidObj.price = 0
|
||||
SarresidObj.payStatus = "true"
|
||||
elif debitNow >= 1:
|
||||
SarresidObj.price = debitNow
|
||||
SarresidObj.sarresidDT = sarResidDateTime01
|
||||
else:
|
||||
return HttpResponse("خطا در مورد مبلغ")
|
||||
SarresidObj.save()
|
||||
|
||||
|
||||
if str(nahvePardakht) == 'naghd':
|
||||
nahvePardakht = 'نقد'
|
||||
elif str(nahvePardakht) == 'kartbekart':
|
||||
nahvePardakht = 'کارت به کارت'
|
||||
elif str(nahvePardakht) == 'kartkhan':
|
||||
nahvePardakht = 'کارت خوان'
|
||||
elif str(nahvePardakht) == 'takhfif':
|
||||
nahvePardakht = 'تخفیف'
|
||||
elif str(nahvePardakht) == 'check':
|
||||
nahvePardakht = 'چک'
|
||||
|
||||
articleHesabDariObj = Article()
|
||||
articleHesabDariObj.debit = 0
|
||||
articleHesabDariObj.credit = int(sarresidPrice)
|
||||
|
||||
if desc_sar:
|
||||
articleHesabDariObj.desc = "پرداختی بابت نسیه" + " | " + desc_sar
|
||||
else:
|
||||
articleHesabDariObj.desc = "پرداختی بابت نسیه"
|
||||
|
||||
articleHesabDariObj.customer = customerObj
|
||||
articleHesabDariObj.nahvePardakht = nahvePardakht
|
||||
articleHesabDariObj.user = user_op
|
||||
articleHesabDariObj.save()
|
||||
|
||||
return redirect(request.META.get('HTTP_REFERER'))
|
||||
|
||||
except:
|
||||
return HttpResponse("خطا در ثبت ")
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||||
def rezayat_dashboard(request):
|
||||
# دریافت تمامی نظرسنجیهای ثبت شده معتبر (که تاریخ نظرسنجی دارند)
|
||||
valid_surveys = Action.objects.filter(
|
||||
nazarDT__isnull=False,
|
||||
grade__in=[5, 10, 15, 20]
|
||||
)
|
||||
|
||||
# ۱. محاسبات شاخصهای کلیدی (KPIs)
|
||||
total_surveys = valid_surveys.count()
|
||||
avg_grade = valid_surveys.aggregate(Avg('grade'))['grade__avg'] or 0
|
||||
|
||||
# ۲. استخراج دادهها برای نمودار توزیع رضایت
|
||||
grade_distribution = valid_surveys.values('grade').annotate(count=Count('id'))
|
||||
chart_dict = {20: 0, 15: 0, 10: 0, 5: 0}
|
||||
for item in grade_distribution:
|
||||
g = item['grade']
|
||||
if g in chart_dict:
|
||||
chart_dict[g] = item['count']
|
||||
|
||||
chart_labels = ['عالی (۲۰)', 'متوسط (۱۵)', 'ضعیف (۱۰)', 'بد (۵)']
|
||||
chart_data_list = [chart_dict[20], chart_dict[15], chart_dict[10], chart_dict[5]]
|
||||
|
||||
# ۳. تبدیل امن آرایهها به JSON برای Chart.js
|
||||
chart_labels_json = json.dumps(chart_labels, ensure_ascii=False)
|
||||
chart_data_json = json.dumps(chart_data_list)
|
||||
has_chart_data = sum(chart_data_list) > 0
|
||||
|
||||
# ۴. واکشی لیست مشتریان نیازمند پیگیری (شامل متوسط ۱۵، ضعیف ۱۰ و بد ۵)
|
||||
low_score_actions = Action.objects.filter(
|
||||
nazarDT__isnull=False,
|
||||
grade__in=[5, 10, 15] # <-- امتیاز ۱۵ (متوسط) نیز اضافه شد
|
||||
).order_by('-nazarDT')
|
||||
|
||||
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
|
||||
|
||||
context = {
|
||||
'total_surveys': total_surveys,
|
||||
'avg_grade': round(avg_grade, 1),
|
||||
'chart_labels_json': chart_labels_json,
|
||||
'chart_data_json': chart_data_json,
|
||||
'has_chart_data': has_chart_data,
|
||||
'low_score_actions': low_score_actions,
|
||||
# تعداد کل موارد نیازمند پیگیری (متوسط + ضعیف + بد)
|
||||
'unhappy_count': chart_dict[15] + chart_dict[10] + chart_dict[5],
|
||||
'allUsers': allUsers,
|
||||
}
|
||||
|
||||
return render(request, 'admin_panel/report_rezayat.html', context)
|
||||
@@ -0,0 +1,458 @@
|
||||
from django.db import transaction, connection
|
||||
from django.db.models import Sum, Q
|
||||
from django.shortcuts import redirect, render, get_object_or_404
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
|
||||
from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
|
||||
from ..models import StoreRoom, ProductCategory, Product, ProductInStore, StoreTransferRequest
|
||||
from ..services import (create_transfer_request, approve_transfer_request,
|
||||
reject_transfer_request, cancel_transfer_request)
|
||||
from ._helpers import logger, _safe_int
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.OnlyAnbar', raise_exception=True)
|
||||
def stores(request):
|
||||
if request.user.username not in ('admin', 'office1', 'office2', 'office3', 'khatibi'):
|
||||
all_store = StoreRoom.objects.filter(user__id=request.user.id).order_by('id')[:20]
|
||||
else:
|
||||
all_store = StoreRoom.objects.all().order_by('id')[:20]
|
||||
data = {'all_store': all_store}
|
||||
return render(request, 'admin_panel/stores.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.OnlyAnbar', raise_exception=True)
|
||||
def store(request, s_id):
|
||||
productCategory = ProductCategory.objects.all()
|
||||
storeO = get_object_or_404(StoreRoom, id=s_id)
|
||||
searchStatus = False
|
||||
searchQS = request.POST.get('searchQS', '').strip()
|
||||
mojodi = request.POST.get('mojodi', '')
|
||||
|
||||
# ۱. محاسبه موجودی فیزیکی کل کالاها با یک کوئری تکضرب و سریع GROUP BY
|
||||
stock_balances = ProductInStore.objects.filter(
|
||||
StoreRoom=storeO, year=1400
|
||||
).values('Product_id').annotate(
|
||||
total=Sum('s_in') - Sum('s_out')
|
||||
).order_by()
|
||||
stock_dict = {item['Product_id']: item['total'] for item in stock_balances}
|
||||
|
||||
# ۲. محاسبه کل کالاهای در انتظار خروج
|
||||
pending_out_balances = StoreTransferRequest.objects.filter(
|
||||
source_store=storeO, status=StoreTransferRequest.Status.PENDING
|
||||
).values('product_id').annotate(
|
||||
total=Sum('quantity')
|
||||
).order_by()
|
||||
pending_out_dict = {item['product_id']: item['total'] for item in pending_out_balances}
|
||||
|
||||
# ۳. محاسبه کل کالاهای در انتظار ورود
|
||||
pending_in_balances = StoreTransferRequest.objects.filter(
|
||||
target_store=storeO, status=StoreTransferRequest.Status.PENDING
|
||||
).values('product_id').annotate(
|
||||
total=Sum('quantity')
|
||||
).order_by()
|
||||
pending_in_dict = {item['product_id']: item['total'] for item in pending_in_balances}
|
||||
|
||||
# ۴. استخراج سریع تمام آیدیهای منحصربهفرد مرتبط با این انبار در سطح حافظه
|
||||
all_pids = set(stock_dict.keys()) | set(pending_out_dict.keys()) | set(pending_in_dict.keys())
|
||||
|
||||
# ۵. کوئری نهایی فوقبهینه برای خواندن کالاها (حل باگ N+1 با دستور select_related)
|
||||
products_qs = Product.objects.select_related('ProductCategory').filter(id__in=all_pids)
|
||||
|
||||
# اعمال فیلتر متنی جستجو در سطح دیتابیس
|
||||
if searchQS:
|
||||
products_qs = products_qs.filter(product_name__icontains=searchQS)
|
||||
searchStatus = True
|
||||
|
||||
# 🛠️ اصلاح فرآیند ترتیب: استفاده از 'id' جهت انطباق ۱۰۰٪ با ترتیب پیشفرض و سنتی دیتابیس شما
|
||||
# نکته: اگر مایلید بر اساس زمان ایجاد مرتب شود، میتوانید آن را به 'createdDT' یا '-createdDT' تغییر دهید.
|
||||
products_list = list(products_qs.order_by('id'))
|
||||
|
||||
# ۶. معماری زمان ثابت O(1): چسباندن پرسرعت دیتاها در حافظه رم سرور
|
||||
for prd in products_list:
|
||||
prd.mojodi = stock_dict.get(prd.id, 0)
|
||||
prd.pending_out = pending_out_dict.get(prd.id, 0)
|
||||
prd.pending_in = pending_in_dict.get(prd.id, 0)
|
||||
|
||||
# ۷. اعمال فیلترهای منطقی موجودی در لایه پایتون
|
||||
if mojodi == "0":
|
||||
products_list = [p for p in products_list if p.mojodi <= 0]
|
||||
searchStatus = True
|
||||
elif mojodi == "1":
|
||||
products_list = [p for p in products_list if p.mojodi > 0]
|
||||
searchStatus = True
|
||||
|
||||
data = {
|
||||
'storeO': storeO,
|
||||
'productSum': products_list,
|
||||
's_id': s_id,
|
||||
'searchStatus': searchStatus,
|
||||
"mojodi": mojodi,
|
||||
'djsearchQS': searchQS,
|
||||
'productCategory': productCategory
|
||||
}
|
||||
return render(request, 'admin_panel/store.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.OnlyAnbar', raise_exception=True)
|
||||
def operations(request, s_id):
|
||||
page = request.GET.get('page', 1)
|
||||
productCategory = ProductCategory.objects.all()
|
||||
storeO = get_object_or_404(StoreRoom, id=s_id)
|
||||
|
||||
# بهینهسازی کلیدی: استفاده از select_related برای جلوگیری از کندی و پر شدن RAM دیتابیس
|
||||
operationsList = ProductInStore.objects.filter(
|
||||
StoreRoom=storeO, year=1400
|
||||
).select_related('Product').order_by('-createdDT')
|
||||
|
||||
paginator = Paginator(operationsList, 25)
|
||||
try:
|
||||
data_list = paginator.page(page)
|
||||
except PageNotAnInteger:
|
||||
data_list = paginator.page(1)
|
||||
except EmptyPage:
|
||||
data_list = paginator.page(paginator.num_pages)
|
||||
|
||||
data = {
|
||||
'storeO': storeO,
|
||||
'operationsList': data_list,
|
||||
's_id': s_id,
|
||||
'productCategory': productCategory
|
||||
}
|
||||
return render(request, 'admin_panel/operations.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.OnlyAnbar', raise_exception=True)
|
||||
def operationProduct(request, s_id, p_id):
|
||||
productCategory = ProductCategory.objects.all()
|
||||
storeO = get_object_or_404(StoreRoom, id=s_id)
|
||||
|
||||
# بهینهسازی کلیدی در فیلتر اختصاصی محصول
|
||||
operationsList = ProductInStore.objects.filter(
|
||||
StoreRoom=storeO, year=1400, Product__id=p_id
|
||||
).select_related('Product').order_by('-createdDT')
|
||||
|
||||
data = {
|
||||
'storeO': storeO,
|
||||
'operationsList': operationsList,
|
||||
's_id': s_id,
|
||||
'productCategory': productCategory
|
||||
}
|
||||
return render(request, 'admin_panel/operations.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.OnlyAnbar', raise_exception=True)
|
||||
def newOperation(request, s_id, p_id):
|
||||
productCategory = ProductCategory.objects.all()
|
||||
storeO = StoreRoom.objects.get(id=s_id)
|
||||
allStores = StoreRoom.objects.exclude(id=s_id)
|
||||
product = Product.objects.get(id=p_id)
|
||||
data = {'s_id': s_id,'p_id': p_id, 'allStores': allStores, 'storeO': storeO, 'product':product, 'productCategory': productCategory}
|
||||
return render(request, 'admin_panel/newOperation.html', data )
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.OnlyAnbar', raise_exception=True)
|
||||
def store_transfers(request, s_id):
|
||||
"""
|
||||
داشبورد و کارتابل جامع مدیریت انتقالهای انبار
|
||||
"""
|
||||
storeO = get_object_or_404(StoreRoom, id=s_id)
|
||||
|
||||
# ۱. کارتابل ورودی (بارهایی که به سمت این انبار آمده و منتظر تایید ما هستند)
|
||||
# استفاده از select_related برای جلوگیری از صدها کوئری اضافی به دیتابیس
|
||||
incoming_pending = StoreTransferRequest.objects.filter(
|
||||
target_store=storeO,
|
||||
status=StoreTransferRequest.Status.PENDING
|
||||
).select_related('product', 'source_store', 'requester').order_by('-created_at')
|
||||
|
||||
# ۲. کارتابل خروجی (بارهایی که ما فرستادیم و منتظر تایید انبار مقصد هستیم)
|
||||
outgoing_pending = StoreTransferRequest.objects.filter(
|
||||
source_store=storeO,
|
||||
status=StoreTransferRequest.Status.PENDING
|
||||
).select_related('product', 'target_store', 'requester').order_by('-created_at')
|
||||
|
||||
# ۳. تاریخچه انتقالات اخیر (۵۰ مورد آخر تعیین تکلیف شده برای این انبار)
|
||||
history_list = StoreTransferRequest.objects.filter(
|
||||
Q(source_store=storeO) | Q(target_store=storeO)
|
||||
).exclude(
|
||||
status=StoreTransferRequest.Status.PENDING
|
||||
).select_related('product', 'source_store', 'target_store', 'approver').order_by('-action_date')[:50]
|
||||
|
||||
data = {
|
||||
'storeO': storeO,
|
||||
's_id': s_id,
|
||||
'incoming_pending': incoming_pending,
|
||||
'outgoing_pending': outgoing_pending,
|
||||
'history_list': history_list,
|
||||
}
|
||||
return render(request, 'admin_panel/store_transfers.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.OnlyAnbar', raise_exception=True)
|
||||
@require_POST
|
||||
def process_transfer_action(request, transfer_id):
|
||||
"""
|
||||
پردازش امن اقدامات انباردار (تایید، رد، انصراف) با تراکنشهای اتمیک
|
||||
"""
|
||||
action = request.POST.get('action')
|
||||
reason = request.POST.get('reason', '').strip()
|
||||
|
||||
# دریافت درخواست انتقال
|
||||
transfer = get_object_or_404(StoreTransferRequest, id=transfer_id)
|
||||
|
||||
# تعیین انباری که کاربر از آنجا اقدام کرده جهت ریدایرکت صحیح
|
||||
redirect_store_id = transfer.target_store_id if action in ['approve', 'reject'] else transfer.source_store_id
|
||||
|
||||
try:
|
||||
if action == 'approve':
|
||||
approve_transfer_request(request_id=transfer.id, approver=request.user)
|
||||
messages.success(request, f"انتقال {transfer.quantity} عدد {transfer.product.product_name} با موفقیت تایید و به موجودی انبار اضافه شد.")
|
||||
|
||||
elif action == 'reject':
|
||||
if not reason:
|
||||
messages.error(request, "برای رد کردن بار، وارد کردن علت رد الزامی است.")
|
||||
return redirect('admin_panel:store_transfers', s_id=redirect_store_id)
|
||||
|
||||
reject_transfer_request(request_id=transfer.id, rejector=request.user, reason=reason)
|
||||
messages.warning(request, f"درخواست انتقال بار رد شد و به انبار مبدا اطلاع داده شد.")
|
||||
|
||||
elif action == 'cancel':
|
||||
cancel_transfer_request(request_id=transfer.id, user=request.user)
|
||||
messages.info(request, "درخواست انتقال توسط شما لغو گردید.")
|
||||
|
||||
else:
|
||||
messages.error(request, "دستور ارسالی نامعتبر است.")
|
||||
|
||||
except ValidationError as e:
|
||||
# نمایش خطاهای منطقی (مثلاً کمبود موجودی انبار مبدا در لحظه تایید)
|
||||
error_text = ' '.join(e.messages) if hasattr(e, 'messages') else str(e)
|
||||
messages.error(request, f"خطا در انجام عملیات: {error_text}")
|
||||
except PermissionError as e:
|
||||
messages.error(request, str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing transfer action #{transfer_id}: {str(e)}", exc_info=True)
|
||||
messages.error(request, "خطای سیستمی در پردازش تراکنش رخ داد.")
|
||||
|
||||
return redirect('admin_panel:store_transfers', s_id=redirect_store_id)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewStore', raise_exception=True)
|
||||
def newStore(request):
|
||||
storeName = request.POST.get('storeName', '')
|
||||
newStore = StoreRoom()
|
||||
newStore.store_name = storeName
|
||||
newStore.save()
|
||||
|
||||
return redirect('/panel/store/')
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.OnlyAnbar', raise_exception=True)
|
||||
def productAdd(request):
|
||||
namePrd = request.POST.get('namePrd', '')
|
||||
category = request.POST.get('category', '')
|
||||
price = request.POST.get('price', '')
|
||||
mojodiAval = request.POST.get('mojodiAval', '')
|
||||
storeroom = request.POST.get('storeroom', '')
|
||||
|
||||
newProduct = Product()
|
||||
newProduct.product_name = namePrd
|
||||
newProduct.ProductCategory = ProductCategory.objects.get(id=category)
|
||||
newProduct.product_price = price
|
||||
newProduct.save()
|
||||
|
||||
prd_in_store = ProductInStore()
|
||||
prd_in_store.Product = newProduct
|
||||
prd_in_store.StoreRoom = StoreRoom.objects.get(id=storeroom)
|
||||
prd_in_store.s_in = mojodiAval
|
||||
prd_in_store.desc = "موجودی اولیه: "+str(mojodiAval)
|
||||
prd_in_store.movement_type = ProductInStore.MovementType.INITIAL
|
||||
prd_in_store.save()
|
||||
|
||||
#return HttpResponse(installDate)
|
||||
return redirect('/panel/store/1')
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewStore', raise_exception=True)
|
||||
def productEdit(request):
|
||||
prd_code = request.POST.get('prd_code', '')
|
||||
namePrd = request.POST.get('namePrd', '')
|
||||
category = request.POST.get('category', '')
|
||||
price = request.POST.get('price', '')
|
||||
|
||||
#return HttpResponse(prd_code)
|
||||
|
||||
editProduct = Product.objects.get(id=prd_code)
|
||||
editProduct.product_name = namePrd
|
||||
editProduct.ProductCategory = ProductCategory.objects.get(id=category)
|
||||
editProduct.product_price = price
|
||||
editProduct.save()
|
||||
|
||||
return redirect('/panel/store/1')
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewStore', raise_exception=True)
|
||||
def productDel(request, p_id, s_id):
|
||||
Product.objects.get(id=p_id).delete()
|
||||
return redirect('/panel/store/'+str(s_id))
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.OnlyAnbar', raise_exception=True)
|
||||
def operation(request):
|
||||
if request.method != 'POST':
|
||||
return HttpResponseBadRequest("متد ارسالی نامعتبر است.")
|
||||
|
||||
user_op = request.user
|
||||
user_store_room = get_object_or_404(StoreRoom, owners=user_op)
|
||||
|
||||
# استفاده از توابع امن اختصاصی پروژه برای جداسازی مقادیر عددی
|
||||
prd_id = _safe_int(request.POST.get('prd_id'))
|
||||
action_type = request.POST.get('actionType', '')
|
||||
tedad = _safe_int(request.POST.get('tedad'))
|
||||
azanbar = _safe_int(request.POST.get('azanbar'))
|
||||
beanbar = _safe_int(request.POST.get('beanbar'))
|
||||
desc = request.POST.get('desc', '').strip()
|
||||
|
||||
if tedad <= 0:
|
||||
return HttpResponseBadRequest("تعداد وارد شده باید بیشتر از صفر باشد.")
|
||||
|
||||
s_room = get_object_or_404(StoreRoom, id=azanbar)
|
||||
product_obj = get_object_or_404(Product, id=prd_id)
|
||||
|
||||
try:
|
||||
# =====================================================================
|
||||
# بخش اول: انتقال بین انبارها (واگذاری کامل به لایه سرویس)
|
||||
# =====================================================================
|
||||
if action_type == 'transfer':
|
||||
if not beanbar:
|
||||
return HttpResponse("انبار مقصد مشخص نشده است.")
|
||||
|
||||
target_store = get_object_or_404(StoreRoom, id=_safe_int(beanbar))
|
||||
|
||||
# فراخوانی لایه سرویس برای ثبت درخواست انتقال (بدون تغییر مستقیم موجودی)
|
||||
create_transfer_request(
|
||||
source_store=s_room,
|
||||
target_store=target_store,
|
||||
product=product_obj,
|
||||
quantity=tedad,
|
||||
requester=request.user,
|
||||
desc=desc or f"درخواست انتقال از انبار {s_room.store_name} به {target_store.store_name}"
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Transfer request created: {tedad}x {product_obj.product_name} from Store #{s_room.id} to Store #{target_store.id} by User #{request.user.id}"
|
||||
)
|
||||
return redirect('/panel/store/' + str(s_room.id))
|
||||
|
||||
# =====================================================================
|
||||
# بخش دوم: ورود و خروج دستی (کنترل مستقیم با Transaction و Lock)
|
||||
# =====================================================================
|
||||
elif action_type in ['vorod', 'khoroj']:
|
||||
user_s_room = get_object_or_404(StoreRoom, owners=user_op)
|
||||
if not user_op.is_superuser and s_room != user_s_room:
|
||||
return HttpResponse("شما دسترسی ورود / خروج به این انبار ندارید")
|
||||
with transaction.atomic():
|
||||
# ساخت کوئری پایه برای دریافت تراکنشهای سال جاری کالا
|
||||
qs = ProductInStore.objects.filter(
|
||||
Product=product_obj, StoreRoom=s_room, year=1400
|
||||
)
|
||||
|
||||
# قفلگذاری ردیفها برای جلوگیری از Race Condition (در دیتابیسهای پشتیبان)
|
||||
if connection.features.has_select_for_update:
|
||||
qs = qs.select_for_update()
|
||||
|
||||
locked_records = list(qs)
|
||||
|
||||
# --- الف) عملیات ورود دستی ---
|
||||
if action_type == 'vorod':
|
||||
ProductInStore.objects.create(
|
||||
StoreRoom=s_room,
|
||||
Product=product_obj,
|
||||
s_in=tedad,
|
||||
s_out=0,
|
||||
year=1400,
|
||||
desc=f"ورود: {tedad} به انبار {s_room.store_name}#{desc}",
|
||||
movement_type=ProductInStore.MovementType.DIRECT_IN
|
||||
)
|
||||
|
||||
# --- ب) عملیات خروج دستی ---
|
||||
elif action_type == 'khoroj':
|
||||
# محاسبه سریع موجودی در رم بدون فشار مجدد به دیتابیس
|
||||
total_in = sum(item.s_in for item in locked_records if item.s_in)
|
||||
total_out = sum(item.s_out for item in locked_records if item.s_out)
|
||||
mojodi_prd = total_in - total_out
|
||||
|
||||
if mojodi_prd < tedad:
|
||||
# تطابق ۱۰۰ درصدی با پاسخ متنی مورد انتظار فرانتاند
|
||||
return HttpResponse("موجودی کافی نیست، موجودی فعلی: " + str(mojodi_prd))
|
||||
|
||||
ProductInStore.objects.create(
|
||||
StoreRoom=s_room,
|
||||
Product=product_obj,
|
||||
s_in=0,
|
||||
s_out=tedad,
|
||||
year=1400,
|
||||
desc=f"خروج: {tedad} از انبار {s_room.store_name}#{desc}",
|
||||
movement_type=ProductInStore.MovementType.DIRECT_OUT
|
||||
)
|
||||
|
||||
else:
|
||||
return HttpResponseBadRequest("نوع عملیات نامعتبر است.")
|
||||
|
||||
except ValidationError as e:
|
||||
# مدیریت خطاهای اعتبارسنجی (مثل کمبود موجودی در لایه سرویس یا یکسان بودن انبارها)
|
||||
error_msg = ' '.join(e.messages) if hasattr(e, 'messages') else str(e)
|
||||
logger.warning(f"Validation error in store operation: {error_msg}")
|
||||
return HttpResponse(error_msg)
|
||||
|
||||
except PermissionDenied as e:
|
||||
# مدیریت خطای دسترسی کاربر به انبار
|
||||
logger.warning(f"Permission denied in store operation for user {request.user.id}: {str(e)}")
|
||||
return HttpResponse("شما دسترسی لازم برای انجام این عملیات در این انبار را ندارید.", status=403)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Critical error in store operation view: {str(e)}", exc_info=True)
|
||||
return HttpResponse("خطای سیستمی رخ داد. تراکنش لغو شد.", status=500)
|
||||
|
||||
return redirect('/panel/store/' + str(s_room.id))
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def productMojodi(request, p_id):
|
||||
|
||||
# پیدا کردن انبار کاربر با متد filter به جای get برای جلوگیری از کرش سیستم
|
||||
store_user = StoreRoom.objects.filter(owners=request.user).first()
|
||||
|
||||
# اگر کاربر هنوز مالک هیچ انباری نشده است
|
||||
if not store_user:
|
||||
return HttpResponse("کاربر مالکیت انبار ندارد") # یا هر مقداری که فرانتاند انتظار دارد
|
||||
|
||||
# محاسبه موجودی برای انبار پیدا شده
|
||||
totals = ProductInStore.objects.filter(
|
||||
Product_id=p_id,
|
||||
StoreRoom=store_user,
|
||||
year=1400
|
||||
).aggregate(
|
||||
total_in=Sum('s_in'),
|
||||
total_out=Sum('s_out')
|
||||
)
|
||||
|
||||
total_in = totals['total_in'] or 0
|
||||
total_out = totals['total_out'] or 0
|
||||
mojodi_prd = total_in - total_out
|
||||
|
||||
return HttpResponse(str(mojodi_prd))
|
||||
Reference in new issue
Block a user