226 lines
11 KiB
Python
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()
|