534 lines
31 KiB
Python
534 lines
31 KiB
Python
from functools import wraps
|
|
from django.contrib import messages
|
|
from django.core.exceptions import PermissionDenied, ValidationError
|
|
from django.core.paginator import Paginator
|
|
from django.db import transaction
|
|
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
|
from django.db.models.deletion import ProtectedError
|
|
from django.http import FileResponse, JsonResponse
|
|
from django.shortcuts import get_object_or_404, redirect, render
|
|
from django.urls import reverse
|
|
from django.utils import timezone
|
|
from django.views.decorators.http import require_POST
|
|
from . import policies, services, validators, selectors, customer_picker, access
|
|
from .forms import BankAccountForm, NoticeForm, ReceiptForm
|
|
from .models import (CompanyBankAccount, PaymentNotice, PaymentReceipt, PaymentReceiptRevision,
|
|
PaymentAuditEvent, ReceiptDecision)
|
|
|
|
|
|
def financial_required(staff=False, allow_payment_link=False):
|
|
def decorate(view):
|
|
@wraps(view)
|
|
def wrapped(request, *args, **kwargs):
|
|
blocked = access.prepare_payment_request(request, staff=staff, allow_payment_link=allow_payment_link)
|
|
if blocked is not None:
|
|
return blocked
|
|
response = view(request, *args, **kwargs)
|
|
if getattr(request, 'payment_access_link', None):
|
|
response['Referrer-Policy'] = 'same-origin'
|
|
response['X-Robots-Tag'] = 'noindex, nofollow'
|
|
return response
|
|
return wrapped
|
|
return decorate
|
|
|
|
|
|
def staff_context(**kwargs):
|
|
return {'active_staff_section': 'payments', 'active_tab_title': 'اعلامیهها و فیشهای واریزی', **kwargs}
|
|
|
|
|
|
@financial_required()
|
|
def home(request):
|
|
return render(request, 'payments/home.html', {'financial_page': True})
|
|
|
|
|
|
def complete_notice(notice):
|
|
from .deposit_identifier import normalize_deposit_identifier
|
|
notice.deposit_id = normalize_deposit_identifier(notice.deposit_id)
|
|
snapshot = notice.account_snapshot
|
|
approved = getattr(notice, 'approved_total', None)
|
|
if approved is None:
|
|
approved = notice.receipts.filter(status='APPROVED', revisions__version=F('current_version'), revisions__destination_mismatch=False).aggregate(total=Sum('revisions__amount'))['total'] or 0
|
|
notice.approved_amount = approved
|
|
notice.remaining_amount = max(0, notice.amount-approved)
|
|
notice.settlement_label = 'تسویه شده' if approved >= notice.amount else ('پرداخت جزئی' if approved else 'در انتظار فیش تأییدشده')
|
|
# Complete copy uses immutable data, including fields absent from the compact UI.
|
|
from apps.core.templatetags.jalali import jalali_datetime
|
|
notice.copy_text = '\n'.join(f'{label}: {value}' for label, value in [
|
|
('اعلامیه', notice.number), ('تاریخ صدور', jalali_datetime(notice.created_at)), ('موضوع', notice.subject), ('مبلغ (ریال)', f'{notice.amount:,}'),
|
|
('نام حساب', snapshot.get('name')), ('بانک', snapshot.get('bank')), ('صاحب حساب', snapshot.get('holder')),
|
|
('شماره حساب', snapshot.get('account_number')), ('شبا', snapshot.get('iban')),
|
|
('کارت', snapshot.get('card_number')), ('شناسه واریز', notice.deposit_id),
|
|
('فاکتور', notice.invoice_number), ('توضیحات', notice.description)] if value)
|
|
if notice.deposit_id:
|
|
notice.copy_text += '\nوارد کردن شناسه واریز هنگام پرداخت الزامی و اجباری است؛ آن را در فرم بانکی، موبایلبانک یا اینترنتبانک وارد کنید.'
|
|
return notice
|
|
|
|
|
|
@financial_required()
|
|
def notices(request):
|
|
items = selectors.notices_with_totals(policies.visible(PaymentNotice.objects.all(), request.user))
|
|
query = request.GET.get('q', '').strip()
|
|
if query:
|
|
items = items.filter(Q(subject__icontains=query) | Q(invoice_number__icontains=validators.digits(query)))
|
|
page = Paginator(items, 20).get_page(request.GET.get('page'))
|
|
page.object_list = selectors.attach_notice_receipts(page.object_list, policies.visible(PaymentReceipt.objects.all(), request.user))
|
|
for notice in page:
|
|
complete_notice(notice)
|
|
params = request.GET.copy()
|
|
params.pop('page', None)
|
|
return render(request, 'payments/notices.html', {'page_obj': page, 'query': query, 'query_string':params.urlencode(), 'open_notice': request.GET.get('notice', ''), 'financial_page': True})
|
|
|
|
|
|
@financial_required(allow_payment_link=True)
|
|
def customer_notice_detail(request, public_id):
|
|
notice = get_object_or_404(selectors.notices_with_totals(
|
|
access.visible_payments(request, PaymentNotice.objects.all())), public_id=public_id)
|
|
selectors.attach_notice_receipts([notice], access.visible_payments(request, PaymentReceipt.objects.all()))
|
|
return render(request, 'payments/customer_notice_detail.html', access.customer_context(request,
|
|
notice=complete_notice(notice), open_notice=str(notice.public_id), financial_page=True))
|
|
|
|
|
|
@require_POST
|
|
@financial_required(allow_payment_link=True)
|
|
def customer_notice_share_link(request, public_id):
|
|
notice = get_object_or_404(access.visible_payments(request, PaymentNotice.objects.all()),
|
|
public_id=public_id, customer__is_active=True)
|
|
url = access.notice_share_url(request, notice)
|
|
response = JsonResponse({'url': url})
|
|
response['Cache-Control'] = 'private, no-store'
|
|
return response
|
|
|
|
|
|
def receipt_form_page(request, customer, staff=False):
|
|
link = getattr(request, 'payment_access_link', None)
|
|
initial = access.receipt_initial(request, customer)
|
|
form = ReceiptForm(request.POST or None, request.FILES or None, customer=customer, initial=initial,
|
|
notice_ids=[link.notice_id] if link else None)
|
|
if request.method == 'POST' and form.is_valid():
|
|
try:
|
|
receipt = services.submit_receipt(customer if link else request.user, customer, form.cleaned_data, access_link=link)
|
|
messages.success(request, 'فیش ثبت شد و در انتظار بررسی است.')
|
|
if request.headers.get('Accept') == 'application/json':
|
|
return JsonResponse({'redirect_url': reverse('staff_payments:receipt_detail' if staff else 'payments:receipt_detail', args=[receipt.public_id])})
|
|
return redirect('staff_payments:receipt_detail' if staff else 'payments:receipt_detail', public_id=receipt.public_id)
|
|
except ValidationError as error:
|
|
form.add_error(None, error)
|
|
if request.method == 'POST' and request.headers.get('Accept') == 'application/json':
|
|
return JsonResponse({'errors': form.errors.get_json_data()}, status=422)
|
|
selected = form['notice_id'].value()
|
|
available = list(form.fields['notice_id'].queryset) if staff else selectors.recent_pending_notice_choices(form)
|
|
notice_data = [selectors.notice_form_data(n, staff=staff) for n in available]
|
|
from apps.attachments.models import AttachmentPolicy
|
|
image_policy = AttachmentPolicy.objects.filter(attachment_type='IMAGE', is_enabled=True).first()
|
|
ctx = {'form': form, 'selected_notice': selected, 'notice_data': notice_data, 'financial_page': True,
|
|
'max_bytes': image_policy.max_file_size if image_policy else 0, 'staff_form': staff, 'customer': customer,
|
|
'account_requirements': {str(a.pk): a.requires_deposit_id for a in form.fields['destination_account'].queryset}}
|
|
return render(request, 'payments/receipt_form.html', staff_context(**ctx) if staff else access.customer_context(request, **ctx))
|
|
|
|
|
|
@financial_required(allow_payment_link=True)
|
|
def receipt_new(request):
|
|
return receipt_form_page(request, request.payment_customer)
|
|
|
|
|
|
@financial_required(allow_payment_link=True)
|
|
def receipt_list(request):
|
|
items = selectors.receipts_with_current_values(access.visible_payments(request, PaymentReceipt.objects.all()).select_related('notice'))
|
|
return render(request, 'payments/receipt_list.html', access.customer_context(request, financial_page=True,
|
|
page_obj=Paginator(items, 20).get_page(request.GET.get('page'))))
|
|
|
|
|
|
@financial_required(staff=True)
|
|
def staff_receipt_customer(request):
|
|
if not policies.can_create(request.user):
|
|
raise PermissionDenied
|
|
from django import forms
|
|
form = forms.Form(request.POST or None, initial=customer_picker.initial_customer(request))
|
|
# Reuse the same active-customer selector and display labels as notice issuance.
|
|
form.fields['customer'] = NoticeForm().fields['customer']
|
|
if request.method == 'POST' and form.is_valid():
|
|
return redirect('staff_payments:receipt_new', customer_id=form.cleaned_data['customer'].public_id)
|
|
return render(request, 'payments/staff_form.html',staff_context(form=form,heading='ثبت فیش برای مشتری',submit_label='ادامه', **customer_picker.picker_context(request, form)))
|
|
|
|
|
|
@financial_required(staff=True)
|
|
def customer_search(request):
|
|
if not policies.can_create(request.user):
|
|
raise PermissionDenied
|
|
response = JsonResponse(customer_picker.search_results(request.user, request.GET.get('q', '')))
|
|
response['Cache-Control'] = 'private, no-store'
|
|
return response
|
|
|
|
|
|
@financial_required(staff=True)
|
|
def staff_receipt_new(request, customer_id):
|
|
if not policies.can_create(request.user):
|
|
raise PermissionDenied
|
|
from apps.accounts.models import User
|
|
customer = get_object_or_404(User.objects.select_related('customer_profile'), public_id=customer_id, account_type='CUSTOMER', is_active=True)
|
|
return receipt_form_page(request, customer, staff=True)
|
|
|
|
|
|
def receipt_context(request, public_id, staff):
|
|
receipt = get_object_or_404(access.visible_payments(request, PaymentReceipt.objects.select_related('customer__customer_profile', 'notice', 'owning_unit')), public_id=public_id)
|
|
revision = receipt.revisions.select_related('destination', 'origin_bank', 'cycle').get(version=receipt.current_version)
|
|
cycle = revision.cycle
|
|
services.audit(request.payment_customer, 'RECEIPT_VIEWED', receipt)
|
|
decisions = ReceiptDecision.objects.filter(cycle__revision__receipt=receipt).select_related('user', 'cycle__revision').order_by('created_at', 'pk')
|
|
return {'receipt': receipt, 'revision': revision, 'cycle': cycle, 'decisions': decisions,
|
|
'payment_link_access': getattr(request, 'payment_access_link', None),
|
|
'revision_events': PaymentAuditEvent.objects.filter(object_type='payments.paymentreceipt', object_id=str(receipt.public_id), action__in=['RECEIPT_SUBMITTED', 'RECEIPT_REVISED']).order_by('created_at', 'pk'),
|
|
'can_approve': staff and policies.can_decide(request.user, receipt, cycle),
|
|
'can_senior': staff and policies.can_decide(request.user, receipt, cycle, senior=True),
|
|
'can_assign': staff and not cycle.assignments.exists() and policies.has_permission(request.user, 'manage_payment_approval_policy'),
|
|
'staff_detail': staff, 'financial_page': True, 'versions': receipt.revisions.select_related('cycle').order_by('version'),
|
|
'duplicate_image': PaymentReceiptRevision.objects.filter(sha256=revision.sha256,
|
|
receipt__in=policies.visible(PaymentReceipt.objects.all(),request.user)).exclude(receipt=receipt).exists() if staff else False}
|
|
|
|
|
|
@financial_required(allow_payment_link=True)
|
|
def receipt_detail(request, public_id):
|
|
return render(request, 'payments/receipt_detail.html', receipt_context(request, public_id, False))
|
|
|
|
|
|
@financial_required(staff=True)
|
|
def staff_receipt_detail(request, public_id):
|
|
return render(request, 'payments/receipt_detail.html', staff_context(**receipt_context(request, public_id, True)))
|
|
|
|
|
|
@financial_required(staff=None, allow_payment_link=True)
|
|
def receipt_image(request, public_id):
|
|
try:
|
|
version = int(request.GET.get('version', '1'))
|
|
except ValueError:
|
|
from django.http import Http404
|
|
raise Http404
|
|
revision = get_object_or_404(PaymentReceiptRevision.objects.select_related('receipt'), receipt__public_id=public_id, version=version)
|
|
if not access.visible_payments(request, PaymentReceipt.objects.filter(pk=revision.receipt_id)).exists():
|
|
raise PermissionDenied
|
|
services.audit(request.payment_customer, 'RECEIPT_FILE_REQUESTED', revision.receipt, {'version': revision.version})
|
|
# Authorize EVERY access, including debug: financial media never uses MEDIA_URL.
|
|
response = FileResponse(revision.image.open('rb'), content_type=revision.mime_type,
|
|
as_attachment=request.GET.get('download') == '1', filename=revision.original_name)
|
|
response['Cache-Control'] = 'private, no-store'
|
|
response['X-Content-Type-Options'] = 'nosniff'
|
|
return response
|
|
|
|
|
|
@financial_required(staff=True)
|
|
@require_POST
|
|
def decision(request, public_id):
|
|
get_object_or_404(policies.visible(PaymentReceipt.objects.all(),request.user),public_id=public_id)
|
|
try:
|
|
version = int(request.POST.get('version', '0'))
|
|
receipt = services.decide_receipt(request.user, public_id, version, request.POST.get('action'), request.POST.get('reason', ''))
|
|
messages.success(request, 'تصمیم ثبت شد. ' + receipt.get_status_display())
|
|
except (ValidationError, ValueError) as error:
|
|
messages.error(request, '؛ '.join(error.messages) if isinstance(error, ValidationError) else 'نسخه فیش معتبر نیست.')
|
|
return redirect('staff_payments:receipt_detail', public_id=public_id)
|
|
|
|
|
|
@financial_required(staff=True)
|
|
@require_POST
|
|
def assign_policy(request, public_id):
|
|
receipt = get_object_or_404(policies.visible(PaymentReceipt.objects.all(), request.user), public_id=public_id)
|
|
try:
|
|
services.bind_unassigned_cycle(request.user, receipt)
|
|
messages.success(request, 'افراد تأییدکننده تعیین شدند.')
|
|
except ValidationError as error:
|
|
messages.error(request, '؛ '.join(error.messages))
|
|
return redirect('staff_payments:receipt_detail', public_id=public_id)
|
|
|
|
|
|
def filtered(queryset, request):
|
|
query = validators.digits(request.GET.get('q', ''))
|
|
if query:
|
|
condition = Q(customer__username__icontains=query) | Q(customer__mobile__icontains=query) | Q(customer__customer_profile__first_name__icontains=query) | Q(customer__customer_profile__last_name__icontains=query) | Q(customer__customer_profile__province__icontains=query) | Q(customer__customer_profile__city__icontains=query)
|
|
# PN/PR numbers are UUID prefixes, not database columns.
|
|
prefix = query.upper().removeprefix('PN-').removeprefix('PR-').replace('-', '')
|
|
if prefix and len(prefix) <= 32 and all(c in '0123456789ABCDEF' for c in prefix):
|
|
uuid_prefix = '-'.join(prefix[start:end] for start, end in [(0, 8), (8, 12), (12, 16), (16, 20), (20, 32)] if len(prefix) > start)
|
|
condition |= Q(public_id__istartswith=uuid_prefix)
|
|
if queryset.model == PaymentNotice:
|
|
condition |= Q(subject__icontains=query) | Q(invoice_number__icontains=query)
|
|
else:
|
|
condition |= Q(notice__invoice_number__icontains=query) | Q(revisions__tracking_number__icontains=query)
|
|
queryset = queryset.filter(condition)
|
|
for key, field in [('account', 'account_id' if queryset.model == PaymentNotice else 'revisions__destination_id'), ('unit', 'owning_unit_id'), ('customer', 'customer_id')]:
|
|
value = request.GET.get(key, '')
|
|
if value.isdecimal():
|
|
queryset = queryset.filter(**{field: value})
|
|
for key, op in [('from', 'gte'), ('to', 'lte')]:
|
|
value = request.GET.get(key, '')
|
|
if value:
|
|
try:
|
|
import datetime
|
|
parsed = datetime.date.fromisoformat(value)
|
|
queryset = queryset.filter(**{f'created_at__date__{op}': parsed})
|
|
except ValueError:
|
|
messages.error(request, 'بازه تاریخ معتبر نیست.')
|
|
return queryset.distinct()
|
|
|
|
|
|
@financial_required(staff=True)
|
|
def desk(request):
|
|
tab = request.GET.get('tab', 'receipts')
|
|
if tab not in {'receipts', 'notices'}:
|
|
tab = 'receipts'
|
|
mine = tab == 'receipts' and (request.GET.get('mine') == '1' or ('tab' not in request.GET and 'mine' not in request.GET))
|
|
receipts = selectors.receipts_with_current_values(policies.visible(PaymentReceipt.objects.select_related('customer__customer_profile', 'notice__created_by__staff_profile'), request.user))
|
|
notice_items = selectors.notices_with_totals(policies.visible(PaymentNotice.objects.select_related('customer__customer_profile', 'created_by__staff_profile', 'account'), request.user))
|
|
if tab == 'notices':
|
|
items = filtered(notice_items, request)
|
|
state = request.GET.get('notice_state', '')
|
|
if state == 'settled':
|
|
items = items.filter(cancelled_at__isnull=True, approved_total__gte=F('amount'))
|
|
elif state == 'pending':
|
|
items = items.filter(cancelled_at__isnull=True, approved_total__lt=F('amount'))
|
|
elif state == 'cancelled':
|
|
items = items.filter(cancelled_at__isnull=False)
|
|
else:
|
|
items = filtered(receipts, request)
|
|
status = request.GET.get('status', '')
|
|
if status in PaymentReceipt.Status.values:
|
|
items = items.filter(status=status)
|
|
if request.GET.get('kind') == 'independent':
|
|
items = items.filter(notice__isnull=True)
|
|
elif request.GET.get('kind') == 'linked':
|
|
items = items.filter(notice__isnull=False)
|
|
if mine:
|
|
my_decision = ReceiptDecision.objects.filter(cycle__revision__receipt_id=OuterRef('pk'),
|
|
cycle__revision__version=OuterRef('current_version'), user=request.user)
|
|
items = items.filter(status__in=['PENDING','PARTIAL'], revisions__version=F('current_version'),
|
|
revisions__cycle__assignments__user=request.user).annotate(
|
|
my_decision_exists=Exists(my_decision)).filter(my_decision_exists=False).distinct()
|
|
page = Paginator(items.order_by('-created_at','-pk'), 25).get_page(request.GET.get('page'))
|
|
if tab == 'notices':
|
|
page.object_list = selectors.attach_notice_receipts(page.object_list, policies.visible(PaymentReceipt.objects.all(), request.user))
|
|
for notice in page.object_list:
|
|
complete_notice(notice)
|
|
else:
|
|
page.object_list = selectors.receipt_review_summaries(page.object_list)
|
|
query = request.GET.copy()
|
|
query.pop('page', None)
|
|
query['tab'] = tab
|
|
if mine:
|
|
query['mine'] = '1'
|
|
else:
|
|
query.pop('mine', None)
|
|
return render(request, 'payments/desk.html', staff_context(page_obj=page, tab=tab, mine=mine, statuses=PaymentReceipt.Status.choices,
|
|
can_create=policies.can_create(request.user), banks=CompanyBankAccount.objects.all(), query_string=query.urlencode()))
|
|
|
|
|
|
@financial_required(staff=True)
|
|
@require_POST
|
|
def generate_deposit_identifier(request):
|
|
if not policies.can_create(request.user):
|
|
raise PermissionDenied
|
|
# Resolve the same active customer as NoticeForm; never trust a posted mobile.
|
|
try:
|
|
customer = NoticeForm().fields['customer'].clean(request.POST.get('customer'))
|
|
mobile = validators.digits(customer.local_mobile)
|
|
import re
|
|
if not re.fullmatch(r'09[0-9]{9}', mobile):
|
|
raise ValidationError('شماره موبایل مشتری معتبر نیست؛ ابتدا اطلاعات مشتری را اصلاح کنید.')
|
|
except ValidationError as error:
|
|
return JsonResponse({'error': '؛ '.join(error.messages)}, status=400)
|
|
from .deposit_identifier import generate_id_algorithm
|
|
response = JsonResponse({'identifier': generate_id_algorithm(mobile), 'customer': str(customer.pk)})
|
|
response['Cache-Control'] = 'no-store'
|
|
return response
|
|
|
|
|
|
@financial_required(staff=True)
|
|
def notice_new(request):
|
|
if not policies.can_create(request.user):
|
|
raise PermissionDenied
|
|
form = NoticeForm(request.POST or None, initial=customer_picker.initial_customer(request))
|
|
if request.method == 'POST' and form.is_valid():
|
|
try:
|
|
notice = services.issue_notice(request.user, form.cleaned_data)
|
|
messages.success(request, 'اعلامیه صادر شد و پیامک در صف اطلاعرسانی قرار گرفت.')
|
|
return redirect('staff_payments:notice_detail', public_id=notice.public_id)
|
|
except ValidationError as error:
|
|
form.add_error(None, error)
|
|
return render(request, 'payments/staff_form.html', staff_context(form=form, heading='ایجاد اعلامیه واریز', submit_label='صدور و اطلاعرسانی', identifier_accounts=list(CompanyBankAccount.objects.filter(requires_deposit_id=True).values_list('pk',flat=True)), **customer_picker.picker_context(request, form)))
|
|
|
|
|
|
@financial_required(staff=True)
|
|
def notice_detail(request, 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):
|
|
request.session.pop('payment_link_share', None)
|
|
else:
|
|
share = {}
|
|
from uuid import uuid4
|
|
from .notifications import notice_sms_pending
|
|
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', ''),
|
|
can_resend_notice_sms=policies.can_create(request.user, notice.owning_unit) and notice.customer.is_active and not notice.cancelled_at,
|
|
notice_sms_pending=notice_sms_pending(notice), notice_sms_request_key=str(uuid4()),
|
|
receipts=policies.visible(notice.receipts.all(), request.user), notifications=selectors.notice_sms_history(notice)))
|
|
|
|
|
|
@financial_required(staff=True)
|
|
@require_POST
|
|
def notice_resend_sms(request, public_id):
|
|
notice = get_object_or_404(policies.notice_actions(PaymentNotice.objects.all(), request.user), public_id=public_id)
|
|
try:
|
|
services.resend_notice_sms(request.user, notice, request.POST.get('request_key', ''))
|
|
messages.success(request, 'درخواست ارسال مجدد پیامک در صف ثبت شد؛ نتیجه ارسال و دریافت در سوابق نمایش داده میشود.')
|
|
except ValidationError as error:
|
|
messages.error(request, '؛ '.join(error.messages))
|
|
return redirect('staff_payments:notice_detail', public_id=public_id)
|
|
|
|
|
|
@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):
|
|
if not policies.can_create(request.user):
|
|
raise PermissionDenied
|
|
notice = get_object_or_404(policies.visible(PaymentNotice.objects.all(), request.user),
|
|
public_id=public_id, customer__is_active=True)
|
|
link, url = access.issue_payment_link(notice=notice)
|
|
services.audit(request.user, 'PAYMENT_LINK_CREATED', link)
|
|
request.session['payment_link_share'] = {'notice': str(public_id), 'url': url}
|
|
return redirect('staff_payments:notice_detail', public_id=public_id)
|
|
|
|
|
|
@financial_required(staff=True)
|
|
def counts(request):
|
|
count = policies.visible(PaymentReceipt.objects.all(), request.user).filter(status__in=['PENDING', 'PARTIAL']).count()
|
|
response = JsonResponse({'pending_receipts': count})
|
|
response['Cache-Control'] = 'private, no-store'
|
|
return response
|
|
|
|
|
|
@financial_required(staff=True)
|
|
def banks(request):
|
|
if not policies.can_manage_banks(request.user):
|
|
raise PermissionDenied
|
|
items = CompanyBankAccount.objects.select_related('bank')
|
|
query = request.GET.get('q','').strip()
|
|
if query:
|
|
items = items.filter(Q(name__icontains=query) | Q(bank__name__icontains=query) | Q(holder__icontains=query))
|
|
for field in ['is_active', 'available_to_staff', 'available_to_customers']:
|
|
if request.GET.get(field) in ['0','1']:
|
|
items = items.filter(**{field: request.GET[field] == '1'})
|
|
params = request.GET.copy()
|
|
params.pop('page', None)
|
|
return render(request, 'payments/banks.html', staff_context(page_obj=Paginator(items,25).get_page(request.GET.get('page')),
|
|
query_string=params.urlencode(), bank_flags=[{'key':key,'label':label,'value':request.GET.get(key,'')} for key,label in [('is_active','فعال کلی'),('available_to_staff','پنل کارشناسان'),('available_to_customers','پنل مشتریان')]], active_staff_section='banks', active_tab_title='بانکها'))
|
|
|
|
|
|
@financial_required(staff=True)
|
|
def bank_edit(request, public_id=None):
|
|
if not policies.can_manage_banks(request.user):
|
|
raise PermissionDenied
|
|
instance = get_object_or_404(CompanyBankAccount, public_id=public_id) if public_id else None
|
|
before = services.configuration_snapshot(instance) if instance else {}
|
|
form = BankAccountForm(request.POST or None, instance=instance)
|
|
if request.method == 'POST' and form.is_valid():
|
|
with transaction.atomic():
|
|
if instance:
|
|
current = CompanyBankAccount.objects.select_for_update().get(pk=instance.pk)
|
|
if form.cleaned_data['revision_token'] != current.updated_at.isoformat():
|
|
form.add_error(None, 'حساب همزمان تغییر کرده است؛ صفحه را تازه کنید.')
|
|
return render(request,'payments/staff_form.html',staff_context(form=form,bank_account=instance,
|
|
heading='ویرایش حساب',submit_label='ذخیره حساب',active_staff_section='banks'))
|
|
before = services.configuration_snapshot(current)
|
|
account = form.save()
|
|
services.audit(request.user, 'BANK_SAVED', account, {'before': before, 'after': services.configuration_snapshot(account)})
|
|
return redirect('staff_payments:banks')
|
|
return render(request,'payments/staff_form.html',staff_context(form=form, bank_account=instance, heading='ویرایش حساب' if instance else 'ایجاد حساب', submit_label='ذخیره حساب', active_staff_section='banks'))
|
|
|
|
|
|
@financial_required(staff=True)
|
|
@require_POST
|
|
def bank_bulk(request):
|
|
if not policies.can_manage_banks(request.user):
|
|
raise PermissionDenied
|
|
field = request.POST.get('field')
|
|
if field not in ['is_active','available_to_staff','available_to_customers'] or request.POST.get('value') not in ['0','1']:
|
|
raise ValidationError('عملیات معتبر نیست.')
|
|
ids = [value for value in request.POST.getlist('accounts') if value.isdecimal()]
|
|
with transaction.atomic():
|
|
for account in CompanyBankAccount.objects.select_for_update().filter(pk__in=ids):
|
|
before = getattr(account, field)
|
|
setattr(account,field,request.POST['value'] == '1')
|
|
account.save(update_fields=[field,'updated_at'])
|
|
services.audit(request.user,'BANK_FLAG_CHANGED',account,{'field':field,'before':before,'after':getattr(account,field)})
|
|
return redirect('staff_payments:banks')
|
|
|
|
|
|
@financial_required(staff=True)
|
|
@require_POST
|
|
def bank_delete(request, public_id):
|
|
if not policies.can_manage_banks(request.user):
|
|
raise PermissionDenied
|
|
account = get_object_or_404(CompanyBankAccount,public_id=public_id)
|
|
try:
|
|
with transaction.atomic():
|
|
services.audit(request.user,'BANK_DELETED',account,account.snapshot())
|
|
account.delete()
|
|
except ProtectedError:
|
|
messages.error(request,'این حساب سابقه مالی دارد؛ به جای حذف آن را غیرفعال کنید.')
|
|
return redirect('staff_payments:banks')
|
|
|
|
|
|
@financial_required(allow_payment_link=True)
|
|
def receipt_revise(request, public_id):
|
|
link = getattr(request, 'payment_access_link', None)
|
|
receipt=get_object_or_404(access.visible_payments(request, PaymentReceipt.objects.all()),public_id=public_id,customer=request.payment_customer,status='REJECTED')
|
|
import jdatetime
|
|
current = receipt.revisions.get(version=receipt.current_version)
|
|
paid = jdatetime.date.fromgregorian(date=current.paid_date)
|
|
initial={'receipt_mode':'linked' if receipt.notice_id else 'independent','notice_id':receipt.notice_id,
|
|
'paid_amount':str(current.amount),'destination_account':current.destination_id,'deposit_id':current.deposit_id,
|
|
'paid_day':str(paid.day),'paid_month':str(paid.month),'paid_year':str(paid.year),
|
|
'origin_bank':current.origin_bank_id,'origin_other':current.origin_other,
|
|
'tracking_number':current.tracking_number,'description':current.description}
|
|
form=ReceiptForm(request.POST or None,request.FILES or None,customer=request.payment_customer,initial=initial,
|
|
notice_ids=[link.notice_id] if link else None)
|
|
if request.method=='POST' and form.is_valid():
|
|
try:
|
|
services.revise_receipt(request.payment_customer,receipt.public_id,int(request.POST.get('version','0')),form.cleaned_data)
|
|
messages.success(request,'نسخه اصلاحی ثبت شد و دوباره بررسی میشود.')
|
|
if request.headers.get('Accept') == 'application/json':
|
|
return JsonResponse({'redirect_url': reverse('payments:receipt_detail', args=[receipt.public_id])})
|
|
return redirect('payments:receipt_detail',public_id=receipt.public_id)
|
|
except (ValidationError,ValueError) as error:
|
|
form.add_error(None,error if isinstance(error,ValidationError) else 'نسخه فیش معتبر نیست.')
|
|
if request.method == 'POST' and request.headers.get('Accept') == 'application/json':
|
|
return JsonResponse({'errors': form.errors.get_json_data()}, status=422)
|
|
from apps.attachments.models import AttachmentPolicy
|
|
policy=AttachmentPolicy.objects.filter(attachment_type='IMAGE',is_enabled=True).first()
|
|
notice_data=[selectors.notice_form_data(n) for n in selectors.recent_pending_notice_choices(form, retain_original=receipt.notice_id)]
|
|
return render(request,'payments/receipt_form.html',{'form':form,'notice_data':notice_data,'max_bytes':policy.max_file_size if policy else 0,
|
|
'payment_link_access': link,
|
|
'account_requirements':{str(a.pk):a.requires_deposit_id for a in form.fields['destination_account'].queryset},
|
|
'financial_page':True,'revision_version':receipt.current_version})
|