first commit

This commit is contained in:
NORBOYEVSAMARIDDIN
2026-02-07 11:18:38 +05:00
commit 493cb58222
228 changed files with 10859 additions and 0 deletions

0
core/__init__.py Normal file
View File

0
core/apps/__init__.py Normal file
View File

View File

View File

@@ -0,0 +1,42 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import AdminPasswordChangeForm, UserChangeForm
from django.utils.translation import gettext_lazy as _
from .models import User
class CustomUserAdmin(UserAdmin):
model = User
change_password_form = AdminPasswordChangeForm
form = UserChangeForm
list_display = ("id", "first_name", "last_name", "phone", "role", "region", "is_active", "is_staff")
list_filter = ("role", "region", "is_staff", "is_superuser", "is_active")
search_fields = ("phone", "first_name", "last_name", "email")
ordering = ("phone",)
autocomplete_fields = ["groups"]
fieldsets = (
(None, {"fields": ("phone", "password")}),
(_("Personal info"), {"fields": ("first_name", "last_name", "email", "region")}),
(_("Permissions"), {
"fields": (
"is_active",
"is_staff",
"is_superuser",
"groups",
"user_permissions",
"role",
),
}),
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
)
add_fieldsets = (
(None, {
"classes": ("wide",),
"fields": ("phone", "password1", "password2", "role", "region", "is_active", "is_staff"),
}),
)
# Register the custom user
admin.site.register(User, CustomUserAdmin)

View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'core.apps.accounts'

View File

@@ -0,0 +1,9 @@
from django.db import models
class RoleChoice(models.TextChoices):
"""
User Role Choice
"""
SUPERUSER = "superuser", "Superuser"
BUSINESSMAN = "businessman", "Businessman"
MANAGER = "manager", "Manager"
EMPLOYEE = "employee", "Employee"

View File

@@ -0,0 +1,37 @@
from django import forms
from django.contrib.auth import authenticate
from django.contrib.auth.forms import AuthenticationForm
class PhoneLoginForm(forms.Form):
phone = forms.CharField(
label="Phone",
max_length=255,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter phone number',
'autofocus': True
})
)
password = forms.CharField(
label="Password",
widget=forms.PasswordInput(attrs={
'class': 'form-control',
'placeholder': 'Enter password'
})
)
def clean(self):
cleaned_data = super().clean()
phone = cleaned_data.get("phone")
password = cleaned_data.get("password")
if phone and password:
from django.contrib.auth import get_user_model
User = get_user_model()
user = authenticate(username=phone, password=password)
if user is None:
raise forms.ValidationError("Invalid phone number or password")
self.user = user
return cleaned_data
def get_user(self):
return getattr(self, 'user', None)

View File

@@ -0,0 +1,22 @@
from django.contrib.auth import base_user
class UserManager(base_user.BaseUserManager):
def create_user(self, phone, password=None, **extra_fields):
if not phone:
raise ValueError("The phone number must be set")
user = self.model(phone=phone, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, phone, password=None, **extra_fields):
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
if extra_fields.get("is_staff") is not True:
raise ValueError("Superuser must have is_staff=True.")
if extra_fields.get("is_superuser") is not True:
raise ValueError("Superuser must have is_superuser=True.")
return self.create_user(phone, password, **extra_fields)

View File

