delete option staff varizi

This commit is contained in:
javad committed 2026-10-06 12:30:28 +03:30
1 parent d99e47ecce
commit b53a3e0c5c
14 files changed
+383 -3

No files matched your search

+1
View File
@@ -46,6 +46,7 @@ def issue_payment_link(*, notice=None, receipt=None):
def active_links():
return PaymentAccessLink.objects.select_related('customer', 'notice', 'receipt').filter(
notice__deleted_at__isnull=True,
revoked_at__isnull=True, expires_at__gt=timezone.now(), customer__is_active=True,
customer__account_type='CUSTOMER')
@@ -0,0 +1,18 @@
# Generated by Django 5.1.1 on 2026-10-06 08:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('payments', '0004_paymentaccesslink_paymentnotification_access_link_and_more'),
]
operations = [
migrations.AddField(
model_name='paymentnotice',
name='deleted_at',
field=models.DateTimeField(blank=True, editable=False, null=True),
),
]
+1
View File
@@ -85,6 +85,7 @@ class PaymentNotice(PublicIdModel, TimeStampedModel):
invoice_number = models.CharField(max_length=100, blank=True)
description = models.TextField(blank=True)
cancelled_at = models.DateTimeField(null=True, blank=True)
deleted_at = models.DateTimeField(null=True, blank=True, editable=False)
class Meta:
ordering = ['-created_at', '-pk']
+23 -1
View File
@@ -1,5 +1,5 @@
from django.db.models import Q
from .models import PaymentAccessConfiguration
from .models import PaymentAccessConfiguration, PaymentNotice
def is_staff(user):
@@ -37,7 +37,29 @@ def can_manage_banks(user):
return is_staff(user) and (user.is_superuser or getattr(profile, 'role', '') == 'CEO' or has_permission(user, 'manage_company_bankaccounts'))
def may_delete_notice(user, notice):
role = getattr(getattr(user, 'staff_profile', None), 'role', '')
return is_staff(user) and (notice.created_by_id == user.pk or user.is_superuser or role in {'CEO', 'GENERAL_ADMIN'})
def can_delete_notice(user, notice):
return may_delete_notice(user, notice) and not notice.deleted_at and not notice.receipts.exists()
def notice_actions(queryset, user):
"""Allow deletion actors to reach this notice without broadening other lists."""
queryset = queryset.filter(deleted_at__isnull=True)
if not is_staff(user):
return queryset.none()
role = getattr(getattr(user, 'staff_profile', None), 'role', '')
if user.is_superuser or role in {'CEO', 'GENERAL_ADMIN'}:
return queryset
return queryset.filter(Q(created_by=user) | Q(pk__in=visible(queryset, user).values('pk')))
def visible(queryset, user):
if queryset.model == PaymentNotice:
queryset = queryset.filter(deleted_at__isnull=True)
if not user.is_authenticated or not user.is_active:
return queryset.none()
if user.account_type == 'CUSTOMER':
+23
View File
@@ -27,6 +27,29 @@ def configuration_snapshot(obj):
return json.loads(serializers.serialize('json', [obj]))[0]['fields']
@transaction.atomic
def delete_notice(actor, notice):
# Receipt submission locks the same customer, then notice. Do not lock access
# links here: anonymous submission already holds its link before the customer.
actor = User.objects.select_related('staff_profile').get(pk=actor.pk)
User.objects.select_for_update().get(pk=notice.customer_id)
notice = PaymentNotice.objects.select_for_update().get(pk=notice.pk)
if not policies.may_delete_notice(actor, notice):
raise PermissionDenied
if notice.receipts.exists():
raise ValidationError('برای این اعلامیه فیش ثبت شده است؛ حذف آن امکان‌پذیر نیست، حتی اگر فیش رد شده باشد.')
if notice.deleted_at:
return
notice.deleted_at = notice.cancelled_at = timezone.now()
notice.save(update_fields=['deleted_at', 'cancelled_at', 'updated_at'])
from apps.notifications.models import NotificationJob
NotificationJob.objects.filter(payment_notification__notice=notice,
status__in=['PENDING', 'RETRYING', 'FAILED']).update(status='CANCELLED',
last_error='اعلامیه واریز حذف شده است.')
audit(actor, 'NOTICE_DELETED', notice, {'number': notice.number, 'customer_id': notice.customer_id,
'subject': notice.subject, 'amount': notice.amount})
@transaction.atomic
def issue_notice(actor, data):
unit = TicketPolicy.primary_unit(actor)
+1
View File
@@ -8,6 +8,7 @@ urlpatterns = [
path('notices/generate-identifier/', views.generate_deposit_identifier, name='generate_identifier'),
path('notices/new/', views.notice_new, name='notice_new'),
path('notices/<uuid:public_id>/', views.notice_detail, name='notice_detail'),
path('notices/<uuid:public_id>/delete/', views.notice_delete, name='notice_delete'),
path('notices/<uuid:public_id>/payment-link/', views.notice_payment_link, name='notice_payment_link'),
path('receipts/new/<uuid:customer_id>/', views.staff_receipt_new, name='receipt_new'),
path('receipts/new/', views.staff_receipt_customer, name='receipt_customer'),
+21 -1
View File
@@ -330,7 +330,7 @@ def notice_new(request):
@financial_required(staff=True)
def notice_detail(request, public_id):
notice = complete_notice(get_object_or_404(policies.visible(PaymentNotice.objects.select_related('customer'), request.user), public_id=public_id))
notice = complete_notice(get_object_or_404(policies.notice_actions(PaymentNotice.objects.select_related('customer'), request.user), public_id=public_id))
services.audit(request.user, 'NOTICE_VIEWED', notice)
share = request.session.get('payment_link_share', {})
if share.get('notice') == str(public_id):
@@ -338,11 +338,31 @@ def notice_detail(request, public_id):
else:
share = {}
return render(request, 'payments/notice_detail.html', staff_context(notice=notice,
can_delete_notice=policies.can_delete_notice(request.user, notice),
may_delete_notice=policies.may_delete_notice(request.user, notice),
can_share_payment_link=policies.can_create(request.user) and notice.customer.is_active,
payment_share_url=share.get('url', ''),
receipts=policies.visible(notice.receipts.all(), request.user), notifications=notice.paymentnotification_set.select_related('job')))
@financial_required(staff=True)
@require_POST
def notice_delete(request, public_id):
notice = get_object_or_404(policies.notice_actions(PaymentNotice.objects.all(), request.user), public_id=public_id)
if not policies.may_delete_notice(request.user, notice):
raise PermissionDenied
if request.POST.get('confirm') != 'delete':
messages.error(request, 'برای حذف اعلامیه، ابتدا هشدار را بخوانید و حذف را تأیید کنید.')
return redirect('staff_payments:notice_detail', public_id=public_id)
try:
services.delete_notice(request.user, notice)
except ValidationError as error:
messages.error(request, '؛ '.join(error.messages))
return redirect('staff_payments:notice_detail', public_id=public_id)
messages.success(request, 'اعلامیه واریز حذف شد و لینک‌های آن دیگر قابل استفاده نیستند.')
return redirect(reverse('staff_payments:desk') + '?tab=notices')
@financial_required(staff=True)
@require_POST
def notice_payment_link(request, public_id):