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"
|
||||
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)
|
||||
|
||||
@@ -1,163 +1,236 @@
|
||||
{% 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 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="cards-container">
|
||||
{% if reports %}
|
||||
{% for report in reports %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
{{ report.device.address }}
|
||||
<!-- 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="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 class="card-number">#{{ forloop.counter }}</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<div class="card-row">
|
||||
<span class="label">Miqdor:</span>
|
||||
<span class="quantity">{{ report.quantity }} dona</span>
|
||||
</div>
|
||||
<div class="card-row">
|
||||
<span class="label">Sana:</span>
|
||||
<span class="value">{{ report.created_at|date:"d.m.Y H:i" }}</span>
|
||||
</div>
|
||||
<div class="card-row">
|
||||
<span class="label">Yaratgan:</span>
|
||||
<span class="value">{{ report.created_by.get_full_name }}</span>
|
||||
</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;
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 4px 24px rgba(0,0,0,0.07);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(0,0,0,0.1);
|
||||
.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; }
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: start;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
.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; }
|
||||
|
||||
.card-title {
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
font-size: 15px;
|
||||
flex: 1;
|
||||
}
|
||||
.empty { text-align: center; padding: 48px 20px; color: #9ca3af; font-style: italic; }
|
||||
|
||||
.card-number {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
font-weight: 600;
|
||||
}
|
||||
.tbl-footer { text-align: right; padding: 12px 2px 0; font-size: 13px; color: #6b7280; }
|
||||
.tbl-footer strong { color: #111827; }
|
||||
|
||||
.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);
|
||||
}
|
||||
.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) {
|
||||
.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;
|
||||
}
|
||||
.page-card { margin: 12px; padding: 18px; }
|
||||
.summary-row { grid-template-columns: 1fr; }
|
||||
.filter-bar { flex-direction: column; }
|
||||
.filter-input{ min-width: 100%; }
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"])
|
||||
|
||||
Reference in New Issue
Block a user