fix mandehesab, bazdid, sms
This commit is contained in:
1 parent
7e5c83bea2
commit
1fd27d0445
22 files changed
+16462
-5918
No files matched your search
@@ -0,0 +1,37 @@
|
||||
# Generated by Django 3.2.25 on 2026-10-16 10:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
"""افزودن ``choices`` به ``Action.actionType``.
|
||||
|
||||
این migration فقط وضعیت مدل را بهروز میکند: Django ``choices`` را در
|
||||
``deconstruct()`` فیلد نگه میدارد، پس بدون این migration دستور
|
||||
``makemigrations --check`` شکست میخورد. تعریف ستون در SQLite هیچ تغییری
|
||||
نمیکند (``choices`` در DDL بازتاب نمییابد) و هیچ دادهای بازنویسی نمیشود.
|
||||
"""
|
||||
|
||||
dependencies = [
|
||||
('admin_panel', '0072_customernamefixjob'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='action',
|
||||
name='actionType',
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
('bazdid', 'بازدید'),
|
||||
('nasb', 'نصب'),
|
||||
('vosol', 'وصول مطالبات'),
|
||||
('hamahangi', 'هماهنگی'),
|
||||
('inCall', 'تماس ورودی'),
|
||||
('outCall', 'تماس خروجی'),
|
||||
],
|
||||
max_length=50,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,37 @@
|
||||
# Generated by Django 3.2.25 on 2026-10-16 10:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
"""افزودن ``choices`` به ``Action.status``.
|
||||
|
||||
این migration فقط وضعیت مدل را بهروز میکند: Django ``choices`` را در
|
||||
``deconstruct()`` فیلد نگه میدارد، پس بدون این migration دستور
|
||||
``makemigrations --check`` شکست میخورد. تعریف ستون در SQLite هیچ تغییری
|
||||
نمیکند (``choices`` در DDL بازتاب نمییابد) و هیچ دادهای بازنویسی نمیشود.
|
||||
|
||||
املای مقادیر عیناً همان چیزی است که در دیتابیس و کد موجود بهکار رفته:
|
||||
``'waite'`` (نه ``'wait'``)، ``'finish'`` و ``'cancell'``.
|
||||
"""
|
||||
|
||||
dependencies = [
|
||||
('admin_panel', '0073_alter_action_actiontype_choices'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='action',
|
||||
name='status',
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
('waite', 'در انتظار (باز)'),
|
||||
('finish', 'انجام شده'),
|
||||
('cancell', 'لغو شده'),
|
||||
],
|
||||
max_length=50,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,49 @@
|
||||
"""یادآوری تأمین موجودی چک: تغییر جهت از «پس از سررسید» به «پیش از سررسید».
|
||||
|
||||
هدف این migration **فقط** همراستاکردن مقدار عددی ``offset_days`` قالب
|
||||
``cheque_registration`` با معنای جدید است (تعداد روز **قبل** از سررسید).
|
||||
|
||||
آنچه این migration عمداً **نمیکند**:
|
||||
|
||||
* ``text`` (متن پیامک) را تغییر نمیدهد — متنی که مدیر در پنل تنظیم کرده دستنخورده
|
||||
میماند. اگر متن فعلی از زمان گذشته حرف میزند («سررسید شده است»)، مدیر میتواند
|
||||
خودش از صفحهٔ «قالبهای پیامک» ویرایشش کند.
|
||||
* ``title``، ``is_enabled`` و ``auto_send`` را تغییر نمیدهد.
|
||||
* هیچ نوع/قالب دیگری را لمس نمیکند.
|
||||
|
||||
مقدار ``offset_days`` روی پیشفرض معنای جدید (۱ روز قبل از سررسید) تنظیم میشود تا
|
||||
نصبهای تازه و نصبهای موجود یکسان رفتار کنند؛ اما اگر مدیر قبلاً عددی غیر از مقدار
|
||||
قدیمی (``1``) گذاشته باشد، به آن دست نمیزنیم تا انتخابش از بین نرود.
|
||||
|
||||
idempotent است؛ اجرای دوباره بیخطر است.
|
||||
"""
|
||||
from django.db import migrations
|
||||
|
||||
SMS_TYPE = "cheque_registration"
|
||||
# مقدار پیشفرضِ منطق قدیم («۱ روز پس از سررسید») که با معنای جدید همعددی است؛
|
||||
# فقط رکوردهایی که هنوز روی همین مقدار هستند همراستا میشوند.
|
||||
OLD_DEFAULT = 1
|
||||
NEW_DEFAULT = 1
|
||||
|
||||
|
||||
def align_cheque_offset(apps, schema_editor):
|
||||
"""``offset_days`` قالب چک را روی پیشفرض جدید بگذارد (بدون لمس متن)."""
|
||||
SMSTemplate = apps.get_model("admin_panel", "SMSTemplate")
|
||||
SMSTemplate.objects.filter(
|
||||
sms_type=SMS_TYPE, offset_days=OLD_DEFAULT,
|
||||
).update(offset_days=NEW_DEFAULT)
|
||||
|
||||
|
||||
def noop(apps, schema_editor):
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("admin_panel", "0074_alter_action_status_choices"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(align_cheque_offset, noop),
|
||||
]
|
||||
@@ -0,0 +1,36 @@
|
||||
"""افزودن متغیر ``{sayyad_id}`` به راهنمای ``help_text`` متن قالب پیامک.
|
||||
|
||||
هدف: راهنمای متغیرها در پنل، «شناسهٔ صیاد چک» را هم نشان دهد.
|
||||
|
||||
آنچه این migration عمداً **نمیکند**:
|
||||
|
||||
* ``text`` (متن واقعی قالبها) را تغییر نمیدهد — فقط ``help_text`` فیلد عوض شده
|
||||
است که در ``deconstruct()`` میآید و state مدل را جلو میبرد.
|
||||
* هیچ رکورد ``SMSTemplate`` را لمس نمیکند.
|
||||
|
||||
idempotent است؛ اجرای دوباره بیخطر است.
|
||||
"""
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("admin_panel", "0075_cheque_funding_before_due"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="smstemplate",
|
||||
name="text",
|
||||
field=models.TextField(
|
||||
help_text=(
|
||||
"متغیرهای قابل استفاده: {name} نام مشتری، {amount} مبلغ، "
|
||||
"{amount_sep} مبلغ با جداکننده، {date} تاریخ جلالی، "
|
||||
"{days} تعداد روز، {check_row} شماره ردیف چک، "
|
||||
"{sayyad_id} شناسه صیاد چک، {owner_name} نام مالک چک."
|
||||
),
|
||||
verbose_name="متن پیامک",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,39 @@
|
||||
"""افزودن متغیرهای ``{balance}`` / ``{balance_sep}`` به راهنمای متن قالب پیامک.
|
||||
|
||||
هدف: راهنمای متغیرها در پنل، «مانده حساب مشتری» را هم نشان دهد و تفاوتش را با
|
||||
مبلغ سررسیدشده روشن کند.
|
||||
|
||||
آنچه این migration عمداً **نمیکند**:
|
||||
|
||||
* ``text`` (متن واقعی قالبها) را تغییر نمیدهد — فقط ``help_text`` فیلد عوض شده
|
||||
است که در ``deconstruct()`` میآید و state مدل را جلو میبرد.
|
||||
* هیچ رکورد ``SMSTemplate`` را لمس نمیکند.
|
||||
|
||||
idempotent است؛ اجرای دوباره بیخطر است.
|
||||
"""
|
||||
from django.db import migrations, models
|
||||
|
||||
HELP_TEXT = (
|
||||
"متغیرهای قابل استفاده: {name} نام مشتری، {amount} مبلغ، "
|
||||
"{amount_sep} مبلغ با جداکننده، {date} تاریخ جلالی، {days} تعداد روز، "
|
||||
"{check_row} شماره ردیف چک، {sayyad_id} شناسه صیاد چک، "
|
||||
"{owner_name} نام مالک چک، {balance} مانده حساب مشتری، "
|
||||
"{balance_sep} مانده حساب با جداکننده. "
|
||||
"(توجه: {balance} مانده کل حساب است و با مبلغ سررسیدشدهٔ چک/نسیه "
|
||||
"که در {amount} میآید تفاوت دارد.)"
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("admin_panel", "0076_alter_smstemplate_text_help"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="smstemplate",
|
||||
name="text",
|
||||
field=models.TextField(help_text=HELP_TEXT, verbose_name="متن پیامک"),
|
||||
),
|
||||
]
|
||||
@@ -22,7 +22,35 @@ class VisitQueue(models.Model):
|
||||
|
||||
|
||||
class Action(models.Model):
|
||||
actionType = models.CharField(max_length=50, null=True, blank=True)
|
||||
"""اقدام/وظیفهٔ CRM مرتبط با مشتری.
|
||||
|
||||
``actionType`` فقط این شش مقدار را میپذیرد؛ همین شش مقدار در فرمهای پنل
|
||||
(``customerActionInc.html``) و در تمام مقایسههای کد (``== 'bazdid'``،
|
||||
``exclude(actionType='vosol')`` و …) استفاده میشوند.
|
||||
|
||||
``status`` نیز همان سه مقدارِ بهکاررفته در کد و قالبها را میپذیرد.
|
||||
املای ``'waite'`` عمداً حفظ شده است: ۲٫۷۱۹ رکورد موجودِ دیتابیس و تمام
|
||||
مقایسههای کد/قالب (``== 'waite'``، ``if filter_status == 'waite'``) با
|
||||
همین املا کار میکنند و تغییرش داده را از موتور جستوجو جدا میکند.
|
||||
"""
|
||||
|
||||
class ActionType(models.TextChoices):
|
||||
BAZDID = 'bazdid', 'بازدید'
|
||||
NASB = 'nasb', 'نصب'
|
||||
VOSOL = 'vosol', 'وصول مطالبات'
|
||||
HAMAHANGI = 'hamahangi', 'هماهنگی'
|
||||
IN_CALL = 'inCall', 'تماس ورودی'
|
||||
OUT_CALL = 'outCall', 'تماس خروجی'
|
||||
|
||||
class Status(models.TextChoices):
|
||||
# املا عیناً مطابق داده و کد فعلی است (نه 'wait').
|
||||
WAITE = 'waite', 'در انتظار (باز)'
|
||||
FINISH = 'finish', 'انجام شده'
|
||||
CANCELL = 'cancell', 'لغو شده'
|
||||
|
||||
actionType = models.CharField(
|
||||
max_length=50, null=True, blank=True, choices=ActionType.choices,
|
||||
)
|
||||
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)
|
||||
@@ -31,7 +59,9 @@ class Action(models.Model):
|
||||
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)
|
||||
status = models.CharField(
|
||||
max_length=50, null=True, blank=True, choices=Status.choices,
|
||||
)
|
||||
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)
|
||||
|
||||
@@ -154,7 +154,11 @@ class SMSTemplate(models.Model):
|
||||
verbose_name="متن پیامک",
|
||||
help_text="متغیرهای قابل استفاده: {name} نام مشتری، {amount} مبلغ، "
|
||||
"{amount_sep} مبلغ با جداکننده، {date} تاریخ جلالی، {days} تعداد روز، "
|
||||
"{check_row} شماره ردیف چک، {owner_name} نام مالک چک.",
|
||||
"{check_row} شماره ردیف چک، {sayyad_id} شناسه صیاد چک، "
|
||||
"{owner_name} نام مالک چک، {balance} مانده حساب مشتری، "
|
||||
"{balance_sep} مانده حساب با جداکننده. "
|
||||
"(توجه: {balance} مانده کل حساب است و با مبلغ سررسیدشدهٔ چک/نسیه "
|
||||
"که در {amount} میآید تفاوت دارد.)",
|
||||
)
|
||||
is_enabled = models.BooleanField(default=True, verbose_name="فعال")
|
||||
auto_send = models.BooleanField(
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
"""مانده حساب مشتری برای استفاده در قالبهای پیامک.
|
||||
|
||||
تعریف (همان قاعدهٔ صفحهٔ حسابداری مشتری و گزارش مالی)::
|
||||
|
||||
مانده حساب = مجموع بدهکار (debit) − مجموع بستانکار (credit)
|
||||
|
||||
نکتهٔ مهم — تفاوت با «مانده سررسیدشده»:
|
||||
|
||||
* **مانده حساب** (این ماژول): تراز کل همهٔ اسناد حسابداری مشتری (``Article``).
|
||||
منبع: ``admin_panel/models/finance.py::Article``.
|
||||
* **مانده/مبلغ سررسیدشده**: مبلغ چکها و نسیههای پرداختنشده در جدول
|
||||
``Sarresid`` (``payStatus='false'``). بسیار متفاوت از مانده حساب است و
|
||||
جای دیگری (``views/finance.py``) محاسبه میشود.
|
||||
|
||||
به همین دلیل این مقدار با کلید مستقل ``{balance}`` در قالبها منتشر میشود و
|
||||
هرگز با مبلغ سررسید (``{amount}`` در پیامک چک/نسیه) قاطی نمیشود.
|
||||
"""
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def customer_balance(customer):
|
||||
"""مانده حساب مشتری (``debit − credit``) بهصورت عدد صحیح.
|
||||
|
||||
اگر مشتری یا اسنادش وجود نداشته باشد ``0`` برمیگردد. هیچ استثنایی پرتاب
|
||||
نمیکند تا مسیر ساخت پیامک هرگز مختل نشود.
|
||||
"""
|
||||
if customer is None or getattr(customer, "pk", None) is None:
|
||||
return 0
|
||||
try:
|
||||
from django.db.models import Sum
|
||||
|
||||
from ..models import Article
|
||||
|
||||
agg = Article.objects.filter(customer=customer).aggregate(
|
||||
d=Sum("debit"), c=Sum("credit"),
|
||||
)
|
||||
debit = agg["d"] or 0
|
||||
credit = agg["c"] or 0
|
||||
return debit - credit
|
||||
except Exception:
|
||||
logger.exception("محاسبهٔ مانده حساب مشتری %s شکست خورد", customer.pk)
|
||||
return 0
|
||||
@@ -5,6 +5,7 @@ from uuid import uuid4
|
||||
from django.db import IntegrityError
|
||||
|
||||
from ..models import SMSDedup, SMSQueue, SMSTemplate, SMSType
|
||||
from .balance import customer_balance
|
||||
from .recipients import resolve_recipients
|
||||
from .render import render_template
|
||||
from .rules import is_sms_enabled
|
||||
@@ -22,6 +23,11 @@ def enqueue_sms(*, customer=None, sms_type, context=None, dedup_key=None,
|
||||
یا آزمایشی)، و ``SMSTemplate.auto_send`` که مدیر در صفحهٔ قالبها برای هر نوع
|
||||
پیامک روشن میکند تا پیامکهای خودکارِ اسکنر بدون صف تأیید ارسال شوند.
|
||||
|
||||
متغیرهای مشترکِ همهٔ قالبها (مثل ``{balance}`` = مانده حساب مشتری) همینجا و
|
||||
یکبار به ``context`` افزوده میشوند تا هر قالبی بتواند از آنها استفاده کند؛
|
||||
فراخوانها لازم نیست هر بار آنها را بسازند. اگر فراخوان خودش مقداری داده باشد
|
||||
(``context``) همان اولویت دارد.
|
||||
|
||||
هیچ استثنایی به فراخوان پرتاب نمیشود؛ خطاها فقط لاگ میشوند تا مسیر اصلی
|
||||
(مثلاً ایجاد مشتری یا اسکنر) هرگز بهخاطر پیامک مختل نشود.
|
||||
"""
|
||||
@@ -30,7 +36,10 @@ def enqueue_sms(*, customer=None, sms_type, context=None, dedup_key=None,
|
||||
template = SMSTemplate.objects.filter(sms_type=sms_type, is_enabled=True).first()
|
||||
if template is None:
|
||||
return 0
|
||||
message = render_template(template.text, context)
|
||||
ctx = dict(context or {})
|
||||
# متغیرهای سراسری قالبها؛ هرگز مقدار فراخوان را بازنویسی نمیکنند.
|
||||
ctx.setdefault("balance", customer_balance(customer))
|
||||
message = render_template(template.text, ctx)
|
||||
# «ارسال خودکار» هر نوع پیامک در صفحهٔ قالبها تعیین میشود؛ خاموش = بازبینی مدیر.
|
||||
approved = approved or template.auto_send
|
||||
|
||||
|
||||
@@ -3,11 +3,21 @@ from persiantools.jdatetime import JalaliDate, JalaliDateTime
|
||||
|
||||
|
||||
class _SafeDict(dict):
|
||||
"""دیکشنری که برای کلیدهای ناشناخته رشته خالی برمیگرداند (بدون خطا)."""
|
||||
"""دیکشنری امن برای ``str.format_map``.
|
||||
|
||||
* کلیدِ ناشناخته ⇒ رشتهٔ خالی (بدون ``KeyError``).
|
||||
* کلیدِ شناختهشده با مقدار ``None`` ⇒ رشتهٔ خالی. فیلدهای اختیاری مدل
|
||||
(مثل ``Sarresid.check_sayyad_id`` یا ``check_row_number``) ممکن است ``NULL``
|
||||
باشند؛ بدون این تبدیل، ``None`` عیناً در متن پیامک چاپ میشد.
|
||||
"""
|
||||
|
||||
def __missing__(self, key):
|
||||
return ""
|
||||
|
||||
def __getitem__(self, key):
|
||||
value = super().__getitem__(key) if key in self else ""
|
||||
return "" if value is None else value
|
||||
|
||||
|
||||
def format_amount(value):
|
||||
try:
|
||||
@@ -35,6 +45,10 @@ def render_template(text, context=None):
|
||||
ctx = dict(context or {})
|
||||
if "amount" in ctx:
|
||||
ctx.setdefault("amount_sep", format_amount(ctx["amount"]))
|
||||
if "balance" in ctx:
|
||||
# مثل «مبلغ»، نسخهٔ جداکنندهدار هم آماده میشود تا در قالبها
|
||||
# ``{balance_sep}`` در دسترس باشد (۳٬۵۰۰٬۰۰۰ → "3,500,000").
|
||||
ctx.setdefault("balance_sep", format_amount(ctx["balance"]))
|
||||
if "date" in ctx:
|
||||
ctx.setdefault("date_raw", ctx["date"])
|
||||
ctx["date"] = format_jalali(ctx["date"])
|
||||
|
||||
+23
-10
@@ -81,36 +81,49 @@ class _Budget:
|
||||
|
||||
|
||||
def scan_cheque_funding(today):
|
||||
"""یادآوری «تأمین موجودی چک» چند روز پس از سررسید چکِ پرداختنشده.
|
||||
"""یادآوری «تأمین موجودی چک» چند روز **پیش از** سررسید چکِ پرداختنشده.
|
||||
|
||||
چند روزِ پس از سررسید از قالب همین نوع (قابل تنظیم از پنل) خوانده میشود و مبلغ
|
||||
چک در متن قرار میگیرد. اگر چک پرداخت شود (``payStatus=true``) دیگر یادآوری
|
||||
نمیگیرد؛ ``dedup_key`` نیز مانع ارسال تکراری در اجراهای بعدی میشود. اگر اسکنر
|
||||
چند روز از دست برود، چکهای «معوقتر» هم واجد شرط میشوند تا یادآوری از دست نرود.
|
||||
تعداد روزِ *قبل* از سررسید از قالب همین نوع (``offset_days`` در دیتابیس،
|
||||
قابل ویرایش از پنل) خوانده میشود؛ مثلاً ``offset_days = 1`` یعنی یک روز پیش
|
||||
از سررسید پیامک میرود تا مشتری فرصت تأمین موجودی داشته باشد.
|
||||
|
||||
پنجرهٔ یادآوری از «سررسید − offset روز» باز میشود و تا خودِ سررسید ادامه
|
||||
دارد؛ پس اگر اسکنر چند روز از دست برود، یادآوری از دست نمیرود (چکهایی که
|
||||
سررسیدشان رسیده هم واجد شرط میمانند). اگر چک پرداخت شود
|
||||
(``payStatus=true``) دیگر یادآوری نمیگیرد؛ ``dedup_key`` نیز مانع ارسال
|
||||
تکراری در اجراهای بعدی میشود.
|
||||
"""
|
||||
template = _template(SMSType.CHEQUE_REGISTRATION)
|
||||
if template is None:
|
||||
return 0
|
||||
budget = _Budget(SMSProviderConfig.load())
|
||||
offset = template.offset_days or 0
|
||||
target = today - timedelta(days=offset)
|
||||
# پنجرهٔ یادآوری: از «سررسید − offset» تا «سررسید». یعنی فقط سررسیدهای
|
||||
# امروز تا offset روز آینده واجد شرطاند و مقادیر دورتر هنوز پیامکی نمیگیرند.
|
||||
# (سررسیدهای گذشته هم داخل شرط میمانند تا یک یادآوری از دست نرود.)
|
||||
window_end = today + timedelta(days=max(offset, 0))
|
||||
qs = Sarresid.objects.filter(
|
||||
type="check", payStatus="false", sarresidDT__date__lte=target,
|
||||
type="check", payStatus="false", sarresidDT__date__lte=window_end,
|
||||
).select_related("customer").order_by("sarresidDT")
|
||||
count = 0
|
||||
for s in qs:
|
||||
if not s.customer or not s.sarresidDT:
|
||||
continue
|
||||
# لحظهٔ باز شدن پنجرهٔ یادآوری = سررسید + offset روز.
|
||||
trigger = _aware(s.sarresidDT) + timedelta(days=offset)
|
||||
due = s.sarresidDT.date()
|
||||
# لحظهٔ باز شدن پنجرهٔ یادآوری = سررسید − offset روز.
|
||||
trigger = _aware(s.sarresidDT) - timedelta(days=offset)
|
||||
if not budget.can_send(trigger):
|
||||
continue
|
||||
days = (today - s.sarresidDT.date()).days
|
||||
# روزهای باقیمانده تا سررسید؛ اگر سررسید دورتر از پنجره باشد پیامک نمیرود.
|
||||
days = (due - today).days
|
||||
if days > offset:
|
||||
continue
|
||||
made = enqueue_sms(
|
||||
customer=s.customer, sms_type=SMSType.CHEQUE_REGISTRATION,
|
||||
context={
|
||||
"name": s.customer.name_family, "amount": s.price,
|
||||
"date": s.sarresidDT, "days": days, "check_row": s.check_row_number,
|
||||
"sayyad_id": s.check_sayyad_id, "owner_name": s.check_owner_name,
|
||||
},
|
||||
dedup_key=f"cheque_fund:{s.id}",
|
||||
)
|
||||
|
||||
@@ -27,11 +27,11 @@ TIMING = {
|
||||
"note": "بیدرنگ پس از ثبت فاکتور ارسال میشود؛ تنظیم زمانی ندارد.",
|
||||
},
|
||||
SMSType.CHEQUE_REGISTRATION: {
|
||||
"kind": AFTER_DUE,
|
||||
"kind": BEFORE_DUE,
|
||||
"field": "offset_days",
|
||||
"label": "چند روز پس از سررسید چک",
|
||||
"label": "چند روز قبل از سررسید چک",
|
||||
"default": 1,
|
||||
"note": "این تعداد روز پس از سررسید چکِ پرداختنشده، پیامک یادآوری "
|
||||
"note": "این تعداد روز پیش از سررسید چکِ پرداختنشده، پیامک یادآوری "
|
||||
"تأمین موجودی با مبلغ چک ارسال میشود.",
|
||||
},
|
||||
SMSType.VISIT: {
|
||||
|
||||
@@ -12,6 +12,7 @@ from persiantools.jdatetime import JalaliDate
|
||||
|
||||
from ..models import (Customer, PhoneNumber, ProductCategory, Product, Factor,
|
||||
FactorIteam, Article, Sarresid, Action, CacheTable)
|
||||
from ..sms.balance import customer_balance
|
||||
from ..sms.phones import (normalize_primary_values,
|
||||
save_primary_normalized)
|
||||
from ..sms.triggers import notify_payment_thanks, notify_welcome
|
||||
@@ -437,14 +438,13 @@ def customerDel(request, c_id):
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customerHesabdari(request ,c_id):
|
||||
customer = Customer.objects.get(id=c_id)
|
||||
# مانده حساب از همان تابع مشترکی میآید که قالبهای پیامک استفاده میکنند تا
|
||||
# عددِ این صفحه و پیامک هرگز واگرا نشوند (منبع حقیقت: sms/balance.py).
|
||||
mandehesabCs = customer_balance(customer)
|
||||
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
|
||||
articls = None
|
||||
|
||||
dateNow = datetime.datetime.now().date
|
||||
data = {'customer': customer, 'mandehesabCs': mandehesabCs, 'articls': articls}
|
||||
|
||||
@@ -64,14 +64,17 @@ TEMPLATE_VARIABLES = [
|
||||
("{date}", "تاریخ جلالی"),
|
||||
("{days}", "تعداد روز"),
|
||||
("{check_row}", "شمارهٔ ردیف چک"),
|
||||
("{sayyad_id}", "شناسهٔ چک صیاد (۱۶ رقم)"),
|
||||
("{owner_name}", "نام مالک چک"),
|
||||
("{balance}", "مانده حساب مشتری (بدهکار − بستانکار)"),
|
||||
("{balance_sep}", "مانده حساب با جداکنندهٔ هزارگان"),
|
||||
]
|
||||
|
||||
# فقط برای نمایش: آیکن و توضیح کوتاه «چه زمانی ارسال میشود» هر نوع (لایهٔ UI، نه دامنه).
|
||||
SMS_TYPE_UI = {
|
||||
SMSType.WELCOME: {"icon": "👋", "when": "بیدرنگ پس از ثبت مشتری"},
|
||||
SMSType.SURVEY: {"icon": "⭐", "when": "بیدرنگ پس از ثبت فاکتور"},
|
||||
SMSType.CHEQUE_REGISTRATION: {"icon": "🧾", "when": "چند روز پس از سررسید چک"},
|
||||
SMSType.CHEQUE_REGISTRATION: {"icon": "🧾", "when": "چند روز پیش از سررسید چک"},
|
||||
SMSType.VISIT: {"icon": "🔧", "when": "پس از گذشت دورهٔ بازدید دستگاه"},
|
||||
SMSType.VISIT_CREATED: {"icon": "📅", "when": "پس از ثبت اقدام بازدید"},
|
||||
SMSType.PAYMENT_THANKS: {"icon": "🙏", "when": "پس از ثبت پرداخت"},
|
||||
@@ -86,7 +89,10 @@ SAMPLE_CONTEXT = {
|
||||
"date": None, # در زمان ساخت ویو با تاریخ امروز پر میشود
|
||||
"days": 2,
|
||||
"check_row": "1234",
|
||||
"sayyad_id": "1234567890123456",
|
||||
"owner_name": "نام مالک",
|
||||
"balance": 3500000,
|
||||
"balance_sep": "3,500,000",
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user