#!/usr/bin/env python """شبیه‌سازیِ «تعویقِ کوتاه‌تر از تاریخ انجامِ هماهنگی» روی دیتابیس واقعی (کپی). سناریوی گزارش‌شده: در فرم «ایجاد وظیفه» نوع = هماهنگی، «تاریخ و ساعتی که کار باید انجام شود» = ۱۴۰۵/۰۸/۰۵ و «تا تاریخ پیگیری نشود» = ۱۴۰۵/۰۸/۰۳ (یعنی قرار **قبل‌تر** از تاریخ کار). این اسکریپت روی یک **کپی موقت** از دیتابیس، یک مشتری آزمایشی می‌سازد و رفتار سیستم را روز‌به‌روز چاپ می‌کند: منابع قرار، فهرست «نیازمند پیگیری»، «عقب‌افتاده»، و اینکه اسکنر پیامک در هر روز چه می‌کند. دیتابیس اصلی دست‌نخورده می‌ماند. اجرا:: .venv/Scripts/python.exe scripts/coordination_hold_order_probe.py .venv/Scripts/python.exe scripts/coordination_hold_order_probe.py --customer 5124 """ import argparse import datetime import os import shutil import sys import tempfile BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if BASE_DIR not in sys.path: sys.path.insert(0, BASE_DIR) os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'khatibi.settings') def _copy_db(source_path, target_dir): target = os.path.join(target_dir, 'probe.sqlite3') shutil.copy2(source_path, target) for suffix in ('-wal', '-shm'): side = source_path + suffix if os.path.exists(side): shutil.copy2(side, target + suffix) return target def _day(offset): return datetime.datetime.combine( datetime.date.today() + datetime.timedelta(days=offset), datetime.time(12, 0), ) def run_variant(label, *, done, hold_offset, action_offset): """یک سناریو را می‌سازد و رفتار روز‌به‌روز را چاپ می‌کند.""" from django.utils import timezone from admin_panel.models import (Action, Customer, PhoneNumber, SMSProviderConfig, SMSQueue) from admin_panel.sms.scanner import scan_visit from admin_panel.visit_policy import (appointment_sources_map, end_of_local_day, followups_on_date, last_visit_map, manual_holds_map, next_appointment_map, overdue_followups, set_visit_hold) tz = timezone.get_current_timezone() now = timezone.now() today = now.astimezone(tz).date() customer = Customer.objects.create( name_family='مشتری آزمایشی — %s' % label, address='آزمایش', ) Customer.objects.filter(pk=customer.pk).update( installDate=today - datetime.timedelta(days=400)) PhoneNumber.objects.create(customer=customer, phone_number='09120000777') customer.refresh_from_db() action_dt = timezone.make_aware(_day(action_offset)) hold_raw = timezone.make_aware(_day(hold_offset)) if hold_offset is not None else None action = Action.objects.create( customer=customer, actionType=Action.ActionType.HAMAHANGI, status=Action.Status.FINISH if done else Action.Status.WAITE, actionDateTime=action_dt, finishDateTime=now if done else None, desc='probe', result='probe' if done else None, ) # همان مسیری که ویو طی می‌کند (شامل پیش‌فرض ۱۴ روز و «فقط رو به جلو»). from admin_panel.views.actions import _apply_visit_hold _apply_visit_hold( customer, {'actionType': 'hamahangi', 'holdRaw': hold_raw, 'status': action.status}, status=action.status, is_new=True, ) customer.refresh_from_db() config = SMSProviderConfig.load() config.scan_since = now - datetime.timedelta(days=600) config.allow_backlog = True config.backlog_max = 50 config.max_per_run = 200 config.save() print('\n' + '=' * 100) print('سناریو %s' % label) print(' تیک «انجام شد»: %s → status=%s' % ('خورده' if done else 'نخورده', action.status)) print(' تاریخ کار (actionDateTime): %s' % action.actionDateTime.astimezone(tz)) print(' تا تاریخ پیگیری نشود: %s' % ( customer.visit_hold_until.astimezone(tz) if customer.visit_hold_until else 'خالی (هیچ تعویقی ثبت نشد)')) # اثرهای جانبی «تیک انجام شد»: لیست وظایف، شمارندهٔ داشبورد، نظرسنجی. print(' در «لیست وظایف باز» (ActionList/داشبورد): %s' % ( 'بله' if action.status == Action.Status.WAITE else 'خیر')) survey = (action.status == Action.Status.FINISH and action.actionType != 'vosol' and action.nazarDT is None and action.createdDT >= now - datetime.timedelta(days=60) and (action.finishDateTime is None or action.finishDateTime >= now - datetime.timedelta(days=30))) print(' در صف «نظرسنجی»: %s' % ('بله' if survey else 'خیر')) # اسکن کامل دیتابیس گران است؛ تصمیمِ اسکنر برای همین مشتری بازسازی می‌شود: # scanner.py:232 if customer.id in appointments: continue # scanner.py:240 trigger = visit_latest_due(anchor, now=now) from admin_panel.visit_policy import cycle_anchor_map, visit_latest_due def scanner_decision(moment): appointments = next_appointment_map(now=moment) if customer.pk in appointments: return 'رد (قرار بعدی دارد)' anchors = cycle_anchor_map(now=moment) anchor = anchors.get(customer.pk) if anchor is None: return 'رد (لنگر ندارد)' trigger = visit_latest_due(anchor, now=moment) if trigger is None or trigger.date() > moment.astimezone(tz).date(): return 'رد (موعد نرسیده)' return 'ارسال یادآوری دوره‌ای' for day_offset in range(0, action_offset + 3): moment = timezone.make_aware(_day(day_offset)) sources = appointment_sources_map(now=moment) nexts = next_appointment_map(now=moment) holds = manual_holds_map(now=moment) entries = dict(sources.get(customer.pk) or {}) day = moment.astimezone(tz).date() follow = followups_on_date(day, now=moment) overdue = overdue_followups(now=moment, target_date=day) visited = last_visit_map(now=moment, customer_ids=[customer.pk]) badge_days = {value.astimezone(tz).date() for value in entries.values()} scan_cell = scanner_decision(moment) print(' %s | منابع=%-34s برچسب=%d قرار | قرار بعدی=%-11s | پیگیری=%s' ' | عقب‌افتاده=%s | قرار دستی=%s | %s' % ( day, '+'.join(sorted(entries)) or '—', len(badge_days), (nexts.get(customer.pk).astimezone(tz).date() if nexts.get(customer.pk) else '—'), 'بله' if customer.pk in follow else 'خیر', 'بله' if customer.pk in overdue else 'خیر', 'بله' if customer.pk in holds else 'خیر', scan_cell, )) _ = visited SMSQueue.objects.filter(customer=customer).delete() Action.objects.filter(customer=customer).delete() def show_customer(customer_id): """وضعیت فعلی یک مشتری واقعی + خط زمانی اقدام‌هایش (فقط خواندن).""" from django.utils import timezone from admin_panel.models import Action, Customer from admin_panel.visit_policy import (appointment_sources_map, last_visit_map, manual_holds_map, next_appointment_map) tz = timezone.get_current_timezone() customer = Customer.objects.filter(pk=customer_id).first() print('\n' + '=' * 100) print('وضعیت مشتری #%s' % customer_id) if customer is None: print(' مشتری پیدا نشد.') return print(' نام: %s | فعال: %s | لغو بازدید: %s' % ( customer.name_family, customer.is_active, customer.laghv_bazdid)) print(' visit_hold_until: %s' % customer.visit_hold_until) sources = appointment_sources_map() print(' منابع قرار: %s' % { k: str(v.astimezone(tz)) for k, v in (sources.get(customer.pk) or {}).items()}) print(' قرار بعدی: %s' % next_appointment_map().get(customer.pk)) print(' قرار دستی معتبر: %s' % (customer.pk in manual_holds_map())) print(' آخرین بازدید: %s' % last_visit_map().get(customer.pk)) print(' اقدام‌ها (۱۰ مورد آخر):') for act in Action.objects.filter(customer_id=customer_id).order_by('-createdDT')[:10]: print(' #%-6s %-10s %-7s action=%-19s finish=%-19s created=%s' % ( act.pk, act.actionType, act.status, act.actionDateTime, act.finishDateTime, act.createdDT)) def main(): parser = argparse.ArgumentParser(description='شبیه‌سازی تعویقِ قبل از تاریخ کار') parser.add_argument('--customer', type=int, default=None, help='شناسهٔ مشتری واقعی') parser.add_argument('--skip-variants', action='store_true') args = parser.parse_args() import django django.setup() from django.conf import settings from django.db import connections source_db = str(settings.DATABASES['default']['NAME']) if not os.path.isabs(source_db): source_db = os.path.join(BASE_DIR, source_db) tmp_dir = tempfile.mkdtemp(prefix='hold_probe_') settings.DATABASES['default']['NAME'] = _copy_db(source_db, tmp_dir) connections.close_all() print('دیتابیسِ کپیِ موقت: %s' % settings.DATABASES['default']['NAME']) if not args.skip_variants: run_variant('بدون تیک «انجام شد»', done=False, hold_offset=3, action_offset=5) run_variant('با تیک «انجام شد»', done=True, hold_offset=3, action_offset=5) run_variant('تعویق بعد از تاریخ کار (سالم)', done=True, hold_offset=10, action_offset=5) run_variant('تعویق خالی + تیک (پیش‌فرض ۱۴ روز)', done=True, hold_offset=None, action_offset=5) run_variant('تعویق خالی + بدون تیک', done=False, hold_offset=None, action_offset=5) if args.customer: show_customer(args.customer) shutil.rmtree(tmp_dir, ignore_errors=True) if __name__ == '__main__': main()