This commit is contained in:
javad committed 2026-10-10 14:21:04 +03:30
1 parent 250cdbab7f
commit 5c6069c3ef
87 files changed
+29209 -3697

No files matched your search

+124
View File
@@ -0,0 +1,124 @@
# Generated by Django 3.2.25 on 2026-10-09 16:52
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('admin_panel', '0056_auto_20260731_1824'),
]
operations = [
migrations.CreateModel(
name='SMSProviderConfig',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('provider', models.CharField(choices=[('sms0098', '0098 (SOAP/zeep)'), ('http', 'HTTP عمومی'), ('console', 'Console (تست/دیباگ)')], default='sms0098', max_length=20, verbose_name='سرویس\u200cدهنده پیامک')),
('is_enabled', models.BooleanField(default=True, help_text='در صورت غیرفعال بودن، worker هیچ پیامکی ارسال نمی\u200cکند.', verbose_name='فعال بودن ارسال پیامک')),
('api_url', models.CharField(blank=True, default='', max_length=500, verbose_name='آدرس سرویس (WSDL یا URL)')),
('username', models.CharField(blank=True, default='', max_length=100, verbose_name='نام کاربری')),
('password', models.CharField(blank=True, default='', max_length=200, verbose_name='رمز عبور')),
('sender', models.CharField(blank=True, default='', max_length=50, verbose_name='شماره خط فرستنده')),
('api_token', models.CharField(blank=True, default='', max_length=255, verbose_name='توکن API (HTTP)')),
('timeout_seconds', models.PositiveIntegerField(default=30, verbose_name='مهلت ارتباط (ثانیه)')),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'تنظیمات سامانه پیامک',
'verbose_name_plural': 'تنظیمات سامانه پیامک',
'permissions': [('ManageSMS', 'مدیریت پیامک')],
},
),
migrations.CreateModel(
name='SMSTemplate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('sms_type', models.CharField(choices=[('welcome', 'خوش\u200cآمدگویی (ایجاد مشتری)'), ('cheque_due', 'یادآوری سررسید چک'), ('nesiye_due', 'یادآوری سررسید نسیه'), ('cheque_registration', 'یادآوری تأمین موجودی چک'), ('debt_due', 'یادآوری بدهی (سررسید وصول مطالبات)'), ('visit', 'یادآوری بازدید خدمات'), ('survey', 'نظرسنجی پس از فاکتور'), ('custom', 'پیام سفارشی')], max_length=40, unique=True, verbose_name='نوع پیامک')),
('title', models.CharField(max_length=100, verbose_name='عنوان')),
('text', models.TextField(help_text='متغیرهای قابل استفاده: {name} نام مشتری، {amount} مبلغ، {amount_sep} مبلغ با جداکننده، {date} تاریخ جلالی، {days} تعداد روز، {check_row} شماره ردیف چک، {owner_name} نام مالک چک.', verbose_name='متن پیامک')),
('is_enabled', models.BooleanField(default=True, verbose_name='فعال')),
('offset_days', models.PositiveSmallIntegerField(default=2, verbose_name='چند روز قبل از سررسید ارسال شود')),
('repeat_interval_days', models.PositiveSmallIntegerField(blank=True, help_text='خالی بگذارید برای ارسال یک\u200cباره؛ مقدار بدهید برای ارسال تکرارشونده.', null=True, verbose_name='فاصله تکرار (روز)')),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'قالب پیامک',
'verbose_name_plural': 'قالب\u200cهای پیامک',
},
),
migrations.AddField(
model_name='phonenumber',
name='is_primary',
field=models.BooleanField(default=False, verbose_name='شماره اصلی (گیرنده پیامک)'),
),
migrations.CreateModel(
name='SMSQueue',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('sms_type', models.CharField(blank=True, choices=[('welcome', 'خوش\u200cآمدگویی (ایجاد مشتری)'), ('cheque_due', 'یادآوری سررسید چک'), ('nesiye_due', 'یادآوری سررسید نسیه'), ('cheque_registration', 'یادآوری تأمین موجودی چک'), ('debt_due', 'یادآوری بدهی (سررسید وصول مطالبات)'), ('visit', 'یادآوری بازدید خدمات'), ('survey', 'نظرسنجی پس از فاکتور'), ('custom', 'پیام سفارشی')], default='', max_length=40, verbose_name='نوع')),
('phone_number', models.CharField(max_length=20, verbose_name='شماره گیرنده')),
('message', models.TextField(verbose_name='متن')),
('status', models.CharField(choices=[('pending', 'در انتظار'), ('processing', 'در حال پردازش'), ('sent', 'ارسال شده'), ('failed', 'ناموفق'), ('cancelled', 'لغو شده')], db_index=True, default='pending', max_length=12, verbose_name='وضعیت')),
('attempts', models.PositiveSmallIntegerField(default=0, verbose_name='تعداد تلاش')),
('max_attempts', models.PositiveSmallIntegerField(default=5, verbose_name='حداکثر تلاش')),
('next_retry_at', models.DateTimeField(blank=True, null=True, verbose_name='زمان تلاش بعدی')),
('last_error', models.TextField(blank=True, default='', verbose_name='آخرین خطا')),
('provider_id', models.CharField(blank=True, default='', max_length=100)),
('provider_code', models.CharField(blank=True, default='', max_length=50)),
('dedup_key', models.CharField(blank=True, help_text='کلید یکتاسازی برای جلوگیری از ارسال تکراری.', max_length=120, null=True, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('sent_at', models.DateTimeField(blank=True, null=True)),
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sms_queue', to='admin_panel.customer', verbose_name='مشتری')),
],
options={
'verbose_name': 'صف پیامک',
'verbose_name_plural': 'صف پیامک\u200cها',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='SMSLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('attempt_no', models.PositiveSmallIntegerField(default=1, verbose_name='شماره تلاش')),
('success', models.BooleanField(default=False, verbose_name='موفق')),
('provider_id', models.CharField(blank=True, default='', max_length=100)),
('provider_code', models.CharField(blank=True, default='', max_length=50)),
('error', models.TextField(blank=True, default='', verbose_name='خطا')),
('created_at', models.DateTimeField(auto_now_add=True)),
('queue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='logs', to='admin_panel.smsqueue', verbose_name='صف')),
],
options={
'verbose_name': 'لاگ پیامک',
'verbose_name_plural': 'لاگ\u200cهای پیامک',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='CustomerSMSSetting',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('sms_type', models.CharField(choices=[('welcome', 'خوش\u200cآمدگویی (ایجاد مشتری)'), ('cheque_due', 'یادآوری سررسید چک'), ('nesiye_due', 'یادآوری سررسید نسیه'), ('cheque_registration', 'یادآوری تأمین موجودی چک'), ('debt_due', 'یادآوری بدهی (سررسید وصول مطالبات)'), ('visit', 'یادآوری بازدید خدمات'), ('survey', 'نظرسنجی پس از فاکتور'), ('custom', 'پیام سفارشی')], max_length=40, verbose_name='نوع پیامک')),
('is_enabled', models.BooleanField(default=True, verbose_name='فعال')),
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sms_settings', to='admin_panel.customer')),
],
options={
'verbose_name': 'تنظیم پیامک مشتری',
'verbose_name_plural': 'تنظیمات پیامک مشتریان',
},
),
migrations.AddIndex(
model_name='smsqueue',
index=models.Index(fields=['status', 'next_retry_at'], name='admin_panel_status_c227cc_idx'),
),
migrations.AlterUniqueTogether(
name='customersmssetting',
unique_together={('customer', 'sms_type')},
),
]
@@ -0,0 +1,115 @@
"""دادهٔ اولیه: متن پیش‌فرض قالب‌های پیامک + علامت‌گذاری شمارهٔ اصلی از فیلدهای قدیمی.
این مایگریشن idempotent است (get_or_create / update_if) و اجرای دوبارهٔ آن بی‌خطر است.
"""
from django.db import migrations
DEFAULT_TEMPLATES = [
{
"sms_type": "welcome",
"title": "خوش‌آمدگویی",
"offset_days": 0,
"repeat_interval_days": None,
"text": "{name} عزیز، از خرید شما سپاسگزاریم. به‌امید دیدار دوباره.",
},
{
"sms_type": "cheque_due",
"title": "یادآوری سررسید چک",
"offset_days": 2,
"repeat_interval_days": None,
"text": "{name} عزیز، {days} روز تا سررسید چک شما به مبلغ {amount_sep} ریال باقی مانده است. لطفاً نسبت به تأمین موجودی اقدام فرمایید.",
},
{
"sms_type": "nesiye_due",
"title": "یادآوری سررسید نسیه",
"offset_days": 2,
"repeat_interval_days": None,
"text": "{name} عزیز، {days} روز تا سررسید بدهی شما به مبلغ {amount_sep} ریال باقی مانده است.",
},
{
"sms_type": "cheque_registration",
"title": "یادآوری تأمین موجودی چک",
"offset_days": 2,
"repeat_interval_days": 3,
"text": "{name} عزیز، سررسید چک شما ({check_row}) نزدیک است. لطفاً موجودی بانکی حساب خود را تأمین فرمایید.",
},
{
"sms_type": "debt_due",
"title": "یادآوری بدهی",
"offset_days": 0,
"repeat_interval_days": None,
"text": "{name} عزیز، مبلغ {amount_sep} ریال بدهی شما باقی مانده است. لطفاً نسبت به تسویه اقدام فرمایید.",
},
{
"sms_type": "visit",
"title": "یادآوری بازدید خدمات",
"offset_days": 0,
"repeat_interval_days": None,
"text": "{name} عزیز، زمان بازدید دوره‌ای دستگاه شما فرا رسیده است. برای تعیین وقت با ما تماس بگیرید.",
},
{
"sms_type": "survey",
"title": "نظرسنجی",
"offset_days": 2,
"repeat_interval_days": None,
"text": "{name} عزیز، نظر شما دربارهٔ خدمات ما برایمان ارزشمند است. لطفاً در نظرسنجی شرکت فرمایید.",
},
{
"sms_type": "custom",
"title": "پیام سفارشی",
"offset_days": 0,
"repeat_interval_days": None,
"text": "",
},
]
def seed_templates(apps, schema_editor):
SMSTemplate = apps.get_model("admin_panel", "SMSTemplate")
for item in DEFAULT_TEMPLATES:
SMSTemplate.objects.get_or_create(
sms_type=item["sms_type"],
defaults={
"title": item["title"],
"text": item["text"],
"is_enabled": True,
"offset_days": item["offset_days"],
"repeat_interval_days": item["repeat_interval_days"],
},
)
def unseed_templates(apps, schema_editor):
SMSTemplate = apps.get_model("admin_panel", "SMSTemplate")
SMSTemplate.objects.filter(
sms_type__in=[t["sms_type"] for t in DEFAULT_TEMPLATES]
).delete()
def backfill_primary_phones(apps, schema_editor):
"""اگر هیچ شمارهٔ اصلی‌ای ثبت نشده، شمارهٔ مطابق phone1 هر مشتری را اصلی می‌کند."""
Customer = apps.get_model("admin_panel", "Customer")
PhoneNumber = apps.get_model("admin_panel", "PhoneNumber")
for customer in Customer.objects.all():
if PhoneNumber.objects.filter(customer=customer, is_primary=True).exists():
continue
phone1 = (customer.phone1 or "").strip()
if not phone1:
continue
PhoneNumber.objects.filter(customer=customer, phone_number=phone1).update(is_primary=True)
def noop(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
("admin_panel", "0057_sms"),
]
operations = [
migrations.RunPython(seed_templates, unseed_templates),
migrations.RunPython(backfill_primary_phones, noop),
]
@@ -0,0 +1,49 @@
# Generated by Django 3.2.25 on 2026-10-09 17:08
from django.db import migrations, models
def stamp_scan_since(apps, schema_editor):
"""روی نصب‌های موجود، خط شروع را روی «همین حالا» مهر می‌کند.
نتیجه: پس از به‌روزرسانی، موارد قدیمی موجود در دیتابیس یک‌باره در صف قرار نمی‌گیرند.
"""
from django.utils import timezone
SMSProviderConfig = apps.get_model('admin_panel', 'SMSProviderConfig')
SMSProviderConfig.objects.filter(scan_since__isnull=True).update(scan_since=timezone.now())
class Migration(migrations.Migration):
dependencies = [
('admin_panel', '0058_seed_sms_templates'),
]
operations = [
migrations.AddField(
model_name='smsproviderconfig',
name='allow_backlog',
field=models.BooleanField(default=False, help_text='اگر فعال باشد، مواردی که پیش از «خط شروع» واجد شرط شده\u200cاند هم (به\u200cاندازهٔ سقف زیر، در هر اجرا) ارسال می\u200cشوند. به\u200cصورت پیش\u200cفرض خاموش است.', verbose_name='اجازهٔ ارسال موارد قدیمی'),
),
migrations.AddField(
model_name='smsproviderconfig',
name='backlog_max',
field=models.PositiveIntegerField(default=0, help_text='فقط وقتی «اجازهٔ ارسال موارد قدیمی» روشن است معنا دارد. صفر = هیچ.', verbose_name='حداکثر موارد قدیمی در هر اجرا'),
),
migrations.AddField(
model_name='smsproviderconfig',
name='max_per_run',
field=models.PositiveIntegerField(default=200, help_text='سقف کلی صف\u200cکردن برای هر نوع پیامک در هر اجرای اسکنر.', verbose_name='حداکثر پیامک هر اسکنر در هر اجرا'),
),
migrations.AddField(
model_name='smsproviderconfig',
name='scan_since',
field=models.DateTimeField(blank=True, help_text='فقط مواردی که پنجرهٔ یادآوری\u200cشان از این لحظه به بعد باز می\u200cشود در صف قرار می\u200cگیرند؛ موارد قدیمی\u200cتر (قبل از این لحظه) نادیده گرفته می\u200cشوند. اگر خالی باشد، در نخستین اجرا به\u200cصورت خودکار روی «همین حالا» تنظیم می\u200cشود.', null=True, verbose_name='خط شروع ارسال'),
),
migrations.AddField(
model_name='smsproviderconfig',
name='send_interval_seconds',
field=models.PositiveIntegerField(default=3, help_text='worker پس از هر ارسال این مدت صبر می\u200cکند تا سرویس\u200cدهنده زیر بار نرود.', verbose_name='فاصلهٔ بین هر پیامک (ثانیه)'),
),
migrations.RunPython(stamp_scan_since, migrations.RunPython.noop),
]
@@ -0,0 +1,32 @@
# Generated by Django 3.2.25 on 2026-10-09 17:15
from django.db import migrations
def rename_manage_sms_permission(apps, schema_editor):
"""نام نمایشی پرمیشن را با قرارداد پروژه هم‌راستا می‌کند.
Django هنگام ``AlterModelOptions`` رکورد Permission موجود را تغییر نمی‌دهد؛
پس نام قبلی (فارسی) روی نصب‌های موجود باقی می‌ماند و باید صریحاً به‌روزرسانی شود.
"""
Permission = apps.get_model('auth', 'Permission')
Permission.objects.filter(
content_type__app_label='admin_panel',
content_type__model='smsproviderconfig',
codename='ManageSMS',
).update(name='ManageSMS')
class Migration(migrations.Migration):
dependencies = [
('admin_panel', '0059_sms_flow_control'),
]
operations = [
migrations.AlterModelOptions(
name='smsproviderconfig',
options={'permissions': [('ManageSMS', 'ManageSMS')], 'verbose_name': 'تنظیمات سامانه پیامک', 'verbose_name_plural': 'تنظیمات سامانه پیامک'},
),
migrations.RunPython(rename_manage_sms_permission, migrations.RunPython.noop),
]
@@ -0,0 +1,119 @@
"""یکسان‌سازی شماره‌های اصلی + زمان‌بندی اختصاصی هر نوع پیامک.
* «شمارهٔ اصلی» از رکورد ``PhoneNumber`` به خودِ ``Customer`` منتقل می‌شود و با مقدارِ
نرمال‌شدهٔ شماره ذخیره می‌گردد تا با افزودن/حذف/ویرایش مداوم مشتریان پایدار بماند.
* فیلد زمانی هر قالب با معنای واقعی همان نوع هم‌راستا می‌شود (مثلاً نظرسنجی = ۳ روز بعد
از صدور فاکتور، و یادآوری ثبت چک = هر ۳ روز تا ثبت اطلاعات چک).
"""
from django.db import migrations, models
def _normalize(raw):
"""نرمال‌سازی شماره (بدون وابستگی به کد اپ، برای اجرای امن روی دادهٔ قدیمی)."""
from integrations.sms.normalize import normalize_mobile
return normalize_mobile(raw)
def backfill_primary_phones(apps, schema_editor):
"""پرچم‌های ``is_primary`` موجود را به مقدار نرمال‌شده روی مشتری منتقل می‌کند."""
PhoneNumber = apps.get_model("admin_panel", "PhoneNumber")
for customer_id, phone_number in (
PhoneNumber.objects.filter(is_primary=True)
.values_list("customer_id", "phone_number")
):
normalized = _normalize((phone_number or "").strip())
if not normalized:
continue
customer = apps.get_model("admin_panel", "Customer").objects.filter(pk=customer_id).first()
if customer is None:
continue
current = [p for p in (customer.sms_primary_phones or "").split(",") if p]
if normalized in current:
continue
current.append(normalized)
customer.sms_primary_phones = ",".join(current)
customer.save(update_fields=["sms_primary_phones"])
def normalize_template_timing(apps, schema_editor):
"""مقادیر زمانیِ بی‌معنی را با معنای درست هر نوع پیامک هم‌راستا می‌کند.
فقط مقادیری که برای آن نوع معنا ندارند اصلاح می‌شوند؛ تنظیمات معنادارِ مدیر دست‌نخورده
می‌ماند.
"""
SMSTemplate = apps.get_model("admin_panel", "SMSTemplate")
# نظرسنجی: «۳ روز بعد از صدور فاکتور» (مقدار پیشین ۲ بود).
SMSTemplate.objects.filter(sms_type="survey", offset_days=2).update(offset_days=3)
# یادآوری ثبت چک: فاصله تکرار ۳ روز و بدون offset سررسید.
for template in SMSTemplate.objects.filter(sms_type="cheque_registration"):
template.offset_days = 0
if not template.repeat_interval_days:
template.repeat_interval_days = 3
template.save(update_fields=["offset_days", "repeat_interval_days"])
# یادآوری بازدید: دورهٔ پیش‌فرض ۹ ماه (قابل تغییر از پنل).
SMSTemplate.objects.filter(
sms_type="visit", repeat_interval_months__isnull=True,
).update(repeat_interval_months=9)
# متن «یادآوری ثبت چک» به معنای جدید (ثبت اطلاعات چک) هم‌راستا می‌شود.
SMSTemplate.objects.filter(sms_type="cheque_registration").update(
title="یادآوری ثبت اطلاعات چک",
text="{name} عزیز، برای چک با سررسید {date} هنوز اطلاعات ثبت چک "
"(شمارهٔ صیادی) در سامانه ثبت نشده است. لطفاً نسبت به ثبت آن اقدام فرمایید.",
)
# سایر انواع: هیچ فیلد زمانی نباید مقدار داشته باشد.
SMSTemplate.objects.filter(
sms_type__in=["welcome", "debt_due", "custom"],
).update(offset_days=0, repeat_interval_days=None, repeat_interval_months=None)
def noop(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
("admin_panel", "0060_sms_permission_label"),
]
operations = [
migrations.AddField(
model_name="customer",
name="sms_primary_phones",
field=models.CharField(
blank=True, default="", max_length=100,
verbose_name="شماره‌های اصلی پیامک",
help_text="شماره‌های نرمال‌شده (+98…) جدا‌شده با کاما؛ خالی = شمارهٔ اول مشتری.",
),
),
# ترتیب مهم است: ابتدا مقادیر فعلی خوانده و منتقل می‌شوند، سپس ستون قدیمی حذف می‌گردد.
migrations.RunPython(backfill_primary_phones, noop),
migrations.RemoveField(model_name="phonenumber", name="is_primary"),
migrations.AddField(
model_name="smstemplate",
name="repeat_interval_months",
field=models.PositiveSmallIntegerField(
blank=True, null=True,
verbose_name="دورهٔ تکرار (ماه)",
help_text="برای «یادآوری بازدید»: چند ماه یک‌بار بازدید تکرار شود. "
"خالی = پیش‌فرض سامانه (۹ ماه).",
),
),
migrations.AlterField(
model_name="smstemplate",
name="offset_days",
field=models.PositiveSmallIntegerField(
default=0,
verbose_name="فاصلهٔ زمانی (روز)",
help_text="معنای این عدد برای هر نوع پیامک متفاوت است و در پنل توضیح داده "
"می‌شود؛ مثلاً «چند روز قبل از سررسید» یا «چند روز بعد از صدور فاکتور».",
),
),
migrations.RunPython(normalize_template_timing, noop),
]
@@ -0,0 +1,23 @@
# Generated by Django 3.2.25 on 2026-10-09 18:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('admin_panel', '0061_sms_timing_and_primary'),
]
operations = [
migrations.AlterField(
model_name='smsproviderconfig',
name='api_url',
field=models.CharField(blank=True, default='http://www.0098sms.com/sendsmslink.aspx', help_text='برای 0098SMS همان آدرس لینکی ارسال است؛ این سرویس WSDL/SOAP ندارد.', max_length=500, verbose_name='آدرس سرویس پیامک (URL)'),
),
migrations.AlterField(
model_name='smsproviderconfig',
name='provider',
field=models.CharField(choices=[('sms0098', '0098SMS (سرویس لینکی HTTP)'), ('http', 'HTTP عمومی'), ('console', 'Console (تست/دیباگ)')], default='sms0098', max_length=20, verbose_name='سرویس\u200cدهنده پیامک'),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 3.2.25 on 2026-10-10 06:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('admin_panel', '0062_sms_link_provider'),
]
operations = [
migrations.AlterField(
model_name='smsqueue',
name='status',
field=models.CharField(choices=[('pending', 'در انتظار تأیید'), ('approved', 'تأییدشده'), ('processing', 'در حال پردازش'), ('sent', 'ارسال شده'), ('failed', 'ناموفق'), ('cancelled', 'لغو شده')], db_index=True, default='pending', max_length=12, verbose_name='وضعیت'),
),
]
@@ -0,0 +1,38 @@
# Generated by Django 3.2.25 on 2026-10-10 07:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('admin_panel', '0063_alter_smsqueue_status'),
]
operations = [
migrations.AddField(
model_name='smsproviderconfig',
name='panel_no',
field=models.CharField(blank=True, default='', help_text='شمارهٔ اختصاصی پنل در وب\u200cسرویس؛ اگر خالی بماند از «شماره خط فرستنده» استفاده می\u200cشود.', max_length=50, verbose_name='شماره پنل (PnlNo)'),
),
migrations.AddField(
model_name='smsproviderconfig',
name='webservice_url',
field=models.CharField(blank=True, default='https://webservice.0098sms.com/service.asmx', help_text='فقط برای provider «وب\u200cسرویس» لازم است؛ ارسال با شناسه (smsid) و استعلام دلیوری از همین آدرس انجام می\u200cشود.', max_length=500, verbose_name='آدرس وب\u200cسرویس (SOAP/WSDL)'),
),
migrations.AddField(
model_name='smsqueue',
name='delivery_checked_at',
field=models.DateTimeField(blank=True, null=True, verbose_name='زمان آخرین استعلام دلیوری'),
),
migrations.AddField(
model_name='smsqueue',
name='delivery_status',
field=models.CharField(blank=True, choices=[('pending', 'در انتظار استعلام'), ('delivered', 'رسیده به گوشی'), ('not_delivered', 'نرسیده به گوشی'), ('expired', 'منقضی\u200cشده'), ('unknown', 'نامشخص')], db_index=True, default='', help_text='خالی = بدون پیگیری. مقدار از استعلام smsdeliveryState پر می\u200cشود.', max_length=16, verbose_name='وضعیت تحویل'),
),
migrations.AlterField(
model_name='smsproviderconfig',
name='provider',
field=models.CharField(choices=[('sms0098', '0098SMS (سرویس لینکی HTTP)'), ('sms0098ws', '0098SMS (وب\u200cسرویس + استعلام دلیوری)'), ('http', 'HTTP عمومی'), ('console', 'Console (تست/دیباگ)')], default='sms0098', max_length=20, verbose_name='سرویس\u200cدهنده پیامک'),
),
]
@@ -0,0 +1,35 @@
# Generated by Django 3.2.25 on 2026-10-10 07:48
from django.db import migrations, models
def scrub_brand(apps, schema_editor):
"""حذف نام برند از متن قالب‌هایی که پیش‌تر با «کتیبه» ذخیره شده‌اند."""
SMSTemplate = apps.get_model("admin_panel", "SMSTemplate")
for template in SMSTemplate.objects.filter(text__contains="کتیبه"):
cleaned = (
template.text.replace("به خانواده کتیبه خوش آمدید. ", "")
.replace(" نزد کتیبه", "")
)
template.text = cleaned
template.save(update_fields=["text"])
def noop(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
('admin_panel', '0064_sms_webservice_delivery'),
]
operations = [
migrations.AddField(
model_name='smsproviderconfig',
name='test_message',
field=models.TextField(blank=True, default='پیامک آزمایشی؛ جهت بررسی صحت ارسال از پنل فرستاده شده است.', help_text='متنی که با دکمهٔ «ارسال پیامک آزمایشی» فرستاده می‌شود؛ قابل ویرایش است.', verbose_name='متن پیامک آزمایشی'),
),
migrations.RunPython(scrub_brand, noop),
]
+42
View File
@@ -0,0 +1,42 @@
# Generated by Django 3.2.25 on 2026-10-10 08:05
from django.db import migrations, models
def backfill_keys(apps, schema_editor):
"""کلیدهای صفِ موجود را به دفتر منتقل می‌کند تا حذف بعدی‌شان باعث بازگشت نشود."""
SMSQueue = apps.get_model("admin_panel", "SMSQueue")
SMSDedup = apps.get_model("admin_panel", "SMSDedup")
keys = SMSQueue.objects.exclude(dedup_key__isnull=True).exclude(
dedup_key=""
).values_list("dedup_key", flat=True)
SMSDedup.objects.bulk_create(
[SMSDedup(key=key) for key in keys], ignore_conflicts=True,
)
def noop(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
('admin_panel', '0065_smsproviderconfig_test_message'),
]
operations = [
migrations.CreateModel(
name='SMSDedup',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', models.CharField(max_length=120, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
options={
'verbose_name': 'کلید پردازش\u200cشدهٔ پیامک',
'verbose_name_plural': 'کلیدهای پردازش\u200cشدهٔ پیامک',
},
),
migrations.RunPython(backfill_keys, noop),
]
@@ -0,0 +1,30 @@
# Generated by Django 3.2.25 on 2026-10-10 08:43
from django.db import migrations, models
def scrub_legacy_phones(apps, schema_editor):
"""پاک‌سازی یک‌بارهٔ فیلدهای تلفن قدیمی (idempotent).
همان قاعدهٔ ``admin_panel.sms.phone_cleanup``؛ اینجا فقط مدل از رجیستری
تاریخی گرفته می‌شود. اجرای دوباره بی‌اثر است.
"""
from admin_panel.sms.phone_cleanup import clean_all
clean_all(apps=apps)
class Migration(migrations.Migration):
dependencies = [
('admin_panel', '0066_smsdedup'),
]
operations = [
migrations.AddField(
model_name='customer',
name='notes',
field=models.TextField(blank=True, default='', help_text='یادداشت آزاد دربارهٔ مشتری؛ متن‌های حذف‌شدهٔ فیلدهای تلفن هم اینجا ثبت می‌شوند.', verbose_name='توضیحات'),
),
migrations.RunPython(scrub_legacy_phones, migrations.RunPython.noop),
]
@@ -0,0 +1,23 @@
# Generated by Django 3.2.25 on 2026-10-10 09:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('admin_panel', '0067_customer_notes'),
]
operations = [
migrations.AlterField(
model_name='smsproviderconfig',
name='allow_backlog',
field=models.BooleanField(default=False, help_text='اگر فعال باشد، مواردی که پیش از «تاریخ شروع دوره» واجد شرط شده\u200cاند هم (به\u200cاندازهٔ سقف زیر، در هر اجرا) ارسال می\u200cشوند. به\u200cصورت پیش\u200cفرض خاموش است تا مشتریان قدیمی مشمول این فیلتر شوند.', verbose_name='اجازهٔ ارسال موارد قدیمی'),
),
migrations.AlterField(
model_name='smsproviderconfig',
name='scan_since',
field=models.DateTimeField(blank=True, help_text='فیلتر شروع دوره: فقط مواردی که «پنجرهٔ یادآوری»شان از این لحظه به بعد باز می\u200cشود در صف قرار می\u200cگیرند؛ موارد قدیمی\u200cتر (مثل مشتریان خیلی قدیمی که مدت\u200cهاست بازدید نشده\u200cاند) نادیده گرفته می\u200cشوند. از پنل به شکل تاریخ شمسی ویرایش می\u200cشود و روی «آغاز همان روز» تنظیم می\u200cگردد. اگر خالی باشد، در نخستین اجرا خودکار روی «همین حالا» تنظیم می\u200cشود.', null=True, verbose_name='تاریخ شروع دوره'),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 3.2.25 on 2026-10-10 10:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('admin_panel', '0068_auto_20261010_1305'),
]
operations = [
migrations.AddField(
model_name='smstemplate',
name='auto_send',
field=models.BooleanField(default=False, help_text='اگر خاموش باشد (پیش\u200cفرض)، پیامک\u200cهای این نوع در وضعیت «در انتظار تأیید» در صف می\u200cمانند تا مدیر آن\u200cها را تأیید کند. با روشن\u200cکردن، پیامکِ ساخته\u200cشده توسط اسکنر مستقیماً تأیید و بلافاصله توسط worker ارسال می\u200cشود.', verbose_name='ارسال خودکار (بدون تأیید مدیر)'),
),
]