Files
khatibi/scripts/coordination_hold_order_probe.py
T
2026-10-22 14:01:08 +03:30

226 lines
11 KiB
Python

#!/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()