Businessman uchun statistikalar qo'shildi!
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-02-19 07:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('management', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='expense',
|
||||||
|
name='expense_type',
|
||||||
|
field=models.CharField(choices=[('salary', 'Maosh'), ('utilities', 'Kommunal to‘lovlar'), ('maintenance', 'Texnik xizmat'), ('food', 'Oziq-ovqat'), ('transport', "Yo'lkira"), ('other', 'Boshqa')], default='other', max_length=20),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -8,7 +8,6 @@ class Expense(models.Model):
|
|||||||
MAINTENANCE = "maintenance", "Texnik xizmat"
|
MAINTENANCE = "maintenance", "Texnik xizmat"
|
||||||
FOOD = "food", "Oziq-ovqat"
|
FOOD = "food", "Oziq-ovqat"
|
||||||
TRANSPORT = "transport", "Yo'lkira"
|
TRANSPORT = "transport", "Yo'lkira"
|
||||||
BUY_TOYS = "buy_toys", "Oʻyinchoqlar sotib olish"
|
|
||||||
OTHER = "other", "Boshqa"
|
OTHER = "other", "Boshqa"
|
||||||
|
|
||||||
amount = models.DecimalField(max_digits=12, decimal_places=2)
|
amount = models.DecimalField(max_digits=12, decimal_places=2)
|
||||||
|
|||||||
@@ -1,164 +1,237 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Hisobotlar{% endblock %}
|
{% block title %}Qurilma Statistikasi{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
|
||||||
<h2 style="margin: 0;">{{ title|default:"Hisobotlar" }}</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="cards-container">
|
<div class="page-card">
|
||||||
{% if reports %}
|
|
||||||
{% for report in reports %}
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<div class="card-title">
|
|
||||||
{{ report.device.address }}
|
|
||||||
</div>
|
|
||||||
<div class="card-number">#{{ forloop.counter }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-content">
|
<!-- HEADER -->
|
||||||
<div class="card-row">
|
<div class="page-header">
|
||||||
<span class="label">Miqdor:</span>
|
<h2 class="page-title">Qurilma Statistikasi</h2>
|
||||||
<span class="quantity">{{ report.quantity }} dona</span>
|
<p class="page-sub">Qurilmalar bo'yicha kirim, xarajat va foyda hisoboti</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-row">
|
|
||||||
<span class="label">Sana:</span>
|
<!-- FILTER -->
|
||||||
<span class="value">{{ report.created_at|date:"d.m.Y H:i" }}</span>
|
<form method="get" class="filter-bar">
|
||||||
</div>
|
<div class="filter-group">
|
||||||
<div class="card-row">
|
<label class="filter-label">Boshlanish sanasi</label>
|
||||||
<span class="label">Yaratgan:</span>
|
<input type="date" name="start_date" value="{{ request.GET.start_date }}" class="filter-input">
|
||||||
<span class="value">{{ report.created_by.get_full_name }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
<div style="grid-column: 1 / -1; text-align: center; padding: 60px 20px; color: #6b7280;">
|
|
||||||
<p style="font-style: italic; margin: 0;">Hech qanday hisobot topilmadi</p>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
<div class="filter-group">
|
||||||
|
<label class="filter-label">Tugash sanasi</label>
|
||||||
|
<input type="date" name="end_date" value="{{ request.GET.end_date }}" class="filter-input">
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label class="filter-label">Qurilma</label>
|
||||||
|
<select name="device" class="filter-input">
|
||||||
|
<option value="">— Barchasi —</option>
|
||||||
|
{% for device in devices %}
|
||||||
|
<option value="{{ device.id }}"
|
||||||
|
{% if request.GET.device == device.id|stringformat:"s" %}selected{% endif %}>
|
||||||
|
{{ device.address }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="filter-actions">
|
||||||
|
<button type="submit" class="btn-filter">Filtrlash</button>
|
||||||
|
<a href="?" class="btn-clear">Tozalash</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- SUMMARY CARDS -->
|
||||||
|
<div class="summary-row">
|
||||||
|
<div class="summary-card card-income">
|
||||||
|
<span class="card-icon">💰</span>
|
||||||
|
<div>
|
||||||
|
<div class="card-label">Jami Kirim</div>
|
||||||
|
<div class="card-value">{{ total_kirim|floatformat:0 }} so'm</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-card card-expense">
|
||||||
|
<span class="card-icon">📉</span>
|
||||||
|
<div>
|
||||||
|
<div class="card-label">Jami Xarajat</div>
|
||||||
|
<div class="card-value">{{ total_xarajat|floatformat:0 }} so'm</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-card {% if total_foyda >= 0 %}card-profit-pos{% else %}card-profit-neg{% endif %}">
|
||||||
|
<span class="card-icon">{% if total_foyda >= 0 %}📈{% else %}📉{% endif %}</span>
|
||||||
|
<div>
|
||||||
|
<div class="card-label">Jami Foyda</div>
|
||||||
|
<div class="card-value">{{ total_foyda|floatformat:0 }} so'm</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TABLE -->
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Qurilma</th>
|
||||||
|
<th>Miqdor</th>
|
||||||
|
<th>Kirim</th>
|
||||||
|
<th>Xarajat</th>
|
||||||
|
<th>Foyda</th>
|
||||||
|
<th>Sana</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% if rows %}
|
||||||
|
{% for row in rows %}
|
||||||
|
<tr>
|
||||||
|
<td class="col-num">{{ forloop.counter }}</td>
|
||||||
|
<td class="col-device">{{ row.device_name }}</td>
|
||||||
|
<td><span class="qty-badge">{{ row.quantity }}</span></td>
|
||||||
|
<td class="col-income">{{ row.kirim|floatformat:0 }} so'm</td>
|
||||||
|
<td class="col-expense">{{ row.xarajat|floatformat:0 }} so'm</td>
|
||||||
|
<td>
|
||||||
|
<span class="foyda-pill {% if row.foyda >= 0 %}foyda-pos{% else %}foyda-neg{% endif %}">
|
||||||
|
{% if row.foyda >= 0 %}+{% endif %}{{ row.foyda|floatformat:0 }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="col-date">{{ row.date }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="empty">Hech qanday ma'lumot topilmadi</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tbl-footer">
|
||||||
|
Jami: <strong>{{ rows|length }}</strong> ta yozuv
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="back-link">
|
||||||
|
<a href="javascript:history.back()">← Orqaga qaytish</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.cards-container {
|
.page-card {
|
||||||
display: grid;
|
max-width: 1000px;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
margin: 32px auto;
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 12px;
|
border-radius: 16px;
|
||||||
padding: 16px;
|
padding: 32px;
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
|
box-shadow: 0 4px 24px rgba(0,0,0,0.07);
|
||||||
transition: all 0.2s ease;
|
}
|
||||||
border-left: 4px solid #10b981;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover {
|
.page-header { margin-bottom: 24px; }
|
||||||
transform: translateY(-2px);
|
.page-title { font-size: 20px; font-weight: 700; color: #111827; margin: 0 0 4px; }
|
||||||
box-shadow: 0 6px 20px rgba(0,0,0,0.1);
|
.page-sub { font-size: 13px; color: #6b7280; margin: 0; }
|
||||||
}
|
|
||||||
|
|
||||||
.card-header {
|
/* FILTER */
|
||||||
display: flex;
|
.filter-bar {
|
||||||
justify-content: space-between;
|
display: flex; align-items: flex-end; flex-wrap: wrap; gap: 12px;
|
||||||
align-items: start;
|
background: #f9fafb; border: 1px solid #e5e7eb;
|
||||||
margin-bottom: 12px;
|
border-radius: 10px; padding: 16px 18px; margin-bottom: 24px;
|
||||||
padding-bottom: 12px;
|
}
|
||||||
border-bottom: 1px solid #e5e7eb;
|
.filter-group { display: flex; flex-direction: column; gap: 4px; }
|
||||||
}
|
.filter-label {
|
||||||
|
font-size: 11px; font-weight: 600; color: #6b7280;
|
||||||
|
text-transform: uppercase; letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
.filter-input {
|
||||||
|
padding: 8px 10px; border-radius: 7px;
|
||||||
|
border: 1.5px solid #d1d5db; background: #fff;
|
||||||
|
font-size: 13px; color: #111827; outline: none;
|
||||||
|
min-width: 160px; transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
.filter-input:focus { border-color: #4f46e5; box-shadow: 0 0 0 3px rgba(79,70,229,0.08); }
|
||||||
|
.filter-actions { display: flex; gap: 8px; align-items: flex-end; }
|
||||||
|
.btn-filter {
|
||||||
|
padding: 9px 22px; background: #4f46e5; color: #fff;
|
||||||
|
border: none; border-radius: 7px; font-size: 13px; font-weight: 600;
|
||||||
|
cursor: pointer; transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.btn-filter:hover { background: #4338ca; }
|
||||||
|
.btn-clear {
|
||||||
|
padding: 9px 14px; background: #fff; color: #6b7280;
|
||||||
|
border: 1.5px solid #d1d5db; border-radius: 7px;
|
||||||
|
font-size: 13px; font-weight: 600; text-decoration: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.btn-clear:hover { border-color: #9ca3af; color: #374151; }
|
||||||
|
|
||||||
.card-title {
|
/* SUMMARY */
|
||||||
font-weight: 700;
|
.summary-row {
|
||||||
color: #1f2937;
|
display: grid; grid-template-columns: repeat(3,1fr);
|
||||||
font-size: 15px;
|
gap: 14px; margin-bottom: 24px;
|
||||||
flex: 1;
|
}
|
||||||
}
|
.summary-card {
|
||||||
|
display: flex; align-items: center; gap: 14px;
|
||||||
|
border-radius: 10px; padding: 16px 18px;
|
||||||
|
border: 1.5px solid transparent;
|
||||||
|
}
|
||||||
|
.card-income { background: #ecfdf5; border-color: #a7f3d0; }
|
||||||
|
.card-expense { background: #fef2f2; border-color: #fecaca; }
|
||||||
|
.card-profit-pos{ background: #eff6ff; border-color: #bfdbfe; }
|
||||||
|
.card-profit-neg{ background: #fef2f2; border-color: #fecaca; }
|
||||||
|
.card-icon { font-size: 1.5rem; }
|
||||||
|
.card-label { font-size: 11px; font-weight: 600; color: #6b7280; text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 3px; }
|
||||||
|
.card-value { font-size: 1.05rem; font-weight: 800; color: #111827; }
|
||||||
|
|
||||||
.card-number {
|
/* TABLE */
|
||||||
font-size: 12px;
|
.table-wrap {
|
||||||
color: #9ca3af;
|
border: 1px solid #e5e7eb; border-radius: 10px;
|
||||||
font-weight: 600;
|
overflow: hidden; overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||||
|
thead tr { background: #f3f4f6; border-bottom: 1.5px solid #e5e7eb; }
|
||||||
|
th {
|
||||||
|
padding: 11px 14px; text-align: left;
|
||||||
|
font-size: 11px; font-weight: 700; color: #6b7280;
|
||||||
|
text-transform: uppercase; letter-spacing: 0.06em; white-space: nowrap;
|
||||||
|
}
|
||||||
|
tbody tr { border-bottom: 1px solid #f3f4f6; transition: background 0.15s; }
|
||||||
|
tbody tr:last-child { border-bottom: none; }
|
||||||
|
tbody tr:hover { background: #fafafa; }
|
||||||
|
td { padding: 12px 14px; vertical-align: middle; color: #374151; }
|
||||||
|
|
||||||
.card-content {
|
.col-num { color: #9ca3af; font-weight: 600; width: 40px; }
|
||||||
display: flex;
|
.col-device { font-weight: 600; color: #111827; }
|
||||||
flex-direction: column;
|
.col-income { color: #059669; font-weight: 600; white-space: nowrap; }
|
||||||
gap: 8px;
|
.col-expense { color: #dc2626; font-weight: 600; white-space: nowrap; }
|
||||||
}
|
.col-date { color: #6b7280; white-space: nowrap; }
|
||||||
|
|
||||||
.card-row {
|
.qty-badge {
|
||||||
display: flex;
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
justify-content: space-between;
|
width: 30px; height: 30px; border-radius: 50%;
|
||||||
font-size: 13px;
|
background: #d1fae5; color: #065f46;
|
||||||
}
|
font-weight: 700; font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.card-row .label {
|
.foyda-pill {
|
||||||
color: #6b7280;
|
display: inline-block; padding: 3px 10px; border-radius: 50px;
|
||||||
font-weight: 500;
|
font-size: 12px; font-weight: 700; white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
.foyda-pos { background: #ecfdf5; color: #059669; }
|
||||||
|
.foyda-neg { background: #fef2f2; color: #dc2626; }
|
||||||
|
|
||||||
.card-row .value {
|
.empty { text-align: center; padding: 48px 20px; color: #9ca3af; font-style: italic; }
|
||||||
color: #374151;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-row .quantity {
|
.tbl-footer { text-align: right; padding: 12px 2px 0; font-size: 13px; color: #6b7280; }
|
||||||
color: #10b981;
|
.tbl-footer strong { color: #111827; }
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
.back-link { margin-top: 20px; }
|
||||||
display: inline-block;
|
.back-link a { color: #4f46e5; font-size: 13px; font-weight: 600; text-decoration: none; }
|
||||||
padding: 8px 14px;
|
.back-link a:hover { text-decoration: underline; }
|
||||||
border-radius: 6px;
|
|
||||||
text-decoration: none;
|
|
||||||
color: #fff;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn.edit {
|
@media (max-width: 768px) {
|
||||||
background: #4f46e5;
|
.page-card { margin: 12px; padding: 18px; }
|
||||||
}
|
.summary-row { grid-template-columns: 1fr; }
|
||||||
|
.filter-bar { flex-direction: column; }
|
||||||
.btn.edit:hover {
|
.filter-input{ min-width: 100%; }
|
||||||
background: #4338ca;
|
}
|
||||||
box-shadow: 0 2px 8px rgba(79,70,229,0.3);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.cards-container {
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.cards-container {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
div[style*="display: flex"] {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -25,7 +25,7 @@ urlpatterns = [
|
|||||||
path("list/warehouse/", views.warehouse_list, name="warehouse_list"),
|
path("list/warehouse/", views.warehouse_list, name="warehouse_list"),
|
||||||
path("list/user/", views.user_list, name="user_list"),
|
path("list/user/", views.user_list, name="user_list"),
|
||||||
path("list/toy-movement/", views.toy_movement_list, name="toy_movement_list"),
|
path("list/toy-movement/", views.toy_movement_list, name="toy_movement_list"),
|
||||||
path("list/reports/", views.report_list, name="report_list"),
|
path("list/reports/", views.device_statistics, name="report_list"),
|
||||||
path("list/toy-movement-statistics/", views.toy_movement_statistics, name="toy_movement_statistics"),
|
path("list/toy-movement-statistics/", views.toy_movement_statistics, name="toy_movement_statistics"),
|
||||||
# Edit
|
# Edit
|
||||||
path("edit/device/<int:pk>/", views.edit_device, name="edit_device"),
|
path("edit/device/<int:pk>/", views.edit_device, name="edit_device"),
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
from django.shortcuts import render
|
from core.apps.management.models import Device, Income, Expense, Warehouse, ToyMovement
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
from core.apps.management.models import Device, Income, Expense, Warehouse, ToyMovement, Report
|
|
||||||
from core.apps.accounts.models import User
|
from core.apps.accounts.models import User
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
from core.apps.management.decorators import role_required
|
from core.apps.management.decorators import role_required
|
||||||
|
from decimal import Decimal
|
||||||
|
from django.db.models import Sum
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@role_required(["manager", "businessman"])
|
@role_required(["manager", "businessman"])
|
||||||
@@ -150,36 +153,114 @@ def device_payment_list(request):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
from django.shortcuts import render
|
|
||||||
from django.db.models import Q
|
|
||||||
|
|
||||||
from core.apps.management.models import Report
|
|
||||||
from core.apps.management.decorators import role_required
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@role_required(['manager', 'businessman'])
|
@role_required(["businessman", "manager"])
|
||||||
def report_list(request):
|
def device_statistics(request):
|
||||||
reports = (
|
"""
|
||||||
Report.objects
|
Har bir ToyMovement uchun kirim / xarajat / foyda hisoblaydi.
|
||||||
.select_related("device", "device__district", "created_by")
|
|
||||||
.order_by("-created_at")
|
|
||||||
)
|
|
||||||
|
|
||||||
if request.user.role == "manager":
|
Formula:
|
||||||
reports = reports.filter(
|
narx1 = oylik maosh / 30
|
||||||
device__district__region=request.user.region
|
narx2 = narx1 / aparat soni (maosh ulushi, qurilma/kun)
|
||||||
)
|
narx3 = kunlik umumiy xarajat / aparat soni (boshqa umumiy / kun)
|
||||||
|
narx4 = arenda / 30 (shu qurilma arenda / kun)
|
||||||
|
xarajat = narx2 + narx3 + narx4
|
||||||
|
kirim = harakat miqdori * o'yinchoq narxi
|
||||||
|
foyda = kirim - xarajat
|
||||||
|
"""
|
||||||
|
|
||||||
return render(
|
# ── FILTERS ──────────────────────────────────────────────────
|
||||||
request,
|
start_date = request.GET.get("start_date")
|
||||||
"common/lists/report_list.html",
|
end_date = request.GET.get("end_date")
|
||||||
{
|
device_id = request.GET.get("device")
|
||||||
"reports": reports,
|
|
||||||
"title": "Hisobotlar"
|
qs = ToyMovement.objects.select_related("device", "created_by").filter(
|
||||||
}
|
device__isnull=False
|
||||||
)
|
)
|
||||||
|
if start_date:
|
||||||
|
qs = qs.filter(created_at__date__gte=start_date)
|
||||||
|
if end_date:
|
||||||
|
qs = qs.filter(created_at__date__lte=end_date)
|
||||||
|
if device_id:
|
||||||
|
qs = qs.filter(device_id=device_id)
|
||||||
|
|
||||||
|
qs = qs.order_by("-created_at")
|
||||||
|
|
||||||
|
# ── SHARED CONSTANTS ─────────────────────────────────────────
|
||||||
|
total_devices = Device.objects.count() or 1
|
||||||
|
|
||||||
|
# Latest toy price
|
||||||
|
latest_income = Income.objects.order_by("-created_at").first()
|
||||||
|
price_per_toy = latest_income.price_per_toy if latest_income else Decimal("0")
|
||||||
|
|
||||||
|
# Total confirmed salary → narx1 → narx2
|
||||||
|
total_salary = Expense.objects.filter(
|
||||||
|
expense_type=Expense.ExpenseType.SALARY,
|
||||||
|
is_confirmed=True,
|
||||||
|
).aggregate(s=Sum("amount"))["s"] or Decimal("0")
|
||||||
|
|
||||||
|
narx1 = total_salary / 30
|
||||||
|
narx2 = narx1 / total_devices
|
||||||
|
|
||||||
|
# General (non-device-specific, non-salary) confirmed expenses → narx3
|
||||||
|
total_general = Expense.objects.filter(
|
||||||
|
is_confirmed=True,
|
||||||
|
device__isnull=True,
|
||||||
|
).exclude(
|
||||||
|
expense_type=Expense.ExpenseType.SALARY
|
||||||
|
).aggregate(s=Sum("amount"))["s"] or Decimal("0")
|
||||||
|
|
||||||
|
narx3 = (total_general / 30) / total_devices
|
||||||
|
|
||||||
|
# ── BUILD ROWS ────────────────────────────────────────────────
|
||||||
|
rows = []
|
||||||
|
total_kirim = Decimal("0")
|
||||||
|
total_xarajat = Decimal("0")
|
||||||
|
total_foyda = Decimal("0")
|
||||||
|
|
||||||
|
for mv in qs:
|
||||||
|
device = mv.device
|
||||||
|
|
||||||
|
# KIRIM
|
||||||
|
kirim = Decimal(mv.quantity) * price_per_toy
|
||||||
|
|
||||||
|
# narx4 — rent for this device per day
|
||||||
|
narx4 = Decimal(device.amount) / 30 if device.amount else Decimal("0")
|
||||||
|
|
||||||
|
# Direct device expenses (maintenance etc.) / 30
|
||||||
|
direct = Expense.objects.filter(
|
||||||
|
device=device, is_confirmed=True
|
||||||
|
).aggregate(s=Sum("amount"))["s"] or Decimal("0")
|
||||||
|
narx_direct = direct / 30
|
||||||
|
|
||||||
|
xarajat = narx2 + narx3 + narx4 + narx_direct
|
||||||
|
foyda = kirim - xarajat
|
||||||
|
|
||||||
|
total_kirim += kirim
|
||||||
|
total_xarajat += xarajat
|
||||||
|
total_foyda += foyda
|
||||||
|
|
||||||
|
rows.append({
|
||||||
|
"device_name": device.address,
|
||||||
|
"quantity": mv.quantity,
|
||||||
|
"kirim": kirim,
|
||||||
|
"xarajat": xarajat,
|
||||||
|
"foyda": foyda,
|
||||||
|
"date": mv.created_at.strftime("%d.%m.%Y %H:%M"),
|
||||||
|
})
|
||||||
|
|
||||||
|
return render(request, "common/lists/report_list.html", {
|
||||||
|
"rows": rows,
|
||||||
|
"devices": Device.objects.order_by("address"),
|
||||||
|
"total_kirim": total_kirim,
|
||||||
|
"total_xarajat": total_xarajat,
|
||||||
|
"total_foyda": total_foyda,
|
||||||
|
"price_per_toy": price_per_toy,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@role_required(["employee"])
|
@role_required(["employee"])
|
||||||
|
|||||||
Reference in New Issue
Block a user