improve bazdid 4
This commit is contained in:
1 parent
8fd881e0ea
commit
825f2a5973
13 files changed
+585
-15
No files matched your search
@@ -97,7 +97,8 @@ class Command(BaseCommand):
|
||||
# آمار نظرسنجیها
|
||||
nazar_stats = Action.objects.filter(
|
||||
nazarDT__range=(today_start, today_end),
|
||||
grade__gt=0
|
||||
grade__gt=0,
|
||||
deleted_at__isnull=True,
|
||||
).aggregate(
|
||||
avg_grade=Avg('grade'),
|
||||
total_count=Count('id')
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 3.2.25 on 2026-10-21 12:31
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('admin_panel', '0079_customer_visit_hold'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='action',
|
||||
name='deleted_at',
|
||||
field=models.DateTimeField(blank=True, default=None, help_text='اگر پر باشد، این اقدام توسط کاربر حذف شده است (status=cancell).', null=True, verbose_name='زمان حذف'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='action',
|
||||
name='deleted_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='deleted_actions', to=settings.AUTH_USER_MODEL, verbose_name='حذف\u200cکننده'),
|
||||
),
|
||||
]
|
||||
@@ -67,6 +67,25 @@ class Action(models.Model):
|
||||
nazarDT = models.DateTimeField(null=True, blank=True, default=None)
|
||||
grade = models.IntegerField(null=True, blank=True, default=None)
|
||||
|
||||
# --- حسابرسی «حذف» (soft-delete از طریق status='cancell') ---
|
||||
# چرا ستون جدا؟ چون ``cancell`` از قبل معنای «لغو برنامه» را دارد و در گزارشها
|
||||
# هم استفاده میشود. با این دو ستون میتوانیم «حذف توسط کاربر» را از «لغو
|
||||
# عادی» تفکیک کنیم، بدون آنکه منطق محاسبات (که فقط waite/finish را میبیند)
|
||||
# دست بخورد.
|
||||
deleted_at = models.DateTimeField(
|
||||
null=True, blank=True, default=None, verbose_name="زمان حذف",
|
||||
help_text="اگر پر باشد، این اقدام توسط کاربر حذف شده است (status=cancell).",
|
||||
)
|
||||
deleted_by = models.ForeignKey(
|
||||
User, on_delete=models.SET_NULL, null=True, blank=True,
|
||||
related_name="deleted_actions", verbose_name="حذفکننده",
|
||||
)
|
||||
|
||||
@property
|
||||
def is_deleted(self):
|
||||
"""آیا این اقدام با «حذف» کنار گذاشته شده است؟ (نه لغو عادی)"""
|
||||
return self.deleted_at is not None
|
||||
|
||||
class Meta:
|
||||
# ایندکس **پوشا (covering)** برای محاسبهٔ «آخرین بازدید»: هر سه ستونِ لازم
|
||||
# برای فیلتر و تجمیع داخل خودِ ایندکس است، پس دیتابیس به جدول اصلی مراجعه
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
{% block BodyCustomer1 %}
|
||||
|
||||
{% include 'admin_panel/customerActionInc.html' %}
|
||||
{% include 'admin_panel/customerActionInc.html' with show_delete=True %}
|
||||
|
||||
<!--
|
||||
<div id="action" class="column is-12-mobile is-12-desktop ">
|
||||
|
||||
@@ -66,7 +66,8 @@
|
||||
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="pr-4 pl-4 pb-4 pt-4">
|
||||
<div class="pr-4 pl-4 pb-4 pt-4 ax-cardrow">
|
||||
<span class="ax-cardrow-main">
|
||||
{% ifequal action.status 'finish' %}
|
||||
<img class="" style="width: 20px; height: 20px;" src="{% static 'admin_panel/icons/check.png' %}">
|
||||
{% endifequal %}
|
||||
@@ -81,6 +82,27 @@
|
||||
|
||||
<span class="is-bazdid">{% ifequal action.actionType 'nasb' %}نصب{% endifequal %}{% ifequal action.actionType 'bazdid' %}بازدید{% endifequal %}{% ifequal action.actionType 'vosol' %}وصول مطالبات{% endifequal %}{% ifequal action.actionType 'hamahangi' %}هماهنگی{% endifequal %}{% ifequal action.actionType 'inCall' %}تماس ورودی{% endifequal %}{% ifequal action.actionType 'outCall' %}تماس خروجی{% endifequal %}</span>
|
||||
<span class="pr-3">{{ action.desc }}</span>
|
||||
</span>
|
||||
{% if show_delete %}
|
||||
{% if action.nazarDT %}
|
||||
<span class="ax-del is-locked" title="این اقدام دارای نظرسنجی ثبتشده است و قابل حذف نیست">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 1a5 5 0 0 0-5 5v3H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-9a2 2 0 0 0-2-2h-1V6a5 5 0 0 0-5-5zm-3 8V6a3 3 0 0 1 6 0v3H9z"/></svg>
|
||||
<span class="ax-del-text">نظرسنجی</span>
|
||||
</span>
|
||||
{% else %}
|
||||
<form method="post" class="ax-del-form"
|
||||
action="{% url 'admin_panel:customerActionDelete' action.customer.id action.id %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="ax-del is-danger"
|
||||
title="حذف این اقدام"
|
||||
data-type="{% ifequal action.actionType 'nasb' %}نصب{% endifequal %}{% ifequal action.actionType 'bazdid' %}بازدید{% endifequal %}{% ifequal action.actionType 'vosol' %}وصول مطالبات{% endifequal %}{% ifequal action.actionType 'hamahangi' %}هماهنگی{% endifequal %}{% ifequal action.actionType 'inCall' %}تماس ورودی{% endifequal %}{% ifequal action.actionType 'outCall' %}تماس خروجی{% endifequal %}"
|
||||
onclick="return confirm('این «' + (this.dataset.type || 'اقدام') + '» حذف شود؟\n\nاقدام از فهرست انتظار/وظایف خارج میشود و پیامک ارسالنشدهٔ مرتبط لغو میگردد. سابقه برای حسابرسی باقی میماند.');">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 3h6l1 2h4v2H4V5h4l1-2zm-3 6h12l-1 12a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2L6 9zm4 3v8h2v-8h-2zm4 0v8h2v-8h-2z"/></svg>
|
||||
<span class="ax-del-text">حذف</span>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12-mobile is-12-desktop actionBoxOpenB">
|
||||
@@ -166,6 +188,42 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* ===== دکمهٔ «حذف» روی کارت اقدام (mobile first) ===== */
|
||||
.ax-cardrow {
|
||||
display: flex; align-items: flex-start; justify-content: space-between;
|
||||
gap: .5rem; flex-wrap: nowrap;
|
||||
}
|
||||
.ax-cardrow-main { flex: 1 1 auto; min-width: 0; }
|
||||
.ax-del-form { margin: 0; flex: 0 0 auto; }
|
||||
.ax-del {
|
||||
display: inline-flex; align-items: center; gap: .25rem;
|
||||
padding: .2rem .45rem; border-radius: .375rem; border: 1px solid #e5e7eb;
|
||||
background: #fff; color: #6b7280; font-size: .68rem; font-weight: 700;
|
||||
cursor: pointer; line-height: 1.5; white-space: nowrap;
|
||||
transition: background-color .15s ease, border-color .15s ease,
|
||||
color .15s ease, box-shadow .15s ease;
|
||||
}
|
||||
.ax-del svg { width: 13px; height: 13px; fill: currentColor; flex: 0 0 auto; }
|
||||
.ax-del:hover { background: #fef2f2; border-color: #fecaca; color: #b91c1c; }
|
||||
.ax-del:focus-visible {
|
||||
outline: 2px solid #fca5a5; outline-offset: 1px;
|
||||
}
|
||||
.ax-del.is-danger { border-color: #fee2e2; color: #dc2626; background: #fff; }
|
||||
.ax-del.is-danger:hover {
|
||||
background: #dc2626; border-color: #dc2626; color: #fff;
|
||||
box-shadow: 0 1px 6px rgba(220, 38, 38, .25);
|
||||
}
|
||||
/* «نظرسنجی دارد» ⇒ دکمه غیرفعال (قاعدهٔ محصولی) */
|
||||
.ax-del.is-locked {
|
||||
cursor: not-allowed; color: #9ca3af; background: #f9fafb;
|
||||
border-color: #e5e7eb;
|
||||
}
|
||||
.ax-del.is-locked:hover { background: #f9fafb; border-color: #e5e7eb; color: #9ca3af; }
|
||||
@media (max-width: 480px) {
|
||||
.ax-del-text { display: none; } /* در موبایل فقط آیکون، برای صرفهجویی فضا */
|
||||
.ax-del { padding: .25rem; }
|
||||
}
|
||||
|
||||
/* ===== مودال «ایجاد/ویرایش وظیفه» — mobile first =====
|
||||
همهٔ کلاسها با پیشوند ax- هستند تا با سایر بخشها تداخل نکنند. */
|
||||
#ActionBox .ax-card {
|
||||
@@ -473,6 +531,16 @@
|
||||
var holdAutoChecked = false;
|
||||
var holdConfirmed = false;
|
||||
|
||||
// لایهٔ دوم محافظت: فرم «حذف» داخل کارتِ قابلکلیک است؛ رویداد را همانجا
|
||||
// متوقف میکنیم تا به هندلرِ بازکردن مودالِ ویرایش نرسد.
|
||||
// بایند «مستقیم» (نه delegated) چون این بلوک با هر ردیف تکرار میشود؛
|
||||
// نامفضا (axdel) + off مانع از انباشتهشدن هندلرها روی ردیفهای تکراری میشود.
|
||||
$('.ax-del-form, .ax-del')
|
||||
.off('.axdel')
|
||||
.on('click.axdel mousedown.axdel', function (e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
// پیکر «تا تاریخ پیگیری نشود» را ریست میکند.
|
||||
// نکته: pDatepicker یک Model برمیگرداند؛ اگر سلکتور هیچ المانی پیدا نکند
|
||||
// مقدار undefined میشود. بدون این محافظ، یک ناهمخوانی نام کلاس باعث
|
||||
@@ -596,7 +664,12 @@
|
||||
showHoldConfirm();
|
||||
});
|
||||
|
||||
$(".actionBoxOpenB").click(function(){
|
||||
$(".actionBoxOpenB").click(function(event){
|
||||
// کلیک روی کنترلهای «حذف» نباید مودال ویرایش را باز کند.
|
||||
// (دکمه داخل کارت است و رویداد به والد بالا میرفت.)
|
||||
if ($(event.target).closest('.ax-del-form, .ax-del').length) {
|
||||
return;
|
||||
}
|
||||
isEditMode = true;
|
||||
$("#sabtBtn05").val("ویرایش");
|
||||
var actionEditUrl = "{% url 'admin_panel:customerEditAction' 1111 2222 %}";
|
||||
|
||||
@@ -37,6 +37,7 @@ urlpatterns = [
|
||||
path('customer/<int:c_id>/nazar/', views.customerNazar, name='customerNazar'),
|
||||
path('customer/<int:c_id>/action/new', views.customerNewAction, name='customerNewAction'),
|
||||
path('customer/<int:c_id>/action/<int:action_id>/edit', views.customerEditAction, name='customerEditAction'),
|
||||
path('customer/<int:c_id>/action/<int:action_id>/delete', views.customerActionDelete, name='customerActionDelete'),
|
||||
path('customer/<int:c_id>/del', views.customerDel, name='customerDel'),
|
||||
path('customer/<int:c_id>/factors/', views.customerFactors, name='customerFactors'),
|
||||
path('customer/<int:c_id>/factor/<int:f_id>', views.customerFactor, name='customerFactor'),
|
||||
|
||||
@@ -12,9 +12,10 @@ from django.utils import timezone
|
||||
from ..models import Action, Customer
|
||||
from ..sms.triggers import notify_visit_scheduled
|
||||
from ..visit_policy import (REASON_CANCEL, REASON_CREATE, REASON_EDIT,
|
||||
as_aware, clear_visit_hold, default_hold_until,
|
||||
normalize_finish_datetime, normalize_hold_until,
|
||||
refresh_last_bazdid, set_visit_hold,
|
||||
ActionDeleteError, as_aware, clear_visit_hold,
|
||||
default_hold_until, normalize_finish_datetime,
|
||||
normalize_hold_until, refresh_last_bazdid,
|
||||
set_visit_hold, soft_delete_action,
|
||||
validate_action_payload)
|
||||
|
||||
|
||||
@@ -27,8 +28,12 @@ def customerAction(request, c_id):
|
||||
filter_type = request.GET.get('actionType', 'all')
|
||||
filter_status = request.GET.get('status', 'all')
|
||||
|
||||
# کوئری پایه برای اقدامات این مشتری
|
||||
# کوئری پایه برای اقدامات این مشتری.
|
||||
# اقدامهای حذفشده (soft-delete) بهطور پیشفرض نمایش داده نمیشوند؛ با
|
||||
# فیلتر صریح `status=cancell` میتوان آنها را دید (برای حسابرسی).
|
||||
actions = Action.objects.filter(customer=customer)
|
||||
if filter_status != 'cancell':
|
||||
actions = actions.filter(deleted_at__isnull=True)
|
||||
|
||||
# اعمال فیلتر بر اساس نوع اقدام (بازدید، نصب و ...)
|
||||
if filter_type != 'all':
|
||||
@@ -57,7 +62,9 @@ def customerAction(request, c_id):
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customerNazar(request ,c_id):
|
||||
customer = Customer.objects.get(id=c_id)
|
||||
actions = Action.objects.filter(customer=customer, nazarDT__isnull=False).order_by("-createdDT")
|
||||
actions = Action.objects.filter(
|
||||
customer=customer, nazarDT__isnull=False, deleted_at__isnull=True,
|
||||
).order_by("-createdDT")
|
||||
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
|
||||
data = {'customer': customer, 'actions': actions, 'allUsers': allUsers}
|
||||
return render(request, 'admin_panel/customer/nazar.html', data)
|
||||
@@ -224,9 +231,43 @@ def customerEditAction(request ,c_id, action_id):
|
||||
return redirect(request.META.get('HTTP_REFERER', 'redirect_if_referer_not_found'))
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def customerActionDelete(request, c_id, action_id):
|
||||
"""«حذف» یک اقدام (soft-delete).
|
||||
|
||||
فقط POST مجاز است (تغییر وضعیت داده). اقدام فیزیکی حذف **نمیشود**؛
|
||||
``status`` به ``cancell`` میرود و ``deleted_at``/``deleted_by`` پر میشوند.
|
||||
|
||||
چرا soft-delete؟ چون سابقهٔ CRM باید قابل حسابرسی بماند و منطق محاسبات
|
||||
(entezar / ActionList / داشبورد) از قبل فقط ``waite``/``finish`` را میبیند؛
|
||||
پس همین مقدار، اقدام را از همهٔ خروجیها کنار میگذارد.
|
||||
"""
|
||||
customer_obj = get_object_or_404(Customer, id=c_id)
|
||||
action = get_object_or_404(Action, id=action_id, customer=customer_obj)
|
||||
back_url = '/panel/customer/%d/action' % customer_obj.pk
|
||||
|
||||
if request.method != 'POST':
|
||||
# دسترسی مستقیم با GET نباید چیزی را عوض کند.
|
||||
return redirect(back_url)
|
||||
|
||||
try:
|
||||
cancelled = soft_delete_action(action, user=request.user)
|
||||
except ActionDeleteError as error:
|
||||
messages.error(request, str(error))
|
||||
return redirect(back_url)
|
||||
|
||||
label = action.get_actionType_display() or 'اقدام'
|
||||
message = '«%s» حذف شد و از فهرستهای انتظار/وظایف خارج شد.' % label
|
||||
if cancelled:
|
||||
message += ' %d پیامک ارسالنشدهٔ مرتبط هم لغو شد.' % cancelled
|
||||
messages.success(request, message)
|
||||
return redirect(back_url)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
def ActionListAll(request):
|
||||
actions = Action.objects.filter().order_by("-id")
|
||||
# اقدامهای حذفشده (soft-delete) در فهرست «همه» نمایش داده نمیشوند.
|
||||
actions = Action.objects.filter(deleted_at__isnull=True).order_by("-id")
|
||||
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
|
||||
data = {'actions': actions, 'allUsers': allUsers}
|
||||
return render(request, 'admin_panel/ActionList.html', data)
|
||||
|
||||
@@ -156,9 +156,10 @@ def report_debtors_no_vosol(request):
|
||||
).values('customer').annotate(c=Count('id')).values('c')
|
||||
|
||||
# استخراج آخرین سابقه اکشن وصول
|
||||
# نکته: اقدام حذفشده/لغوشده نباید «آخرین اقدام وصول» بهحساب بیاید.
|
||||
latest_vosol_qs = Action.objects.filter(
|
||||
customer=OuterRef('pk'), actionType='vosol'
|
||||
).order_by('-createdDT')
|
||||
customer=OuterRef('pk'), actionType='vosol',
|
||||
).exclude(status='cancell').order_by('-createdDT')
|
||||
|
||||
# --- ۳. ساخت کوئری پایه (فقط مشتریان فعال و دارای معوقه) ---
|
||||
base_qs = Customer.objects.filter(is_active=True).annotate(
|
||||
@@ -939,7 +940,8 @@ def rezayat_dashboard(request):
|
||||
# دریافت تمامی نظرسنجیهای ثبت شده معتبر (که تاریخ نظرسنجی دارند)
|
||||
valid_surveys = Action.objects.filter(
|
||||
nazarDT__isnull=False,
|
||||
grade__in=[5, 10, 15, 20]
|
||||
grade__in=[5, 10, 15, 20],
|
||||
deleted_at__isnull=True,
|
||||
)
|
||||
|
||||
# ۱. محاسبات شاخصهای کلیدی (KPIs)
|
||||
@@ -965,7 +967,8 @@ def rezayat_dashboard(request):
|
||||
# ۴. واکشی لیست مشتریان نیازمند پیگیری (شامل متوسط ۱۵، ضعیف ۱۰ و بد ۵)
|
||||
low_score_actions = Action.objects.filter(
|
||||
nazarDT__isnull=False,
|
||||
grade__in=[5, 10, 15] # <-- امتیاز ۱۵ (متوسط) نیز اضافه شد
|
||||
grade__in=[5, 10, 15], # <-- امتیاز ۱۵ (متوسط) نیز اضافه شد
|
||||
deleted_at__isnull=True,
|
||||
).order_by('-nazarDT')
|
||||
|
||||
allUsers = User.objects.filter().exclude(username='admin').exclude(username='fesahat')
|
||||
|
||||
@@ -40,7 +40,9 @@ SENT_STATUSES = [
|
||||
# نیازمند اقدام را نشان میدهد؛ پیامکِ ارسالشده از صف بیرون میرود و سابقهاش در
|
||||
# صفحهٔ «گزارش ارسال» (/panel/sms/report) دیده میشود. این کار جلوی حذفِ ناخواستهٔ
|
||||
# تاریخچه از روی صف را میگیرد و صف را به فهرستِ کاریِ تمیز تبدیل میکند.
|
||||
DONE_STATUSES = [SMSQueue.Status.SENT]
|
||||
# پیامک «لغوشده» هم دیگر کاری برای انجام ندارد (مثلاً چون اقدامِ مرتبطش حذف شده)،
|
||||
# پس از صف بیرون میرود.
|
||||
DONE_STATUSES = [SMSQueue.Status.SENT, SMSQueue.Status.CANCELLED]
|
||||
|
||||
|
||||
def _notice(url, code, **extra):
|
||||
|
||||
@@ -936,3 +936,89 @@ def normalize_finish_datetime(value, *, now=None):
|
||||
"""تاریخ اتمام: اگر خالی بود، زمان ثبت قرار میگیرد (نه ``None``)."""
|
||||
value = as_aware(value)
|
||||
return value if value is not None else _now(now)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# حذف (soft-delete) یک اقدام
|
||||
# ---------------------------------------------------------------------------
|
||||
class ActionDeleteError(Exception):
|
||||
"""حذف مجاز نیست؛ پیام برای نمایش به کاربر."""
|
||||
|
||||
|
||||
def cancel_pending_sms_for_action(customer, *, now=None):
|
||||
"""لغو پیامکهای **ارسالنشدهٔ** «بازدید ثبت شد» همان مشتری.
|
||||
|
||||
چرا با ``action`` پیدا نمیکنیم؟ چون ``SMSQueue`` هیچ ارجاعی به رکورد اقدام
|
||||
ندارد (فقط ``dedup_key`` متنی). برای جلوگیری از ارسال پیامکِ بازدیدی که
|
||||
دیگر وجود ندارد، پیامکهای معلق همان مشتری با نوع ``visit_created`` لغو
|
||||
میشوند. پیامکهای **ارسالشده** دست نمیخورند (سابقه است).
|
||||
|
||||
خروجی: تعداد رکوردهای لغوشده.
|
||||
"""
|
||||
# ایمپورت محلی: sms.models در زمان ایمپورت این ماژول ممکن است آماده نباشد.
|
||||
from .models import SMSQueue, SMSType
|
||||
|
||||
if customer is None or customer.pk is None:
|
||||
return 0
|
||||
now = _now(now)
|
||||
pending = (SMSQueue.Status.PENDING, SMSQueue.Status.APPROVED)
|
||||
return (
|
||||
SMSQueue.objects.filter(
|
||||
customer_id=customer.pk,
|
||||
sms_type=SMSType.VISIT_CREATED,
|
||||
status__in=pending,
|
||||
)
|
||||
.exclude(status=SMSQueue.Status.SENT)
|
||||
.update(status=SMSQueue.Status.CANCELLED, updated_at=now)
|
||||
)
|
||||
|
||||
|
||||
def soft_delete_action(action, *, user=None, now=None):
|
||||
"""«حذف» یک اقدام: soft-delete با ``status='cancell'`` + حسابرسی.
|
||||
|
||||
چرا ``cancell`` و نه ستون ``is_deleted``؟ چون **تمام** محاسبات
|
||||
(:func:`last_visit_map`، :func:`cycle_anchor_map`،
|
||||
:func:`appointment_sources_map`، :func:`followups_on_date`،
|
||||
:func:`overdue_followups`) از قبل فقط ``waite``/``finish`` را میبینند؛ پس
|
||||
همین یک مقدار، اقدام را از همهٔ خروجیها حذف میکند — بدون Manager سفارشی،
|
||||
بدون تغییر ایندکس پوشا و بدون دستزدن به ۳۱ کوئری.
|
||||
|
||||
پیامدهای جانبی که همینجا انجام میشوند:
|
||||
|
||||
۱. ``last_bazdid_dt`` بازمحاسبه میشود (کش کهنه نماند).
|
||||
۲. قرار دستی اگر دیگر مرجعی نداشته باشد، خودکار بیاثر میشود
|
||||
(قاعدهٔ «ستون، مرجع نیست»).
|
||||
۳. پیامکهای **ارسالنشده**ی همان مشتری لغو میشوند.
|
||||
|
||||
ممنوعیت: اقدامی که نظرسنجی ثبتشده دارد (``nazarDT`` پر است) حذف نمیشود؛
|
||||
زیرا هیچ آرشیو/کپیای وجود ندارد و گزارش رضایتسنجی برای همیشه تغییر میکرد.
|
||||
"""
|
||||
if action is None or action.pk is None:
|
||||
raise ActionDeleteError("اقدام معتبر نیست.")
|
||||
if action.status == Action.Status.CANCELL and action.deleted_at is not None:
|
||||
raise ActionDeleteError("این اقدام قبلاً حذف شده است.")
|
||||
if action.nazarDT is not None:
|
||||
raise ActionDeleteError(
|
||||
"این اقدام دارای نظرسنجی ثبتشده است و قابل حذف نیست. "
|
||||
"برای کنار گذاشتن آن، از ویرایش و تغییر وضعیت استفاده کنید."
|
||||
)
|
||||
|
||||
now = _now(now)
|
||||
customer = action.customer
|
||||
|
||||
with transaction.atomic():
|
||||
Action.objects.filter(pk=action.pk).update(
|
||||
status=Action.Status.CANCELL,
|
||||
deleted_at=now,
|
||||
deleted_by=user if getattr(user, "pk", None) else None,
|
||||
)
|
||||
action.status = Action.Status.CANCELL
|
||||
action.deleted_at = now
|
||||
|
||||
if customer is not None:
|
||||
refresh_last_bazdid(customer, reason=REASON_CANCEL, user=user, now=now)
|
||||
cancelled = cancel_pending_sms_for_action(customer, now=now)
|
||||
else:
|
||||
cancelled = 0
|
||||
|
||||
return cancelled
|
||||
Reference in new issue
Block a user