Files
khatibi/admin_panel/sms/phone_cleanup.py
T
2026-10-10 14:21:04 +03:30

140 lines
6.0 KiB
Python

"""پاک‌سازی یک‌بارهٔ شماره‌های قدیمی مشتریان (فیلدهای ``phone1``/``phone2``/``phone3``).
سال‌ها شماره‌ها در سه فیلد متنی ذخیره شده‌اند و به هر شکلی وارد شده‌اند:
بدون صفر ابتدایی (``9133722815``)، با ارقام فارسی، با فاصله/خط تیره، یا کلاهی
متن آزاد (نام، تاریخ نصب، «معرف …»). این ماژول قاعدهٔ تبدیل را یک‌جا تعریف می‌کند
تا هم مهاجرت (اجرای خودکار، یک‌بار) و هم دستور ``fix_customer_phones`` (اجرای
دستی روی دیتابیس production) از یک پیاده‌سازی استفاده کنند.
قاعده (طبق تصمیم کاربر):
* شمارهٔ موبایل — هرجای متن که باشد — استخراج و به شکل استاندارد ``09xxxxxxxxx``
تبدیل می‌شود.
* شمارهٔ ثابت ۸رقمی (مثل ``57387608``) **دست‌نخورده** می‌ماند.
* بقیه (متن آزاد، تاریخ، تکه‌های بی‌معنی) از فیلد شماره خالی می‌شود و متن اصلی‌اش
در فیلد ``Customer.notes`` می‌رود تا چیزی از سیستم گم نشود.
تابع ``clean_legacy_cell`` خودبسنده و idempotent است: بعد از یک‌بار اجرا، خروجی
همان مقدار فعلی است و دیگر توضیحی اضافه نمی‌شود؛ پس اجرای دوباره بی‌خطر است.
"""
import re
from persiantools import digits
from integrations.sms.normalize import normalize_mobile, to_local
_NOTE_HEADER = "— پاک‌سازی خودکار فیلدهای تلفن —"
def _numeric_tokens(raw):
"""تکه‌های رقمی متن، پس از تبدیل ارقام فارسی/عربی به انگلیسی."""
text = digits.fa_to_en(digits.ar_to_fa(str(raw)))
return re.findall(r"\d+", text)
def clean_legacy_cell(raw):
"""یک فیلد تلفن را پاک‌سازی می‌کند.
خروجی ``(value, note)`` است:
* ``value`` مقدار جدید فیلد، یا ``None`` یعنی «دست نزن».
* ``note`` متن اصلی برای افزودن به ``Customer.notes``، یا ``None``.
قاعده: هرچیزِ فیلد که در مقدار جدید نمی‌ماند، **بی‌کم‌وکاست** در
``note`` برمی‌گردد تا هیچ داده‌ای گم نشود.
"""
original = (raw or "").strip()
if not original:
return None, None
# ۱) کل فیلد یک موبایل است (شاید با فاصله/خط تیره/پیش‌شماره).
whole = normalize_mobile(original)
if whole:
local = to_local(whole)
if local == original:
return None, None
return local, original
tokens = _numeric_tokens(original)
# ۲) موبایلی داخل متن پنهان است (مثل «57322182/9130261671» یا «موبایل: 0912…»).
for token in tokens:
local = to_local(token)
if local:
return local, original
# ۳) شمارهٔ ثابت ۸رقمیِ تنها ⇒ دست‌نخورده.
if len(tokens) == 1 and len(tokens[0]) == 8:
return None, None
# ۴) بقیه (متن آزاد، تاریخ، تکهٔ بی‌معنی) ⇒ خالی؛ متن اصلی در یادداشت می‌ماند.
return "", original
def _append_note(notes, lines):
"""خطوط توضیح را به انتهای یادداشت مشتری اضافه می‌کند (با سرصفحهٔ یک‌بار)."""
current = (notes or "").strip()
block = "\n".join(lines)
if _NOTE_HEADER in current:
return f"{current}\n{block}"
if current:
return f"{current}\n\n{_NOTE_HEADER}\n{block}"
return f"{_NOTE_HEADER}\n{block}"
def clean_customer(customer, *, dry_run=False):
"""فیلدهای تلفن یک مشتری را اصلاح و متن‌های حذف‌شده را در ``notes`` می‌ریزد.
خروجی: تعداد فیلدهای تغییرکرده. اگر تغییری نباشد، چیزی ذخیره نمی‌شود.
"""
changed = 0
note_lines = []
updates = {}
for field in ("phone1", "phone2", "phone3"):
value, note = clean_legacy_cell(getattr(customer, field, ""))
if value is not None and value != (getattr(customer, field) or ""):
updates[field] = value
changed += 1
if note:
note_lines.append(f"«{field}»: {note}")
if not changed and not note_lines:
return 0
if dry_run:
return changed or (1 if note_lines else 0)
if note_lines:
customer.notes = _append_note(customer.notes, note_lines)
updates["notes"] = customer.notes
if updates:
for field, value in updates.items():
setattr(customer, field, value)
customer.save(update_fields=list(updates))
return changed
def clean_all(*, dry_run=False, batch_size=500, apps=None):
"""همهٔ مشتریان را پاک‌سازی می‌کند. خروجی: دیکشنری آمار.
``apps`` فقط برای مهاجرت‌ها است تا مدل را از رجیستری تاریخی بگیرد (نه import
زمان‌اجرا)؛ در غیر این صورت مدل زندهٔ ``Customer`` استفاده می‌شود.
"""
if apps is not None:
Customer = apps.get_model("admin_panel", "Customer")
else:
from ..models import Customer
stats = {"customers": 0, "fields": 0, "noted": 0}
qs = Customer.objects.all().only(
"id", "phone1", "phone2", "phone3", "notes"
).order_by("id")
for customer in qs.iterator(chunk_size=batch_size):
before_notes = (customer.notes or "").strip()
changed = clean_customer(customer, dry_run=dry_run)
if changed:
stats["customers"] += 1
stats["fields"] += changed
if (not dry_run) and (customer.notes or "").strip() != before_notes:
stats["noted"] += 1
return stats