@@ -0,0 +1,43 @@
# Generated by Django 6.0.2 on 2026-02-04 12:15
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('phone', models.CharField(max_length=255, unique=True)),
('username', models.CharField(blank=True, max_length=255, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('validated_at', models.DateTimeField(blank=True, null=True)),
('role', models.CharField(choices=[('superuser', 'Superuser'), ('businessman', 'Businessman'), ('manager', 'Manager'), ('employee', 'Employee')], default='employee', max_length=255)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 6.0.2 on 2026-02-04 12:15
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('accounts', '0001_initial'),
('auth', '0012_alter_user_first_name_max_length'),
('management', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='user',
name='region',
field=models.ForeignKey(blank=True, help_text='Only for managers', null=True, on_delete=django.db.models.deletion.SET_NULL, to='management.region'),
),
migrations.AddField(
model_name='user',
name='user_permissions',
field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions'),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 6.0.2 on 2026-02-06 09:54
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0002_initial'),
('management', '0013_rename_name_device_address_remove_device_monthly_fee_and_more'),
]
operations = [
migrations.AddField(
model_name='user',
name='warehouse',
field=models.ForeignKey(blank=True, help_text='Only for employees', null=True, on_delete=django.db.models.deletion.PROTECT, to='management.warehouse'),
),
]

View File

@@ -0,0 +1,43 @@
from django.contrib.auth import models as auth_models
from django.db import models
from .choices import RoleChoice
from .managers import UserManager
from ..management.models import Region, Warehouse
class User(auth_models.AbstractUser):
phone = models.CharField(max_length=255, unique=True)
username = models.CharField(max_length=255, null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
validated_at = models.DateTimeField(null=True, blank=True)
role = models.CharField(
max_length=255,
choices=RoleChoice,
default=RoleChoice.EMPLOYEE,
)
region = models.ForeignKey(
Region,
on_delete=models.SET_NULL,
null=True,
blank=True,
help_text="Only for managers"
)
warehouse = models.ForeignKey(
Warehouse,
on_delete=models.PROTECT,
null=True,
blank=True,
help_text="Only for employees"
)
def get_full_name(self):
return f"{self.first_name} {self.last_name}".strip() or self.phone
USERNAME_FIELD = "phone"
objects = UserManager()
def __str__(self):
return self.phone

View File

@@ -0,0 +1,147 @@
<!-- templates/auth/login.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<style>
:root {
--primary: #4f46e5;
--bg: #f4f6fb;
--text: #111827;
--muted: #6b7280;
--danger: #dc2626;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background: var(--bg);
font-family: system-ui, -apple-system, sans-serif;
display: flex;
align-items: center;
justify-content: center;
padding: env(safe-area-inset-top) 14px env(safe-area-inset-bottom);
}
.card {
width: 100%;
max-width: 420px;
background: #fff;
border-radius: 16px;
padding: 24px 20px 28px;
box-shadow: 0 12px 40px rgba(0,0,0,.08);
}
h1 {
margin: 0 0 24px;
font-size: 24px;
text-align: center;
color: var(--text);
}
.form-group {
margin-bottom: 18px;
}
label {
display: block;
font-size: 13px;
margin-bottom: 6px;
color: var(--muted);
}
.form-control {
width: 100%;
padding: 14px 14px;
font-size: 16px; /* prevents iOS zoom */
border-radius: 12px;
border: 1px solid #d1d5db;
outline: none;
transition: border .15s, box-shadow .15s;
}
.form-control:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(79,70,229,.15);
}
.btn {
width: 100%;
padding: 15px;
border-radius: 14px;
border: none;
background: var(--primary);
color: #fff;
font-size: 16px;
font-weight: 600;
cursor: pointer;
margin-top: 8px;
}
.btn:active {
transform: scale(.99);
}
.error {
background: #fee2e2;
color: var(--danger);
padding: 12px 14px;
border-radius: 12px;
font-size: 14px;
margin-bottom: 16px;
}
@media (min-width: 768px) {
body {
padding: 0;
}
.card {
padding: 32px;
}
h1 {
font-size: 26px;
}
}
</style>
</head>
<body>
<div class="card">
<h1>Sign in</h1>
{% if messages %}
{% for message in messages %}
<div class="error">{{ message }}</div>
{% endfor %}
{% endif %}
{% if form.non_field_errors %}
<div class="error">
{{ form.non_field_errors.0 }}
</div>
{% endif %}
<form method="post" novalidate>
{% csrf_token %}
<div class="form-group">
{{ form.phone.label_tag }}
{{ form.phone }}
</div>
<div class="form-group">
{{ form.password.label_tag }}
{{ form.password }}
</div>
<button type="submit" class="btn">Login</button>
</form>
</div>
</body>
</html>

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1,8 @@
from django.urls import path
from .views import logout_view, login_view, dashboard
urlpatterns = [
path("", login_view, name="login"),
path("logout/", logout_view, name="logout"),
path('dashboard/', dashboard, name='dashboard'),
]

View File

@@ -0,0 +1,43 @@
from django.shortcuts import render, redirect
from django.contrib.auth import login, logout
from .forms import PhoneLoginForm
from django.contrib import messages
from django.contrib.auth.decorators import login_required
def login_view(request):
if request.user.is_authenticated:
return redirect('dashboard')
if request.method == "POST":
form = PhoneLoginForm(request.POST)
if form.is_valid():
user = form.get_user()
login(request, user)
return redirect('dashboard')
else:
messages.error(request, "Invalid phone number or password")
else:
form = PhoneLoginForm()
return render(request, "auth/login.html", {"form": form})
def logout_view(request):
logout(request)
return redirect('login')
@login_required
def dashboard(request):
if request.user.role == "businessman":
return redirect("businessman_dashboard")
elif request.user.role == "manager":
return redirect("manager_dashboard")
elif request.user.role == "employee":
return redirect("employee_dashboard")
else:
return redirect("login")
def csrf_failure(request, reason=""):
logout(request)
return redirect("login")

2
core/apps/logs/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

View File

View File

@@ -0,0 +1,85 @@
from django.contrib import admin
from .models import (
Region, District, Warehouse, Device,
ToyMovement, Income, Expense
)
from core.apps.accounts.models import User # for ForeignKey references
# -------------------------
# Region Admin
# -------------------------
@admin.register(Region)
class RegionAdmin(admin.ModelAdmin):
list_display = ("name",)
search_fields = ("name",)
# -------------------------
# District Admin
# -------------------------
@admin.register(District)
class DistrictAdmin(admin.ModelAdmin):
list_display = ("name", "region")
list_filter = ("region",)
search_fields = ("name", "region__name")
# -------------------------
# Warehouse Admin
# -------------------------
@admin.register(Warehouse)
class WarehouseAdmin(admin.ModelAdmin):
list_display = ("name", "region", "toys_count", "created_at")
list_filter = ("region",)
search_fields = ("name", "region__name")
readonly_fields = ("created_at",)
# -------------------------
# Device Admin
# -------------------------
@admin.register(Device)
class DeviceAdmin(admin.ModelAdmin):
list_display = ("address", "district", "created_at")
list_filter = ("district",)
search_fields = ("name", "district__name",)
readonly_fields = ("created_at",)
# -------------------------
# ToyMovement Admin
# -------------------------
@admin.register(ToyMovement)
class ToyMovementAdmin(admin.ModelAdmin):
list_display = (
"movement_type", "from_warehouse", "to_warehouse",
"device", "quantity", "created_by", "created_at"
)
list_filter = ("movement_type", "from_warehouse", "to_warehouse")
search_fields = ("device__name", "created_by__phone")
autocomplete_fields = ["device", "created_by", "from_warehouse", "to_warehouse"]
readonly_fields = ("created_at",)
# -------------------------
# Income Admin
# -------------------------
@admin.register(Income)
class IncomeAdmin(admin.ModelAdmin):
list_display = ("device", "amount", "created_by", "created_at")
list_filter = ("device",)
search_fields = ("device__name", "created_by__phone")
autocomplete_fields = ["device", "created_by",]
readonly_fields = ("created_at",)
# -------------------------
# Expense Admin
# -------------------------
@admin.register(Expense)
class ExpenseAdmin(admin.ModelAdmin):
list_display = ("amount", "expense_type", "created_by", "confirmed_by", "is_confirmed", "created_at")
list_filter = ("expense_type", "is_confirmed")
search_fields = ("expense_type__name", "created_by__phone", "confirmed_by__phone")
autocomplete_fields = ["created_by", "confirmed_by"]
readonly_fields = ("created_at",)

View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class ModuleConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'core.apps.management'

View File

@@ -0,0 +1,5 @@
TOY_MOVEMENT_TYPE = [
("from_warehouse", "Ombordan → Aparatga"),
("between_warehouses", "Ombordan → Omborga"),
]

View File

@@ -0,0 +1 @@
from .ToyMovementType import *

View File

@@ -0,0 +1,16 @@
from django.core.exceptions import PermissionDenied
def role_required(allowed_roles):
"""
Usage:
@role_required(["manager", "businessman"])
def view(request):
...
"""
def decorator(view_func):
def _wrapped_view(request, *args, **kwargs):
if request.user.role not in allowed_roles:
raise PermissionDenied
return view_func(request, *args, **kwargs)
return _wrapped_view
return decorator

View File

@@ -0,0 +1,19 @@
from django import forms
from ..models import Device, District
class DeviceForm(forms.ModelForm):
class Meta:
model = Device
fields = ["address", "district"]
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None) # get the user from kwargs
super().__init__(*args, **kwargs)
if user is not None:
if user.role == "manager":
# Manager: only districts in the same region
self.fields['district'].queryset = District.objects.filter(region=user.region)
else:
# Businessman: show all districts
self.fields['district'].queryset = District.objects.all()

View File

@@ -0,0 +1,62 @@
from django import forms
from ..models import Expense, Device
from core.apps.accounts.models import User
# Base form
class BaseExpenseForm(forms.ModelForm):
class Meta:
model = Expense
fields = ["amount", "expense_type", "employee", "device"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Show all devices
self.fields["device"].queryset = Device.objects.all()
# Show all employees
self.fields["employee"].queryset = User.objects.filter(role="employee")
def clean(self):
cleaned_data = super().clean()
expense_type = cleaned_data.get("expense_type")
employee = cleaned_data.get("employee")
device = cleaned_data.get("device")
# Salary requires employee
if expense_type == Expense.ExpenseType.SALARY and not employee:
self.add_error("employee", "Employee must be set for Salary expenses.")
# Device required for rent/maintenance
if expense_type in [Expense.ExpenseType.RENT, Expense.ExpenseType.MAINTENANCE] and not device:
self.add_error("device", "Device must be set for this type of expense.")
return cleaned_data
# Employee form: cannot create Salary or Buy Toys
class ExpenseFormEmployee(BaseExpenseForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Remove forbidden types for employee
forbidden = [Expense.ExpenseType.SALARY, Expense.ExpenseType.BUY_TOYS]
self.fields["expense_type"].choices = [
(value, label)
for value, label in Expense.ExpenseType.choices
if value not in forbidden
]
# Manager form: cannot create Buy Toys
class ExpenseFormManager(BaseExpenseForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
forbidden = [Expense.ExpenseType.BUY_TOYS]
self.fields["expense_type"].choices = [
(value, label)
for value, label in Expense.ExpenseType.choices
if value not in forbidden
]
# Businessman form: full access
class ExpenseFormBusinessman(BaseExpenseForm):
pass

View File

@@ -0,0 +1,32 @@
from django import forms
from ..models import Income, Device
class IncomeForm(forms.ModelForm):
class Meta:
model = Income
fields = ["device", "amount"]
def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user", None)
super().__init__(*args, **kwargs)
if self.user is not None:
# Filter devices
if self.user.role == "businessman":
self.fields["device"].queryset = Device.objects.all()
else: # manager or employee
self.fields["device"].queryset = Device.objects.filter(district__region=self.user.region)
# Remove amount for employees
if self.user.role == "employee":
self.fields.pop("amount", None)
def save(self, commit=True):
instance = super().save(commit=False)
if self.user:
instance.created_by = self.user
if getattr(self.user, "role", None) == "employee":
instance.amount = None
if commit:
instance.save()
return instance

View File

@@ -0,0 +1,11 @@
from django import forms
from ..models import Rent
class RentForm(forms.ModelForm):
class Meta:
model = Rent
fields = ['address', 'district', 'device', 'due_date', 'amount']
widgets = {
'due_date': forms.DateInput(attrs={'type': 'date'}),
'amount': forms.NumberInput(attrs={'min': 0}),
}

View File

@@ -0,0 +1,21 @@
from django import forms
from core.apps.management.models import ToyMovement
class ToyMovementFormEmployee(forms.ModelForm):
class Meta:
model = ToyMovement
fields = ["device", "quantity"] # remove from_warehouse
def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user", None) # pass user from view
super().__init__(*args, **kwargs)
def save(self, commit=True):
instance = super().save(commit=False)
instance.movement_type = "from_warehouse"
instance.to_warehouse = None
if self.user and hasattr(self.user, "warehouse"):
instance.from_warehouse = self.user.warehouse
if commit:
instance.save()
return instance

View File

@@ -0,0 +1,30 @@
from django import forms
from core.apps.management.models import ToyMovement, Warehouse, Device
from core.apps.management.choice import TOY_MOVEMENT_TYPE
class ToyMovementForm(forms.ModelForm):
class Meta:
model = ToyMovement
fields = ["movement_type", "from_warehouse", "to_warehouse", "device", "quantity"]
widgets = {
"movement_type": forms.Select(choices=TOY_MOVEMENT_TYPE),
}
def __init__(self, *args, **kwargs):
user = kwargs.pop("user", None)
super().__init__(*args, **kwargs)
self.fields["from_warehouse"].queryset = Warehouse.objects.all()
self.fields["to_warehouse"].queryset = Warehouse.objects.all()
self.fields["device"].queryset = Device.objects.all()
def save(self, commit=True):
instance = super().save(commit=False)
if instance.movement_type == "from_warehouse":
instance.to_warehouse = None
elif instance.movement_type == "between_warehouses":
instance.device = None
if commit:
instance.save()
return instance

View File

@@ -0,0 +1,78 @@
# from django import forms
# from ...accounts.models import User, RoleChoice
# from ...management.models import Region
#
# class UserCreateForm(forms.ModelForm):
# role = forms.ChoiceField(
# choices=[
# (RoleChoice.MANAGER, "Manager"),
# (RoleChoice.EMPLOYEE, "Employee")
# ],
# widget=forms.Select(attrs={"class": "form-control"})
# )
#
# password = forms.CharField(
# widget=forms.PasswordInput(attrs={"class": "form-control"}),
# required=False,
# label="Parol"
# )
#
# region = forms.ModelChoiceField(
# queryset=Region.objects.none(),
# required=False,
# widget=forms.Select(attrs={"class": "form-control"})
# )
#
# class Meta:
# model = User
# fields = ["phone", "password", "role", "first_name", "last_name", "region"]
# widgets = {
# "phone": forms.TextInput(attrs={"class": "form-control"}),
# }
#
# def __init__(self, *args, **kwargs):
# self.creator = kwargs.pop("creator", None)
# super().__init__(*args, **kwargs)
#
# # Password required only on create
# if not self.instance or not self.instance.pk:
# self.fields["password"].required = True
# else:
# self.fields["password"].help_text = "Leave blank to keep current password"
#
# # Manager logic: remove role and region from form
# if self.creator and self.creator.role == "manager":
# if "role" in self.fields:
# self.fields.pop("role")
# if "region" in self.fields:
# self.fields.pop("region")
#
#
#
# # Businessman logic: region queryset
# elif self.creator and self.creator.role == "businessman":
# self.fields["region"].queryset = Region.objects.all()
#
# def save(self, commit=True):
# user = super().save(commit=False)
#
# # Manager-created users must have region set
# if self.creator and self.creator.role == "manager":
# user.region = self.creator.region
#
# # Only force EMPLOYEE if the target user is not a manager
# if user.role != RoleChoice.MANAGER:
# user.role = RoleChoice.EMPLOYEE
#
# # Password
# password = self.cleaned_data.get("password")
# if password:
# user.set_password(password)
# else:
# if user.pk:
# old_user = User.objects.get(pk=user.pk)
# user.password = old_user.password
#
# if commit:
# user.save()
# return user

View File

@@ -0,0 +1,7 @@
from django import forms
from ..models import Warehouse
class WarehouseForm(forms.ModelForm):
class Meta:
model = Warehouse
fields = ["name", "region", "toys_count"]

View File

@@ -0,0 +1,8 @@
from .IncomeForm import *
from .ExpenseForm import *
from .DeviceForm import *
from .WarehouseForm import *
from .UserCreateForm import *
from .ToyMovementEmployeeForm import ToyMovementFormEmployee
from .ToyMovementForm import ToyMovementForm
from .user import *

View File

@@ -0,0 +1,50 @@
# forms/base.py
from django import forms
from core.apps.accounts.models import User
from core.apps.accounts.choices import RoleChoice
class BaseUserForm(forms.ModelForm):
password = forms.CharField(
required=False,
widget=forms.PasswordInput(attrs={"class": "form-control"})
)
role = forms.ChoiceField(
choices=[
(RoleChoice.MANAGER, "Manager"),
(RoleChoice.EMPLOYEE, "Employee"),
],
widget=forms.Select(attrs={"class": "form-control"})
)
class Meta:
model = User
fields = [
"phone",
"password",
"role",
"first_name",
"last_name",
"region",
"warehouse",
]
def clean_role(self):
role = self.cleaned_data["role"]
if role in (RoleChoice.BUSINESSMAN, RoleChoice.SUPERUSER):
raise forms.ValidationError("This role cannot be assigned.")
return role
def save(self, commit=True):
user = super().save(commit=False)
password = self.cleaned_data.get("password")
if password:
user.set_password(password)
else:
if user.pk:
old_user = User.objects.get(pk=user.pk)
user.password = old_user.password
if commit:
user.save()
return user

View File

@@ -0,0 +1,40 @@
# forms/businessman_create.py
from .BaseUserForm import BaseUserForm
from core.apps.accounts.models import RoleChoice
from core.apps.management.models import Region, Warehouse
class UserCreateFormBusinessman(BaseUserForm):
class Meta(BaseUserForm.Meta):
fields = [
"phone",
"password",
"role",
"first_name",
"last_name",
"region",
"warehouse",
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["password"].required = True
self.fields["region"].queryset = Region.objects.all()
self.fields["warehouse"].queryset = Warehouse.objects.all()
def clean(self):
cleaned = super().clean()
role = cleaned.get("role")
warehouse = cleaned.get("warehouse")
if role == RoleChoice.MANAGER:
cleaned["warehouse"] = None
if role == RoleChoice.EMPLOYEE:
if not warehouse:
self.add_error("warehouse", "Warehouse is required for employee")
else:
cleaned["region"] = warehouse.region
return cleaned

View File

@@ -0,0 +1,48 @@
# forms/manager_create.py
from .BaseUserForm import BaseUserForm
from core.apps.accounts.models import RoleChoice
from core.apps.management.models import Warehouse
class UserCreateFormManagerToEmployee(BaseUserForm):
class Meta(BaseUserForm.Meta):
fields = [
"phone",
"password",
"first_name",
"last_name",
"warehouse",
]
def __init__(self, *args, **kwargs):
self.manager = kwargs.pop("manager")
super().__init__(*args, **kwargs)
# Password is required for creation
self.fields["password"].required = True
# Filter warehouses to manager's region
self.fields["warehouse"].queryset = Warehouse.objects.filter(
region=self.manager.region
)
# Hide role field, manager can only create employees
self.fields.pop("role", None)
def save(self, commit=True):
user = super().save(commit=False)
# Always set role to EMPLOYEE
user.role = RoleChoice.EMPLOYEE
# Set region from selected warehouse
if user.warehouse:
user.region = user.warehouse.region
# Only set password if provided
password = self.cleaned_data.get("password")
if password:
user.set_password(password)
if commit:
user.save()
return user

View File

@@ -0,0 +1,31 @@
# forms/businessman_edit.py
from .BaseUserForm import BaseUserForm
from core.apps.accounts.models import RoleChoice
from core.apps.management.models import Warehouse
from django import forms
class UserEditFormBusinessman(BaseUserForm):
class Meta(BaseUserForm.Meta):
fields = BaseUserForm.Meta.fields
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["warehouse"].queryset = Warehouse.objects.all()
if self.instance.role == RoleChoice.MANAGER:
self.fields["warehouse"].widget = forms.HiddenInput()
def clean(self):
cleaned = super().clean()
role = cleaned.get("role")
warehouse = cleaned.get("warehouse")
if role == RoleChoice.MANAGER:
cleaned["warehouse"] = None
if role == RoleChoice.EMPLOYEE and warehouse:
cleaned["region"] = warehouse.region
return cleaned

View File

@@ -0,0 +1,38 @@
# forms/manager_edit.py
from .BaseUserForm import BaseUserForm
from core.apps.management.models import Warehouse
from core.apps.accounts.choices import RoleChoice
class UserEditFormManagerToEmployee(BaseUserForm):
class Meta(BaseUserForm.Meta):
fields = [
"phone",
"password",
"first_name",
"last_name",
"warehouse",
]
def __init__(self, *args, **kwargs):
self.manager = kwargs.pop("manager")
super().__init__(*args, **kwargs)
self.fields.pop("role", None)
# 👇 decision based on edited user's role
if self.instance.role == RoleChoice.MANAGER:
# editing manager → no warehouse
self.fields.pop("warehouse", None)
else:
# editing employee → show warehouse
self.fields["warehouse"].queryset = Warehouse.objects.filter(
region=self.manager.region
)
def save(self, commit=True):
user = super().save(commit=False)
user.region = user.warehouse.region
if commit:
user.save()
return user

View File

@@ -0,0 +1,5 @@
from .BaseUserForm import *
from .UserEditFormBusinessman import *
from .UserCreateFormBusinessman import *
from .UserCreateFormManagerToEmployee import *
from .UserEditFormManagerToEmployee import *

View File

@@ -0,0 +1,79 @@
# Generated by Django 6.0.2 on 2026-02-04 12:15
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='District',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
],
),
migrations.CreateModel(
name='Region',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True)),
],
),
migrations.CreateModel(
name='Device',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('toys_count', models.PositiveIntegerField(default=0)),
('monthly_fee', models.DecimalField(decimal_places=2, max_digits=10)),
('created_at', models.DateTimeField(auto_now_add=True)),
('district', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='management.district')),
],
),
migrations.AddField(
model_name='district',
name='region',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='districts', to='management.region'),
),
migrations.CreateModel(
name='Warehouse',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True)),
('toys_count', models.PositiveIntegerField(default=0)),
('created_at', models.DateTimeField(auto_now_add=True)),
('region', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='warehouses', to='management.region')),
],
),
migrations.CreateModel(
name='ToyMovement',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('movement_type', models.CharField(choices=[('from_warehouse', 'Warehouse → Device'), ('to_device', 'Device refill'), ('between_warehouses', 'Warehouse → Warehouse')], max_length=30)),
('quantity', models.PositiveIntegerField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
('device', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='management.device')),
('from_warehouse', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='outgoing', to='management.warehouse')),
('to_warehouse', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='incoming', to='management.warehouse')),
],
),
migrations.AddField(
model_name='device',
name='warehouse',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='management.warehouse'),
),
migrations.AlterUniqueTogether(
name='district',
unique_together={('region', 'name')},
),
]

