Businessman uchun statistikalar qo'shildi!

This commit is contained in:
Abdulaziz Axmadaliyev
2026-02-19 12:24:00 +05:00
parent 243d879243
commit 241d3c1f20
5 changed files with 339 additions and 168 deletions

View File

@@ -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 tolovlar'), ('maintenance', 'Texnik xizmat'), ('food', 'Oziq-ovqat'), ('transport', "Yo'lkira"), ('other', 'Boshqa')], default='other', max_length=20),
),
]

View File

@@ -8,7 +8,6 @@ class Expense(models.Model):
MAINTENANCE = "maintenance", "Texnik xizmat"
FOOD = "food", "Oziq-ovqat"
TRANSPORT = "transport", "Yo'lkira"
BUY_TOYS = "buy_toys", "Oʻyinchoqlar sotib olish"
OTHER = "other", "Boshqa"
amount = models.DecimalField(max_digits=12, decimal_places=2)

View File

@@ -1,164 +1,237 @@
{% extends "base.html" %}
{% block title %}Hisobotlar{% endblock %}
{% block title %}Qurilma Statistikasi{% endblock %}
{% 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">
{% 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 class="page-card">
<!-- HEADER -->
<div class="page-header">
<h2 class="page-title">Qurilma Statistikasi</h2>
<p class="page-sub">Qurilmalar bo'yicha kirim, xarajat va foyda hisoboti</p>
</div>
<div class="card-content">
<div class="card-row">
<span class="label">Miqdor:</span>
<span class="quantity">{{ report.quantity }} dona</span>
<!-- FILTER -->
<form method="get" class="filter-bar">
<div class="filter-group">
<label class="filter-label">Boshlanish sanasi</label>
<input type="date" name="start_date" value="{{ request.GET.start_date }}" class="filter-input">
</div>
<div class="card-row">
<span class="label">Sana:</span>
<span class="value">{{ report.created_at|date:"d.m.Y H:i" }}</span>
<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="card-row">
<span class="label">Yaratgan:</span>
<span class="value">{{ report.created_by.get_full_name }}</span>
<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 %}
<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>
<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>
<style>
.cards-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 16px;
}
.card {
.page-card {
max-width: 1000px;
margin: 32px auto;
background: #fff;
border-radius: 12px;
padding: 16px;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
transition: all 0.2s ease;
border-left: 4px solid #10b981;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0,0,0,0.1);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid #e5e7eb;
}
.card-title {
font-weight: 700;
color: #1f2937;
font-size: 15px;
flex: 1;
}
.card-number {
font-size: 12px;
color: #9ca3af;
font-weight: 600;
}
.card-content {
display: flex;
flex-direction: column;
gap: 8px;
}
.card-row {
display: flex;
justify-content: space-between;
font-size: 13px;
}
.card-row .label {
color: #6b7280;
font-weight: 500;
}
.card-row .value {
color: #374151;
font-weight: 500;
}
.card-row .quantity {
color: #10b981;
font-weight: 700;
}
.btn {
display: inline-block;
padding: 8px 14px;
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 {
background: #4f46e5;
}
.btn.edit:hover {
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));
border-radius: 16px;
padding: 32px;
box-shadow: 0 4px 24px rgba(0,0,0,0.07);
}
.card {
padding: 14px;
}
}
.page-header { margin-bottom: 24px; }
.page-title { font-size: 20px; font-weight: 700; color: #111827; margin: 0 0 4px; }
.page-sub { font-size: 13px; color: #6b7280; margin: 0; }
@media (max-width: 480px) {
.cards-container {
grid-template-columns: 1fr;
/* FILTER */
.filter-bar {
display: flex; align-items: flex-end; flex-wrap: wrap; gap: 12px;
background: #f9fafb; border: 1px solid #e5e7eb;
border-radius: 10px; padding: 16px 18px; margin-bottom: 24px;
}
.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; }
/* SUMMARY */
.summary-row {
display: grid; grid-template-columns: repeat(3,1fr);
gap: 14px; margin-bottom: 24px;
}
.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; }
/* TABLE */
.table-wrap {
border: 1px solid #e5e7eb; border-radius: 10px;
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; }
.col-num { color: #9ca3af; font-weight: 600; width: 40px; }
.col-device { font-weight: 600; color: #111827; }
.col-income { color: #059669; font-weight: 600; white-space: nowrap; }
.col-expense { color: #dc2626; font-weight: 600; white-space: nowrap; }
.col-date { color: #6b7280; white-space: nowrap; }
.qty-badge {
display: inline-flex; align-items: center; justify-content: center;
width: 30px; height: 30px; border-radius: 50%;
background: #d1fae5; color: #065f46;
font-weight: 700; font-size: 12px;
}
div[style*="display: flex"] {
flex-direction: column;
gap: 10px;
.foyda-pill {
display: inline-block; padding: 3px 10px; border-radius: 50px;
font-size: 12px; font-weight: 700; white-space: nowrap;
}
.foyda-pos { background: #ecfdf5; color: #059669; }
.foyda-neg { background: #fef2f2; color: #dc2626; }
.btn {
width: 100%;
text-align: center;
.empty { text-align: center; padding: 48px 20px; color: #9ca3af; font-style: italic; }
.tbl-footer { text-align: right; padding: 12px 2px 0; font-size: 13px; color: #6b7280; }
.tbl-footer strong { color: #111827; }
.back-link { margin-top: 20px; }
.back-link a { color: #4f46e5; font-size: 13px; font-weight: 600; text-decoration: none; }
.back-link a:hover { text-decoration: underline; }
@media (max-width: 768px) {
.page-card { margin: 12px; padding: 18px; }
.summary-row { grid-template-columns: 1fr; }
.filter-bar { flex-direction: column; }
.filter-input{ min-width: 100%; }
}
}
</style>
{% endblock %}

View File

@@ -25,7 +25,7 @@ urlpatterns = [
path("list/warehouse/", views.warehouse_list, name="warehouse_list"),
path("list/user/", views.user_list, name="user_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"),
# Edit
path("edit/device/<int:pk>/", views.edit_device, name="edit_device"),

View File

@@ -1,8 +1,11 @@
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from core.apps.management.models import Device, Income, Expense, Warehouse, ToyMovement, Report
from core.apps.management.models import Device, Income, Expense, Warehouse, ToyMovement
from core.apps.accounts.models import User
from django.contrib.auth.decorators import login_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
@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
@role_required(['manager', 'businessman'])
def report_list(request):
reports = (
Report.objects
.select_related("device", "device__district", "created_by")
.order_by("-created_at")
)
@role_required(["businessman", "manager"])
def device_statistics(request):
"""
Har bir ToyMovement uchun kirim / xarajat / foyda hisoblaydi.
if request.user.role == "manager":
reports = reports.filter(
device__district__region=request.user.region
)
Formula:
narx1 = oylik maosh / 30
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(
request,
"common/lists/report_list.html",
{
"reports": reports,
"title": "Hisobotlar"
}
# ── FILTERS ──────────────────────────────────────────────────
start_date = request.GET.get("start_date")
end_date = request.GET.get("end_date")
device_id = request.GET.get("device")
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
@role_required(["employee"])