improve bazdid 2

This commit is contained in:
javad committed 2026-10-21 11:28:01 +03:30
1 parent 534a84f977
commit f8d768a724
6 files changed
+374 -92

No files matched your search

+136 -7
View File
@@ -927,6 +927,80 @@ class VisitHoldTests(TestCase):
)
self.assertNotIn(customer.pk, self._ids())
# ---------- ثبت بازدید پس از هماهنگی ⇒ قرار هماهنگی منتفی می‌شود ----------
def test_visit_created_after_coordination_clears_manual_hold(self):
"""سناریوی گزارش‌شده: هماهنگی + قرار، سپس ثبت «بازدید».
انتظار: مشتری از `view=manual` خارج شود و در `view=scheduled` دیده شود.
"""
customer = self._customer('بازدید بعد از هماهنگی')
Action.objects.create(
customer=customer, actionType='hamahangi', status='finish',
# «چند دقیقه پیش تماس گرفتم» — از مرز لحظه‌ای فاصله دارد.
actionDateTime=timezone.now() - timedelta(minutes=5),
finishDateTime=timezone.now() - timedelta(minutes=5),
)
set_visit_hold(customer, timezone.now() + timedelta(days=2))
self.assertIn(customer.pk, self._ids('manual'))
# بازدید برنامه‌ریزی‌شده (بدون تیک «انجام شد»).
self._post_action(
customer, actionType='bazdid',
dateTimeAnjam=self._ms(timezone.now() + timedelta(hours=2)),
desc='بازدید برنامه‌ریزی شد',
)
customer.refresh_from_db()
self.assertIsNone(customer.visit_hold_until, 'قرار دستی باید پاک شود')
self.assertNotIn(customer.pk, self._ids('manual'))
self.assertIn(customer.pk, self._ids('scheduled'))
def test_visit_created_after_coordination_without_hold_still_schedules(self):
customer = self._customer('بدون قرار قبلی')
self._post_action(
customer, actionType='bazdid',
dateTimeAnjam=self._ms(timezone.now() + timedelta(days=3)),
desc='بازدید',
)
self.assertIn(customer.pk, self._ids('scheduled'))
def test_old_visit_does_not_void_newer_coordination(self):
"""بازدید قدیمی‌تر نباید قرار هماهنگیِ جدیدتر را باطل کند."""
customer = self._customer('هماهنگی جدیدتر')
Action.objects.create(
customer=customer, actionType='bazdid', status='finish',
actionDateTime=timezone.now() - timedelta(days=400),
finishDateTime=timezone.now() - timedelta(days=400),
)
Action.objects.create(
customer=customer, actionType='hamahangi', status='finish',
actionDateTime=timezone.now() - timedelta(minutes=5),
finishDateTime=timezone.now() - timedelta(minutes=5),
)
set_visit_hold(customer, timezone.now() + timedelta(days=20))
self.assertIn(customer.pk, self._ids('manual'))
def test_scheduled_visit_earlier_today_still_counts(self):
"""بازدیدِ برنامه‌ریزی‌شده برای «امروز» تا پایان امروز معتبر است.
رگرسیون: فیلتر قبلی لحظه‌ای بود و بازدیدی که ساعتش چند دقیقه پیش گذشته
بود از فهرست ناپدید می‌شد.
"""
customer = self._customer('بازدید امروز')
Action.objects.create(
customer=customer, actionType='bazdid', status='waite',
actionDateTime=timezone.now() - timedelta(minutes=30),
)
self.assertIn(customer.pk, self._ids('scheduled'))
def test_scheduled_visit_yesterday_is_not_upcoming(self):
customer = self._customer('بازدید دیروز')
Action.objects.create(
customer=customer, actionType='bazdid', status='waite',
actionDateTime=timezone.now() - timedelta(days=1),
)
self.assertNotIn(customer.pk, self._ids('scheduled'))
# ---------- تفکیک چهارگروهی و فیلترها ----------
def test_groups_and_filters(self):
@@ -937,23 +1011,31 @@ class VisitHoldTests(TestCase):
# b: قرار دستی (هماهنگی با تاریخ تماس «همین حالا» + ستون قرار ۲۰ روز بعد).
Action.objects.create(customer=b, actionType='hamahangi', status='finish',
actionDateTime=timezone.now(), finishDateTime=timezone.now())
actionDateTime=timezone.now() - timedelta(minutes=5),
finishDateTime=timezone.now() - timedelta(minutes=5))
set_visit_hold(b, timezone.now() + timedelta(days=20))
Action.objects.create(customer=c, actionType='bazdid', status='waite',
actionDateTime=timezone.now() + timedelta(days=7))
# d: هم قرار دستی و هم بازدید برنامه‌ریزی‌شده.
Action.objects.create(customer=d, actionType='hamahangi', status='finish',
actionDateTime=timezone.now(), finishDateTime=timezone.now())
set_visit_hold(d, timezone.now() + timedelta(days=25))
# d: بازدید برنامه‌ریزی‌شده (قدیمی‌تر) + قرار دستی (جدیدتر).
# ترتیب مهم است: اگر بازدید *پس از* هماهنگی ثبت شود، هماهنگی منتفی
# محسوب می‌شود (تست جداگانهٔ زیر). اینجا برعکس است تا گروه D پوشش داده شود.
Action.objects.create(customer=d, actionType='bazdid', status='waite',
actionDateTime=timezone.now() + timedelta(days=9))
Action.objects.create(customer=d, actionType='hamahangi', status='finish',
actionDateTime=timezone.now() - timedelta(minutes=5),
finishDateTime=timezone.now() - timedelta(minutes=5))
set_visit_hold(d, timezone.now() + timedelta(days=25))
counts = self._entezar().context['counts']
self.assertEqual(counts['uncoordinated'], 1) # a
self.assertEqual(counts['manual'], 1) # b
self.assertEqual(counts['scheduled'], 1) # c
# عددِ کارت باید با فهرستی که با کلیک باز می‌شود بخواند:
# view=manual گروه‌های B و D را نشان می‌دهد.
self.assertEqual(counts['manual'], 2) # b + d
self.assertEqual(counts['scheduled'], 2) # c + d
self.assertEqual(counts['manual_only'], 1) # b
self.assertEqual(counts['scheduled_only'], 1) # c
self.assertEqual(counts['both'], 1) # d
self.assertEqual(counts['total'], 4)
self.assertEqual(counts['coordinated'], 3)
@@ -969,6 +1051,53 @@ class VisitHoldTests(TestCase):
self.assertEqual(self._ids('nonsense'), {a.pk})
self.assertEqual(self._entezar('nonsense').context['view'], 'uncoordinated')
def test_card_number_matches_filtered_list(self):
"""عدد هر کارت باید با تعداد ردیف‌های همان فیلتر یکی باشد.
رگرسیون: کارت «قرار هماهنگی» فقط گروه B را می‌شمرد، ولی فیلتر
``view=manual`` گروه D را هم نشان می‌داد؛ کاربر عدد ۰ می‌دید و با کلیک
ردیف پیدا می‌کرد.
"""
b = self._customer('قرار دستی تنها')
Action.objects.create(customer=b, actionType='hamahangi', status='finish',
actionDateTime=timezone.now() - timedelta(minutes=5),
finishDateTime=timezone.now() - timedelta(minutes=5))
set_visit_hold(b, timezone.now() + timedelta(days=20))
d = self._customer('هم قرار و هم بازدید')
Action.objects.create(customer=d, actionType='bazdid', status='waite',
actionDateTime=timezone.now() + timedelta(days=9))
Action.objects.create(customer=d, actionType='hamahangi', status='finish',
actionDateTime=timezone.now() - timedelta(minutes=5),
finishDateTime=timezone.now() - timedelta(minutes=5))
set_visit_hold(d, timezone.now() + timedelta(days=25))
counts = self._entezar().context['counts']
self.assertEqual(counts['manual'], len(self._ids('manual')))
self.assertEqual(counts['scheduled'], len(self._ids('scheduled')))
self.assertEqual(counts['coordinated'], len(self._ids('coordinated')))
self.assertEqual(counts['uncoordinated'], len(self._ids('uncoordinated')))
self.assertEqual(counts['total'], len(self._ids('all')))
# وقتی مشتری‌ای هر دو نوع قرار را دارد، راهنمای بالای صفحه باید
# توضیح بدهد که چرا جمع دو کارت با «با قرار» برابر نیست.
self.assertGreaterEqual(counts['both'], 1)
self.assertIn('پس در هر دو کارت شمرده می‌شوند', self._entezar().content.decode())
def test_stats_cards_have_minimal_descriptions(self):
"""هر کارت آمار باید یک توضیح کوتاه داشته باشد تا معنی‌اش روشن باشد."""
html = self._entezar().content.decode()
# سرِ نوار آمار + توضیح «عقب‌افتاده»
self.assertIn('ez-stats-title', html)
self.assertIn('ez-stats-hint', html)
# پنج توضیح، یکی برای هر کارت
self.assertEqual(html.count('class="ez-desc"'), 5)
for phrase in ('هنوز وقت بازدید تعیین نشده',
'وقت بازدیدشان مشخص است',
'تعویق دستی از فرم هماهنگی',
'اقدام «بازدید» با تاریخ آینده'):
self.assertIn(phrase, html, f'توضیح «{phrase}» روی کارت‌ها نیست')
# ---------- اسکنر پیامک هم قرار دستی را می‌بیند ----------
def test_scanner_skips_customer_with_manual_hold(self):
from admin_panel.models import PhoneNumber, SMSProviderConfig, SMSQueue