View File

@@ -0,0 +1,46 @@
# Generated by Django 6.0.2 on 2026-02-04 12:41
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('management', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ExpenseType',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True)),
],
),
migrations.CreateModel(
name='Expense',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('amount', models.DecimalField(decimal_places=2, max_digits=12)),
('is_confirmed', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('confirmed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='confirmed_expenses', to=settings.AUTH_USER_MODEL)),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
('expense_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='expenses', to='management.expensetype')),
],
),
migrations.CreateModel(
name='Income',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('amount', models.DecimalField(decimal_places=2, max_digits=12)),
('created_at', models.DateTimeField(auto_now_add=True)),
('confirmed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='confirmed_income', to=settings.AUTH_USER_MODEL)),
('device', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='management.device')),
('reported_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='reported_income', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@@ -0,0 +1,125 @@
from django.db import migrations
def load_data(apps, schema_editor):
Region = apps.get_model("management", "Region")
District = apps.get_model("management", "District")
data = {
"Qoraqalpogʻiston Respublikasi": [
"Amudaryo tumani", "Beruniy tumani", "Chimboy tumani",
"Ellikqalʼa tumani", "Kegeyli tumani", "Moʻynoq tumani",
"Nukus tumani", "Qanlikoʻl tumani", "Qoʻngʻirot tumani",
"Qoraoʻzak tumani", "Shumanay tumani", "Taxiatosh tumani",
"Taxtakoʻpir tumani", "Toʻrtkoʻl tumani", "Xoʻjayli tumani"
],
"Andijon viloyati": [
"Andijon tumani", "Asaka tumani", "Baliqchi tumani",
"Boʻz tumani", "Buloqboshi tumani", "Izboskan tumani",
"Jalaquduq tumani", "Marhamat tumani", "Oltinkoʻl tumani",
"Paxtaobod tumani", "Qoʻrgʻontepa tumani",
"Shahrixon tumani", "Ulugʻnor tumani", "Xoʻjaobod tumani"
],
"Buxoro viloyati": [
"Buxoro tumani", "Gʻijduvon tumani", "Jondor tumani",
"Kogon tumani", "Olot tumani", "Peshku tumani",
"Qorakoʻl tumani", "Qorovulbozor tumani",
"Romitan tumani", "Shofirkon tumani", "Vobkent tumani"
],
"Fargʻona viloyati": [
"Bagʻdod tumani", "Beshariq tumani", "Dangʻara tumani",
"Fargʻona tumani", "Furqat tumani", "Oltiariq tumani",
"Qoʻshtepa tumani", "Quva tumani", "Rishton tumani",
"Soʻx tumani", "Toshloq tumani", "Uchkoʻprik tumani",
"Yozyovon tumani"
],
"Jizzax viloyati": [
"Arnasoy tumani", "Baxmal tumani", "Doʻstlik tumani",
"Forish tumani", "Gʻallaorol tumani", "Mirzachoʻl tumani",
"Paxtakor tumani", "Sharof Rashidov tumani",
"Yangiobod tumani", "Zomin tumani", "Zafarobod tumani"
],
"Xorazm viloyati": [
"Bogʻot tumani", "Gurlan tumani", "Hazorasp tumani",
"Qoʻshkoʻpir tumani", "Shovot tumani",
"Urganch tumani", "Xazorasp tumani", "Xiva tumani",
"Yangiariq tumani", "Yangibozor tumani"
],
"Namangan viloyati": [
"Chortoq tumani", "Chust tumani", "Kosonsoy tumani",
"Mingbuloq tumani", "Namangan tumani",
"Norin tumani", "Pop tumani", "Toʻraqoʻrgʻon tumani",
"Uchqoʻrgʻon tumani", "Uychi tumani", "Yangiqoʻrgʻon tumani"
],
"Navoiy viloyati": [
"Karmana tumani", "Konimex tumani", "Navbahor tumani",
"Nurota tumani", "Qiziltepa tumani",
"Tomdi tumani", "Uchquduq tumani", "Xatirchi tumani"
],
"Qashqadaryo viloyati": [
"Chiroqchi tumani", "Dehqonobod tumani",
"Gʻuzor tumani", "Kasbi tumani", "Kitob tumani",
"Koson tumani", "Mirishkor tumani", "Muborak tumani",
"Nishon tumani", "Qamashi tumani",
"Qarshi tumani", "Shahrisabz tumani",
"Yakkabogʻ tumani"
],
"Samarqand viloyati": [
"Bulungʻur tumani", "Ishtixon tumani",
"Jomboy tumani", "Kattaqoʻrgʻon tumani",
"Narpay tumani", "Nurobod tumani",
"Oqdaryo tumani", "Paxtachi tumani",
"Pastdargʻom tumani", "Payariq tumani",
"Qoʻshrabot tumani", "Samarqand tumani",
"Toyloq tumani", "Urgut tumani"
],
"Surxondaryo viloyati": [
"Angor tumani", "Bandixon tumani", "Boysun tumani",
"Denov tumani", "Jarqoʻrgʻon tumani",
"Muzrabot tumani", "Oltinsoy tumani",
"Qiziriq tumani", "Qumqoʻrgʻon tumani",
"Sariosiyo tumani", "Sherobod tumani",
"Shoʻrchi tumani", "Termiz tumani", "Uzun tumani"
],
"Sirdaryo viloyati": [
"Boyovut tumani", "Guliston tumani",
"Mirzaobod tumani", "Oqoltin tumani",
"Sayxunobod tumani", "Sardoba tumani",
"Xovos tumani"
],
"Toshkent viloyati": [
"Angren tumani", "Bekobod tumani",
"Boʻka tumani", "Boʻstonliq tumani",
"Chinoz tumani", "Ohangaron tumani",
"Oqqoʻrgʻon tumani", "Parkent tumani",
"Piskent tumani", "Quyi Chirchiq tumani",
"Yangiyoʻl tumani", "Yuqori Chirchiq tumani",
"Zangiota tumani"
],
"Toshkent shahri": [
"Bektemir tumani", "Chilonzor tumani",
"Hamza tumani", "Mirobod tumani",
"Mirzo Ulugʻbek tumani", "Olmazor tumani",
"Sergeli tumani", "Shayxontohur tumani",
"Uchtepa tumani", "Yakkasaroy tumani",
"Yashnobod tumani", "Yunusobod tumani"
],
}
for region_name, districts in data.items():
region, _ = Region.objects.get_or_create(name=region_name)
District.objects.bulk_create(
[District(name=d, region=region) for d in districts],
ignore_conflicts=True
)
class Migration(migrations.Migration):
dependencies = [
("management", "0002_expensetype_expense_income"),
]
operations = [
migrations.RunPython(load_data),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0.2 on 2026-02-05 05:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('management', '0003_load_uzbekistan_regions_districts'),
]
operations = [
migrations.AlterField(
model_name='device',
name='name',
field=models.CharField(max_length=100, unique=True),
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 6.0.2 on 2026-02-05 06:05
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('management', '0004_alter_device_name'),
]
operations = [
migrations.AlterField(
model_name='toymovement',
name='from_warehouse',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='outgoing', to='management.warehouse'),
),
migrations.AlterField(
model_name='toymovement',
name='movement_type',
field=models.CharField(choices=[('from_warehouse', 'Warehouse → Device'), ('between_warehouses', 'Warehouse → Warehouse')], max_length=30),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 6.0.2 on 2026-02-05 06:16
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('management', '0005_alter_toymovement_from_warehouse_and_more'),
]
operations = [
migrations.RemoveField(
model_name='device',
name='warehouse',
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 6.0.2 on 2026-02-05 07:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('management', '0006_remove_device_warehouse'),
]
operations = [
migrations.AlterField(
model_name='expense',
name='expense_type',
field=models.CharField(choices=[('rent', 'Rent'), ('salary', 'Salary'), ('utilities', 'Utilities'), ('maintenance', 'Maintenance'), ('other', 'Other')], default='other', max_length=20),
),
migrations.DeleteModel(
name='ExpenseType',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0.2 on 2026-02-05 07:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('management', '0007_alter_expense_expense_type_delete_expensetype'),
]
operations = [
migrations.AlterField(
model_name='expense',
name='expense_type',
field=models.CharField(choices=[('rent', 'Ijara'), ('salary', 'Maosh'), ('utilities', 'Kommunal tolovlar'), ('maintenance', 'Texnik xizmat'), ('other', 'Boshqa')], default='other', max_length=20),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0.2 on 2026-02-05 07:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('management', '0008_alter_expense_expense_type'),
]
operations = [
migrations.AlterField(
model_name='expense',
name='expense_type',
field=models.CharField(choices=[('rent', 'Ijara'), ('salary', 'Maosh'), ('utilities', 'Kommunal tolovlar'), ('maintenance', 'Texnik xizmat'), ('food', 'Oziq-ovqat'), ('transport', "Yo'lkira"), ('other', 'Boshqa')], default='other', max_length=20),
),
]

View File

@@ -0,0 +1,31 @@
# Generated by Django 6.0.2 on 2026-02-05 08:03
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('management', '0009_alter_expense_expense_type'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='expense',
name='device',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='management.device'),
),
migrations.AddField(
model_name='expense',
name='employee',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='salaries', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='expense',
name='expense_type',
field=models.CharField(choices=[('rent', 'Ijara'), ('salary', 'Maosh'), ('utilities', 'Kommunal tolovlar'), ('maintenance', 'Texnik xizmat'), ('food', 'Oziq-ovqat'), ('transport', "Yo'lkira"), ('buy_toys', 'Oʻyinchoqlar sotib olish'), ('other', 'Boshqa')], default='other', max_length=20),
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 6.0.2 on 2026-02-05 08:04
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('management', '0010_expense_device_expense_employee_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='expense',
name='confirmed_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='confirmed_expenses', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -0,0 +1,41 @@
# Generated by Django 6.0.2 on 2026-02-05 09:37
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('management', '0011_alter_expense_confirmed_by'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RemoveField(
model_name='income',
name='reported_by',
),
migrations.AddField(
model_name='income',
name='created_by',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.PROTECT, related_name='created_incomes', to=settings.AUTH_USER_MODEL),
preserve_default=False,
),
migrations.AddField(
model_name='income',
name='is_confirmed',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='income',
name='confirmed_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='confirmed_incomes', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='income',
name='device',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='incomes', to='management.device'),
),
]

View File

@@ -0,0 +1,44 @@
# Generated by Django 6.0.2 on 2026-02-06 07:08
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('management', '0012_remove_income_reported_by_income_created_by_and_more'),
]
operations = [
migrations.RenameField(
model_name='device',
old_name='name',
new_name='address',
),
migrations.RemoveField(
model_name='device',
name='monthly_fee',
),
migrations.RemoveField(
model_name='device',
name='toys_count',
),
migrations.AlterField(
model_name='toymovement',
name='movement_type',
field=models.CharField(choices=[('from_warehouse', 'Ombordan → Aparatga'), ('between_warehouses', 'Ombordan → Omborga')], max_length=30),
),
migrations.CreateModel(
name='Rent',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('address', models.CharField(max_length=100, unique=True)),
('due_date', models.DateField()),
('amount', models.IntegerField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('device', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='device_rents', to='management.device')),
('district', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='district_rents', to='management.district')),
],
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0.2 on 2026-02-06 12:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('management', '0013_rename_name_device_address_remove_device_monthly_fee_and_more'),
]
operations = [
migrations.AlterField(
model_name='income',
name='amount',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 6.0.2 on 2026-02-06 12:54
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('management', '0014_alter_income_amount'),
]
operations = [
migrations.RemoveField(
model_name='income',
name='confirmed_by',
),
]

View File

@@ -0,0 +1,8 @@
from .region import *
from .device import *
from .income import *
from .district import *
from .toyMovement import *
from .warehouse import *
from .expense import *
from .rent import *

View File

@@ -0,0 +1,10 @@
from django.db import models
from .district import District
class Device(models.Model):
address = models.CharField(max_length=100, unique=True)
district = models.ForeignKey(District, on_delete=models.PROTECT)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.address

View File

@@ -0,0 +1,16 @@
from django.db import models
from .region import Region
class District(models.Model):
region = models.ForeignKey(
Region,
on_delete=models.CASCADE,
related_name="districts"
)
name = models.CharField(max_length=100)
class Meta:
unique_together = ("region", "name")
def __str__(self):
return f"{self.name}{self.region.name}"

View File

@@ -0,0 +1,47 @@
from django.db import models
from core.apps.management.models import Device
class Expense(models.Model):
class ExpenseType(models.TextChoices):
RENT = "rent", "Ijara"
SALARY = "salary", "Maosh"
UTILITIES = "utilities", "Kommunal tolovlar"
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)
expense_type = models.CharField(
max_length=20,
choices=ExpenseType.choices,
default=ExpenseType.OTHER,
)
# Conditional fields
employee = models.ForeignKey("accounts.User", related_name="salaries", null=True, blank=True, on_delete=models.PROTECT)
device = models.ForeignKey(Device, null=True, blank=True, on_delete=models.PROTECT)
created_by = models.ForeignKey("accounts.User", on_delete=models.PROTECT)
confirmed_by = models.ForeignKey(
"accounts.User", on_delete=models.PROTECT,
null=True, blank=True, related_name="confirmed_expenses"
)
is_confirmed = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
def clean(self):
from django.core.exceptions import ValidationError
# Salary requires employee
if self.expense_type == self.ExpenseType.SALARY and not self.employee:
raise ValidationError({"employee": "Employee must be set for Salary expenses."})
# Device required for rent/utilities/maintenance
if self.expense_type in [
self.ExpenseType.RENT,
self.ExpenseType.MAINTENANCE
] and not self.device:
raise ValidationError({"device": "Device must be set for this type of expense."})

View File

@@ -0,0 +1,10 @@
from django.db import models
from .device import Device
class Income(models.Model):
device = models.ForeignKey(Device, related_name='incomes',on_delete=models.PROTECT)
amount = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
created_by = models.ForeignKey("accounts.User", on_delete=models.PROTECT, related_name="created_incomes")
is_confirmed = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)

View File

@@ -0,0 +1,7 @@
from django.db import models
class Region(models.Model):
name = models.CharField(max_length=100, unique=True)
def __str__(self):
return self.name

View File

@@ -0,0 +1,15 @@
from django.db import models
from .district import District
from .device import Device
class Rent(models.Model):
address = models.CharField(max_length=100, unique=True)
district = models.ForeignKey(District, related_name="district_rents", on_delete=models.PROTECT)
device = models.ForeignKey(Device, related_name="device_rents", on_delete=models.PROTECT)
due_date = models.DateField()
amount = models.IntegerField()
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.address

View File

@@ -0,0 +1,23 @@
from django.db import models
from .device import Device
from .warehouse import Warehouse
from ..choice import TOY_MOVEMENT_TYPE
class ToyMovement(models.Model):
movement_type = models.CharField(max_length=30, choices=TOY_MOVEMENT_TYPE)
from_warehouse = models.ForeignKey(
Warehouse, on_delete=models.PROTECT,
related_name="outgoing"
)
to_warehouse = models.ForeignKey(
Warehouse, on_delete=models.PROTECT,
related_name="incoming",
null=True, blank=True
)
device = models.ForeignKey(
Device, on_delete=models.PROTECT,
null=True, blank=True
)
quantity = models.PositiveIntegerField()
created_by = models.ForeignKey("accounts.User", on_delete=models.PROTECT)
created_at = models.DateTimeField(auto_now_add=True)

View File

@@ -0,0 +1,15 @@
from django.db import models
from .region import Region
class Warehouse(models.Model):
name = models.CharField(max_length=100, unique=True)
region = models.ForeignKey(
Region,
on_delete=models.PROTECT,
related_name="warehouses"
)
toys_count = models.PositiveIntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name

View File

@@ -0,0 +1,239 @@
<!DOCTYPE html>
<html lang="uz">
<head>
<meta charset="UTF-8">
<title>{% block title %}Xvatayka{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<style>
* { box-sizing: border-box; }
body {
margin: 0;
font-family: 'Segoe UI', Roboto, system-ui, -apple-system, sans-serif;
background: #f4f5f7;
color: #1f2937;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Header */
header {
background: #4f46e5;
color: #fff;
padding: 16px 24px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
header .logo { font-size: 22px; font-weight: 700; letter-spacing: 1px; }
#menu-btn {
position: absolute;
left: 24px; top: 50%;
transform: translateY(-50%);
background: #fff; border: none; padding: 6px 10px;
border-radius: 8px; cursor: pointer;
display: flex; align-items: center; justify-content: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
#menu-btn svg { stroke: #4f46e5; }
/* Sidebar */
#sidebar {
background: #fff;
border-right: 1px solid #e5e7eb;
padding: 20px;
width: 240px;
position: fixed;
top: 0;
left: -260px;
height: 100vh; /* full viewport height */
overflow: hidden; /* non-scrollable */
transition: left 0.3s ease, box-shadow 0.3s ease;
z-index: 1000;
display: flex;
flex-direction: column;
justify-content: space-between; /* keeps logout at bottom */
box-shadow: 0 0 20px rgba(0,0,0,0.05);
}
#sidebar.active { left: 0; }
#sidebar ul {
list-style: none;
padding: 0;
margin: 0;
}
#sidebar ul li { margin-bottom: 16px; }
#sidebar ul li a {
text-decoration: none;
color: #1f2937;
font-weight: 500;
display: block;
padding: 12px 16px;
border-radius: 8px;
transition: all 0.25s ease;
background: #f9fafb;
white-space: nowrap; /* prevent wrapping */
overflow: hidden;
text-overflow: ellipsis;
}
#sidebar ul li a:hover {
background: #4f46e5;
color: #fff;
box-shadow: 0 2px 8px rgba(79,70,229,0.3);
}
/* Logout button fixed at bottom */
.logout-container {
margin-top: auto;
flex-shrink: 0;
}
.logout-btn {
width: 100%;
background: #ef4444;
color: #fff;
border: none;
padding: 10px 14px;
border-radius: 10px;
font-weight: 600;
font-size: 14px;
cursor: pointer;
transition: all 0.25s ease;
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
}
.logout-btn:hover {
background: #dc2626;
box-shadow: 0 3px 8px rgba(0,0,0,0.15);
}
main {
flex: 1;
padding: 10px;
transition: margin-left 0.3s ease;
background: #f4f5f7;
min-height: calc(100vh - 64px);
}
main.shifted { margin-left: 240px; }
/* Cards */
.cards-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
.card {
background: #fff;
border-radius: 12px;
padding: 16px 20px;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
transition: transform 0.2s, box-shadow 0.2s;
}
.card:hover { transform: translateY(-2px); box-shadow: 0 6px 16px rgba(0,0,0,0.08); }
.card-row { margin-bottom: 8px; font-size: 14px; color: #374151; }
.card-row strong { color: #111827; }
.card-actions { margin-top: 12px; }
.card-actions .btn {
display: inline-block;
padding: 6px 10px;
border-radius: 6px;
text-decoration: none;
color: #fff;
font-weight: bold;
font-size: 13px;
margin-right: 6px;
transition: all 0.2s ease;
}
.card-actions .btn.edit { background: #4f46e5; }
.card-actions .btn.confirm { background: #10b981; }
.card-actions .btn.decline { background: #ef4444; }
.card-actions .btn:hover { opacity: 0.85; transform: translateY(-1px); }
/* Responsive */
@media (max-width: 1024px) {
#sidebar { width: 220px; }
main.shifted { margin-left: 220px; }
}
@media (max-width: 768px) {
#sidebar { width: 200px; left: -220px; }
#sidebar.active { left: 0; box-shadow: 2px 0 12px rgba(0,0,0,0.2); }
main.shifted { margin-left: 0; } /* overlay on small screens */
}
@media (max-width: 640px) {
.cards-container { grid-template-columns: 1fr; }
.card { padding: 14px 16px; }
.card-row { font-size: 13px; }
.card-actions .btn { font-size: 12px; padding: 5px 8px; margin-bottom: 4px; }
}
@media (max-width: 480px) {
header { padding: 12px 16px; }
#menu-btn { left: 16px; padding: 4px 8px; }
.logo { font-size: 18px; }
#sidebar { padding: 16px; }
.logout-btn { font-size: 13px; padding: 8px 10px; }
}
</style>
</head>
<body>
<header>
<button id="menu-btn">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M4 5h16"/>
<path d="M4 12h16"/>
<path d="M4 19h16"/>
</svg>
</button>
<div class="logo">Xvatayka</div>
</header>
<nav id="sidebar">
<ul>
<li><a href="{% url 'dashboard' %}">Dashboard</a></li>
<li><a href="{% url 'expense_list' %}">Xarajatlar</a></li>
<li><a href="{% url 'income_list' %}">Kirimlar</a></li>
<li><a href="{% url 'toy_movement_list' %}">Oʻyinchoq harakatlari</a></li>
{% if user.role == "manager" or user.role == "businessman" %}
<li><a href="{% url 'device_list' %}">Aparatlar</a></li>
<li><a href="{% url 'warehouse_list' %}">Omborlar</a></li>
<li><a href="{% url 'user_list' %}">Foydalanuvchilar</a></li>
{% endif %}
</ul>
<div class="logout-container">
{% if user.is_authenticated %}
<form method="post" action="{% url 'logout' %}">
{% csrf_token %}
<button type="submit" class="logout-btn">Chiqish</button>
</form>
{% endif %}
</div>
</nav>
<main id="main-content">
{% block content %}{% endblock %}
</main>
<script>
const menuBtn = document.getElementById('menu-btn');
const sidebar = document.getElementById('sidebar');
const mainContent = document.getElementById('main-content');
menuBtn.addEventListener('click', () => {
sidebar.classList.toggle('active');
mainContent.classList.toggle('shifted');
});
// Optional: click outside to close
document.addEventListener('click', (e) => {
if (!sidebar.contains(e.target) && !menuBtn.contains(e.target)) {
sidebar.classList.remove('active');
mainContent.classList.remove('shifted');
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,117 @@
{% extends "base.html" %}
{% block content %}
<br>
<h2 style="text-align:center; margin-bottom:24px; color:#111827;">Businessman Paneli</h2>
<div class="dashboard-grid">
<a href="{% url 'create_user' %}" class="dashboard-card">
<div class="icon">👤</div>
<div class="label">Foydalanuvchi yaratish</div>
</a>
<a href="{% url 'create_device' %}" class="dashboard-card">
<div class="icon">💻</div>
<div class="label">Aparat yaratish</div>
</a>
<a href="{% url 'create_expense' %}" class="dashboard-card">
<div class="icon">💸</div>
<div class="label">Xarajat qo'shish</div>
</a>
<a href="{% url 'create_income' %}" class="dashboard-card">
<div class="icon">📥</div>
<div class="label">Kirim qo'shish</div>
</a>
<a href="{% url 'create_warehouse' %}" class="dashboard-card">
<div class="icon">🏬</div>
<div class="label">Ombor yaratish</div>
</a>
<a href="{% url 'create_toy_movement' %}" class="dashboard-card">
<div class="icon">🚚</div>
<div class="label">Oʻyinchoq harakati yaratish</div>
</a>
</div>
<style>
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 16px;
max-width: 960px;
margin: 0 auto;
padding: 0 10px;
}
.dashboard-card {
background: #fff;
border-radius: 16px;
padding: 20px 12px;
text-align: center;
text-decoration: none;
color: #111827;
font-weight: 600;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
transition: transform 0.2s, box-shadow 0.2s, background 0.2s;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.dashboard-card .icon {
font-size: 32px;
margin-bottom: 10px;
}
.dashboard-card:hover {
transform: translateY(-4px);
box-shadow: 0 6px 20px rgba(0,0,0,0.1);
background: #4f46e5;
color: #fff;
}
.dashboard-card:hover .icon {
color: #fff;
}
.label {
font-size: 14px;
text-align: center;
}
/* Mobile adjustments: keep grid layout, adjust size */
@media (max-width: 768px) {
.dashboard-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 12px;
}
.dashboard-card {
padding: 16px 10px;
}
.dashboard-card .icon {
font-size: 28px;
margin-bottom: 8px;
}
.label {
font-size: 13px;
}
}
@media (max-width: 480px) {
.dashboard-grid {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px;
}
.dashboard-card {
padding: 14px 8px;
}
.dashboard-card .icon {
font-size: 24px;
margin-bottom: 6px;
}
.label {
font-size: 12px;
}
}
</style>
{% endblock %}

View File

@@ -0,0 +1,147 @@
{% extends "base.html" %}
{% block title %}{{ title|default:"Yaratish" }}{% endblock %}
{% block content %}
<div class="form-container">
<h2>{{ title|default:"Yaratish" }}</h2>
<a href="{% url 'dashboard' %}" class="back-btn">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 18l-6-6 6-6"/>
</svg>
Orqaga
</a>
<form method="post" novalidate>
{% csrf_token %}
{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% if field.help_text %}
<small class="help-text">{{ field.help_text }}</small>
{% endif %}
{% for error in field.errors %}
<div class="error">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
<button type="submit" class="submit-btn">Saqlash</button>
</form>
</div>
<style>
.form-container {
max-width: 480px;
margin: 0 auto;
background: #fff;
padding: 24px 28px;
border-radius: 12px;
box-shadow: 0 6px 18px rgba(0,0,0,0.06);
}
.form-container h2 {
margin-bottom: 16px;
font-size: 22px;
color: #111827;
text-align: center;
}
.back-btn {
display: inline-flex;
align-items: center;
gap: 6px;
margin-bottom: 20px;
padding: 8px 16px;
background: #f3f4f6;
color: #4f46e5;
text-decoration: none;
font-weight: 500;
font-size: 14px;
border-radius: 9999px;
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
transition: all 0.2s ease;
}
.back-btn:hover {
background: #4f46e5;
color: #fff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.back-btn .icon {
width: 16px;
height: 16px;
}
.form-group {
margin-bottom: 16px;
display: flex;
flex-direction: column;
transition: all 0.2s ease;
}
.form-group label {
margin-bottom: 6px;
font-weight: 600;
color: #374151;
}
.form-group input,
.form-group select {
padding: 10px 14px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.form-group input:focus,
.form-group select:focus {
border-color: #4f46e5;
box-shadow: 0 0 0 2px rgba(79,70,229,0.2);
}
.help-text {
font-size: 12px;
color: #6b7280;
margin-top: 4px;
}
.error {
font-size: 12px;
color: #ef4444;
margin-top: 4px;
}
.submit-btn {
width: 100%;
padding: 10px 0;
background: #4f46e5;
color: #fff;
font-weight: 600;
font-size: 15px;
border: none;
border-radius: 10px;
cursor: pointer;
transition: background 0.2s, transform 0.2s;
}
.submit-btn:hover {
background: #4338ca;
transform: translateY(-1px);
}
/* Mobile adjustments */
@media (max-width: 480px) {
.form-container {
padding: 20px 16px;
}
}
</style>
{% endblock %}

View File

@@ -0,0 +1,165 @@
{% extends "base.html" %}
{% block title %}{{ title|default:"Yaratish" }}{% endblock %}
{% block content %}
<div class="form-container">
<h2>{{ title|default:"Yaratish" }}</h2>
<a href="{% url 'dashboard' %}" class="back-btn">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 18l-6-6 6-6"/>
</svg>
Orqaga
</a>
<form method="post" novalidate>
{% csrf_token %}
{% for field in form %}
<div class="form-group" id="group-{{ field.name }}">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% if field.help_text %}
<small class="help-text">{{ field.help_text }}</small>
{% endif %}
{% for error in field.errors %}
<div class="error">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
<button type="submit" class="submit-btn">
{% if title %}{{ title }}{% else %}Saqlash{% endif %}
</button>
</form>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
const typeSelect = document.getElementById("id_expense_type");
const employeeGroup = document.getElementById("group-employee");
const deviceGroup = document.getElementById("group-device");
function toggleFields() {
if (!typeSelect) return;
const value = typeSelect.value;
// Hide both initially
if (employeeGroup) employeeGroup.style.display = "none";
if (deviceGroup) deviceGroup.style.display = "none";
if (value === "salary") {
if (employeeGroup) employeeGroup.style.display = "flex";
} else if (value === "rent" || value === "maintenance") {
if (deviceGroup) deviceGroup.style.display = "flex";
}
}
toggleFields();
if (typeSelect) typeSelect.addEventListener("change", toggleFields);
});
</script>
<style>
.form-container {
max-width: 480px;
margin: 40px auto;
background: #fff;
padding: 24px 28px;
border-radius: 12px;
box-shadow: 0 6px 18px rgba(0,0,0,0.06);
}
.form-container h2 {
margin-bottom: 16px;
font-size: 22px;
color: #111827;
text-align: center;
}
.back-btn {
display: inline-flex;
align-items: center;
gap: 6px;
margin-bottom: 20px;
padding: 8px 16px;
background: #f3f4f6;
color: #4f46e5;
text-decoration: none;
font-weight: 500;
font-size: 14px;
border-radius: 9999px;
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
transition: all 0.2s ease;
}
.back-btn:hover {
background: #4f46e5;
color: #fff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.back-btn .icon { width: 16px; height: 16px; }
.form-group {
margin-bottom: 16px;
display: flex;
flex-direction: column;
}
.form-group label {
margin-bottom: 6px;
font-weight: 600;
color: #374151;
}
.form-group input,
.form-group select {
padding: 10px 14px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.form-group input:focus,
.form-group select:focus {
border-color: #4f46e5;
box-shadow: 0 0 0 2px rgba(79,70,229,0.2);
}
.help-text {
font-size: 12px;
color: #6b7280;
margin-top: 4px;
}
.error {
font-size: 12px;
color: #ef4444;
margin-top: 4px;
}
.submit-btn {
width: 100%;
padding: 10px 0;
background: #4f46e5;
color: #fff;
font-weight: 600;
font-size: 15px;
border: none;
border-radius: 10px;
cursor: pointer;
transition: background 0.2s, transform 0.2s;
}
.submit-btn:hover {
background: #4338ca;
transform: translateY(-1px);
}
/* Mobile adjustments */
@media (max-width: 480px) {
.form-container { padding: 20px 16px; }
}
</style>
{% endblock %}

View File

@@ -0,0 +1,150 @@
{% extends "base.html" %}
{% block title %}{{ title|default:"Yaratish" }}{% endblock %}
{% block content %}
<div class="form-container">
<h2>{{ title|default:"Yaratish" }}</h2>
<a href="{% url 'dashboard' %}" class="back-btn">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 18l-6-6 6-6"/>
</svg>
Orqaga
</a>
<form method="post" novalidate>
{% csrf_token %}
{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% if field.help_text %}
<small class="help-text">{{ field.help_text }}</small>
{% endif %}
{% for error in field.errors %}
<div class="error">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
<button type="submit" class="submit-btn">
{% if title %}{{ title }}{% else %}Saqlash{% endif %}
</button>
</form>
</div>
<style>
.form-container {
max-width: 480px;
margin: 0 auto;
background: #fff;
padding: 24px 28px;
border-radius: 12px;
box-shadow: 0 6px 18px rgba(0,0,0,0.06);
}
.form-container h2 {
margin-bottom: 16px;
font-size: 22px;
color: #111827;
text-align: center;
}
.back-btn {
display: inline-flex;
align-items: center;
gap: 6px;
margin-bottom: 20px;
padding: 8px 16px;
background: #f3f4f6;
color: #4f46e5;
text-decoration: none;
font-weight: 500;
font-size: 14px;
border-radius: 9999px;
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
transition: all 0.2s ease;
}
.back-btn:hover {
background: #4f46e5;
color: #fff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.back-btn .icon {
width: 16px;
height: 16px;
}
.form-group {
margin-bottom: 16px;
display: flex;
flex-direction: column;
transition: all 0.2s ease;
}
.form-group label {
margin-bottom: 6px;
font-weight: 600;
color: #374151;
}
.form-group input,
.form-group select {
padding: 10px 14px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.form-group input:focus,
.form-group select:focus {
border-color: #4f46e5;
box-shadow: 0 0 0 2px rgba(79,70,229,0.2);
}
.help-text {
font-size: 12px;
color: #6b7280;
margin-top: 4px;
}
.error {
font-size: 12px;
color: #ef4444;
margin-top: 4px;
}
.submit-btn {
width: 100%;
padding: 10px 0;
background: #4f46e5;
color: #fff;
font-weight: 600;
font-size: 15px;
border: none;
border-radius: 10px;
cursor: pointer;
transition: background 0.2s, transform 0.2s;
}
.submit-btn:hover {
background: #4338ca;
transform: translateY(-1px);
}
/* Mobile adjustments */
@media (max-width: 480px) {
.form-container {
padding: 20px 16px;
}
}
</style>
{% endblock %}

View File

@@ -0,0 +1,196 @@
{% extends "base.html" %}
{% block title %}{{ title|default:"Yaratish" }}{% endblock %}
{% block content %}
<div class="form-container">
<h2>{{ title|default:"Yaratish" }}</h2>
<a href="{% url 'dashboard' %}" class="back-btn">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 18l-6-6 6-6"/>
</svg>
Orqaga
</a>
<form method="post" novalidate>
{% csrf_token %}
{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% if field.help_text %}
<small class="help-text">{{ field.help_text }}</small>
{% endif %}
{% for error in field.errors %}
<div class="error">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
<button type="submit" class="submit-btn">Saqlash</button>
</form>
</div>
<style>
.form-container {
max-width: 480px;
margin: 40px auto;
background: #fff;
padding: 24px 28px;
border-radius: 12px;
box-shadow: 0 6px 18px rgba(0,0,0,0.06);
}
.form-container h2 {
margin-bottom: 16px;
font-size: 22px;
color: #111827;
text-align: center;
}
.back-btn {
display: inline-flex;
align-items: center;
gap: 6px;
margin-bottom: 20px;
padding: 8px 16px;
background: #f3f4f6;
color: #4f46e5;
text-decoration: none;
font-weight: 500;
font-size: 14px;
border-radius: 9999px;
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
transition: all 0.2s ease;
}
.back-btn:hover {
background: #4f46e5;
color: #fff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.back-btn .icon {
width: 16px;
height: 16px;
}
.form-group {
margin-bottom: 16px;
display: flex;
flex-direction: column;
}
.form-group label {
margin-bottom: 6px;
font-weight: 600;
color: #374151;
}
.form-group input,
.form-group select {
padding: 10px 14px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.form-group input:focus,
.form-group select:focus {
border-color: #4f46e5;
box-shadow: 0 0 0 2px rgba(79,70,229,0.2);
}
.help-text {
font-size: 12px;
color: #6b7280;
margin-top: 4px;
}
.error {
font-size: 12px;
color: #ef4444;
margin-top: 4px;
}
.submit-btn {
width: 100%;
padding: 12px 0;
background: #4f46e5;
color: #fff;
font-weight: 600;
font-size: 15px;
border: none;
border-radius: 10px;
cursor: pointer;
transition: background 0.2s, transform 0.2s;
}
.submit-btn:hover {
background: #4338ca;
transform: translateY(-1px);
}
@media (max-width: 480px) {
.form-container {
padding: 20px 16px;
}
}
</style>
<script>
document.addEventListener("DOMContentLoaded", function() {
const movementType = document.getElementById("id_movement_type");
const toWarehouse = document.getElementById("id_to_warehouse");
const device = document.getElementById("id_device");
function toggleFields() {
const value = movementType.value;
if (value === "from_warehouse") { // Ombordan -> Aparatga
toWarehouse.parentElement.style.display = "none";
device.parentElement.style.display = "";
} else if (value === "between_warehouses") { // Ombordan -> Omborga
device.parentElement.style.display = "none";
toWarehouse.parentElement.style.display = "";
} else {
toWarehouse.parentElement.style.display = "";
device.parentElement.style.display = "";
}
}
// Update movement_type when to_warehouse is selected
if (toWarehouse) {
toWarehouse.addEventListener("change", function() {
if (toWarehouse.value) {
movementType.value = "between_warehouses";
toggleFields();
}
});
}
// Update movement_type when device is selected
if (device) {
device.addEventListener("change", function() {
if (device.value) {
movementType.value = "from_warehouse";
toggleFields();
}
});
}
// Run on page load
if (movementType) {
toggleFields();
movementType.addEventListener("change", toggleFields);
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,184 @@
{% extends "base.html" %}
{% block title %}{{ title|default:"Yaratish" }}{% endblock %}
{% block content %}
<div class="form-container">
<h2>{{ title|default:"Yaratish" }}</h2>
<a href="{% url 'dashboard' %}" class="back-btn">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 18l-6-6 6-6"/>
</svg>
Orqaga
</a>
<form method="post" novalidate>
{% csrf_token %}
{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% if field.help_text %}
<small class="help-text">{{ field.help_text }}</small>
{% endif %}
{% for error in field.errors %}
<div class="error">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
<button type="submit" class="submit-btn">Saqlash</button>
</form>
</div>
<style>
.form-container {
max-width: 480px;
margin: 0 auto;
background: #fff;
padding: 24px 28px;
border-radius: 12px;
box-shadow: 0 6px 18px rgba(0,0,0,0.06);
}
.form-container h2 {
margin-bottom: 16px;
font-size: 22px;
color: #111827;
text-align: center;
}
.back-btn {
display: inline-flex;
align-items: center;
gap: 6px;
margin-bottom: 20px;
padding: 8px 16px;
background: #f3f4f6;
color: #4f46e5;
text-decoration: none;
font-weight: 500;
font-size: 14px;
border-radius: 9999px; /* pill shape */
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
transition: all 0.2s ease;
}
.back-btn:hover {
background: #4f46e5;
color: #fff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.back-btn .icon {
width: 16px;
height: 16px;
}
.form-group {
margin-bottom: 16px;
display: flex;
flex-direction: column;
transition:all 0.2s ease;
}
.form-group label {
margin-bottom: 6px;
font-weight: 600;
color: #374151;
}
.form-group input,
.form-group select {
padding: 10px 14px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.form-group input:focus,
.form-group select:focus {
border-color: #4f46e5;
box-shadow: 0 0 0 2px rgba(79,70,229,0.2);
}
.help-text {
font-size: 12px;
color: #6b7280;
margin-top: 4px;
}
.error {
font-size: 12px;
color: #ef4444;
margin-top: 4px;
}
.submit-btn {
width: 100%;
padding: 10px 0;
background: #4f46e5;
color: #fff;
font-weight: 600;
font-size: 15px;
border: none;
border-radius: 10px;
cursor: pointer;
transition: background 0.2s, transform 0.2s;
}
.submit-btn:hover {
background: #4338ca;
transform: translateY(-1px);
}
/* Responsive adjustments */
@media (max-width: 480px) {
.form-container {
padding: 20px 16px;
}
}
</style>
<script>
document.addEventListener("DOMContentLoaded", function () {
const roleField = document.getElementById("id_role");
const regionGroup = document.getElementById("id_region")?.closest(".form-group");
const warehouseGroup = document.getElementById("id_warehouse")?.closest(".form-group");
function toggleFields() {
if (!roleField) return;
const role = roleField.value;
// Manager
if (role === "manager") {
if (regionGroup) regionGroup.style.display = "block";
if (warehouseGroup) warehouseGroup.style.display = "none";
}
// Employee
if (role === "employee") {
if (regionGroup) regionGroup.style.display = "none";
if (warehouseGroup) warehouseGroup.style.display = "block";
}
}
toggleFields();
if (roleField) {
roleField.addEventListener("change", toggleFields);
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html>

View File

@@ -0,0 +1,147 @@
{% extends "base.html" %}
{% block title %}{{ title|default:"Tahrirlash" }}{% endblock %}
{% block content %}
<div class="form-container">
<h2>{{ title|default:"Tahrirlash" }}</h2>
<a href="{% url 'device_list' %}" class="back-btn">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 18l-6-6 6-6"/>
</svg>
Orqaga
</a>
<form method="post" novalidate>
{% csrf_token %}
{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% if field.help_text %}
<small class="help-text">{{ field.help_text }}</small>
{% endif %}
{% for error in field.errors %}
<div class="error">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
<button type="submit" class="submit-btn">Saqlash</button>
</form>
</div>
<style>
.form-container {
max-width: 480px;
margin: 0 auto;
background: #fff;
padding: 24px 28px;
border-radius: 12px;
box-shadow: 0 6px 18px rgba(0,0,0,0.06);
}
.form-container h2 {
margin-bottom: 16px;
font-size: 22px;
color: #111827;
text-align: center;
}
.back-btn {
display: inline-flex;
align-items: center;
gap: 6px;
margin-bottom: 20px;
padding: 8px 16px;
background: #f3f4f6;
color: #4f46e5;
text-decoration: none;
font-weight: 500;
font-size: 14px;
border-radius: 9999px;
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
transition: all 0.2s ease;
}
.back-btn:hover {
background: #4f46e5;
color: #fff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.back-btn .icon {
width: 16px;
height: 16px;
}
.form-group {
margin-bottom: 16px;
display: flex;
flex-direction: column;
transition:all 0.2s ease;
}
.form-group label {
margin-bottom: 6px;
font-weight: 600;
color: #374151;
}
.form-group input,
.form-group select {
padding: 10px 14px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.form-group input:focus,
.form-group select:focus {
border-color: #4f46e5;
box-shadow: 0 0 0 2px rgba(79,70,229,0.2);
}
.help-text {
font-size: 12px;
color: #6b7280;
margin-top: 4px;
}
.error {
font-size: 12px;
color: #ef4444;
margin-top: 4px;
}
.submit-btn {
width: 100%;
padding: 10px 0;
background: #4f46e5;
color: #fff;
font-weight: 600;
font-size: 15px;
border: none;
border-radius: 10px;
cursor: pointer;
transition: background 0.2s, transform 0.2s;
}
.submit-btn:hover {
background: #4338ca;
transform: translateY(-1px);
}
/* Mobile adjustments */
@media (max-width: 480px) {
.form-container {
padding: 20px 16px;
}
}
</style>
{% endblock %}

View File

@@ -0,0 +1,139 @@
{% extends "base.html" %}
{% block title %}Create Expense{% endblock %}
{% block content %}
<style>
.card {
max-width: 520px;
margin: 40px auto;
background: #fff;
border-radius: 16px;
padding: 24px;
box-shadow: 0 12px 40px rgba(0,0,0,.08);
}
h1 {
margin: 0 0 20px;
font-size: 22px;
text-align: center;
}
.field {
margin-bottom: 16px;
}
label {
display: block;
font-size: 13px;
margin-bottom: 6px;
color: #6b7280;
font-weight: 500;
}
select, input {
width: 100%;
padding: 14px;
font-size: 16px;
border-radius: 12px;
border: 1px solid #d1d5db;
background: #f9fafb;
}
button {
width: 100%;
margin-top: 12px;
padding: 15px;
border: none;
border-radius: 14px;
background: #4f46e5;
color: #fff;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
button:hover {
background: #4338ca;
}
.error {
background: #fee2e2;
color: #991b1b;
padding: 12px;
border-radius: 12px;
margin-bottom: 14px;
font-size: 14px;
}
</style>
<div class="card">
<h1>Create Expense</h1>
{% if form.non_field_errors %}
<div class="error">{{ form.non_field_errors.0 }}</div>
{% endif %}
<form method="POST">
{% csrf_token %}
<div class="field" id="field-expense_type">
{{ form.expense_type.label_tag }}
{{ form.expense_type }}
{{ form.expense_type.errors }}
</div>
<div class="field" id="field-amount">
{{ form.amount.label_tag }}
{{ form.amount }}
{{ form.amount.errors }}
</div>
<div class="field" id="field-employee" style="display:none;">
{{ form.employee.label_tag }}
{{ form.employee }}
{{ form.employee.errors }}
</div>
<div class="field" id="field-device" style="display:none;">
{{ form.device.label_tag }}
{{ form.device }}
{{ form.device.errors }}
</div>
<button type="submit">
{% if title %}{{ title }}{% else %}Create{% endif %}
</button>
</form>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
const typeSelect = document.getElementById("id_expense_type");
const employeeField = document.getElementById("field-employee");
const deviceField = document.getElementById("field-device");
function toggleFields() {
if (!typeSelect) return;
const value = typeSelect.value;
// Reset both
employeeField.style.display = "none";
deviceField.style.display = "none";
if (value === "salary") {
employeeField.style.display = "block";
} else if (value === "rent" || value === "maintenance") {
deviceField.style.display = "block";
}
}
if (typeSelect) {
toggleFields();
typeSelect.addEventListener("change", toggleFields);
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,98 @@
{% extends "base.html" %}
{% block title %}Create Income{% endblock %}
{% block content %}
<style>
.card {
max-width: 520px;
margin: 40px auto;
background: #fff;
border-radius: 16px;
padding: 24px;
box-shadow: 0 12px 40px rgba(0,0,0,.08);
}
h1 {
margin: 0 0 20px;
font-size: 22px;
text-align: center;
}
.field {
margin-bottom: 16px;
}
label {
display: block;
font-size: 13px;
margin-bottom: 6px;
color: #6b7280;
font-weight: 500;
}
select, input {
width: 100%;
padding: 14px;
font-size: 16px;
border-radius: 12px;
border: 1px solid #d1d5db;
background: #f9fafb;
}
button {
width: 100%;
margin-top: 12px;
padding: 15px;
border: none;
border-radius: 14px;
background: #4f46e5;
color: #fff;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
button:hover {
background: #4338ca;
}
.error {
background: #fee2e2;
color: #991b1b;
padding: 12px;
border-radius: 12px;
margin-bottom: 14px;
font-size: 14px;
}
</style>
<div class="card">
<h1>Create Income</h1>
{% if form.non_field_errors %}
<div class="error">{{ form.non_field_errors.0 }}</div>
{% endif %}
<form method="POST">
{% csrf_token %}
<div class="field" id="field-device">
{{ form.device.label_tag }}
{{ form.device }}
{{ form.device.errors }}
</div>
<div class="field" id="field-amount">
{{ form.amount.label_tag }}
{{ form.amount }}
{{ form.amount.errors }}
</div>
<button type="submit">
{% if title %}{{ title }}{% else %}Create{% endif %}
</button>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,142 @@
{% extends "base.html" %}
{% block title %}Create Toy Movement{% endblock %}
{% block content %}
<style>
.card {
max-width: 520px;
margin: 40px auto;
background: #fff;
border-radius: 16px;
padding: 24px;
box-shadow: 0 12px 40px rgba(0,0,0,.08);
}
h1 {
margin: 0 0 20px;
font-size: 22px;
text-align: center;
}
.field {
margin-bottom: 16px;
}
label {
display: block;
font-size: 13px;
margin-bottom: 6px;
color: #6b7280;
font-weight: 500;
}
select, input {
width: 100%;
padding: 14px;
font-size: 16px;
border-radius: 12px;
border: 1px solid #d1d5db;
background: #f9fafb;
}
button {
width: 100%;
margin-top: 12px;
padding: 15px;
border: none;
border-radius: 14px;
background: #4f46e5;
color: #fff;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
button:hover {
background: #4338ca;
}
.error {
background: #fee2e2;
color: #991b1b;
padding: 12px;
border-radius: 12px;
margin-bottom: 14px;
font-size: 14px;
}
</style>
<div class="card">
<h1>Create Toy Movement</h1>
{% if form.non_field_errors %}
<div class="error">{{ form.non_field_errors.0 }}</div>
{% endif %}
<form method="POST">
{% csrf_token %}
{% if user_role != "employee" %}
<div class="field" id="field-movement_type">
{{ form.movement_type.label_tag }}
{{ form.movement_type }}
{{ form.movement_type.errors }}
</div>
{% endif %}
<div class="field" id="field-from_warehouse">
{{ form.from_warehouse.label_tag }}
{{ form.from_warehouse }}
{{ form.from_warehouse.errors }}
</div>
<div class="field" id="field-to_warehouse">
{{ form.to_warehouse.label_tag }}
{{ form.to_warehouse }}
{{ form.to_warehouse.errors }}
</div>
<div class="field" id="field-device">
{{ form.device.label_tag }}
{{ form.device }}
{{ form.device.errors }}
</div>
<div class="field">
{{ form.quantity.label_tag }}
{{ form.quantity }}
{{ form.quantity.errors }}
</div>
<button type="submit">Create</button>
</form>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
const typeSelect = document.getElementById("id_movement_type");
const toWarehouse = document.getElementById("field-to_warehouse");
const device = document.getElementById("field-device");
function toggleFields() {
if (!typeSelect) return;
const value = typeSelect.value;
if (value === "from_warehouse") {
toWarehouse.style.display = "none";
device.style.display = "block";
} else if (value === "between_warehouses") {
toWarehouse.style.display = "block";
device.style.display = "none";
}
}
if (typeSelect) {
toggleFields();
typeSelect.addEventListener("change", toggleFields);
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,183 @@
{% extends "base.html" %}
{% block title %}{{ title|default:"Yaratish" }}{% endblock %}
{% block content %}
<div class="form-container">
<h2>{{ title|default:"Yaratish" }}</h2>
<a href="{% url 'user_list' %}" class="back-btn">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 18l-6-6 6-6"/>
</svg>
Orqaga
</a>
<form method="post" novalidate>
{% csrf_token %}
{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% if field.help_text %}
<small class="help-text">{{ field.help_text }}</small>
{% endif %}
{% for error in field.errors %}
<div class="error">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
<button type="submit" class="submit-btn">Saqlash</button>
</form>
</div>
<style>
.form-container {
max-width: 480px;
margin: 0 auto;
background: #fff;
padding: 24px 28px;
border-radius: 12px;
box-shadow: 0 6px 18px rgba(0,0,0,0.06);
}
.form-container h2 {
margin-bottom: 16px;
font-size: 22px;
color: #111827;
text-align: center;
}
.back-btn {
display: inline-flex;
align-items: center;
gap: 6px;
margin-bottom: 20px;
padding: 8px 16px;
background: #f3f4f6;
color: #4f46e5;
text-decoration: none;
font-weight: 500;
font-size: 14px;
border-radius: 9999px; /* pill shape */
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
transition: all 0.2s ease;
}
.back-btn:hover {
background: #4f46e5;
color: #fff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.back-btn .icon {
width: 16px;
height: 16px;
}
.form-group {
margin-bottom: 16px;
display: flex;
flex-direction: column;
transition:all 0.2s ease;
}
.form-group label {
margin-bottom: 6px;
font-weight: 600;
color: #374151;
}
.form-group input,
.form-group select {
padding: 10px 14px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.form-group input:focus,
.form-group select:focus {
border-color: #4f46e5;
box-shadow: 0 0 0 2px rgba(79,70,229,0.2);
}
.help-text {
font-size: 12px;
color: #6b7280;
margin-top: 4px;
}
.error {
font-size: 12px;
color: #ef4444;
margin-top: 4px;
}
.submit-btn {
width: 100%;
padding: 10px 0;
background: #4f46e5;
color: #fff;
font-weight: 600;
font-size: 15px;
border: none;
border-radius: 10px;
cursor: pointer;
transition: background 0.2s, transform 0.2s;
}
.submit-btn:hover {
background: #4338ca;
transform: translateY(-1px);
}
/* Responsive adjustments */
@media (max-width: 480px) {
.form-container {
padding: 20px 16px;
}
}
</style>
<script>
document.addEventListener("DOMContentLoaded", function () {
const roleField = document.getElementById("id_role");
const regionGroup = document.getElementById("id_region")?.closest(".form-group");
const warehouseGroup = document.getElementById("id_warehouse")?.closest(".form-group");
function toggleFields() {
if (!roleField) return;
const role = roleField.value;
// Manager
if (role === "manager") {
if (regionGroup) regionGroup.style.display = "block";
if (warehouseGroup) warehouseGroup.style.display = "none";
}
// Employee
if (role === "employee") {
if (regionGroup) regionGroup.style.display = "none";
if (warehouseGroup) warehouseGroup.style.display = "block";
}
}
toggleFields();
if (roleField) {
roleField.addEventListener("change", toggleFields);
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html>

View File

@@ -0,0 +1,33 @@
{% load custom_filters %}
<h2>{{ title }}</h2>
<table class="table">
<thead>
<tr>
{% for field in fields %}
<th>{{ field_translations|get_item:field|default:field|capfirst }}</th>
{% endfor %}
{% if model_name != "toy_movement" %}
<th>Amallar</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for obj in objects %}
<tr>
{% for field in fields %}
<td>{{ obj|attr:field }}</td>
{% endfor %}
{% if model_name != "toy_movement" %}
<td>
<a href="{% url 'edit_'|add:model_name obj.pk %}" class="edit-btn">Tahrirlash</a>
</td>
{% endif %}
</tr>
{% empty %}
<tr>
<td colspan="{{ fields|length|add:'1' }}">Hech narsa topilmadi</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@@ -0,0 +1,24 @@
{% extends "base.html" %}
{% block title %}Aparatlar{% endblock %}
{% block content %}
<h2 style="margin-bottom:20px;">{{ title|default:"Aparatlar" }}</h2>
<div class="cards-container">
{% for device in devices %}
<div class="card">
<div class="card-row"><strong>Address:</strong> {{ device.address }}</div>
<div class="card-row"><strong>Hudud:</strong> {{ device.district.name }}</div>
{% if role == "manager" or role == "businessman" %}
<div class="card-actions">
<a href="{% url 'edit_device' device.pk %}" class="btn edit">Tahrirlash</a>
</div>
{% endif %}
</div>
{% empty %}
<p style="text-align:center; font-style:italic;">Hech narsa topilmadi</p>
{% endfor %}
</div>
{% endblock %}

View File

@@ -0,0 +1,36 @@
{% extends "base.html" %}
{% load custom_filters %}
{% block title %}Xarajatlar{% endblock %}
{% block content %}
<h2 style="margin-bottom:20px;">{{ title|default:"Xarajatlar" }}</h2>
<div class="cards-container">
{% for obj in expenses %}
<div class="card">
<div class="card-row"><strong>Miqdor:</strong> {{ obj.amount }}</div>
<div class="card-row"><strong>Tur:</strong> {{ obj|attr:"expense_type" }}</div>
{% if obj.device %}<div class="card-row"><strong>Aparat:</strong> {{ obj.device.name }}</div>{% else %}{% endif %}
{% if obj.employee %}<div class="card-row"><strong>Hodim:</strong> {{ obj.employee.get_full_name }}</div>{% else %}{% endif %}
<div class="card-row"><strong>Yaratgan:</strong> {{ obj.created_by.get_full_name }}</div>
<div class="card-row"><strong>Tasdiqlanganmi:</strong> {% if obj.confirmed_by %}{{ obj.confirmed_by.get_full_name }}{% else %}Yo'q{% endif %}</div>
<div class="card-row"><strong>Yaratilgan sana:</strong> {{ obj.created_at|date:"d.m.Y H:i" }}</div>
{% if role == "manager" or role == "businessman" %}
<div class="card-actions">
{% if not obj.is_confirmed %}
<a href="{% url 'confirm_expense' obj.pk %}" class="btn confirm"></a>
<a href="{% url 'decline_expense' obj.pk %}" class="btn decline"></a>
{% endif %}
{% if role == "businessman" %}
<a href="{% url 'edit_expense' obj.pk %}" class="btn edit">Tahrirlash</a>
{% endif %}
</div>
{% endif %}
</div>
{% empty %}
<p style="text-align:center; font-style:italic;">Hech narsa topilmadi</p>
{% endfor %}
</div>
{% endblock %}

View File

@@ -0,0 +1,92 @@
{% extends "base.html" %}
{% block title %}Kirimlar{% endblock %}
{% block content %}
<h2 style="margin-bottom:20px;">{{ title|default:"Kirimlar" }}</h2>
<div class="cards-container">
{% for income in incomes %}
<div class="card">
<div class="card-row"><strong>Miqdor:</strong> {{ income.amount|default:"-" }}</div>
<div class="card-row"><strong>Aparat:</strong> {% if income.device %}{{ income.device.address }}{% else %}-{% endif %}</div>
<div class="card-row"><strong>Yaratgan:</strong> {{ income.created_by.get_full_name }}</div>
<div class="card-row"><strong>Yaratilgan sana:</strong> {{ income.created_at|date:"d.m.Y H:i" }}</div>
{% if role == "manager" or role == "businessman" %}
<div class="card-actions">
<a href="{% url 'edit_income' income.pk %}" class="btn edit">Tahrirlash</a>
</div>
{% endif %}
</div>
{% empty %}
<p style="text-align:center; font-style:italic;">Hech narsa topilmadi</p>
{% endfor %}
</div>
<style>
.cards-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
.card {
background: #fff;
border-radius: 12px;
padding: 16px 20px;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
transition: transform 0.2s, box-shadow 0.2s;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0,0,0,0.08);
}
.card-row {
margin-bottom: 8px;
font-size: 14px;
color: #374151;
}
.card-row strong {
color: #111827;
}
.card-actions {
margin-top: 12px;
}
.card-actions .btn {
display: inline-block;
padding: 6px 10px;
border-radius: 6px;
text-decoration: none;
color: #fff;
font-weight: bold;
font-size: 13px;
margin-right: 6px;
transition: all 0.2s ease;
}
.card-actions .btn.edit { background: #4f46e5; }
.card-actions .btn.confirm { background: #10b981; }
.card-actions .btn.decline { background: #ef4444; }
.card-actions .btn:hover {
opacity: 0.85;
transform: translateY(-1px);
}
@media (max-width: 640px) {
.card-row { font-size: 13px; }
.card-actions .btn { font-size: 12px; padding: 5px 8px; }
}
@media (max-width: 480px) {
.cards-container { grid-template-columns: 1fr; }
.card { padding: 14px 16px; }
}
</style>
{% endblock %}

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
</body>
</html>

View File

@@ -0,0 +1,34 @@
{% extends "base.html" %}
{% block title %}O'yinchoq harakatlari{% endblock %}
{% block content %}
<h2 style="margin-bottom:20px;">{{ title|default:"O'yinchoq harakatlari" }}</h2>
{% if role == "manager" or role == "businessman" %}
<div class="card-actions">
<a href="{% url 'create_toy_movement_auto' %}" class="btn edit">Yaratish</a>
</div>
<br>
{% endif %}
<div class="cards-container">
{% for tm in toy_movements %}
<div class="card">
<div class="card-row"><strong>Tur:</strong> {{ tm.get_movement_type_display }}</div>
<div class="card-row"><strong>From:</strong> {{ tm.from_warehouse.name }}</div>
{% if tm.to_warehouse %}
<div class="card-row"><strong>To:</strong> {{ tm.to_warehouse.name }}</div>
{% endif %}
{% if tm.device %}
<div class="card-row"><strong>Aparat:</strong> {{ tm.device.name }}</div>
{% endif %}
<div class="card-row"><strong>Miqdor:</strong> {{ tm.quantity }}</div>
<div class="card-row"><strong>Yaratgan:</strong> {{ tm.created_by.get_full_name }}</div>
<div class="card-row"><strong>Sana:</strong> {{ tm.created_at|date:"d.m.Y H:i" }}</div>
</div>
{% empty %}
<p style="text-align:center; font-style:italic;">Hech narsa topilmadi</p>
{% endfor %}
</div>
{% endblock %}

View File

@@ -0,0 +1,28 @@
{% extends "base.html" %}
{% block title %}Foydalanuvchilar{% endblock %}
{% block content %}
<h2 style="margin-bottom:20px;">{{ title|default:"Foydalanuvchilar" }}</h2>
<div class="cards-container">
{% for user in users %}
<div class="card">
<div class="card-row"><strong>Ism:</strong> {{ user.get_full_name }}</div>
<div class="card-row"><strong>Telefon:</strong> {{ user.phone }}</div>
<div class="card-row"><strong>Rol:</strong> {{ user.role }}</div>
{% if user.region %}
<div class="card-row"><strong>Hudud:</strong> {{ user.region.name }}</div>
{% endif %}
{% if role != "manager" or user.role != "manager" %}
<div class="card-actions">
<a href="{% url 'edit_user' user.pk %}" class="btn edit">Tahrirlash</a>
</div>
{% endif %}
</div>
{% empty %}
<p style="text-align:center; font-style:italic;">Hech narsa topilmadi</p>
{% endfor %}
</div>
{% endblock %}

View File

@@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block title %}Omborlar{% endblock %}
{% block content %}
<h2 style="margin-bottom:20px;">{{ title|default:"Omborlar" }}</h2>
<div class="cards-container">
{% for wh in warehouses %}
<div class="card">
<div class="card-row"><strong>Nomi:</strong> {{ wh.name }}</div>
<div class="card-row"><strong>Hudud:</strong> {{ wh.region.name }}</div>
<div class="card-row"><strong>O'yinchoqlar:</strong> {{ wh.toys_count }}</div>
{% if role == "businessman" %}
<div class="card-actions">
<a href="{% url 'edit_warehouse' wh.pk %}" class="btn edit">Tahrirlash</a>
</div>
{% endif %}
</div>
{% empty %}
<p style="text-align:center; font-style:italic;">Hech narsa topilmadi</p>
{% endfor %}
</div>
{% endblock %}

View File

@@ -0,0 +1,105 @@
{% extends "base.html" %}
{% block content %}
<h2 style="text-align:center; margin-bottom:24px; color:#111827;">Hodim Paneli</h2>
<div class="dashboard-grid">
<a href="{% url 'create_income' %}" class="dashboard-card">
<div class="icon">📥</div>
<div class="label">Kirim qo'shish</div>
</a>
<a href="{% url 'create_expense' %}" class="dashboard-card">
<div class="icon">💸</div>
<div class="label">Xarajat qo'shish</div>
</a>
<a href="{% url 'create_toy_movement' %}" class="dashboard-card">
<div class="icon">🚚</div>
<div class="label">Oʻyinchoq harakati yaratish</div>
</a>
</div>
<style>
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 16px;
max-width: 960px;
margin: 0 auto;
padding: 0 10px;
}
.dashboard-card {
background: #fff;
border-radius: 16px;
padding: 20px 12px;
text-align: center;
text-decoration: none;
color: #111827;
font-weight: 600;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
transition: transform 0.2s, box-shadow 0.2s, background 0.2s;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.dashboard-card .icon {
font-size: 32px;
margin-bottom: 10px;
}
.dashboard-card:hover {
transform: translateY(-4px);
box-shadow: 0 6px 20px rgba(0,0,0,0.1);
background: #4f46e5;
color: #fff;
}
.dashboard-card:hover .icon {
color: #fff;
}
.label {
font-size: 14px;
text-align: center;
}
/* Mobile adjustments: keep grid layout, adjust size */
@media (max-width: 768px) {
.dashboard-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 12px;
}
.dashboard-card {
padding: 16px 10px;
}
.dashboard-card .icon {
font-size: 28px;
margin-bottom: 8px;
}
.label {
font-size: 13px;
}
}
@media (max-width: 480px) {
.dashboard-grid {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px;
}
.dashboard-card {
padding: 14px 8px;
}
.dashboard-card .icon {
font-size: 24px;
margin-bottom: 6px;
}
.label {
font-size: 12px;
}
}
</style>
{% endblock %}

View File

@@ -0,0 +1,112 @@
{% extends "base.html" %}
{% block content %}
<h2 style="text-align:center; margin-bottom:24px; color:#111827;">Manager Paneli</h2>
<div class="dashboard-grid">
<a href="{% url 'create_user' %}" class="dashboard-card">
<div class="icon">👤</div>
<div class="label">Foydalanuvchi yaratish</div>
</a>
<a href="{% url 'create_device' %}" class="dashboard-card">
<div class="icon">💻</div>
<div class="label">Aparat yaratish</div>
</a>
<a href="{% url 'create_expense' %}" class="dashboard-card">
<div class="icon">💸</div>
<div class="label">Xarajat qo'shish</div>
</a>
<a href="{% url 'create_income' %}" class="dashboard-card">
<div class="icon">📥</div>
<div class="label">Kirim qo'shish</div>
</a>
<a href="{% url 'create_toy_movement' %}" class="dashboard-card">
<div class="icon">🚚</div>
<div class="label">Oʻyinchoq harakati yaratish</div>
</a>
</div>
<style>
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 16px;
max-width: 960px;
margin: 0 auto;
padding: 0 10px;
}
.dashboard-card {
background: #fff;
border-radius: 16px;
padding: 20px 12px;
text-align: center;
text-decoration: none;
color: #111827;
font-weight: 600;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
transition: transform 0.2s, box-shadow 0.2s, background 0.2s;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.dashboard-card .icon {
font-size: 32px;
margin-bottom: 10px;
}
.dashboard-card:hover {
transform: translateY(-4px);
box-shadow: 0 6px 20px rgba(0,0,0,0.1);
background: #4f46e5;
color: #fff;
}
.dashboard-card:hover .icon {
color: #fff;
}
.label {
font-size: 14px;
text-align: center;
}
/* Mobile adjustments: keep grid layout, adjust size */
@media (max-width: 768px) {
.dashboard-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 12px;
}
.dashboard-card {
padding: 16px 10px;
}
.dashboard-card .icon {
font-size: 28px;
margin-bottom: 8px;
}
.label {
font-size: 13px;
}
}
@media (max-width: 480px) {
.dashboard-grid {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px;
}
.dashboard-card {
padding: 14px 8px;
}
.dashboard-card .icon {
font-size: 24px;
margin-bottom: 6px;
}
.label {
font-size: 12px;
}
}
</style>
{% endblock %}

View File

@@ -0,0 +1 @@
from .custom_filters import *

View File

@@ -0,0 +1,16 @@
from django import template
register = template.Library()
@register.filter
def attr(obj, name):
value = getattr(obj, name, None)
# use the display method if it exists (for TextChoices)
get_display = getattr(obj, f"get_{name}_display", None)
if callable(get_display):
return get_display()
return value
@register.filter
def get_item(dictionary, key):
return dictionary.get(key)

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1 @@
from .field_translations import *

View File

@@ -0,0 +1,14 @@
FIELD_TRANSLATIONS_UZ = {
"movement_type": "Harakat turi",
"from_warehouse": "Ombor (chiqish)",
"to_warehouse": "Ombor (kirish)",
"device": "Aparat",
"quantity": "Oʻyinchoqlar soni",
"created_by": "Yaratgan",
"created_at": "Yaratilgan sana",
"name": "Nomi",
"region": "Viloyat",
"district": "Tuman",
"toys_count": "Oʻyinchoqlar soni",
"monthly_fee": "Oylik toʻlov"
}

View File

@@ -0,0 +1,39 @@
from django.urls import path
from . import views
urlpatterns = [
path("businessman/dashboard/", views.businessman_dashboard, name="businessman_dashboard"),
path("manager/dashboard/", views.manager_dashboard, name="manager_dashboard"),
path('employee/dashboard/', views.employee_dashboard, name='employee_dashboard'),
# Create
path("create/device/", views.create_device, name="create_device"),
path("create/income/", views.create_income, name="create_income"),
path("create/expense/", views.create_expense, name="create_expense"),
path("create/warehouse/", views.create_warehouse, name="create_warehouse"),
path("create/user/", views.create_user, name="create_user"),
path("create/toy-movement/", views.create_toy_movement, name="create_toy_movement"),
path("create/toy-movement/auto/", views.create_toy_movement_auto, name="create_toy_movement_auto"),
# # List
path("list/device/", views.device_list, name="device_list"),
path("list/income/", views.income_list, name="income_list"),
path("list/expense/", views.expense_list, name="expense_list"),
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"),
# Edit
path("edit/device/<int:pk>/", views.edit_device, name="edit_device"),
path("edit/income/<int:pk>/", views.edit_income, name="edit_income"),
path("edit/expense/<int:pk>/", views.edit_expense, name="edit_expense"),
path("edit/warehouse/<int:pk>/", views.edit_warehouse, name="edit_warehouse"),
path("edit/user/<int:pk>/", views.edit_user, name="edit_user"),
# path("edit/toy-movement/<int:pk>/", views.edit_toy_movement, name="edit_toy_movement"),
#Expense Confirm or Decline
path("expense/confirm/<int:pk>/", views.confirm_expense, name="confirm_expense"),
path("expense/decline/<int:pk>/", views.decline_expense, name="decline_expense"),
]

View File

@@ -0,0 +1,4 @@
from .businessman_dashboard import *
from .manager_dashboard import *
from .employee_dashboard import *
from .common import *

View File

@@ -0,0 +1,11 @@
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from core.apps.management.models import Device, Income, Expense, Warehouse, ToyMovement
from core.apps.accounts.models import User
from core.apps.management.decorators import role_required
from ..translations import FIELD_TRANSLATIONS_UZ
@login_required
@role_required(["businessman"])
def businessman_dashboard(request):
return render(request, "businessman/businessman_dashboard.html", {"role": request.user.role})

View File

@@ -0,0 +1,5 @@
from .create import *
from .list import *
from .edit import *
from .confirm_expense import *
from .decline_expense import *

View File

@@ -0,0 +1,17 @@
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, redirect
from ...models import Expense
from core.apps.management.decorators import role_required
@login_required
@role_required(["manager", "businessman"])
def confirm_expense(request, pk):
expense = get_object_or_404(Expense, pk=pk)
if not expense.is_confirmed:
expense.is_confirmed = True
expense.confirmed_by = request.user
expense.save(update_fields=["is_confirmed", "confirmed_by"])
return redirect(request.META.get("HTTP_REFERER", "/"))

View File

@@ -0,0 +1,220 @@
from core.apps.management.forms import DeviceForm, IncomeForm, WarehouseForm, UserCreateForm, ExpenseFormEmployee, \
ExpenseFormManager, ExpenseFormBusinessman
from django.db import transaction
from django.shortcuts import render, redirect
from core.apps.management.forms import ToyMovementForm, ToyMovementFormEmployee
from django.contrib.auth.decorators import login_required
from core.apps.management.decorators import role_required
from core.apps.management.forms import UserCreateFormManagerToEmployee, UserCreateFormBusinessman
from core.apps.management.models import ToyMovement
@login_required
@role_required(["manager", "businessman"])
def create_device(request):
form = DeviceForm(request.POST or None, user=request.user)
if form.is_valid():
form.save()
return redirect("dashboard")
return render(request, "common/create/device_create.html", {
"form": form,
"title": "Aparat Yaratish"
})
@login_required
def create_income(request):
if request.method == "POST":
form = IncomeForm(request.POST, user=request.user)
if form.is_valid():
income = form.save(commit=False)
if request.user.role == "employee":
income.amount = None
income.save()
return redirect("income_list")
else:
form = IncomeForm(user=request.user)
return render(request, "common/create/income_create.html", {"form": form})
@login_required
def create_expense(request):
user = request.user
# select form based on role
if user.role == "employee":
form_class = ExpenseFormEmployee
elif user.role == "manager":
form_class = ExpenseFormManager
else: # businessman or superuser
form_class = ExpenseFormBusinessman
if request.method == "POST":
form = form_class(request.POST)
if form.is_valid():
with transaction.atomic():
expense = form.save(commit=False)
expense.created_by = user
# AUTO CONFIRM for manager & businessman
if user.role in ["manager", "businessman"]:
expense.is_confirmed = True
expense.confirmed_by = user
expense.save()
return redirect("dashboard")
else:
form = form_class()
return render(request, "common/create/expense_create.html", {
"form": form,
"title": "Xarajat qoʻshish",
"user_role": user.role
})
@login_required
@role_required(["businessman"])
def create_warehouse(request):
form = WarehouseForm(request.POST or None)
if form.is_valid():
form.save()
return redirect("businessman_dashboard")
return render(request, "common/create/warehouse_create.html", {
"form": form,
"title": "Sklad Yaratish"
})
@login_required
@role_required(["manager", "businessman"])
def create_user(request):
if request.user.role == "businessman":
form_class = UserCreateFormBusinessman
form_kwargs = {}
redirect_to = "businessman_dashboard"
else: # manager
form_class = UserCreateFormManagerToEmployee
form_kwargs = {"manager": request.user}
redirect_to = "manager_dashboard"
form = form_class(request.POST or None, **form_kwargs)
if form.is_valid():
form.save()
return redirect(redirect_to)
return render(request, "common/create/user_create.html", {
"form": form,
"title": "Foydalanuvchi yaratish",
})
@login_required
def create_toy_movement(request):
user = request.user
# Choose form based on role
form_class = ToyMovementFormEmployee if user.role == "employee" else ToyMovementForm
if request.method == "POST":
form = form_class(request.POST, user=user)
if form.is_valid():
with transaction.atomic():
movement = form.save(commit=False)
# Stock validation
from_wh = movement.from_warehouse
if from_wh.toys_count < movement.quantity:
form.add_error("quantity", "Not enough toys in warehouse.")
return render(
request,
"common/create/toy_movement_create.html",
{"form": form, "user_role": user.role}
)
# Deduct from source warehouse
from_wh.toys_count -= movement.quantity
from_wh.save()
# Add to destination warehouse if moving between warehouses
if movement.movement_type == "between_warehouses" and movement.to_warehouse:
to_wh = movement.to_warehouse
to_wh.toys_count += movement.quantity
to_wh.save()
# Set creator
movement.created_by = user
movement.save()
return redirect("dashboard")
else:
form = form_class(user=user)
return render(
request,
"common/create/toy_movement_create.html",
{"form": form, "user_role": user.role}
)
@login_required
@role_required(["manager", "businessman"])
def create_toy_movement_auto(request):
user = request.user
# Only employees can use this auto-creation
if request.method == "POST":
# We force movement_type to "between_warehouses"
movement = ToyMovement(
movement_type="between_warehouses",
from_warehouse=user.warehouse,
to_warehouse_id=request.POST.get("to_warehouse"),
device=None, # not used for between_warehouses
quantity=int(request.POST.get("quantity", 0)),
created_by=user
)
# Stock validation
from_wh = movement.from_warehouse
if from_wh.toys_count < movement.quantity:
form = ToyMovementForm(user=user) # just render empty form with message
return render(
request,
"common/create/toy_movement_create.html",
{
"form": form,
"user_role": user.role,
"error": "Not enough toys in warehouse."
}
)
with transaction.atomic():
# Update warehouse stock
from_wh.toys_count -= movement.quantity
from_wh.save()
# Save movement
movement.save()
return redirect("dashboard")
# GET request → render the create form
form = ToyMovementForm(user=user)
# Pre-fill movement_type and from_warehouse
form.fields["movement_type"].initial = "between_warehouses"
form.fields["from_warehouse"].initial = user.warehouse
# Optionally, disable editing these fields
form.fields["movement_type"].widget.attrs["readonly"] = True
form.fields["from_warehouse"].widget.attrs["readonly"] = True
return render(
request,
"common/create/toy_movement_create.html",
{"form": form, "user_role": user.role}
)

View File

@@ -0,0 +1,14 @@
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, redirect
from ...models import Expense
from core.apps.management.decorators import role_required
@login_required
@role_required(["manager", "businessman"])
def decline_expense(request, pk):
expense = get_object_or_404(Expense, pk=pk)
if not expense.is_confirmed:
expense.delete()
return redirect(request.META.get("HTTP_REFERER", "/"))

Some files were not shown because too many files have changed in this diff Show More