first commit
This commit is contained in:
0
core/__init__.py
Normal file
0
core/__init__.py
Normal file
0
core/apps/__init__.py
Normal file
0
core/apps/__init__.py
Normal file
0
core/apps/accounts/__init__.py
Normal file
0
core/apps/accounts/__init__.py
Normal file
42
core/apps/accounts/admin.py
Normal file
42
core/apps/accounts/admin.py
Normal 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)
|
||||
6
core/apps/accounts/apps.py
Normal file
6
core/apps/accounts/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AccountsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'core.apps.accounts'
|
||||
9
core/apps/accounts/choices.py
Normal file
9
core/apps/accounts/choices.py
Normal 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"
|
||||
37
core/apps/accounts/forms.py
Normal file
37
core/apps/accounts/forms.py
Normal 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)
|
||||
22
core/apps/accounts/managers.py
Normal file
22
core/apps/accounts/managers.py
Normal 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)
|
||||
43
core/apps/accounts/migrations/0001_initial.py
Normal file
43
core/apps/accounts/migrations/0001_initial.py
Normal 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,
|
||||
},
|
||||
),
|
||||
]
|
||||
28
core/apps/accounts/migrations/0002_initial.py
Normal file
28
core/apps/accounts/migrations/0002_initial.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
20
core/apps/accounts/migrations/0003_user_warehouse.py
Normal file
20
core/apps/accounts/migrations/0003_user_warehouse.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
0
core/apps/accounts/migrations/__init__.py
Normal file
0
core/apps/accounts/migrations/__init__.py
Normal file
43
core/apps/accounts/models.py
Normal file
43
core/apps/accounts/models.py
Normal 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
|
||||
147
core/apps/accounts/templates/auth/login.html
Normal file
147
core/apps/accounts/templates/auth/login.html
Normal 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>
|
||||
3
core/apps/accounts/tests.py
Normal file
3
core/apps/accounts/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
8
core/apps/accounts/urls.py
Normal file
8
core/apps/accounts/urls.py
Normal 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'),
|
||||
]
|
||||
43
core/apps/accounts/views.py
Normal file
43
core/apps/accounts/views.py
Normal 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
2
core/apps/logs/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
0
core/apps/management/__init__.py
Normal file
0
core/apps/management/__init__.py
Normal file
85
core/apps/management/admin.py
Normal file
85
core/apps/management/admin.py
Normal 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",)
|
||||
6
core/apps/management/apps.py
Normal file
6
core/apps/management/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ModuleConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'core.apps.management'
|
||||
5
core/apps/management/choice/ToyMovementType.py
Normal file
5
core/apps/management/choice/ToyMovementType.py
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
TOY_MOVEMENT_TYPE = [
|
||||
("from_warehouse", "Ombordan → Aparatga"),
|
||||
("between_warehouses", "Ombordan → Omborga"),
|
||||
]
|
||||
1
core/apps/management/choice/__init__.py
Normal file
1
core/apps/management/choice/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .ToyMovementType import *
|
||||
16
core/apps/management/decorators.py
Normal file
16
core/apps/management/decorators.py
Normal 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
|
||||
19
core/apps/management/forms/DeviceForm.py
Normal file
19
core/apps/management/forms/DeviceForm.py
Normal 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()
|
||||
62
core/apps/management/forms/ExpenseForm.py
Normal file
62
core/apps/management/forms/ExpenseForm.py
Normal 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
|
||||
32
core/apps/management/forms/IncomeForm.py
Normal file
32
core/apps/management/forms/IncomeForm.py
Normal 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
|
||||
11
core/apps/management/forms/RentForm.py
Normal file
11
core/apps/management/forms/RentForm.py
Normal 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}),
|
||||
}
|
||||
21
core/apps/management/forms/ToyMovementEmployeeForm.py
Normal file
21
core/apps/management/forms/ToyMovementEmployeeForm.py
Normal 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
|
||||
30
core/apps/management/forms/ToyMovementForm.py
Normal file
30
core/apps/management/forms/ToyMovementForm.py
Normal 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
|
||||
78
core/apps/management/forms/UserCreateForm.py
Normal file
78
core/apps/management/forms/UserCreateForm.py
Normal 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
|
||||
7
core/apps/management/forms/WarehouseForm.py
Normal file
7
core/apps/management/forms/WarehouseForm.py
Normal 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"]
|
||||
8
core/apps/management/forms/__init__.py
Normal file
8
core/apps/management/forms/__init__.py
Normal 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 *
|
||||
50
core/apps/management/forms/user/BaseUserForm.py
Normal file
50
core/apps/management/forms/user/BaseUserForm.py
Normal 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
|
||||
40
core/apps/management/forms/user/UserCreateFormBusinessman.py
Normal file
40
core/apps/management/forms/user/UserCreateFormBusinessman.py
Normal 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
|
||||
@@ -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
|
||||
31
core/apps/management/forms/user/UserEditFormBusinessman.py
Normal file
31
core/apps/management/forms/user/UserEditFormBusinessman.py
Normal 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
|
||||
@@ -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
|
||||
5
core/apps/management/forms/user/__init__.py
Normal file
5
core/apps/management/forms/user/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .BaseUserForm import *
|
||||
from .UserEditFormBusinessman import *
|
||||
from .UserCreateFormBusinessman import *
|
||||
from .UserCreateFormManagerToEmployee import *
|
||||
from .UserEditFormManagerToEmployee import *
|
||||
79
core/apps/management/migrations/0001_initial.py
Normal file
79
core/apps/management/migrations/0001_initial.py
Normal 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')},
|
||||
),
|
||||
]
|
||||
@@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
]
|
||||
18
core/apps/management/migrations/0004_alter_device_name.py
Normal file
18
core/apps/management/migrations/0004_alter_device_name.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
@@ -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 to‘lovlar'), ('maintenance', 'Texnik xizmat'), ('other', 'Boshqa')], default='other', max_length=20),
|
||||
),
|
||||
]
|
||||
@@ -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 to‘lovlar'), ('maintenance', 'Texnik xizmat'), ('food', 'Oziq-ovqat'), ('transport', "Yo'lkira"), ('other', 'Boshqa')], default='other', max_length=20),
|
||||
),
|
||||
]
|
||||
@@ -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 to‘lovlar'), ('maintenance', 'Texnik xizmat'), ('food', 'Oziq-ovqat'), ('transport', "Yo'lkira"), ('buy_toys', 'Oʻyinchoqlar sotib olish'), ('other', 'Boshqa')], default='other', max_length=20),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
18
core/apps/management/migrations/0014_alter_income_amount.py
Normal file
18
core/apps/management/migrations/0014_alter_income_amount.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
0
core/apps/management/migrations/__init__.py
Normal file
0
core/apps/management/migrations/__init__.py
Normal file
8
core/apps/management/models/__init__.py
Normal file
8
core/apps/management/models/__init__.py
Normal 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 *
|
||||
10
core/apps/management/models/device.py
Normal file
10
core/apps/management/models/device.py
Normal 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
|
||||
16
core/apps/management/models/district.py
Normal file
16
core/apps/management/models/district.py
Normal 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}"
|
||||
47
core/apps/management/models/expense.py
Normal file
47
core/apps/management/models/expense.py
Normal 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 to‘lovlar"
|
||||
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."})
|
||||
10
core/apps/management/models/income.py
Normal file
10
core/apps/management/models/income.py
Normal 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)
|
||||
7
core/apps/management/models/region.py
Normal file
7
core/apps/management/models/region.py
Normal 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
|
||||
15
core/apps/management/models/rent.py
Normal file
15
core/apps/management/models/rent.py
Normal 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
|
||||
23
core/apps/management/models/toyMovement.py
Normal file
23
core/apps/management/models/toyMovement.py
Normal 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)
|
||||
15
core/apps/management/models/warehouse.py
Normal file
15
core/apps/management/models/warehouse.py
Normal 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
|
||||
239
core/apps/management/templates/base.html
Normal file
239
core/apps/management/templates/base.html
Normal 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>
|
||||
@@ -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 %}
|
||||
147
core/apps/management/templates/common/create/device_create.html
Normal file
147
core/apps/management/templates/common/create/device_create.html
Normal 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 %}
|
||||
165
core/apps/management/templates/common/create/expense_create.html
Normal file
165
core/apps/management/templates/common/create/expense_create.html
Normal 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 %}
|
||||
150
core/apps/management/templates/common/create/income_create.html
Normal file
150
core/apps/management/templates/common/create/income_create.html
Normal 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 %}
|
||||
@@ -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 %}
|
||||
184
core/apps/management/templates/common/create/user_create.html
Normal file
184
core/apps/management/templates/common/create/user_create.html
Normal 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 %}
|
||||
@@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
147
core/apps/management/templates/common/edit/device_edit.html
Normal file
147
core/apps/management/templates/common/edit/device_edit.html
Normal 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 %}
|
||||
139
core/apps/management/templates/common/edit/expense_edit.html
Normal file
139
core/apps/management/templates/common/edit/expense_edit.html
Normal 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 %}
|
||||
98
core/apps/management/templates/common/edit/income_edit.html
Normal file
98
core/apps/management/templates/common/edit/income_edit.html
Normal 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 %}
|
||||
@@ -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 %}
|
||||
183
core/apps/management/templates/common/edit/user_edit.html
Normal file
183
core/apps/management/templates/common/edit/user_edit.html
Normal 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 %}
|
||||
@@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
33
core/apps/management/templates/common/list.html
Normal file
33
core/apps/management/templates/common/list.html
Normal 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>
|
||||
24
core/apps/management/templates/common/lists/device_list.html
Normal file
24
core/apps/management/templates/common/lists/device_list.html
Normal 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 %}
|
||||
@@ -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 %}
|
||||
92
core/apps/management/templates/common/lists/income_list.html
Normal file
92
core/apps/management/templates/common/lists/income_list.html
Normal 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 %}
|
||||
10
core/apps/management/templates/common/lists/rent_list.html
Normal file
10
core/apps/management/templates/common/lists/rent_list.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -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 %}
|
||||
28
core/apps/management/templates/common/lists/user_list.html
Normal file
28
core/apps/management/templates/common/lists/user_list.html
Normal 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 %}
|
||||
@@ -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 %}
|
||||
105
core/apps/management/templates/employee/employee_dashboard.html
Normal file
105
core/apps/management/templates/employee/employee_dashboard.html
Normal 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 %}
|
||||
112
core/apps/management/templates/manager/manager_dashboard.html
Normal file
112
core/apps/management/templates/manager/manager_dashboard.html
Normal 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 %}
|
||||
1
core/apps/management/templatetags/__init__.py
Normal file
1
core/apps/management/templatetags/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .custom_filters import *
|
||||
16
core/apps/management/templatetags/custom_filters.py
Normal file
16
core/apps/management/templatetags/custom_filters.py
Normal 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)
|
||||
3
core/apps/management/tests.py
Normal file
3
core/apps/management/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
1
core/apps/management/translations/__init__.py
Normal file
1
core/apps/management/translations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .field_translations import *
|
||||
14
core/apps/management/translations/field_translations.py
Normal file
14
core/apps/management/translations/field_translations.py
Normal 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"
|
||||
}
|
||||
39
core/apps/management/urls.py
Normal file
39
core/apps/management/urls.py
Normal 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"),
|
||||
]
|
||||
4
core/apps/management/views/__init__.py
Normal file
4
core/apps/management/views/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from .businessman_dashboard import *
|
||||
from .manager_dashboard import *
|
||||
from .employee_dashboard import *
|
||||
from .common import *
|
||||
11
core/apps/management/views/businessman_dashboard.py
Normal file
11
core/apps/management/views/businessman_dashboard.py
Normal 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})
|
||||
5
core/apps/management/views/common/__init__.py
Normal file
5
core/apps/management/views/common/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .create import *
|
||||
from .list import *
|
||||
from .edit import *
|
||||
from .confirm_expense import *
|
||||
from .decline_expense import *
|
||||
17
core/apps/management/views/common/confirm_expense.py
Normal file
17
core/apps/management/views/common/confirm_expense.py
Normal 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", "/"))
|
||||
220
core/apps/management/views/common/create.py
Normal file
220
core/apps/management/views/common/create.py
Normal 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}
|
||||
)
|
||||
14
core/apps/management/views/common/decline_expense.py
Normal file
14
core/apps/management/views/common/decline_expense.py
Normal 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
Reference in New Issue
Block a user