gozaresh vosol motalebat
This commit is contained in:
1 parent
0d867367f9
commit
99a86c7073
4 files changed
+404
-1
No files matched your search
@@ -94,6 +94,9 @@
|
||||
گزارش
|
||||
</a>
|
||||
|
||||
<a href="{% url 'admin_panel:report_debtors_no_vosol' %}" style="padding-right: 10px;" class="navbar-item">
|
||||
گزارش وصول مطالبات
|
||||
</a>
|
||||
|
||||
<a href="{% url 'admin_panel:inventory_movements_report' %}" style="padding-right: 10px;" class="navbar-item">
|
||||
گزارش جامع انبار
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
{% extends "./base.html" %}
|
||||
{% load custom_tags %}
|
||||
|
||||
{% if perms.admin_panel.ViewReportMali %}
|
||||
{% block bodyPage %}
|
||||
<style>
|
||||
/* استایل پایه کارتها */
|
||||
.risk-card {
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #eee;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.risk-card:hover { transform: translateY(-3px); box-shadow: 0 4px 15px rgba(0,0,0,0.05); }
|
||||
.border-danger { border-bottom: 4px solid #f14668 !important; }
|
||||
.border-warning { border-bottom: 4px solid #ffdd57 !important; }
|
||||
|
||||
/* استایل جدول و باکس توضیحات */
|
||||
.table-responsive-wrapper { width: 100%; overflow-x: auto; border-radius: 8px; border: 1px solid #eaeaea; }
|
||||
.table th, .table td { vertical-align: middle !important; }
|
||||
.desc-box {
|
||||
max-width: 250px;
|
||||
white-space: normal;
|
||||
background: #f8fafc;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
border-right: 3px solid #3273dc;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.5;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
/* استایل هدرهای قابل سورت */
|
||||
.sortable-th { cursor: pointer; color: #363636; text-decoration: none !important; display: inline-flex; align-items: center; gap: 5px; }
|
||||
.sortable-th:hover { color: #3273dc; }
|
||||
.sort-icon { font-size: 0.8rem; }
|
||||
.sort-icon.active { color: #3273dc; }
|
||||
.sort-icon.inactive { color: #dbdbdb; }
|
||||
|
||||
/* ========================================= */
|
||||
/* تنظیمات اختصاصی موبایل (Responsive Design) */
|
||||
/* ========================================= */
|
||||
@media screen and (max-width: 768px) {
|
||||
.risk-card { padding: 1rem 0.5rem !important; }
|
||||
.risk-card .title { font-size: 1.1rem !important; margin-bottom: 0.2rem !important; }
|
||||
.risk-card .heading { font-size: 0.65rem !important; margin-bottom: 0.5rem !important; letter-spacing: 0; }
|
||||
|
||||
/* بهبود نمایش کنترل پنل در موبایل */
|
||||
.control-panel-box { padding: 1rem !important; }
|
||||
.control-panel-box .label { font-size: 0.8rem; margin-bottom: 0.3rem; }
|
||||
|
||||
/* کوچک کردن فونتهای جدول برای جا شدن بهتر در موبایل */
|
||||
.table th, .table td { font-size: 0.8rem; padding: 0.5rem 0.4rem !important; }
|
||||
.desc-box { font-size: 0.75rem; min-width: 180px; }
|
||||
.sortable-th { font-size: 0.75rem; }
|
||||
|
||||
/* دکمههای جدول در موبایل */
|
||||
.action-btn span:last-child { display: none; } /* مخفی کردن متن دکمه، فقط آیکون میماند */
|
||||
.action-btn { padding: 0.5rem !important; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container is-fluid mt-4 mb-6">
|
||||
<div class="columns m-0 is-multiline">
|
||||
<div class="column is-12">
|
||||
|
||||
<div class="mb-5">
|
||||
<h2 class="title is-5 is-size-6-mobile mb-4 has-text-grey-dark">
|
||||
<i class="fas fa-chart-line has-text-info"></i>
|
||||
داشبورد هوشمند وصول مطالبات (رها شده)
|
||||
</h2>
|
||||
|
||||
<!-- داشبورد آماری دقیق و تفکیک شده -->
|
||||
<div class="columns is-multiline is-mobile">
|
||||
<div class="column is-6-mobile is-3-desktop">
|
||||
<div class="box risk-card border-danger has-text-centered p-4">
|
||||
<p class="heading has-text-grey">مشتریان هرگز پیگیری نشده</p>
|
||||
<p class="title is-size-4 has-text-dark">{{ stats.never_count }} <span class="is-size-6 has-text-grey">نفر</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-6-mobile is-3-desktop">
|
||||
<div class="box risk-card border-danger has-text-centered p-4">
|
||||
<p class="heading has-text-grey">مبلغ (هرگز پیگیری نشده)</p>
|
||||
<p class="title is-size-4 has-text-danger-dark toCurrency">{{ stats.never_amount }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-6-mobile is-3-desktop">
|
||||
<div class="box risk-card border-warning has-text-centered p-4">
|
||||
<p class="heading has-text-grey">رها شده (بیش از {{ current_days }} روز)</p>
|
||||
<p class="title is-size-4 has-text-dark">{{ stats.expired_count }} <span class="is-size-6 has-text-grey">نفر</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-6-mobile is-3-desktop">
|
||||
<div class="box risk-card border-warning has-text-centered p-4">
|
||||
<p class="heading has-text-grey">مبلغ (پیگیری رها شده)</p>
|
||||
<p class="title is-size-4 has-text-danger-dark toCurrency">{{ stats.expired_amount }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- کنترل پنل فیلترها و زمان (بهینهشده برای موبایل) -->
|
||||
<div class="box mb-4 has-background-white-ter control-panel-box">
|
||||
<form method="GET" action="">
|
||||
<input type="hidden" name="sort" value="{{ current_sort }}">
|
||||
<input type="hidden" name="dir" value="{{ current_dir }}">
|
||||
|
||||
<div class="columns is-multiline is-vcentered">
|
||||
<div class="column is-12-mobile is-5-tablet">
|
||||
<div class="field">
|
||||
<label class="label">وضعیت پیگیری:</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select name="filter_type" class="is-size-7-mobile">
|
||||
<option value="all" {% if current_filter == 'all' %}selected{% endif %}>همه نیازمند پیگیری</option>
|
||||
<option value="never" {% if current_filter == 'never' %}selected{% endif %}>فقط هرگز پیگیری نشدهها</option>
|
||||
<option value="expired" {% if current_filter == 'expired' %}selected{% endif %}>فقط پیگیریهای رها شده</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12-mobile is-4-tablet">
|
||||
<div class="field">
|
||||
<label class="label">بدون اقدام بیش از (روز):</label>
|
||||
<div class="control">
|
||||
<input type="number" name="days" class="input is-size-7-mobile" value="{{ current_days }}" min="1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-12-mobile is-3-tablet">
|
||||
<div class="field">
|
||||
<!-- در موبایل لیبل خالی میگذاریم تا ارتفاع حفظ شود، در دسکتاپ مخفی است -->
|
||||
<label class="label is-hidden-mobile"> </label>
|
||||
<div class="control">
|
||||
<button type="submit" class="button is-info is-fullwidth">
|
||||
<i class="fas fa-filter ml-2"></i> اعمال فیلتر
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- جدول دادهها -->
|
||||
<div class="table-responsive-wrapper shadow-sm mb-4">
|
||||
<table class="table m-auto has-text-centered is-bordered is-striped is-narrow is-fullwidth is-hoverable mb-0">
|
||||
<thead class="has-background-light">
|
||||
<tr>
|
||||
<th style="width: 40px;">#</th>
|
||||
<th>نام مشتری</th>
|
||||
<th>شماره تماس</th>
|
||||
<th>مانده کل حساب</th>
|
||||
|
||||
<!-- قابلیت Sort دوطرفه -->
|
||||
<th>
|
||||
<a class="sortable-th" href="?filter_type={{ current_filter }}&days={{ current_days }}&sort=amount&dir={% if current_sort == 'amount' and current_dir == 'desc' %}asc{% else %}desc{% endif %}">
|
||||
مبلغ معوقه
|
||||
<i class="fas fa-sort{% if current_sort == 'amount' %}-{{ current_dir }}{% endif %} sort-icon {% if current_sort == 'amount' %}active{% else %}inactive{% endif %}"></i>
|
||||
</a>
|
||||
</th>
|
||||
|
||||
<th>
|
||||
<a class="sortable-th" href="?filter_type={{ current_filter }}&days={{ current_days }}&sort=oldest_date&dir={% if current_sort == 'oldest_date' and current_dir == 'desc' %}asc{% else %}desc{% endif %}">
|
||||
تاریخ قدیمیترین تاخیر
|
||||
<i class="fas fa-sort{% if current_sort == 'oldest_date' %}-{{ current_dir }}{% endif %} sort-icon {% if current_sort == 'oldest_date' %}active{% else %}inactive{% endif %}"></i>
|
||||
</a>
|
||||
</th>
|
||||
|
||||
<th style="min-width: 230px;">
|
||||
<a class="sortable-th" href="?filter_type={{ current_filter }}&days={{ current_days }}&sort=last_action&dir={% if current_sort == 'last_action' and current_dir == 'desc' %}asc{% else %}desc{% endif %}">
|
||||
آخرین سابقه وصول
|
||||
<i class="fas fa-sort{% if current_sort == 'last_action' %}-{{ current_dir }}{% endif %} sort-icon {% if current_sort == 'last_action' %}active{% else %}inactive{% endif %}"></i>
|
||||
</a>
|
||||
</th>
|
||||
|
||||
<th>عملیات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for customer in page_obj %}
|
||||
<tr>
|
||||
<td>{{ forloop.counter0|add:page_obj.start_index }}</td>
|
||||
|
||||
<td>
|
||||
<a target="_blank" href="{% url 'admin_panel:customerNew' customer.id %}" class="has-text-link has-text-weight-bold">
|
||||
{{ customer.name_family }}
|
||||
</a>
|
||||
</td>
|
||||
|
||||
<td dir="ltr" class="has-text-weight-medium">
|
||||
<div class="is-size-7-mobile">{{ customer.phone1|default:"-" }}</div>
|
||||
{% if customer.phone2 %}<div class="is-size-7-mobile">{{ customer.phone2 }}</div>{% endif %}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span class="toCurrency has-text-grey-dark is-size-7">{{ customer.net_debt }}</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span class="toCurrency has-text-danger has-text-weight-bold is-size-6 is-size-7-mobile">
|
||||
{{ customer.overdue_amount }}
|
||||
</span>
|
||||
<br>
|
||||
<span class="tag is-light is-danger is-small mt-1">{{ customer.overdue_count }} فقره</span>
|
||||
</td>
|
||||
|
||||
<td class="has-text-danger-dark has-text-weight-semibold is-size-7">
|
||||
{{ customer.oldest_overdue_date | toJalali }}
|
||||
</td>
|
||||
|
||||
<td style="text-align: right;">
|
||||
{% if customer.last_vosol_date %}
|
||||
<div class="has-text-grey-dark is-size-7 has-text-weight-semibold">
|
||||
<i class="far fa-calendar-alt ml-1"></i> {{ customer.last_vosol_date | toJalali }}
|
||||
</div>
|
||||
<div class="desc-box has-text-grey">
|
||||
{{ customer.last_vosol_desc|default:"(بدون توضیحات ثبت شده)" }}
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="tag is-danger is-light has-text-weight-bold is-size-7-mobile">هرگز پیگیری نشده</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<!-- استفاده از کلاس action-btn برای ریسپانسیو شدن دکمه در موبایل -->
|
||||
<a target="_blank" href="{% url 'admin_panel:customerAction' customer.id %}" class="button is-small is-info is-light has-text-weight-bold action-btn">
|
||||
<span class="icon is-small"><i class="fas fa-phone-alt"></i></span>
|
||||
<span>ثبت تماس</span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="8" class="has-text-grey p-5">
|
||||
<i class="fas fa-check-circle has-text-success is-size-4 mb-2"></i><br>
|
||||
با این تنظیمات و فیلترها، هیچ مشتری بدهکار رها شدهای یافت نشد.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- صفحهبندی -->
|
||||
<div class="mt-4 is-flex is-justify-content-center">
|
||||
<div class="buttons">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="?page={{ page_obj.previous_page_number }}&filter_type={{ current_filter }}&days={{ current_days }}&sort={{ current_sort }}&dir={{ current_dir }}" class="button is-light is-small-mobile">صفحه قبل</a>
|
||||
{% endif %}
|
||||
{% if page_obj.has_next %}
|
||||
<a href="?page={{ page_obj.next_page_number }}&filter_type={{ current_filter }}&days={{ current_days }}&sort={{ current_sort }}&dir={{ current_dir }}" class="button is-light is-small-mobile">صفحه بعد</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block jsEnd %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// فرمتکننده ایمن و رفع خطای NaN
|
||||
$('.toCurrency').each(function() {
|
||||
var rawValue = $(this).text().trim();
|
||||
// بررسی میکنیم که متغیر خالی، نال یا None رشتهای نباشد
|
||||
if(rawValue && rawValue !== 'None' && rawValue !== '') {
|
||||
// حذف هر کاراکتری به جز اعداد و علامت منفی
|
||||
var cleanValue = rawValue.replace(/[^\d-]/g, '');
|
||||
if(!isNaN(cleanValue) && cleanValue !== '') {
|
||||
$(this).text(Number(cleanValue).toLocaleString('en-US'));
|
||||
} else {
|
||||
$(this).text('0');
|
||||
}
|
||||
} else {
|
||||
$(this).text('0');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
@@ -13,6 +13,7 @@ urlpatterns = [
|
||||
path('report/', views.report, name='report'),
|
||||
path('report/inventory-movements/', views.inventory_movements_report, name='inventory_movements_report'),
|
||||
path('report/dashboard/', views.financial_dashboard, name='financial_dashboard'),
|
||||
path('report/debtors-no-vosol/', views.report_debtors_no_vosol, name='report_debtors_no_vosol'),
|
||||
path('report/rezayat_dashboard/', views.rezayat_dashboard, name='rezayat_dashboard'),
|
||||
path('hesabdari/', views.hesabdari, name='hesabdari'),
|
||||
path('hesabdari/sarresid_nashode/', views.hesabdari_residnashone, name='hesabdari_residnashone'),
|
||||
|
||||
+112
-1
@@ -22,7 +22,7 @@ from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest # بخشBadRequest جدید
|
||||
from django.db import transaction, connection
|
||||
from django.db.models import Count, Sum, Q, Avg, F, Subquery, OuterRef, IntegerField
|
||||
from django.db.models import Count, Sum, Q, Avg, F, Subquery, OuterRef, IntegerField, Exists, Min, Max, Case, When
|
||||
from django.contrib.auth.models import Permission, User
|
||||
from django.contrib.auth import authenticate, login, logout
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
@@ -196,6 +196,117 @@ def report(request):
|
||||
data = {'nahvePardakhat': nahvePardakhat, 'userHa':userHa}
|
||||
return render(request, 'admin_panel/report.html', data)
|
||||
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||||
def report_debtors_no_vosol(request):
|
||||
now = datetime.datetime.now()
|
||||
|
||||
# --- ۱. دریافت و اعتبارسنجی پارامترهای فیلتر از فرانتاند ---
|
||||
try:
|
||||
days_threshold = int(request.GET.get('days', 20))
|
||||
if days_threshold < 0:
|
||||
days_threshold = 20
|
||||
except ValueError:
|
||||
days_threshold = 20
|
||||
|
||||
filter_type = request.GET.get('filter_type', 'all') # all, never, expired
|
||||
sort_field = request.GET.get('sort', 'amount') # amount, oldest_date, last_action
|
||||
sort_dir = request.GET.get('dir', 'desc') # asc, desc
|
||||
|
||||
threshold_date = now - datetime.timedelta(days=days_threshold)
|
||||
|
||||
# --- ۲. ساخت Subquery های فوقسریع و ایمن ---
|
||||
sq_debit = Article.objects.filter(customer=OuterRef('pk')).values('customer').annotate(s=Sum('debit')).values('s')
|
||||
sq_credit = Article.objects.filter(customer=OuterRef('pk')).values('customer').annotate(s=Sum('credit')).values('s')
|
||||
|
||||
sq_overdue_amt = Sarresid.objects.filter(
|
||||
customer=OuterRef('pk'), payStatus='false', sarresidDT__lte=now
|
||||
).values('customer').annotate(s=Sum('price')).values('s')
|
||||
|
||||
sq_oldest_date = Sarresid.objects.filter(
|
||||
customer=OuterRef('pk'), payStatus='false', sarresidDT__lte=now
|
||||
).values('customer').annotate(min_dt=Min('sarresidDT')).values('min_dt')
|
||||
|
||||
sq_overdue_count = Sarresid.objects.filter(
|
||||
customer=OuterRef('pk'), payStatus='false', sarresidDT__lte=now
|
||||
).values('customer').annotate(c=Count('id')).values('c')
|
||||
|
||||
# استخراج آخرین سابقه اکشن وصول
|
||||
latest_vosol_qs = Action.objects.filter(
|
||||
customer=OuterRef('pk'), actionType='vosol'
|
||||
).order_by('-createdDT')
|
||||
|
||||
# --- ۳. ساخت کوئری پایه (فقط مشتریان فعال و دارای معوقه) ---
|
||||
base_qs = Customer.objects.filter(is_active=True).annotate(
|
||||
t_debit=Coalesce(Subquery(sq_debit, output_field=IntegerField()), Value(0)),
|
||||
t_credit=Coalesce(Subquery(sq_credit, output_field=IntegerField()), Value(0)),
|
||||
overdue_amount=Coalesce(Subquery(sq_overdue_amt, output_field=IntegerField()), Value(0)),
|
||||
overdue_count=Coalesce(Subquery(sq_overdue_count, output_field=IntegerField()), Value(0)),
|
||||
oldest_overdue_date=Subquery(sq_oldest_date),
|
||||
last_vosol_date=Subquery(latest_vosol_qs.values('createdDT')[:1]),
|
||||
last_vosol_desc=Subquery(latest_vosol_qs.values('desc')[:1])
|
||||
).annotate(
|
||||
net_debt=F('t_debit') - F('t_credit')
|
||||
).filter(
|
||||
overdue_amount__gt=0 # شرط اساسی: فقط کسانی که معوقه سررسید گذشته دارند
|
||||
)
|
||||
|
||||
# استخراج مشتریانی که نیازمند پیگیری هستند (هرگز پیگیری نشدهاند یا موعد پیگیریشان گذشته است)
|
||||
action_needed_qs = base_qs.filter(
|
||||
Q(last_vosol_date__isnull=True) | Q(last_vosol_date__lt=threshold_date)
|
||||
)
|
||||
|
||||
# --- ۴. محاسبه آمار دقیق (بدون خطا) برای داشبورد ---
|
||||
# استفاده از Case/When برای جلوگیری کامل از خطای None در Aggregate
|
||||
stats = action_needed_qs.aggregate(
|
||||
never_count=Coalesce(Sum(Case(When(last_vosol_date__isnull=True, then=1), default=Value(0), output_field=IntegerField())), Value(0)),
|
||||
never_amount=Coalesce(Sum(Case(When(last_vosol_date__isnull=True, then=F('overdue_amount')), default=Value(0), output_field=IntegerField())), Value(0)),
|
||||
expired_count=Coalesce(Sum(Case(When(last_vosol_date__isnull=False, then=1), default=Value(0), output_field=IntegerField())), Value(0)),
|
||||
expired_amount=Coalesce(Sum(Case(When(last_vosol_date__isnull=False, then=F('overdue_amount')), default=Value(0), output_field=IntegerField())), Value(0)),
|
||||
)
|
||||
|
||||
# --- ۵. اعمال فیلتر کشویی (Dropdown) ---
|
||||
if filter_type == 'never':
|
||||
final_qs = action_needed_qs.filter(last_vosol_date__isnull=True)
|
||||
elif filter_type == 'expired':
|
||||
final_qs = action_needed_qs.filter(last_vosol_date__isnull=False)
|
||||
else: # all
|
||||
final_qs = action_needed_qs
|
||||
|
||||
# --- ۶. اعمال مرتبسازی (Sorting) پویا ---
|
||||
if sort_field == 'amount':
|
||||
order_expr = F('overdue_amount').desc() if sort_dir == 'desc' else F('overdue_amount').asc()
|
||||
elif sort_field == 'oldest_date':
|
||||
order_expr = F('oldest_overdue_date').desc(nulls_last=True) if sort_dir == 'desc' else F('oldest_overdue_date').asc(nulls_last=True)
|
||||
elif sort_field == 'last_action':
|
||||
order_expr = F('last_vosol_date').desc(nulls_last=True) if sort_dir == 'desc' else F('last_vosol_date').asc(nulls_first=True)
|
||||
else:
|
||||
order_expr = F('overdue_amount').desc()
|
||||
|
||||
final_qs = final_qs.order_by(order_expr)
|
||||
|
||||
# --- ۷. صفحهبندی ایمن ---
|
||||
paginator = Paginator(final_qs, 50)
|
||||
page_number = request.GET.get('page', 1)
|
||||
try:
|
||||
page_obj = paginator.page(page_number)
|
||||
except (PageNotAnInteger, EmptyPage):
|
||||
page_obj = paginator.page(1)
|
||||
|
||||
context = {
|
||||
'page_obj': page_obj,
|
||||
'page_name': 'debtors_no_vosol',
|
||||
'stats': stats,
|
||||
# پارامترهای کنترل پنل
|
||||
'current_days': days_threshold,
|
||||
'current_filter': filter_type,
|
||||
'current_sort': sort_field,
|
||||
'current_dir': sort_dir,
|
||||
}
|
||||
|
||||
return render(request, 'admin_panel/report_debtors_no_vosol.html', context)
|
||||
|
||||
@login_required(login_url='/panel/p_login/')
|
||||
@permission_required('admin_panel.ViewReportMali', raise_exception=True)
|
||||
def inventory_movements_report(request):
|
||||
|
||||
Reference in new issue
Block a user