improve bazdid 3
This commit is contained in:
1 parent
f8d768a724
commit
f86d060e4d
6 files changed
+337
-121
No files matched your search
@@ -62,7 +62,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% for action in actions %}
|
{% for action in actions %}
|
||||||
<div customer_id='{{ action.customer.id }}' action_id='{{ action.id }}' class="columns is-mobile is-multiline is-gapless actionBox {{ bgBox }}">
|
<div customer_id='{{ action.customer.id }}' action_id='{{ action.id }}'
|
||||||
|
customer_hold='{% if action.customer.visit_hold_until %}{{ action.customer.visit_hold_until|date:"c" }}{% endif %}'
|
||||||
|
class="columns is-mobile is-multiline is-gapless actionBox {{ bgBox }}">
|
||||||
<div class="column is-12-mobile is-12-desktop actionBoxDesc actionBoxOpenB">
|
<div class="column is-12-mobile is-12-desktop actionBoxDesc actionBoxOpenB">
|
||||||
<div class="pr-4 pl-4 pb-4 pt-4">
|
<div class="pr-4 pl-4 pb-4 pt-4">
|
||||||
{% ifequal action.status 'finish' %}
|
{% ifequal action.status 'finish' %}
|
||||||
@@ -113,7 +115,9 @@
|
|||||||
<p style="text-align: center; margin-bottom: 20px; color: #d90000; font-size: 18px;"> کارهای مانده از روزهای قبل</p>
|
<p style="text-align: center; margin-bottom: 20px; color: #d90000; font-size: 18px;"> کارهای مانده از روزهای قبل</p>
|
||||||
|
|
||||||
{% for action in actionPr %}
|
{% for action in actionPr %}
|
||||||
<div customer_id='{{ action.customer.id }}' action_id='{{ action.id }}' class="columns is-mobile is-multiline is-gapless actionBox {{ bgBox }}">
|
<div customer_id='{{ action.customer.id }}' action_id='{{ action.id }}'
|
||||||
|
customer_hold='{% if action.customer.visit_hold_until %}{{ action.customer.visit_hold_until|date:"c" }}{% endif %}'
|
||||||
|
class="columns is-mobile is-multiline is-gapless actionBox {{ bgBox }}">
|
||||||
<div class="column is-12-mobile is-12-desktop actionBoxDesc actionBoxOpenB">
|
<div class="column is-12-mobile is-12-desktop actionBoxDesc actionBoxOpenB">
|
||||||
<div class="pr-4 pl-4 pb-4 pt-4">
|
<div class="pr-4 pl-4 pb-4 pt-4">
|
||||||
{% ifequal action.status 'finish' %}
|
{% ifequal action.status 'finish' %}
|
||||||
@@ -474,6 +478,26 @@
|
|||||||
// مقدار undefined میشود. بدون این محافظ، یک ناهمخوانی نام کلاس باعث
|
// مقدار undefined میشود. بدون این محافظ، یک ناهمخوانی نام کلاس باعث
|
||||||
// TypeError میشد و بقیهٔ منطقِ هندلر (پنهانکردن فیلدها، برداشتن تیک خودکار)
|
// TypeError میشد و بقیهٔ منطقِ هندلر (پنهانکردن فیلدها، برداشتن تیک خودکار)
|
||||||
// اجرا نمیشد.
|
// اجرا نمیشد.
|
||||||
|
// پرکردن پیکر «تا تاریخ پیگیری نشود» از مقدار ذخیرهشدهٔ مشتری.
|
||||||
|
// بدون این، هنگام بازکردن مجدد یک اقدام، فیلد همیشه خالی دیده میشد
|
||||||
|
// (مقدار در دیتابیس بود ولی مودال آن را بارگذاری نمیکرد).
|
||||||
|
function seedHoldPicker(isoValue) {
|
||||||
|
var box = $("#visitHoldUntil");
|
||||||
|
if (!isoValue) {
|
||||||
|
resetHoldPicker();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var ms = new Date(isoValue).getTime();
|
||||||
|
if (isNaN(ms)) {
|
||||||
|
resetHoldPicker();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (dateTimePickerHoldUntil && typeof dateTimePickerHoldUntil.setDate === 'function') {
|
||||||
|
dateTimePickerHoldUntil.setDate(ms);
|
||||||
|
}
|
||||||
|
box.val(String(ms));
|
||||||
|
}
|
||||||
|
|
||||||
function resetHoldPicker() {
|
function resetHoldPicker() {
|
||||||
if (dateTimePickerHoldUntil && typeof dateTimePickerHoldUntil.setDate === 'function') {
|
if (dateTimePickerHoldUntil && typeof dateTimePickerHoldUntil.setDate === 'function') {
|
||||||
dateTimePickerHoldUntil.setDate();
|
dateTimePickerHoldUntil.setDate();
|
||||||
@@ -576,8 +600,10 @@
|
|||||||
isEditMode = true;
|
isEditMode = true;
|
||||||
$("#sabtBtn05").val("ویرایش");
|
$("#sabtBtn05").val("ویرایش");
|
||||||
var actionEditUrl = "{% url 'admin_panel:customerEditAction' 1111 2222 %}";
|
var actionEditUrl = "{% url 'admin_panel:customerEditAction' 1111 2222 %}";
|
||||||
var action_id = $(this).parent().attr("action_id");
|
var row = $(this).parent();
|
||||||
var customer_id = $(this).parent().attr("customer_id");
|
var action_id = row.attr("action_id");
|
||||||
|
var customer_id = row.attr("customer_id");
|
||||||
|
var customerHold = row.attr("customer_hold") || "";
|
||||||
actionEditUrl = actionEditUrl.replace('1111', customer_id);
|
actionEditUrl = actionEditUrl.replace('1111', customer_id);
|
||||||
actionEditUrl = actionEditUrl.replace('2222', action_id);
|
actionEditUrl = actionEditUrl.replace('2222', action_id);
|
||||||
$("#ActionBox").prop("action", actionEditUrl);
|
$("#ActionBox").prop("action", actionEditUrl);
|
||||||
@@ -602,6 +628,9 @@
|
|||||||
// فیلد «تا تاریخ پیگیری نشود» با نوع اقدام همراستا شود (در حالت ویرایش هم).
|
// فیلد «تا تاریخ پیگیری نشود» با نوع اقدام همراستا شود (در حالت ویرایش هم).
|
||||||
closeHoldHelp();
|
closeHoldHelp();
|
||||||
toggleCoordinationField(ActionParseData[0]['fields']['actionType']);
|
toggleCoordinationField(ActionParseData[0]['fields']['actionType']);
|
||||||
|
// مقدار ذخیرهشدهٔ «قرار بعدی» را در همان فیلد نشان بده (بعد از toggle،
|
||||||
|
// چون toggle برای انواع غیرهماهنگی فیلد را پاک میکند).
|
||||||
|
seedHoldPicker(customerHold);
|
||||||
$("#desc").val(ActionParseData[0]['fields']['desc']);
|
$("#desc").val(ActionParseData[0]['fields']['desc']);
|
||||||
$("#userID").val(ActionParseData[0]['fields']['forUser']);
|
$("#userID").val(ActionParseData[0]['fields']['forUser']);
|
||||||
if(ActionParseData[0]['fields']['status'] == 'finish'){
|
if(ActionParseData[0]['fields']['status'] == 'finish'){
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
.ez-stats { grid-auto-flow: row; grid-template-columns: repeat(5, 1fr); overflow: visible; }
|
.ez-stats { grid-auto-flow: row; grid-template-columns: repeat(6, 1fr); overflow: visible; }
|
||||||
}
|
}
|
||||||
.ez-stat {
|
.ez-stat {
|
||||||
display: flex; flex-direction: column; gap: .18rem;
|
display: flex; flex-direction: column; gap: .18rem;
|
||||||
@@ -40,6 +40,7 @@
|
|||||||
.ez-stat.success { border-top-color: #16a34a; }
|
.ez-stat.success { border-top-color: #16a34a; }
|
||||||
.ez-stat.warning { border-top-color: #d97706; }
|
.ez-stat.warning { border-top-color: #d97706; }
|
||||||
.ez-stat.link { border-top-color: #0284c7; }
|
.ez-stat.link { border-top-color: #0284c7; }
|
||||||
|
.ez-stat.violet { border-top-color: #7c3aed; }
|
||||||
|
|
||||||
.ez-stat-top {
|
.ez-stat-top {
|
||||||
display: flex; align-items: baseline; justify-content: space-between; gap: .4rem;
|
display: flex; align-items: baseline; justify-content: space-between; gap: .4rem;
|
||||||
@@ -128,6 +129,7 @@
|
|||||||
.ez-badge.success { background: #dcfce7; color: #15803d; }
|
.ez-badge.success { background: #dcfce7; color: #15803d; }
|
||||||
.ez-badge.warning { background: #fef3c7; color: #b45309; }
|
.ez-badge.warning { background: #fef3c7; color: #b45309; }
|
||||||
.ez-badge.mixed { background: #e0f2fe; color: #0369a1; }
|
.ez-badge.mixed { background: #e0f2fe; color: #0369a1; }
|
||||||
|
.ez-badge.coordination { background: #ede9fe; color: #6d28d9; }
|
||||||
|
|
||||||
/* تأخیر بهصورت چیپ (خوانا در یک نگاه) */
|
/* تأخیر بهصورت چیپ (خوانا در یک نگاه) */
|
||||||
.ez-late {
|
.ez-late {
|
||||||
@@ -202,10 +204,8 @@
|
|||||||
<b>بیش از {{ counts.period_months }} ماه</b> گذشته باشد.
|
<b>بیش از {{ counts.period_months }} ماه</b> گذشته باشد.
|
||||||
روی هر کارت بزنید تا فقط همان گروه نمایش داده شود؛
|
روی هر کارت بزنید تا فقط همان گروه نمایش داده شود؛
|
||||||
<b>کل = بدون قرار + با قرار</b>.
|
<b>کل = بدون قرار + با قرار</b>.
|
||||||
{% if counts.both %}
|
سه کارتِ آخر سه نوع قرارِ جدا هستند؛ یک مشتری میتواند بیش از
|
||||||
({{ counts.both }} مشتری هم قرار هماهنگی دارند و هم بازدید
|
یکی داشته باشد، پس جمع آنها ممکن است از «با قرار» بیشتر شود.
|
||||||
برنامهریزیشده، پس در هر دو کارت شمرده میشوند.)
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -259,6 +259,16 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="ez-desc">اقدام «بازدید» با تاریخ آینده ثبت شده است</span>
|
<span class="ez-desc">اقدام «بازدید» با تاریخ آینده ثبت شده است</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<a href="?view=coordination&sort={{ sort }}{% if searchVal %}&searchVal={{ searchVal }}&searchOn={{ searchOn }}{% endif %}"
|
||||||
|
class="ez-stat violet {% if view == 'coordination' %}is-active{% endif %}"
|
||||||
|
title="اقدام «هماهنگی» با تاریخ پیگیری در آینده">
|
||||||
|
<span class="ez-stat-top">
|
||||||
|
<span class="ez-lbl">هماهنگی</span>
|
||||||
|
<span class="ez-num" style="color:#7c3aed">{{ counts.coordination }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="ez-desc">تماس گرفته شده و پیگیری به آینده موکول شده است</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# تفکیک مدت تأخیر در نمای جاری #}
|
{# تفکیک مدت تأخیر در نمای جاری #}
|
||||||
@@ -293,21 +303,23 @@
|
|||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table class="table ez-table">
|
<table class="table ez-table">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col style="width: 23%">
|
<col style="width: 21%">
|
||||||
<col style="width: 13%">
|
<col style="width: 12%">
|
||||||
|
<col style="width: 10%">
|
||||||
<col style="width: 10%">
|
<col style="width: 10%">
|
||||||
<col style="width: 11%">
|
|
||||||
<col style="width: 10%">
|
<col style="width: 10%">
|
||||||
<col style="width: 9%">
|
<col style="width: 9%">
|
||||||
|
<col style="width: 8%">
|
||||||
|
<col style="width: 9%">
|
||||||
<col style="width: 11%">
|
<col style="width: 11%">
|
||||||
<col style="width: 13%">
|
|
||||||
</colgroup>
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>نام و نام خانوادگی</th>
|
<th>نام و نام خانوادگی</th>
|
||||||
<th>وضعیت قرار</th>
|
<th>وضعیت قرار</th>
|
||||||
<th>قرار هماهنگی</th>
|
<th>قرار دستی</th>
|
||||||
<th>بازدید برنامهریزیشده</th>
|
<th>بازدید برنامهریزیشده</th>
|
||||||
|
<th>هماهنگی</th>
|
||||||
<th>آخرین بازدید</th>
|
<th>آخرین بازدید</th>
|
||||||
<th>تأخیر</th>
|
<th>تأخیر</th>
|
||||||
<th>تلفن</th>
|
<th>تلفن</th>
|
||||||
@@ -328,23 +340,26 @@
|
|||||||
</td>
|
</td>
|
||||||
{% with appt=appointment_rows|row_for:customer.id %}
|
{% with appt=appointment_rows|row_for:customer.id %}
|
||||||
<td data-label="وضعیت" class="ez-nolabel">
|
<td data-label="وضعیت" class="ez-nolabel">
|
||||||
{% if appt.manual and appt.scheduled or appt.manual and appt.coordination %}
|
{% if appt.source_count > 1 %}
|
||||||
<span class="ez-badge mixed">قرار + بازدید</span>
|
<span class="ez-badge mixed">{{ appt.source_count }} قرار</span>
|
||||||
{% elif appt.manual %}
|
{% elif appt.manual %}
|
||||||
<span class="ez-badge warning">قرار هماهنگی</span>
|
<span class="ez-badge warning">قرار دستی</span>
|
||||||
{% elif appt.scheduled or appt.coordination %}
|
{% elif appt.scheduled %}
|
||||||
<span class="ez-badge success">بازدید برنامهریزیشده</span>
|
<span class="ez-badge success">بازدید برنامه</span>
|
||||||
|
{% elif appt.coordination %}
|
||||||
|
<span class="ez-badge coordination">هماهنگی</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="ez-badge danger">بدون قرار</span>
|
<span class="ez-badge danger">بدون قرار</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td data-label="قرار هماهنگی" class="ez-num ez-nowrap">
|
<td data-label="قرار دستی" class="ez-num ez-nowrap">
|
||||||
{% if appt.manual %}{{ appt.manual|toJalali }}{% else %}<span class="ez-muted">—</span>{% endif %}
|
{% if appt.manual %}{{ appt.manual|toJalali }}{% else %}<span class="ez-muted">—</span>{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td data-label="بازدید برنامه" class="ez-num ez-nowrap">
|
<td data-label="بازدید برنامه" class="ez-num ez-nowrap">
|
||||||
{% if appt.scheduled %}{{ appt.scheduled|toJalali }}
|
{% if appt.scheduled %}{{ appt.scheduled|toJalali }}{% else %}<span class="ez-muted">—</span>{% endif %}
|
||||||
{% elif appt.coordination %}{{ appt.coordination|toJalali }}<div class="ez-sub">در انتظار تماس</div>
|
</td>
|
||||||
{% else %}<span class="ez-muted">—</span>{% endif %}
|
<td data-label="هماهنگی" class="ez-num ez-nowrap">
|
||||||
|
{% if appt.coordination %}{{ appt.coordination|toJalali }}{% else %}<span class="ez-muted">—</span>{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td data-label="آخرین بازدید" class="ez-num ez-nowrap lastView">
|
<td data-label="آخرین بازدید" class="ez-num ez-nowrap lastView">
|
||||||
{% if appt.last_visit %}{{ appt.last_visit|toJalali }}{% else %}<span class="ez-muted">بدون بازدید</span>{% endif %}
|
{% if appt.last_visit %}{{ appt.last_visit|toJalali }}{% else %}<span class="ez-muted">بدون بازدید</span>{% endif %}
|
||||||
@@ -382,7 +397,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8" class="has-text-centered py-5">
|
<td colspan="9" class="has-text-centered py-5">
|
||||||
<span class="has-text-grey">موردی در این نما پیدا نشد.</span>
|
<span class="has-text-grey">موردی در این نما پیدا نشد.</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -91,21 +91,26 @@ def _action_payload_from_request(request):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _apply_visit_hold(customer_obj, payload, *, status):
|
def _apply_visit_hold(customer_obj, payload, *, status, is_new):
|
||||||
"""همراستاکردن «قرار بعدی» با اقدام ثبت/ویرایششده.
|
"""همراستاکردن «قرار بعدی» با اقدام ثبت/ویرایششده.
|
||||||
|
|
||||||
قواعد (یکسان برای ایجاد و ویرایش — قبلاً ایجاد این را انجام نمیداد):
|
قواعد:
|
||||||
|
|
||||||
* فقط برای اقدام نوع «هماهنگی» معنا دارد؛
|
* برای اقدام نوع «هماهنگی»:
|
||||||
- اگر تاریخ قرار داده شده باشد ⇒ ``set_visit_hold`` (فقط رو به جلو)
|
- تاریخ قرار داده شده ⇒ ``set_visit_hold`` (فقط رو به جلو)
|
||||||
- اگر «انجام شد» تیک خورده و تاریخ خالی است ⇒ پیشفرض ۱۴ روز
|
- «انجام شد» تیک خورده و تاریخ خالی ⇒ پیشفرض ۱۴ روز
|
||||||
- اگر هماهنگی لغو شده باشد ⇒ قرار پاک میشود
|
- لغو شده ⇒ (در ایجاد) پاک میشود
|
||||||
* **هر اقدام دیگری** (از جمله «بازدید») ⇒ قرار دستی پاک میشود؛ چون هدفِ
|
* برای **هر اقدام دیگری** (از جمله «بازدید»):
|
||||||
قرار (پیگیری در آینده) با انجام/برنامهریزی بازدید منتفی است.
|
- در حالت **ایجاد**، این اقدام آخرین فعالیت مشتری است ⇒ قرار دستی پاک
|
||||||
|
میشود (سناریوی گزارششده: ثبت بازدید پس از هماهنگی).
|
||||||
|
- در حالت **ویرایش**، رکوردی تاریخی است و ممکن است قرارِ فعلی از یک
|
||||||
|
هماهنگیِ *جدیدتر* آمده باشد؛ پس دست نمیزنیم. اعتبارسنجی
|
||||||
|
:func:`manual_holds_map` (شرط «آخرین فعالیت = هماهنگی») خودش تصمیم
|
||||||
|
میگیرد. بدون این تفکیک، ویرایش یک اقدام قدیمی قرار معتبر را پاک میکرد.
|
||||||
"""
|
"""
|
||||||
is_coordination = payload['actionType'] == Action.ActionType.HAMAHANGI
|
is_coordination = payload['actionType'] == Action.ActionType.HAMAHANGI
|
||||||
if not is_coordination or status == Action.Status.CANCELL:
|
if not is_coordination or status == Action.Status.CANCELL:
|
||||||
return clear_visit_hold(customer_obj)
|
return clear_visit_hold(customer_obj) if is_new else True
|
||||||
|
|
||||||
raw = payload.get('holdRaw')
|
raw = payload.get('holdRaw')
|
||||||
if raw is not None:
|
if raw is not None:
|
||||||
@@ -152,7 +157,7 @@ def customerNewAction(request ,c_id):
|
|||||||
# last_bazdid_dt فقط از تابع واحد بازمحاسبه میشود؛ نه از finishDateTime.
|
# last_bazdid_dt فقط از تابع واحد بازمحاسبه میشود؛ نه از finishDateTime.
|
||||||
refresh_last_bazdid(customer_obj, reason=REASON_CREATE, user=request.user)
|
refresh_last_bazdid(customer_obj, reason=REASON_CREATE, user=request.user)
|
||||||
# «قرار بعدی» فقط برای هماهنگی.
|
# «قرار بعدی» فقط برای هماهنگی.
|
||||||
_apply_visit_hold(customer_obj, payload, status=newAction.status)
|
_apply_visit_hold(customer_obj, payload, status=newAction.status, is_new=True)
|
||||||
|
|
||||||
# پیامک «بازدید شما برای تاریخ … ثبت شد» فقط برای اقدام از نوع بازدید.
|
# پیامک «بازدید شما برای تاریخ … ثبت شد» فقط برای اقدام از نوع بازدید.
|
||||||
if newAction.actionType == Action.ActionType.BAZDID:
|
if newAction.actionType == Action.ActionType.BAZDID:
|
||||||
@@ -210,9 +215,10 @@ def customerEditAction(request ,c_id, action_id):
|
|||||||
# (تا مقداری که این رکورد ساخته بود با منبع حقیقت همراستا شود).
|
# (تا مقداری که این رکورد ساخته بود با منبع حقیقت همراستا شود).
|
||||||
reason = REASON_CANCEL if (was_done and not payload['is_done']) else REASON_EDIT
|
reason = REASON_CANCEL if (was_done and not payload['is_done']) else REASON_EDIT
|
||||||
refresh_last_bazdid(customer_obj, reason=reason, user=request.user)
|
refresh_last_bazdid(customer_obj, reason=reason, user=request.user)
|
||||||
# «قرار بعدی»: با ویرایش همراستا میشود؛ اگر اقدام دیگر هماهنگی نباشد یا
|
# «قرار بعدی»: با ویرایش همراستا میشود. برای اقدام غیرهماهنگی عمداً
|
||||||
# هماهنگی لغو شود، قرار دستی پاک میشود.
|
# پاک نمیکنیم؛ رکورد ویرایششده ممکن است قدیمیتر از یک هماهنگیِ جدیدتر
|
||||||
_apply_visit_hold(customer_obj, payload, status=newAction.status)
|
# باشد و پاککردن، قرار معتبر را از بین ببرد.
|
||||||
|
_apply_visit_hold(customer_obj, payload, status=newAction.status, is_new=False)
|
||||||
|
|
||||||
#return redirect('/panel/customer/'+str(c_id)+'/action')
|
#return redirect('/panel/customer/'+str(c_id)+'/action')
|
||||||
return redirect(request.META.get('HTTP_REFERER', 'redirect_if_referer_not_found'))
|
return redirect(request.META.get('HTTP_REFERER', 'redirect_if_referer_not_found'))
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ from ..visit_policy import (SOURCE_COORDINATION, SOURCE_MANUAL, SOURCE_SCHEDULED
|
|||||||
|
|
||||||
# نماهای صفحهٔ «انتظار» (فیلتر ?view=). پیشفرض = فقط «بدون قرار» تا رفتار
|
# نماهای صفحهٔ «انتظار» (فیلتر ?view=). پیشفرض = فقط «بدون قرار» تا رفتار
|
||||||
# قبلی صفحه برای کاربران حفظ شود.
|
# قبلی صفحه برای کاربران حفظ شود.
|
||||||
ENTEZAR_VIEWS = ('uncoordinated', 'manual', 'scheduled', 'coordinated', 'all')
|
ENTEZAR_VIEWS = ('uncoordinated', 'manual', 'scheduled', 'coordination',
|
||||||
|
'coordinated', 'all')
|
||||||
ENTEZAR_DEFAULT_VIEW = 'uncoordinated'
|
ENTEZAR_DEFAULT_VIEW = 'uncoordinated'
|
||||||
# حالتهای مرتبسازی (?sort=):
|
# حالتهای مرتبسازی (?sort=):
|
||||||
# oldest ⇒ قدیمیترین بازدید اول (پیشفرض/اولویت کارشناس)
|
# oldest ⇒ قدیمیترین بازدید اول (پیشفرض/اولویت کارشناس)
|
||||||
@@ -341,18 +342,17 @@ def customerEdit(request, c_id):
|
|||||||
|
|
||||||
@login_required(login_url='/panel/p_login/')
|
@login_required(login_url='/panel/p_login/')
|
||||||
def entezar(request):
|
def entezar(request):
|
||||||
"""فهرست «منتظر بازدید» + تفکیک «با قرار» و «بدون قرار».
|
"""فهرست «منتظر بازدید» + تفکیک بر اساس **نوع قرار**.
|
||||||
|
|
||||||
منبع حقیقت تاریخ بازدید ``visit_policy.last_visit_map`` است و منبع «قرار بعدی»
|
منبع حقیقت تاریخ بازدید ``visit_policy.last_visit_map`` است و منبع قرارها
|
||||||
``appointment_sources_map`` (سه منبع: قرار دستی، بازدید برنامهریزیشده،
|
``appointment_sources_map`` که سه منبع مستقل دارد:
|
||||||
هماهنگیِ در انتظار). هر مشتری دقیقاً در یکی از چهار گروه زیر است:
|
|
||||||
|
|
||||||
* ``A`` — معوق بدون هیچ قرار (پیشفرض نمایش؛ رفتار قبلی صفحه)
|
* ``manual`` — قرار دستی روی مشتری (تعویق از فرم هماهنگی)
|
||||||
* ``B`` — معوق با قرار دستی (تعویق از فرم هماهنگی)
|
* ``scheduled`` — اقدام «بازدید» با تاریخ آینده
|
||||||
* ``C`` — معوق با بازدید برنامهریزیشده یا هماهنگی در انتظار
|
* ``coordination`` — اقدام «هماهنگی» با تاریخ پیگیری در آینده
|
||||||
* ``D`` — معوق با بیش از یک منبعِ قرار
|
|
||||||
|
|
||||||
فیلتر با ``?view=`` و قابل ترکیب با جستوجوی آدرس است.
|
هر فیلتر فقط منبع خودش را نشان میدهد (یک مشتری میتواند چند منبع داشته
|
||||||
|
باشد و در چند فیلتر دیده شود). فیلتر با ``?view=`` و قابل ترکیب با جستوجو.
|
||||||
"""
|
"""
|
||||||
searchVal = request.GET.get('searchVal', '')
|
searchVal = request.GET.get('searchVal', '')
|
||||||
searchOn = request.GET.get('searchOn', '')
|
searchOn = request.GET.get('searchOn', '')
|
||||||
@@ -403,29 +403,35 @@ def entezar(request):
|
|||||||
if visits.get(customer_id) is None or visits[customer_id] <= due_cut
|
if visits.get(customer_id) is None or visits[customer_id] <= due_cut
|
||||||
]
|
]
|
||||||
|
|
||||||
# تفکیک چهارگروهی (MECE): هر مشتری فقط در یک گروه.
|
# تفکیک بر اساس **منبعِ** قرار: هر مشتری میتواند صفر تا سه منبع داشته باشد
|
||||||
groups = {'A': [], 'B': [], 'C': [], 'D': []}
|
# (قرار دستی، بازدید برنامهریزیشده، هماهنگی). قبلاً «هماهنگی» با «بازدید»
|
||||||
|
# در یک گروه جمع میشد و در نمای «بازدید برنامهریزیشده» دیده میشد؛ حالا
|
||||||
|
# هر منبع گروه نمایشیِ خودش را دارد.
|
||||||
|
with_manual, with_scheduled, with_coordination = set(), set(), set()
|
||||||
for customer_id in stale_ids:
|
for customer_id in stale_ids:
|
||||||
customer_sources = sources.get(customer_id) or {}
|
customer_sources = sources.get(customer_id) or {}
|
||||||
manual = SOURCE_MANUAL in customer_sources
|
if SOURCE_MANUAL in customer_sources:
|
||||||
scheduled = (SOURCE_SCHEDULED in customer_sources
|
with_manual.add(customer_id)
|
||||||
or SOURCE_COORDINATION in customer_sources)
|
if SOURCE_SCHEDULED in customer_sources:
|
||||||
if manual and scheduled:
|
with_scheduled.add(customer_id)
|
||||||
groups['D'].append(customer_id)
|
if SOURCE_COORDINATION in customer_sources:
|
||||||
elif manual:
|
with_coordination.add(customer_id)
|
||||||
groups['B'].append(customer_id)
|
|
||||||
elif scheduled:
|
|
||||||
groups['C'].append(customer_id)
|
|
||||||
else:
|
|
||||||
groups['A'].append(customer_id)
|
|
||||||
|
|
||||||
selected = {
|
coordinated_ids = [cid for cid in stale_ids if cid in (
|
||||||
'uncoordinated': groups['A'],
|
with_manual | with_scheduled | with_coordination)]
|
||||||
'manual': groups['B'] + groups['D'],
|
selected_map = {
|
||||||
'scheduled': groups['C'] + groups['D'],
|
# نداشتن هیچ قرار
|
||||||
'coordinated': groups['B'] + groups['C'] + groups['D'],
|
'uncoordinated': [cid for cid in stale_ids if cid not in (
|
||||||
|
with_manual | with_scheduled | with_coordination)],
|
||||||
|
# هر مشتری که این منبع را دارد (حتی اگر منبع دیگری هم داشته باشد)
|
||||||
|
'manual': [cid for cid in stale_ids if cid in with_manual],
|
||||||
|
'scheduled': [cid for cid in stale_ids if cid in with_scheduled],
|
||||||
|
'coordination': [cid for cid in stale_ids if cid in with_coordination],
|
||||||
|
# اجتماع هر سه منبع
|
||||||
|
'coordinated': coordinated_ids,
|
||||||
'all': stale_ids,
|
'all': stale_ids,
|
||||||
}[view]
|
}
|
||||||
|
selected = selected_map[view]
|
||||||
|
|
||||||
# «مدت تأخیر» = از چه زمانی بازدید عقب افتاده است (به روز).
|
# «مدت تأخیر» = از چه زمانی بازدید عقب افتاده است (به روز).
|
||||||
# مبنای محاسبه «موعد بازدید» است، نه خود بازدید: موعد = بازدید + دوره.
|
# مبنای محاسبه «موعد بازدید» است، نه خود بازدید: موعد = بازدید + دوره.
|
||||||
@@ -491,17 +497,14 @@ def entezar(request):
|
|||||||
|
|
||||||
counts = {
|
counts = {
|
||||||
'total': len(stale_ids),
|
'total': len(stale_ids),
|
||||||
'uncoordinated': len(groups['A']),
|
'uncoordinated': len(selected_map['uncoordinated']),
|
||||||
# توجه: عددِ هر کارت باید **همان چیزی** باشد که با کلیک روی آن کارت
|
# عددِ هر کارت دقیقاً همان چیزی است که با کلیک روی آن دیده میشود؛
|
||||||
# دیده میشود. فیلتر ``view=manual`` گروههای B و D را نشان میدهد و
|
# چون یک مشتری میتواند چند منبع داشته باشد، جمع کارتها ممکن است
|
||||||
# ``view=scheduled`` گروههای C و D را؛ پس اینجا هم همان اجتماع
|
# از «با قرار» بیشتر شود. در راهنمای بالای صفحه توضیح داده میشود.
|
||||||
# شمرده میشود (قبلاً فقط B/C شمرده میشد و عدد با فهرست نمیخواند).
|
'manual': len(selected_map['manual']),
|
||||||
'manual': len(groups['B']) + len(groups['D']),
|
'scheduled': len(selected_map['scheduled']),
|
||||||
'scheduled': len(groups['C']) + len(groups['D']),
|
'coordination': len(selected_map['coordination']),
|
||||||
'manual_only': len(groups['B']),
|
'coordinated': len(selected_map['coordinated']),
|
||||||
'scheduled_only': len(groups['C']),
|
|
||||||
'both': len(groups['D']),
|
|
||||||
'coordinated': len(groups['B']) + len(groups['C']) + len(groups['D']),
|
|
||||||
'selected': len(selected),
|
'selected': len(selected),
|
||||||
'buckets': buckets,
|
'buckets': buckets,
|
||||||
# برای متن راهنمای بالای صفحه («بیش از N ماه از بازدید گذشته»).
|
# برای متن راهنمای بالای صفحه («بیش از N ماه از بازدید گذشته»).
|
||||||
@@ -524,19 +527,16 @@ def entezar(request):
|
|||||||
appointment_rows = {}
|
appointment_rows = {}
|
||||||
for customer in customerHa.object_list:
|
for customer in customerHa.object_list:
|
||||||
entry = sources.get(customer.id) or {}
|
entry = sources.get(customer.id) or {}
|
||||||
|
flags = [key for key in (SOURCE_MANUAL, SOURCE_SCHEDULED,
|
||||||
|
SOURCE_COORDINATION) if key in entry]
|
||||||
appointment_rows[customer.id] = {
|
appointment_rows[customer.id] = {
|
||||||
'manual': entry.get(SOURCE_MANUAL),
|
'manual': entry.get(SOURCE_MANUAL),
|
||||||
'scheduled': entry.get(SOURCE_SCHEDULED),
|
'scheduled': entry.get(SOURCE_SCHEDULED),
|
||||||
'coordination': entry.get(SOURCE_COORDINATION),
|
'coordination': entry.get(SOURCE_COORDINATION),
|
||||||
'overdue_days': overdue_days.get(customer.id, 0),
|
'overdue_days': overdue_days.get(customer.id, 0),
|
||||||
'last_visit': visits.get(customer.id),
|
'last_visit': visits.get(customer.id),
|
||||||
'group': (
|
# تعداد منابع (۰ تا ۳) — قالب بر اساس آن برچسب وضعیت را میسازد.
|
||||||
'D' if (SOURCE_MANUAL in entry and (
|
'source_count': len(flags),
|
||||||
SOURCE_SCHEDULED in entry or SOURCE_COORDINATION in entry))
|
|
||||||
else 'B' if SOURCE_MANUAL in entry
|
|
||||||
else 'C' if (SOURCE_SCHEDULED in entry or SOURCE_COORDINATION in entry)
|
|
||||||
else 'A'
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return render(request, 'admin_panel/entezar.html', {
|
return render(request, 'admin_panel/entezar.html', {
|
||||||
|
|||||||
+63
-24
@@ -104,7 +104,7 @@
|
|||||||
| کنترل | تقویم شمسی (همان `pDatepicker` پروژه) |
|
| کنترل | تقویم شمسی (همان `pDatepicker` پروژه) |
|
||||||
| پیشفرض | اگر خالی بماند و تیک «انجام شد» زده شود ⇒ **+۱۴ روز** |
|
| پیشفرض | اگر خالی بماند و تیک «انجام شد» زده شود ⇒ **+۱۴ روز** |
|
||||||
| سقف | **۶ ماه** (اعتبارسنجی سروری) |
|
| سقف | **۶ ماه** (اعتبارسنجی سروری) |
|
||||||
| حالت ویرایش | فیلد با نوع اقدام همراستا میشود (نمایش/پنهان) |
|
| حالت ویرایش | فیلد با نوع اقدام همراستا میشود (نمایش/پنهان) **و مقدار ذخیرهشدهٔ مشتری در آن بارگذاری میشود** |
|
||||||
|
|
||||||
نکتهٔ مهم: فیلد «تاریخ و ساعتی که کار باید انجام شود» (`dateTimeAnjam`) همچنان
|
نکتهٔ مهم: فیلد «تاریخ و ساعتی که کار باید انجام شود» (`dateTimeAnjam`) همچنان
|
||||||
«زمان تماس» است و دست نخورده باقی میماند. دو مفهوم، دو فیلد.
|
«زمان تماس» است و دست نخورده باقی میماند. دو مفهوم، دو فیلد.
|
||||||
@@ -177,6 +177,30 @@
|
|||||||
اما کادر هشدار برای هماهنگیِ انجامشده بهصورت اطلاعاتی نمایش داده میشود.
|
اما کادر هشدار برای هماهنگیِ انجامشده بهصورت اطلاعاتی نمایش داده میشود.
|
||||||
* **باز شدن مجدد مودال**: تیک، هشدار و وضعیت تأیید همیشه ریست میشوند.
|
* **باز شدن مجدد مودال**: تیک، هشدار و وضعیت تأیید همیشه ریست میشوند.
|
||||||
|
|
||||||
|
#### بارگذاری مقدار ذخیرهشده در حالت ویرایش
|
||||||
|
|
||||||
|
هر ردیف اقدام، مقدار فعلی `Customer.visit_hold_until` را بهصورت ISO در
|
||||||
|
صفت `customer_hold` نگه میدارد. هنگام کلیک روی ردیف (باز کردن فرم ویرایش)،
|
||||||
|
تابع `seedHoldPicker()` آن را به تقویم شمسی و فیلد مخفی `visitHoldUntil`
|
||||||
|
مینویسد.
|
||||||
|
|
||||||
|
> **باگی که این بخش رفع میکند:** قبلاً فرم ویرایش مقدار را از دیتابیس
|
||||||
|
> نمیخواند. مقدار در سرور **درست ذخیره میشد** ولی چون فیلد هنگام باز شدن
|
||||||
|
> مجدد خالی نمایش داده میشد، کاربر فکر میکرد «ثبت نشده است».
|
||||||
|
|
||||||
|
ترتیب کار مهم است: `seedHoldPicker()` **بعد از** `toggleCoordinationField()`
|
||||||
|
صدا زده میشود، چون برای انواع غیرهماهنگی، `toggle` فیلد را پاک میکند.
|
||||||
|
|
||||||
|
#### ایمنی ویرایش رکوردهای قدیمی
|
||||||
|
|
||||||
|
در حالت **ویرایش**، تغییر نوع اقدام به غیرهماهنگی، قرار دستی را پاک
|
||||||
|
**نمیکند**؛ زیرا رکورد ویرایششده ممکن است قدیمیتر از یک هماهنگیِ جدیدتر
|
||||||
|
باشد و پاککردن، قرار معتبر را از بین میبرد. اعتبارسنجی `manual_holds_map`
|
||||||
|
(شرط «آخرین فعالیت = هماهنگی») خودش قرار را بیاثر میکند.
|
||||||
|
|
||||||
|
در حالت **ایجاد** برعکس است: اقدام تازه آخرین فعالیت مشتری است، پس ثبت
|
||||||
|
بازدید جدید قرار قبلی را پاک میکند.
|
||||||
|
|
||||||
### ۴.۳ راهنمای بازشونده (فقط «هماهنگی»)
|
### ۴.۳ راهنمای بازشونده (فقط «هماهنگی»)
|
||||||
|
|
||||||
زیر فیلد تاریخ پیگیری، یک دکمهٔ راهنما با آیکون `!` قرار دارد که پنل توضیحات را
|
زیر فیلد تاریخ پیگیری، یک دکمهٔ راهنما با آیکون `!` قرار دارد که پنل توضیحات را
|
||||||
@@ -231,40 +255,51 @@ visit_hold_until = max(مقدار فعلی، تاریخ جدید)
|
|||||||
|
|
||||||
## ۵. صفحهٔ انتظار: چهار گروه و فیلترها
|
## ۵. صفحهٔ انتظار: چهار گروه و فیلترها
|
||||||
|
|
||||||
هر مشتری معوق **دقیقاً در یکی** از این چهار گروه است:
|
تفکیک بر اساس **نوعِ منبع** است، نه «چند گروه ثابت». هر مشتری میتواند صفر تا
|
||||||
|
سه منبع داشته باشد:
|
||||||
|
|
||||||
| گروه | تعریف |
|
| منبع | تعریف | فیلتر |
|
||||||
|---|---|
|
|---|---|---|
|
||||||
| **A** | معوق، بدون هیچ قرار |
|
| `manual` | ستون `visit_hold_until` (تعویق از فرم هماهنگی) | `?view=manual` |
|
||||||
| **B** | معوق، فقط قرار دستی (`manual`) |
|
| `scheduled` | اقدام `bazdid` + `waite` با تاریخ آینده | `?view=scheduled` |
|
||||||
| **C** | معوق، فقط بازدید برنامهریزیشده یا هماهنگی در انتظار |
|
| `coordination` | اقدام `hamahangi` (waite/finish) با تاریخ آینده | `?view=coordination` |
|
||||||
| **D** | معوق، هم قرار دستی و هم بازدید برنامهریزیشده |
|
|
||||||
|
> **باگ قبلی:** `coordination` با `scheduled` در یک گروه جمع میشد؛ نتیجه اینکه
|
||||||
|
> مشتریانی که فقط «هماهنگی» داشتند در نمای «بازدید برنامهریزیشده» دیده
|
||||||
|
> میشدند و برچسب «بازدید برنامهریزیشده» میگرفتند. حالا هر منبع فیلتر و
|
||||||
|
> ستون خودش را دارد.
|
||||||
|
|
||||||
### فیلترها (قابل بوکمارک و ترکیب با جستوجو)
|
### فیلترها (قابل بوکمارک و ترکیب با جستوجو)
|
||||||
|
|
||||||
| URL | نمایش | شامل |
|
| URL | نمایش | شامل |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `/panel/entezar/` (بدون پارامتر) | **پیشفرض = فقط A** (رفتار قبلی صفحه) | A |
|
| `/panel/entezar/` (بدون پارامتر) | **پیشفرض = بدون قرار** (رفتار قبلی) | بدون هیچ منبع |
|
||||||
| `?view=uncoordinated` | بدون قرار | A |
|
| `?view=uncoordinated` | بدون قرار | بدون هیچ منبع |
|
||||||
| `?view=manual` | قرار هماهنگی | B + D |
|
| `?view=manual` | قرار دستی | هر مشتری با `manual` |
|
||||||
| `?view=scheduled` | بازدید برنامهریزیشده | C + D |
|
| `?view=scheduled` | بازدید برنامهریزیشده | هر مشتری با `scheduled` |
|
||||||
| `?view=coordinated` | همهٔ دارای قرار | B + C + D |
|
| `?view=coordination` | هماهنگی | هر مشتری با `coordination` |
|
||||||
| `?view=all` | همهٔ معوقها | A + B + C + D |
|
| `?view=coordinated` | همهٔ دارای قرار | اجتماع هر سه منبع |
|
||||||
| مقدار نامعتبر | برگشت به پیشفرض، **بدون خطا** | A |
|
| `?view=all` | همهٔ معوقها | همه |
|
||||||
|
| مقدار نامعتبر | برگشت به پیشفرض، **بدون خطا** | بدون هیچ منبع |
|
||||||
|
|
||||||
|
چون یک مشتری میتواند چند منبع داشته باشد، در چند فیلتر دیده میشود؛ اما در
|
||||||
|
`?view=coordinated` فقط **یکبار** شمرده میشود. به همین دلیل جمع سه کارت منبع
|
||||||
|
میتواند از «با قرار» بیشتر باشد (صفحه این را در راهنمای بالای خود توضیح میدهد).
|
||||||
|
|
||||||
### ستونهای جدول
|
### ستونهای جدول
|
||||||
|
|
||||||
| ستون | توضیح |
|
| ستون | توضیح |
|
||||||
|---|---|
|
|---|---|
|
||||||
| وضعیت قرار | برچسب «با قرار» / «بدون قرار» |
|
| وضعیت قرار | «بدون قرار» / «قرار دستی» / «بازدید برنامه» / «هماهنگی» / «N قرار» |
|
||||||
| قرار هماهنگی | تاریخ شمسی قرار دستی (یا —) |
|
| قرار دستی | تاریخ شمسی قرار دستی (یا —) |
|
||||||
| بازدید برنامهریزیشده | تاریخ شمسی قرار `Action` (یا —) |
|
| بازدید برنامهریزیشده | تاریخ شمسی اقدام بازدید با تاریخ آینده (یا —) |
|
||||||
|
| هماهنگی | تاریخ شمسی اقدام هماهنگی با تاریخ آینده (یا —) |
|
||||||
| آخرین بازدید | بدون تغییر نسبت به قبل |
|
| آخرین بازدید | بدون تغییر نسبت به قبل |
|
||||||
|
|
||||||
### شمارندهٔ داشبورد
|
### شمارندهٔ داشبورد
|
||||||
|
|
||||||
`CacheTable('9mahbazdid')` = **گروه A** (کارِ فوری = بدون قرار). هر بار باز شدن
|
`CacheTable('9mahbazdid')` = **بدون قرار** (کارِ فوری). هر بار باز شدن صفحه
|
||||||
صفحه بهروز میشود.
|
بهروز میشود. با تغییر تفکیک، این عدد **تغییر نمیکند**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -461,8 +496,11 @@ python manage.py test tests
|
|||||||
## ۹. آنچه تغییر نکرد
|
## ۹. آنچه تغییر نکرد
|
||||||
|
|
||||||
- منطق «چه کسی عقبافتاده است» (همان `last_visit_map` + `due_cut`).
|
- منطق «چه کسی عقبافتاده است» (همان `last_visit_map` + `due_cut`).
|
||||||
- قاعدهٔ اعتبار قرار دستی (وجود هماهنگی فعال).
|
- قاعدهٔ اعتبار قرار دستی (وجود هماهنگی فعال + آخرین فعالیت بودن).
|
||||||
- اسکنر پیامک، `recalc_last_bazdid` و چرخهٔ ۹ ماهه.
|
- `visit_policy.py` بهطور کامل: `next_appointment_map`، `scheduled_visits_map`،
|
||||||
|
`coordination_holds_map` — پس **اسکنر پیامک**، `recalc_last_bazdid` و چرخهٔ
|
||||||
|
۹ ماهه دقیقاً مثل قبل کار میکنند.
|
||||||
|
- شمارندهٔ داشبورد (`CacheTable('9mahbazdid')` = بدون قرار).
|
||||||
- لینکهای قدیمی `?view=...` و رفتار پیشفرض (نمای «بدون قرار»).
|
- لینکهای قدیمی `?view=...` و رفتار پیشفرض (نمای «بدون قرار»).
|
||||||
|
|
||||||
## ۱۰. پارامترهای URL
|
## ۱۰. پارامترهای URL
|
||||||
@@ -470,9 +508,10 @@ python manage.py test tests
|
|||||||
```
|
```
|
||||||
/panel/entezar/ نمای پیشفرض (بدون قرار، قدیمیترین)
|
/panel/entezar/ نمای پیشفرض (بدون قرار، قدیمیترین)
|
||||||
?view=all همهٔ معوقها
|
?view=all همهٔ معوقها
|
||||||
?view=coordinated با قرار
|
?view=coordinated با قرار (اجتماع سه منبع)
|
||||||
?view=manual فقط قرار هماهنگی
|
?view=manual فقط قرار دستی
|
||||||
?view=scheduled فقط بازدید برنامهریزیشده
|
?view=scheduled فقط بازدید برنامهریزیشده
|
||||||
|
?view=coordination فقط هماهنگی
|
||||||
?sort=overdue بیشترین تأخیر اول
|
?sort=overdue بیشترین تأخیر اول
|
||||||
?sort=newest جدیدترین بازدید اول
|
?sort=newest جدیدترین بازدید اول
|
||||||
?searchVal=رضایی&searchOn=name جستوجوی نام
|
?searchVal=رضایی&searchOn=name جستوجوی نام
|
||||||
|
|||||||
+140
-13
@@ -804,6 +804,100 @@ class VisitHoldTests(TestCase):
|
|||||||
def _ms(self, moment):
|
def _ms(self, moment):
|
||||||
return str(int(moment.timestamp() * 1000))
|
return str(int(moment.timestamp() * 1000))
|
||||||
|
|
||||||
|
def _post_edit(self, customer, action, **payload):
|
||||||
|
base = {'desc': 'تست', 'userID': self.user.id}
|
||||||
|
base.update(payload)
|
||||||
|
for key in [k for k, v in list(base.items()) if v is None]:
|
||||||
|
base.pop(key)
|
||||||
|
return self.client.post(
|
||||||
|
reverse('admin_panel:customerEditAction',
|
||||||
|
kwargs={'c_id': customer.pk, 'action_id': action.pk}),
|
||||||
|
base,
|
||||||
|
HTTP_REFERER='/panel/customer/%d/action' % customer.pk,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------- ویرایش: تبدیل بازدید به هماهنگی با تاریخ پیگیری ----------
|
||||||
|
def test_editing_action_into_coordination_saves_hold(self):
|
||||||
|
"""تبدیل یک اقدام «بازدید» موجود به «هماهنگی» باید تاریخ پیگیری را ذخیره کند."""
|
||||||
|
customer = self._customer('تبدیل به هماهنگی')
|
||||||
|
action = Action.objects.create(
|
||||||
|
customer=customer, actionType='bazdid', status='waite',
|
||||||
|
actionDateTime=timezone.now() + timedelta(days=5),
|
||||||
|
desc='بازدید اولیه', forUser=self.user,
|
||||||
|
)
|
||||||
|
target = timezone.now() + timedelta(days=20)
|
||||||
|
|
||||||
|
response = self._post_edit(
|
||||||
|
customer, action,
|
||||||
|
actionType='hamahangi',
|
||||||
|
dateTimeAnjam=self._ms(timezone.now()),
|
||||||
|
visitHoldUntil=self._ms(target),
|
||||||
|
is_done='on',
|
||||||
|
FinishDateTime=self._ms(timezone.now()),
|
||||||
|
result='هماهنگ شد',
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
customer.refresh_from_db()
|
||||||
|
action.refresh_from_db()
|
||||||
|
self.assertEqual(action.actionType, 'hamahangi')
|
||||||
|
self.assertIsNotNone(customer.visit_hold_until, 'تاریخ پیگیری ذخیره نشد')
|
||||||
|
self.assertEqual(customer.visit_hold_until.date(), target.date())
|
||||||
|
|
||||||
|
def test_edit_modal_prefills_saved_hold(self):
|
||||||
|
"""هنگام بازکردن مجدد اقدام، فیلد باید مقدار ذخیرهشده را نشان دهد.
|
||||||
|
|
||||||
|
رگرسیون: مودال مقدار را از دیتابیس نمیخواند، پس همیشه خالی دیده میشد
|
||||||
|
(کاربر فکر میکرد «ثبت نشده»).
|
||||||
|
"""
|
||||||
|
customer = self._customer('نمایش قرار در ویرایش')
|
||||||
|
Action.objects.create(
|
||||||
|
customer=customer, actionType='hamahangi', status='finish',
|
||||||
|
actionDateTime=timezone.now() - timedelta(minutes=5),
|
||||||
|
finishDateTime=timezone.now() - timedelta(minutes=5),
|
||||||
|
desc='هماهنگی', forUser=self.user,
|
||||||
|
)
|
||||||
|
set_visit_hold(customer, timezone.now() + timedelta(days=18))
|
||||||
|
|
||||||
|
html = self.client.get(
|
||||||
|
reverse('admin_panel:customerAction', kwargs={'c_id': customer.pk})
|
||||||
|
).content.decode()
|
||||||
|
# ردیفها مقدار ذخیرهشده را بهصورت ISO در خود دارند...
|
||||||
|
self.assertIn('customer_hold=', html)
|
||||||
|
self.assertIn('function seedHoldPicker', html)
|
||||||
|
self.assertIn('seedHoldPicker(customerHold)', html)
|
||||||
|
|
||||||
|
def test_editing_old_visit_does_not_wipe_newer_hold(self):
|
||||||
|
"""ویرایش یک اقدام قدیمی نباید قرار هماهنگیِ جدیدتر را پاک کند."""
|
||||||
|
customer = self._customer('ویرایش قدیمی')
|
||||||
|
old_visit = Action.objects.create(
|
||||||
|
customer=customer, actionType='bazdid', status='finish',
|
||||||
|
actionDateTime=timezone.now() - timedelta(days=400),
|
||||||
|
finishDateTime=timezone.now() - timedelta(days=400),
|
||||||
|
desc='بازدید قدیمی', forUser=self.user,
|
||||||
|
)
|
||||||
|
Action.objects.create(
|
||||||
|
customer=customer, actionType='hamahangi', status='finish',
|
||||||
|
actionDateTime=timezone.now() - timedelta(minutes=5),
|
||||||
|
finishDateTime=timezone.now() - timedelta(minutes=5),
|
||||||
|
desc='هماهنگی جدید', forUser=self.user,
|
||||||
|
)
|
||||||
|
set_visit_hold(customer, timezone.now() + timedelta(days=20))
|
||||||
|
|
||||||
|
# فقط توضیح اقدام قدیمی را عوض میکنیم
|
||||||
|
self._post_edit(
|
||||||
|
customer, old_visit,
|
||||||
|
actionType='bazdid',
|
||||||
|
dateTimeAnjam=self._ms(timezone.now() - timedelta(days=400)),
|
||||||
|
is_done='on',
|
||||||
|
FinishDateTime=self._ms(timezone.now() - timedelta(days=400)),
|
||||||
|
desc='توضیح اصلاح شد',
|
||||||
|
)
|
||||||
|
customer.refresh_from_db()
|
||||||
|
self.assertIsNotNone(customer.visit_hold_until,
|
||||||
|
'ویرایش اقدام قدیمی قرار معتبر را پاک کرد')
|
||||||
|
self.assertIn(customer.pk, self._ids('manual'))
|
||||||
|
|
||||||
# ---------- ریسک ۱: تاریخِ «امروز» تا پایان روز معتبر است ----------
|
# ---------- ریسک ۱: تاریخِ «امروز» تا پایان روز معتبر است ----------
|
||||||
def test_hold_until_today_hides_customer_until_end_of_day(self):
|
def test_hold_until_today_hides_customer_until_end_of_day(self):
|
||||||
customer = self._customer('قرار امروز')
|
customer = self._customer('قرار امروز')
|
||||||
@@ -1030,13 +1124,11 @@ class VisitHoldTests(TestCase):
|
|||||||
|
|
||||||
counts = self._entezar().context['counts']
|
counts = self._entezar().context['counts']
|
||||||
self.assertEqual(counts['uncoordinated'], 1) # a
|
self.assertEqual(counts['uncoordinated'], 1) # a
|
||||||
# عددِ کارت باید با فهرستی که با کلیک باز میشود بخواند:
|
# هر کارت فقط منبع خودش را میشمارد؛ d هر دو منبع را دارد ⇒ در هر سه
|
||||||
# view=manual گروههای B و D را نشان میدهد.
|
# کارت شمرده میشود (قرار دستی، بازدید، با قرار).
|
||||||
self.assertEqual(counts['manual'], 2) # b + d
|
self.assertEqual(counts['manual'], 2) # b + d
|
||||||
self.assertEqual(counts['scheduled'], 2) # c + d
|
self.assertEqual(counts['scheduled'], 2) # c + d
|
||||||
self.assertEqual(counts['manual_only'], 1) # b
|
self.assertEqual(counts['coordination'], 0) # هیچکدام هماهنگیِ فعال ندارد
|
||||||
self.assertEqual(counts['scheduled_only'], 1) # c
|
|
||||||
self.assertEqual(counts['both'], 1) # d
|
|
||||||
self.assertEqual(counts['total'], 4)
|
self.assertEqual(counts['total'], 4)
|
||||||
self.assertEqual(counts['coordinated'], 3)
|
self.assertEqual(counts['coordinated'], 3)
|
||||||
|
|
||||||
@@ -1044,6 +1136,7 @@ class VisitHoldTests(TestCase):
|
|||||||
self.assertEqual(self._ids('all'), {a.pk, b.pk, c.pk, d.pk})
|
self.assertEqual(self._ids('all'), {a.pk, b.pk, c.pk, d.pk})
|
||||||
self.assertEqual(self._ids('manual'), {b.pk, d.pk})
|
self.assertEqual(self._ids('manual'), {b.pk, d.pk})
|
||||||
self.assertEqual(self._ids('scheduled'), {c.pk, d.pk})
|
self.assertEqual(self._ids('scheduled'), {c.pk, d.pk})
|
||||||
|
self.assertEqual(self._ids('coordination'), set())
|
||||||
self.assertEqual(self._ids('coordinated'), {b.pk, c.pk, d.pk})
|
self.assertEqual(self._ids('coordinated'), {b.pk, c.pk, d.pk})
|
||||||
|
|
||||||
def test_invalid_view_falls_back_to_default(self):
|
def test_invalid_view_falls_back_to_default(self):
|
||||||
@@ -1075,14 +1168,47 @@ class VisitHoldTests(TestCase):
|
|||||||
counts = self._entezar().context['counts']
|
counts = self._entezar().context['counts']
|
||||||
self.assertEqual(counts['manual'], len(self._ids('manual')))
|
self.assertEqual(counts['manual'], len(self._ids('manual')))
|
||||||
self.assertEqual(counts['scheduled'], len(self._ids('scheduled')))
|
self.assertEqual(counts['scheduled'], len(self._ids('scheduled')))
|
||||||
|
self.assertEqual(counts['coordination'], len(self._ids('coordination')))
|
||||||
self.assertEqual(counts['coordinated'], len(self._ids('coordinated')))
|
self.assertEqual(counts['coordinated'], len(self._ids('coordinated')))
|
||||||
self.assertEqual(counts['uncoordinated'], len(self._ids('uncoordinated')))
|
self.assertEqual(counts['uncoordinated'], len(self._ids('uncoordinated')))
|
||||||
self.assertEqual(counts['total'], len(self._ids('all')))
|
self.assertEqual(counts['total'], len(self._ids('all')))
|
||||||
|
|
||||||
# وقتی مشتریای هر دو نوع قرار را دارد، راهنمای بالای صفحه باید
|
def test_card_number_matches_list_for_every_new_view(self):
|
||||||
# توضیح بدهد که چرا جمع دو کارت با «با قرار» برابر نیست.
|
"""هر سه منبع، فیلتر مستقل خودش را دارد و عدد کارت با فهرست میخواند."""
|
||||||
self.assertGreaterEqual(counts['both'], 1)
|
# فقط هماهنگی (بدون بازدید) — موردِ باگ گزارششده
|
||||||
self.assertIn('پس در هر دو کارت شمرده میشوند', self._entezar().content.decode())
|
coord = self._customer('فقط هماهنگی')
|
||||||
|
Action.objects.create(customer=coord, actionType='hamahangi', status='finish',
|
||||||
|
actionDateTime=timezone.now() + timedelta(days=8),
|
||||||
|
finishDateTime=timezone.now() - timedelta(minutes=5))
|
||||||
|
# فقط بازدید
|
||||||
|
sched = self._customer('فقط بازدید')
|
||||||
|
Action.objects.create(customer=sched, actionType='bazdid', status='waite',
|
||||||
|
actionDateTime=timezone.now() + timedelta(days=5))
|
||||||
|
# هر دو
|
||||||
|
both = self._customer('بازدید و هماهنگی')
|
||||||
|
Action.objects.create(customer=both, actionType='bazdid', status='waite',
|
||||||
|
actionDateTime=timezone.now() + timedelta(days=6))
|
||||||
|
Action.objects.create(customer=both, actionType='hamahangi', status='finish',
|
||||||
|
actionDateTime=timezone.now() + timedelta(days=9),
|
||||||
|
finishDateTime=timezone.now() - timedelta(minutes=5))
|
||||||
|
|
||||||
|
counts = self._entezar().context['counts']
|
||||||
|
# «هماهنگی» هرگز در نمای «بازدید برنامهریزیشده» ظاهر نمیشود.
|
||||||
|
self.assertNotIn(coord.pk, self._ids('scheduled'))
|
||||||
|
self.assertIn(coord.pk, self._ids('coordination'))
|
||||||
|
# و برعکس.
|
||||||
|
self.assertNotIn(sched.pk, self._ids('coordination'))
|
||||||
|
self.assertIn(sched.pk, self._ids('scheduled'))
|
||||||
|
# مشتریِ هر دو منبع، در هر دو نما هست.
|
||||||
|
self.assertIn(both.pk, self._ids('scheduled'))
|
||||||
|
self.assertIn(both.pk, self._ids('coordination'))
|
||||||
|
# اما در «با قرار» فقط یکبار شمرده میشود.
|
||||||
|
self.assertIn(both.pk, self._ids('coordinated'))
|
||||||
|
|
||||||
|
for key in ('manual', 'scheduled', 'coordination', 'coordinated',
|
||||||
|
'uncoordinated', 'total'):
|
||||||
|
self.assertEqual(counts[key], len(self._ids(
|
||||||
|
'all' if key == 'total' else key)), f'عدد کارت {key} با فهرست نمیخواند')
|
||||||
|
|
||||||
def test_stats_cards_have_minimal_descriptions(self):
|
def test_stats_cards_have_minimal_descriptions(self):
|
||||||
"""هر کارت آمار باید یک توضیح کوتاه داشته باشد تا معنیاش روشن باشد."""
|
"""هر کارت آمار باید یک توضیح کوتاه داشته باشد تا معنیاش روشن باشد."""
|
||||||
@@ -1090,12 +1216,13 @@ class VisitHoldTests(TestCase):
|
|||||||
# سرِ نوار آمار + توضیح «عقبافتاده»
|
# سرِ نوار آمار + توضیح «عقبافتاده»
|
||||||
self.assertIn('ez-stats-title', html)
|
self.assertIn('ez-stats-title', html)
|
||||||
self.assertIn('ez-stats-hint', html)
|
self.assertIn('ez-stats-hint', html)
|
||||||
# پنج توضیح، یکی برای هر کارت
|
# پنج توضیح برای کارتهای بدونقرار/باقرار/… و ششمی برای هماهنگی
|
||||||
self.assertEqual(html.count('class="ez-desc"'), 5)
|
self.assertEqual(html.count('class="ez-desc"'), 6)
|
||||||
for phrase in ('هنوز وقت بازدید تعیین نشده',
|
for phrase in ('هنوز وقت بازدید تعیین نشده',
|
||||||
'وقت بازدیدشان مشخص است',
|
'وقت بازدیدشان مشخص است',
|
||||||
'تعویق دستی از فرم هماهنگی',
|
'تعویق دستی از فرم هماهنگی',
|
||||||
'اقدام «بازدید» با تاریخ آینده'):
|
'اقدام «بازدید» با تاریخ آینده',
|
||||||
|
'پیگیری به آینده موکول شده است'):
|
||||||
self.assertIn(phrase, html, f'توضیح «{phrase}» روی کارتها نیست')
|
self.assertIn(phrase, html, f'توضیح «{phrase}» روی کارتها نیست')
|
||||||
|
|
||||||
# ---------- اسکنر پیامک هم قرار دستی را میبیند ----------
|
# ---------- اسکنر پیامک هم قرار دستی را میبیند ----------
|
||||||
@@ -1338,7 +1465,7 @@ class EntezarLayoutTests(TestCase):
|
|||||||
def test_column_widths_sum_to_one_hundred(self):
|
def test_column_widths_sum_to_one_hundred(self):
|
||||||
widths = [float(w) for w in re.findall(
|
widths = [float(w) for w in re.findall(
|
||||||
r'<col style="width: (\d+(?:\.\d+)?)%">', self.html)]
|
r'<col style="width: (\d+(?:\.\d+)?)%">', self.html)]
|
||||||
self.assertEqual(len(widths), 8, 'تعداد ستونها باید ۸ باشد')
|
self.assertEqual(len(widths), 9, 'تعداد ستونها باید ۹ باشد')
|
||||||
self.assertAlmostEqual(sum(widths), 100.0, places=1)
|
self.assertAlmostEqual(sum(widths), 100.0, places=1)
|
||||||
|
|
||||||
def test_row_number_is_not_a_column(self):
|
def test_row_number_is_not_a_column(self):
|
||||||
|
|||||||
Reference in new issue
Block a user