improve bazdid 6

This commit is contained in:
javad committed 2026-10-22 14:01:08 +03:30
1 parent 99c25a8216
commit fa73a734a0
21 files changed
+32760 -7985

No files matched your search

+370
View File
@@ -0,0 +1,370 @@
#!/usr/bin/env python
"""ممیزیِ فقط-خواندنیِ رفتار «قرار / پیگیری» روی دیتابیس واقعی.
این اسکریپت هیچ نوشتنی روی دیتابیسِ ورودی انجام نمی‌دهد: ابتدا یک **کپی موقت**
از فایل sqlite (به‌همراه ``-wal``/``-shm``) می‌سازد و همهٔ کوئری‌ها و توابع
``admin_panel.visit_policy`` را روی همان کپی اجرا می‌کند.
اجرا::
.venv/Scripts/python.exe scripts/entezar_action_audit.py
.venv/Scripts/python.exe scripts/entezar_action_audit.py --db data9/xxx.sqlite3
.venv/Scripts/python.exe scripts/entezar_action_audit.py --samples 10
"""
import argparse
import os
import shutil
import sys
import tempfile
from collections import Counter
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):
"""کپی امنِ dbfile + فایل‌های WAL در پوشهٔ موقت؛ مسیر کپی برگردانده می‌شود."""
target = os.path.join(target_dir, 'audit_copy.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 _aware(value, tz):
from django.utils import timezone
if value is None:
return None
if timezone.is_naive(value):
return timezone.make_aware(value, tz)
return value
def _local_date(value, tz):
value = _aware(value, tz)
return None if value is None else value.astimezone(tz).date()
def _pct(part, whole):
return 0.0 if not whole else round(100.0 * part / whole, 1)
def main():
parser = argparse.ArgumentParser(description='ممیزی فقط-خواندنی صفحهٔ انتظار')
parser.add_argument('--db', default=None, help='مسیر دیتابیس (پیش‌فرض: settings)')
parser.add_argument('--samples', type=int, default=5, help='تعداد نمونه در خروجی')
parser.add_argument('--customer', type=int, default=None,
help='شناسهٔ مشتری برای نمایش خط زمانی کامل اقدام‌ها')
args = parser.parse_args()
import django
django.setup()
from django.conf import settings
from django.utils import timezone
from admin_panel.models import Action, Customer
from admin_panel.visit_policy import (SOURCE_COORDINATION, SOURCE_MANUAL,
appointment_sources_map,
followups_on_date, last_visit_map,
overdue_followups)
source_db = args.db or str(settings.DATABASES['default']['NAME'])
if not os.path.isabs(source_db):
source_db = os.path.join(BASE_DIR, source_db)
if not os.path.exists(source_db):
raise SystemExit('دیتابیس پیدا نشد: %s' % source_db)
tmp_dir = tempfile.mkdtemp(prefix='entezar_audit_')
settings.DATABASES['default']['NAME'] = _copy_db(source_db, tmp_dir)
from django.db import connections
connections.close_all()
print('دیتابیسِ کپیِ موقت: %s' % settings.DATABASES['default']['NAME'])
now = timezone.now()
tz = timezone.get_current_timezone()
today = now.astimezone(tz).date()
# ---------------- داده‌های پایه ----------------
customers = Customer.objects.all().values_list(
'id', 'name_family', 'is_active', 'laghv_bazdid', 'visit_hold_until', 'installDate',
)
customer_rows = {row[0]: row for row in customers}
coord_rows = list(
Action.objects.filter(
actionType=Action.ActionType.HAMAHANGI,
status__in=(Action.Status.WAITE, Action.Status.FINISH),
deleted_at__isnull=True,
).values_list('customer_id', 'id', 'actionDateTime', 'finishDateTime',
'createdDT', 'status')
)
visit_rows = list(
Action.objects.filter(
actionType=Action.ActionType.BAZDID,
status=Action.Status.FINISH,
).values_list('customer_id', 'id', 'actionDateTime', 'finishDateTime', 'createdDT')
)
pending_rows = list(
Action.objects.filter(
actionType=Action.ActionType.BAZDID,
status=Action.Status.WAITE,
).values_list('customer_id', 'id', 'actionDateTime', 'finishDateTime', 'createdDT')
)
print('مشتریان: %d | هماهنگی فعال: %d | بازدید انجام‌شده: %d | بازدید در انتظار: %d'
% (len(customer_rows), len(coord_rows), len(visit_rows), len(pending_rows)))
visits = last_visit_map(now=now)
sources = appointment_sources_map(now=now)
overdue = overdue_followups(now=now, target_date=today)
today_followups = followups_on_date(today, now=now)
last_coord = {}
for cid, aid, adt, fdt, created, status in coord_rows:
current = last_coord.get(cid)
created_aware = _aware(created, tz)
if current is None or created_aware > current['created']:
last_coord[cid] = {
'id': aid, 'actionDateTime': adt, 'finishDateTime': fdt,
'created': created_aware, 'status': status,
}
visits_by_customer = {}
for row in visit_rows:
visits_by_customer.setdefault(row[0], []).append(row)
def effective(dt, fdt):
"""قاعدهٔ ``effective_visit_dt``: actionDateTime اگر گذشته باشد، وگرنه finish."""
for value in (_aware(dt, tz), _aware(fdt, tz)):
if value is not None and value <= now:
return value
return None
def analyse(cid):
"""تشخیص علت حضور در «نیازمند پیگیری»."""
coord = last_coord.get(cid)
done = visits_by_customer.get(cid) or []
fact = {
'coord_day': _local_date(coord['actionDateTime'], tz) if coord else None,
'coord_created': coord['created'] if coord else None,
'coord_status': coord['status'] if coord else None,
'visits_total': len(done),
'visit_after_coord_created': False,
'visit_dt_after_coord_day': False,
'last_visit': visits.get(cid),
'last_visit_day': _local_date(visits.get(cid), tz),
}
if coord is not None:
for _cid, _aid, adt, fdt, created in done:
if _aware(created, tz) > coord['created']:
fact['visit_after_coord_created'] = True
eff = effective(adt, fdt)
if eff is not None and fact['coord_day'] is not None \
and eff.astimezone(tz).date() > fact['coord_day']:
fact['visit_dt_after_coord_day'] = True
return fact
# ---------------- ۱) نمای missed ----------------
print('\n=== ۱) ?view=today&missed=1 (پیگیری‌های عقب‌افتاده) ===')
missed_ids = [cid for cid in overdue if cid in customer_rows
and customer_rows[cid][2] and not customer_rows[cid][3]
and customer_rows[cid][1]]
print('تعداد ردیف: %d' % len(missed_ids))
reasons = Counter()
samples = []
facts = {}
for cid in missed_ids:
fact = facts[cid] = analyse(cid)
if fact['visits_total'] == 0:
reasons['بدون هیچ بازدید انجام‌شده'] += 1
elif fact['visit_after_coord_created']:
reasons['بازدید انجام‌شده پس از ثبت هماهنگی'] += 1
elif fact['visit_dt_after_coord_day']:
reasons['تاریخ بازدید بعد از موعد هماهنگی است'] += 1
else:
reasons['بازدید انجام‌شده اما قدیمی‌تر از هماهنگی'] += 1
if len(samples) < args.samples and fact['visits_total']:
samples.append((cid, customer_rows[cid][1], fact))
for label, count in reasons.most_common():
print(' - %-60s %6d (%s%%)' % (label, count, _pct(count, len(missed_ids))))
with_visit = sum(1 for f in facts.values() if f['visits_total'])
after_coord = sum(1 for f in facts.values() if f['visit_after_coord_created'])
last_visit_after = sum(
1 for f in facts.values()
if f['last_visit_day'] and f['coord_day'] and f['last_visit_day'] > f['coord_day']
)
print(' => دارای حداقل یک بازدید انجام‌شده: %d (%s%%)'
% (with_visit, _pct(with_visit, len(missed_ids))))
print(' => بازدید پس از ثبت هماهنگی: %d (%s%%)'
% (after_coord, _pct(after_coord, len(missed_ids))))
print(' => آخرین بازدید بعد از موعدِ هماهنگی: %d (%s%%)'
% (last_visit_after, _pct(last_visit_after, len(missed_ids))))
statuses = Counter(f['coord_status'] for f in facts.values())
print(' => وضعیت آخرین هماهنگی: %s' % dict(statuses))
finish_without_hold = sum(
1 for cid in missed_ids
if facts[cid]['coord_status'] == Action.Status.FINISH
and not (customer_rows[cid][4] and customer_rows[cid][4] > now)
)
print(' => هماهنگیِ finish بدون قرار دستیِ فعال: %d' % finish_without_hold)
pending_old = sum(1 for cid in missed_ids
if any(row[0] == cid for row in pending_rows))
print(' => دارای بازدید در انتظارِ قدیمی (قرار منقضی): %d' % pending_old)
holds_in_missed = sum(1 for cid in missed_ids
if customer_rows[cid][4] and customer_rows[cid][4] > now)
print(' => دارای «قرار دستی» آینده و باز هم در missed: %d' % holds_in_missed)
valid_holds_in_missed = sum(
1 for cid in missed_ids if SOURCE_MANUAL in (sources.get(cid) or {}))
print(' => دارای «قرار دستی» **معتبر** و باز هم در missed: %d'
% valid_holds_in_missed)
for cid, name, fact in samples:
print(' نمونه #%s %s | موعد هماهنگی: %s | وضعیت هماهنگی: %s | آخرین بازدید: %s'
% (cid, name, fact['coord_day'], fact['coord_status'], fact['last_visit_day']))
# ---------------- ۲) نمای today ----------------
print('\n=== ۲) ?view=today (نیازمند پیگیری امروز) ===')
eligible_today = [cid for cid in today_followups if cid in customer_rows
and customer_rows[cid][2] and not customer_rows[cid][3]
and customer_rows[cid][1]]
print('تعداد ردیف: %d' % len(eligible_today))
combo = Counter()
for cid in eligible_today:
entry = dict(sources.get(cid) or {})
entry.update(today_followups.get(cid) or {})
combo[tuple(sorted(entry))] += 1
print('ترکیب منابعِ «قرار» در این نما:')
for keys, count in combo.most_common():
print(' - %-45s %6d' % ('+'.join(keys) or 'بدون منبع', count))
today_facts = [analyse(cid) for cid in eligible_today]
done_today = sum(1 for f in today_facts if f['visits_total'])
done_after = sum(1 for f in today_facts if f['visit_after_coord_created'])
def followup_day(cid):
entry = today_followups.get(cid) or {}
return _local_date(min(entry.values()), tz) if entry else None
last_after = sum(
1 for cid, f in zip(eligible_today, today_facts)
if f['last_visit_day'] and followup_day(cid)
and f['last_visit_day'] >= followup_day(cid)
)
print(' => دارای بازدید انجام‌شده: %d (%s%%)'
% (done_today, _pct(done_today, len(eligible_today))))
print(' => بازدید پس از ثبت هماهنگی: %d (%s%%)'
% (done_after, _pct(done_after, len(eligible_today))))
print(' => آخرین بازدید در/بعد از همان روزِ موعد: %d' % last_after)
for cid, fact in list(zip(eligible_today, today_facts))[:args.samples or 0]:
print(' #%-6s %-28s موعد=%-12s آخرین بازدید=%-12s منابع=%s'
% (cid, customer_rows[cid][1], followup_day(cid),
fact['last_visit_day'], sorted(today_followups.get(cid) or {})))
# تفکیک موعدهای امروز: هماهنگیِ «انجام‌شده» (زمان تماس) یا قرار دستی؟
today_coord_status = Counter()
today_with_hold = 0
for cid in eligible_today:
if SOURCE_COORDINATION in (today_followups.get(cid) or {}):
coord = last_coord.get(cid)
today_coord_status[coord['status'] if coord else '?'] += 1
if SOURCE_MANUAL in (sources.get(cid) or {}):
today_with_hold += 1
print(' => وضعیت آخرین اقدام هماهنگی (ردیف‌های دارای منبع هماهنگی): %s'
% dict(today_coord_status))
print(' => از این ردیف‌ها، دارای قرار دستیِ آینده (یعنی بعداً هم می‌آید): %d'
% today_with_hold)
# ---------------- ۳) برچسب «N قرار» ----------------
print('\n=== ۳) برچسب وضعیت «N قرار» (source_count) ===')
source_count = Counter()
for cid, entry in sources.items():
if cid in customer_rows and customer_rows[cid][2]:
source_count[len(entry)] += 1
for n in sorted(source_count):
print(' %d منبع: %6d مشتری' % (n, source_count[n]))
pairs = Counter()
same_day = 0
different_day = 0
for cid, entry in sources.items():
if len(entry) > 1:
pairs['+'.join(sorted(entry))] += 1
if SOURCE_MANUAL in entry and SOURCE_COORDINATION in entry:
if _local_date(entry[SOURCE_MANUAL], tz) == _local_date(
entry[SOURCE_COORDINATION], tz):
same_day += 1
else:
different_day += 1
print(' از ترکیب manual+coordination: هم‌روز=%d | مختلف=%d'
% (same_day, different_day))
print(' ترکیب مشتریان با بیش از یک منبع:')
for keys, count in pairs.most_common():
print(' - %-45s %6d' % (keys, count))
for cid in [cid for cid, e in sources.items() if len(e) > 1][:args.samples or 0]:
print(' نمونه #%s %s: %s'
% (cid, customer_rows[cid][1],
{k: str(_local_date(v, tz)) for k, v in sources[cid].items()}))
# ---------------- ۴) ستون قرار دستی ----------------
print('\n=== ۴) ستون visit_hold_until («تا تاریخ پیگیری نشود») ===')
holds = [(cid, _aware(row[4], tz)) for cid, row in customer_rows.items() if row[4]]
future = [(cid, until) for cid, until in holds if until > now]
past = [(cid, until) for cid, until in holds if until <= now]
valid_future = sum(
1 for cid, _ in future
if SOURCE_MANUAL in (sources.get(cid) or {})
)
print('پرشده: %d | آینده: %d | گذشته (کهنه): %d | آینده و معتبر: %d'
% (len(holds), len(future), len(past), valid_future))
print(' => از %d قرارِ آینده، %d مورد بی‌اعتبار است (آخرین فعالیت هماهنگی نیست).'
% (len(future), len(future) - valid_future))
invalid_reason = Counter()
for cid, _until in future:
if cid not in last_coord:
invalid_reason['بدون اقدام هماهنگی فعال'] += 1
elif cid in visits_by_customer and any(
_aware(row[4], tz) > last_coord[cid]['created']
for row in visits_by_customer[cid]):
invalid_reason['بازدید ثبت‌شده بعد از هماهنگی'] += 1
elif SOURCE_MANUAL not in (sources.get(cid) or {}):
invalid_reason['سایر اقدام‌های بعد از هماهنگی'] += 1
print(' دلایل بی‌اعتباری: %s' % dict(invalid_reason))
# ---------------- ۵) هماهنگی ----------------
print('\n=== ۵) اقدام‌های هماهنگی ===')
print('وضعیت: %s' % dict(Counter(row[5] for row in coord_rows)))
with_hold_field = sum(1 for cid in last_coord
if customer_rows.get(cid) and customer_rows[cid][4])
print('مشتری با هماهنگی فعال و ستونِ قرار پرشده: %d' % with_hold_field)
# ---------------- ۶) خط زمانی یک مشتری ----------------
if args.customer:
cid = args.customer
row = customer_rows.get(cid)
print('\n=== ۶) خط زمانی مشتری #%s ===' % cid)
if row is None:
print(' مشتری پیدا نشد.')
else:
print(' نام: %s | فعال: %s | لغو بازدید: %s | قرار دستی: %s'
% (row[1], row[2], row[3], row[4]))
print(' در missed: %s | در today: %s | منابع: %s'
% (cid in overdue, cid in today_followups, sources.get(cid)))
for act in Action.objects.filter(customer_id=cid).order_by('createdDT'):
print(' #%-6s %-10s %-7s action=%-19s finish=%-19s created=%-19s deleted=%s'
% (act.pk, act.actionType, act.status,
_aware(act.actionDateTime, tz), _aware(act.finishDateTime, tz),
_aware(act.createdDT, tz), act.deleted_at is not None))
shutil.rmtree(tmp_dir, ignore_errors=True)
if __name__ == '__main__':
main()