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,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: {
|
||||
|
||||
Reference in new issue
Block a user