"""پاک‌سازی یک‌بارهٔ شماره‌های قدیمی مشتریان (فیلدهای ``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