diff --git a/config/urls.py b/config/urls.py index cf9cb73..eb11c88 100644 --- a/config/urls.py +++ b/config/urls.py @@ -10,6 +10,7 @@ from django.views.static import serve from config.env import env + def home(request): return HttpResponse("OK") @@ -20,17 +21,12 @@ urlpatterns = [ path("", include("core.apps.shared.urls")), path("", include("core.apps.management.urls")), ] -urlpatterns += [ - path("admin/", admin.site.urls), - # path("accounts/", include("django.contrib.auth.urls")), - path("i18n/", include("django.conf.urls.i18n")), -] +urlpatterns += [path("admin/", admin.site.urls), path("i18n/", include("django.conf.urls.i18n"))] if env.bool("SILK_ENABLED", False): urlpatterns += [] if env.str("PROJECT_ENV") == "debug": - urlpatterns += [ - ] + urlpatterns += [] urlpatterns += [ re_path("static/(?P.*)", serve, {"document_root": settings.STATIC_ROOT}), re_path("media/(?P.*)", serve, {"document_root": settings.MEDIA_ROOT}), -] \ No newline at end of file +] diff --git a/core/apps/accounts/employee.py b/core/apps/accounts/employee.py new file mode 100644 index 0000000..0f0ac8a --- /dev/null +++ b/core/apps/accounts/employee.py @@ -0,0 +1,10 @@ +from django.db import models + + +class Employee(models.Model): + user = models.ForeignKey("accounts.User", on_delete=models.PROTECT) + device = models.ManyToManyField("management.Device") + + + def __str__(self): + return self.user.first_name \ No newline at end of file diff --git a/core/apps/accounts/migrations/0004_alter_user_region_alter_user_warehouse.py b/core/apps/accounts/migrations/0004_alter_user_region_alter_user_warehouse.py new file mode 100644 index 0000000..a429c0a --- /dev/null +++ b/core/apps/accounts/migrations/0004_alter_user_region_alter_user_warehouse.py @@ -0,0 +1,25 @@ +# Generated by Django 5.2.7 on 2026-02-17 11:44 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0003_user_warehouse'), + ('management', '0022_alter_district_region'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='region', + field=models.ForeignKey(blank=True, help_text='Faqat menejerlar uchun', null=True, on_delete=django.db.models.deletion.SET_NULL, to='management.region'), + ), + migrations.AlterField( + model_name='user', + name='warehouse', + field=models.ForeignKey(blank=True, help_text='Faqat xodimlar uchun', null=True, on_delete=django.db.models.deletion.PROTECT, to='management.warehouse'), + ), + ] diff --git a/core/apps/accounts/models.py b/core/apps/accounts/models.py index a5b9b29..bff704d 100644 --- a/core/apps/accounts/models.py +++ b/core/apps/accounts/models.py @@ -22,7 +22,7 @@ class User(auth_models.AbstractUser): on_delete=models.SET_NULL, null=True, blank=True, - help_text="Only for managers" + help_text="Faqat menejerlar uchun" ) warehouse = models.ForeignKey( @@ -30,7 +30,7 @@ class User(auth_models.AbstractUser): on_delete=models.PROTECT, null=True, blank=True, - help_text="Only for employees" + help_text="Faqat xodimlar uchun" ) def get_full_name(self): diff --git a/core/apps/management/forms/DeviceForm.py b/core/apps/management/forms/DeviceForm.py index 571fc8b..df5de07 100644 --- a/core/apps/management/forms/DeviceForm.py +++ b/core/apps/management/forms/DeviceForm.py @@ -1,19 +1,31 @@ from django import forms from ..models import Device, District + class DeviceForm(forms.ModelForm): class Meta: model = Device - fields = ["address", "district"] + fields = ["address", "district", "amount", "due_date"] + labels = { + "address": "Manzil", + "district": "Tuman", + "amount": "Summa", + "due_date": "To‘lov muddati", + } + widgets = { + "due_date": forms.DateInput( + attrs={"type": "date"} + ) + } def __init__(self, *args, **kwargs): - user = kwargs.pop('user', None) # get the user from kwargs + user = kwargs.pop("user", None) super().__init__(*args, **kwargs) - if user is not None: + if user: if user.role == "manager": - # Manager: only districts in the same region - self.fields['district'].queryset = District.objects.filter(region=user.region) + self.fields["district"].queryset = District.objects.filter( + region=user.region + ) else: - # Businessman: show all districts - self.fields['district'].queryset = District.objects.all() \ No newline at end of file + self.fields["district"].queryset = District.objects.all() diff --git a/core/apps/management/forms/DevicePaymentForm.py b/core/apps/management/forms/DevicePaymentForm.py new file mode 100644 index 0000000..00f6871 --- /dev/null +++ b/core/apps/management/forms/DevicePaymentForm.py @@ -0,0 +1,24 @@ +from django import forms +from ..models import Device + + +class DevicePaymentForm(forms.ModelForm): + class Meta: + model = Device + fields = ["is_paid"] # 🔥 Only this field + + widgets = { + "is_paid": forms.CheckboxInput(attrs={"class": "form-check-input"}) + } + + def save(self, commit=True): + instance = super().save(commit=False) + + # 🔥 Ensure employee can ONLY mark as True + if not instance.is_paid: + instance.is_paid = True + + if commit: + instance.save() + + return instance diff --git a/core/apps/management/forms/ExpenseForm.py b/core/apps/management/forms/ExpenseForm.py index 9723d0c..abd7b79 100644 --- a/core/apps/management/forms/ExpenseForm.py +++ b/core/apps/management/forms/ExpenseForm.py @@ -2,42 +2,75 @@ from django import forms from ..models import Expense, Device from core.apps.accounts.models import User -# Base form + +# ========================= +# Base Form +# ========================= class BaseExpenseForm(forms.ModelForm): + class Meta: model = Expense - fields = ["amount", "expense_type", "employee", "device"] + fields = ["amount", "expense_type", "employee", "device", "comment"] + labels = { + "amount": "Summa", + "expense_type": "Xarajat turi", + "employee": "Xodim", + "device": "Qurilma", + "comment": "Izoh", + } + widgets = { + "comment": forms.Textarea(attrs={"rows": 3}) + } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + # Show all devices self.fields["device"].queryset = Device.objects.all() - # Show all employees + + # Show only employees self.fields["employee"].queryset = User.objects.filter(role="employee") + # Comment is optional unless expense_type == other + self.fields["comment"].required = False + def clean(self): cleaned_data = super().clean() + expense_type = cleaned_data.get("expense_type") employee = cleaned_data.get("employee") device = cleaned_data.get("device") + comment = cleaned_data.get("comment") + + # OTHER requires comment + if expense_type == Expense.ExpenseType.OTHER and not comment: + self.add_error("comment", "Iltimos 'boshqa' tanlovi uchun izoh qoldiring!") # 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.") + # Maintenance requires device + if expense_type == Expense.ExpenseType.MAINTENANCE and not device: + self.add_error("device", "Device must be set for Maintenance expenses.") return cleaned_data -# Employee form: cannot create Salary or Buy Toys +# ========================= +# Employee Form +# ========================= 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] + + # Employee cannot create Salary or Buy Toys + forbidden = [ + Expense.ExpenseType.SALARY, + Expense.ExpenseType.BUY_TOYS, + ] + self.fields["expense_type"].choices = [ (value, label) for value, label in Expense.ExpenseType.choices @@ -45,11 +78,19 @@ class ExpenseFormEmployee(BaseExpenseForm): ] -# Manager form: cannot create Buy Toys +# ========================= +# Manager Form +# ========================= class ExpenseFormManager(BaseExpenseForm): + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - forbidden = [Expense.ExpenseType.BUY_TOYS] + + # Manager cannot create Buy Toys + forbidden = [ + Expense.ExpenseType.BUY_TOYS, + ] + self.fields["expense_type"].choices = [ (value, label) for value, label in Expense.ExpenseType.choices @@ -57,6 +98,8 @@ class ExpenseFormManager(BaseExpenseForm): ] -# Businessman form: full access +# ========================= +# Businessman Form +# ========================= class ExpenseFormBusinessman(BaseExpenseForm): - pass \ No newline at end of file + pass diff --git a/core/apps/management/forms/IncomeForm.py b/core/apps/management/forms/IncomeForm.py index 8aebfb0..d481225 100644 --- a/core/apps/management/forms/IncomeForm.py +++ b/core/apps/management/forms/IncomeForm.py @@ -1,10 +1,11 @@ from django import forms -from ..models import Income, Device +from ..models import Income, Device, Warehouse + class IncomeForm(forms.ModelForm): class Meta: model = Income - fields = ["device", "amount"] + fields = ["device", "amount", "warehouse"] def __init__(self, *args, **kwargs): self.user = kwargs.pop("user", None) @@ -13,8 +14,10 @@ class IncomeForm(forms.ModelForm): 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["warehouse"].queryset = Warehouse.objects.all() + elif self.user.role == "manager": + self.fields["warehouse"].queryset = Warehouse.objects.filter(region=self.user.region) + elif self.user.role == "employee": self.fields["device"].queryset = Device.objects.filter(district__region=self.user.region) # Remove amount for employees diff --git a/core/apps/management/forms/ReportForm.py b/core/apps/management/forms/ReportForm.py new file mode 100644 index 0000000..1690c7a --- /dev/null +++ b/core/apps/management/forms/ReportForm.py @@ -0,0 +1,61 @@ +from django import forms +from core.apps.management.models import Device + + +class ReportForm(forms.Form): + device = forms.ModelChoiceField( + queryset=Device.objects.all(), + label="Qurilma", + required=True, + widget=forms.Select(attrs={ + 'class': 'form-select', + }), + error_messages={ + 'required': 'Qurilmani tanlang', + 'invalid_choice': 'Tanlangan qurilma topilmadi' + } + ) + + quantity = forms.IntegerField( + min_value=1, + label="So'nggi miqdor", + required=True, + widget=forms.NumberInput(attrs={ + 'class': 'form-input', + 'placeholder': 'Miqdorni kiriting', + 'min': '1' + }), + error_messages={ + 'required': 'Miqdorni kiriting', + 'invalid': 'Miqdor raqam bo\'lishi kerak', + 'min_value': 'Miqdor 1 dan katta bo\'lishi kerak' + } + ) + + def __init__(self, *args, **kwargs): + user = kwargs.pop("user", None) + super().__init__(*args, **kwargs) + self.user = user + + # ✅ SHOW ALL DEVICES - don't filter by warehouse + # Reports need to see all devices, not just warehouse devices + self.fields["device"].queryset = Device.objects.all().order_by('address') + + # Show count for debugging + device_count = self.fields["device"].queryset.count() + if device_count == 0: + self.fields["device"].help_text = "⚠️ Hech qanday qurilma topilmadi. Admindan so'rab qurilmalar qo'shing." + + def clean_device(self): + device = self.cleaned_data.get('device') + if not device: + raise forms.ValidationError("Qurilmani tanlang") + return device + + def clean_quantity(self): + quantity = self.cleaned_data.get('quantity') + if quantity is None: + raise forms.ValidationError("Miqdorni kiriting") + if quantity < 1: + raise forms.ValidationError("Miqdor 1 dan katta bo'lishi kerak") + return quantity \ No newline at end of file diff --git a/core/apps/management/forms/ToyMovementForm.py b/core/apps/management/forms/ToyMovementForm.py index 4c4552c..ba47ecf 100644 --- a/core/apps/management/forms/ToyMovementForm.py +++ b/core/apps/management/forms/ToyMovementForm.py @@ -2,10 +2,18 @@ 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"] + labels = { + "movement_type": "Harakat turi", + "from_warehouse": "Qaysi ombordan", + "to_warehouse": "Qaysi omborga", + "device": "Qurilma", + "quantity": "Soni", + } widgets = { "movement_type": forms.Select(choices=TOY_MOVEMENT_TYPE), } @@ -13,18 +21,36 @@ class ToyMovementForm(forms.ModelForm): 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) + def clean(self): + cleaned_data = super().clean() - if instance.movement_type == "from_warehouse": - instance.to_warehouse = None - elif instance.movement_type == "between_warehouses": - instance.device = None + movement_type = cleaned_data.get("movement_type") + from_wh = cleaned_data.get("from_warehouse") + to_wh = cleaned_data.get("to_warehouse") + device = cleaned_data.get("device") + quantity = cleaned_data.get("quantity") - if commit: - instance.save() - return instance \ No newline at end of file + if not from_wh: + self.add_error("from_warehouse", "Source warehouse is required.") + + if quantity and quantity <= 0: + self.add_error("quantity", "Quantity must be greater than 0.") + + if movement_type == "from_warehouse": + if not device: + self.add_error("device", "Device is required for this movement type.") + cleaned_data["to_warehouse"] = None + + if movement_type == "between_warehouses": + if not to_wh: + self.add_error("to_warehouse", "Destination warehouse required.") + if from_wh and to_wh and from_wh == to_wh: + self.add_error("to_warehouse", "Cannot move to the same warehouse.") + cleaned_data["device"] = None + + return cleaned_data diff --git a/core/apps/management/forms/WarehouseForm.py b/core/apps/management/forms/WarehouseForm.py index 559a4a1..0c37505 100644 --- a/core/apps/management/forms/WarehouseForm.py +++ b/core/apps/management/forms/WarehouseForm.py @@ -4,4 +4,9 @@ from ..models import Warehouse class WarehouseForm(forms.ModelForm): class Meta: model = Warehouse - fields = ["name", "region", "toys_count"] \ No newline at end of file + fields = ["name", "region", "toys_count"] + labels = { + "name": "Nomi", + "region": "Hudud", + "toys_count": "O‘yinchoqlar soni", + } \ No newline at end of file diff --git a/core/apps/management/forms/__init__.py b/core/apps/management/forms/__init__.py index e924d8f..691acfb 100644 --- a/core/apps/management/forms/__init__.py +++ b/core/apps/management/forms/__init__.py @@ -5,4 +5,6 @@ from .WarehouseForm import * from .UserCreateForm import * from .ToyMovementEmployeeForm import ToyMovementFormEmployee from .ToyMovementForm import ToyMovementForm -from .user import * \ No newline at end of file +from .user import * +from .DevicePaymentForm import * +from .ReportForm import * diff --git a/core/apps/management/forms/user/BaseUserForm.py b/core/apps/management/forms/user/BaseUserForm.py index 9ac1c9f..96c5854 100644 --- a/core/apps/management/forms/user/BaseUserForm.py +++ b/core/apps/management/forms/user/BaseUserForm.py @@ -27,6 +27,15 @@ class BaseUserForm(forms.ModelForm): "region", "warehouse", ] + labels = { + "phone": "Telefon raqami", + "password": "Parol", + "role": "Lavozim", + "first_name": "Ism", + "last_name": "Familiya", + "region": "Hudud", + "warehouse": "Ombor", + } def clean_role(self): role = self.cleaned_data["role"] diff --git a/core/apps/management/migrations/0016_rent_created_by_rent_is_paid_rent_paid_at.py b/core/apps/management/migrations/0016_rent_created_by_rent_is_paid_rent_paid_at.py new file mode 100644 index 0000000..2898474 --- /dev/null +++ b/core/apps/management/migrations/0016_rent_created_by_rent_is_paid_rent_paid_at.py @@ -0,0 +1,31 @@ +# Generated by Django 5.2.7 on 2026-02-12 06:57 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('management', '0015_remove_income_confirmed_by'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='rent', + name='created_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='created_rents', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='rent', + name='is_paid', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='rent', + name='paid_at', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/core/apps/management/migrations/0017_expense_comment.py b/core/apps/management/migrations/0017_expense_comment.py new file mode 100644 index 0000000..c12e13c --- /dev/null +++ b/core/apps/management/migrations/0017_expense_comment.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2026-02-12 09:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('management', '0016_rent_created_by_rent_is_paid_rent_paid_at'), + ] + + operations = [ + migrations.AddField( + model_name='expense', + name='comment', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/core/apps/management/migrations/0018_income_warehouse.py b/core/apps/management/migrations/0018_income_warehouse.py new file mode 100644 index 0000000..f1df392 --- /dev/null +++ b/core/apps/management/migrations/0018_income_warehouse.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.7 on 2026-02-12 13:27 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('management', '0017_expense_comment'), + ] + + operations = [ + migrations.AddField( + model_name='income', + name='warehouse', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='warehouse_incomes', to='management.warehouse'), + ), + ] diff --git a/core/apps/management/migrations/0019_alter_expense_expense_type.py b/core/apps/management/migrations/0019_alter_expense_expense_type.py new file mode 100644 index 0000000..23b726d --- /dev/null +++ b/core/apps/management/migrations/0019_alter_expense_expense_type.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2026-02-13 06:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('management', '0018_income_warehouse'), + ] + + operations = [ + migrations.AlterField( + model_name='expense', + name='expense_type', + field=models.CharField(choices=[('salary', 'Maosh'), ('utilities', 'Kommunal to‘lovlar'), ('maintenance', 'Texnik xizmat'), ('food', 'Oziq-ovqat'), ('transport', "Yo'lkira"), ('buy_toys', 'Oʻyinchoqlar sotib olish'), ('other', 'Boshqa')], default='other', max_length=20), + ), + ] diff --git a/core/apps/management/migrations/0020_device_amount_device_due_date_device_is_paid.py b/core/apps/management/migrations/0020_device_amount_device_due_date_device_is_paid.py new file mode 100644 index 0000000..b4fcd8e --- /dev/null +++ b/core/apps/management/migrations/0020_device_amount_device_due_date_device_is_paid.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.7 on 2026-02-13 09:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('management', '0019_alter_expense_expense_type'), + ] + + operations = [ + migrations.AddField( + model_name='device', + name='amount', + field=models.IntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='device', + name='due_date', + field=models.DateField(blank=True, null=True), + ), + migrations.AddField( + model_name='device', + name='is_paid', + field=models.BooleanField(default=False), + ), + ] diff --git a/core/apps/management/migrations/0021_report.py b/core/apps/management/migrations/0021_report.py new file mode 100644 index 0000000..9d13ac8 --- /dev/null +++ b/core/apps/management/migrations/0021_report.py @@ -0,0 +1,26 @@ +# Generated by Django 5.2.7 on 2026-02-13 12:56 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('management', '0020_device_amount_device_due_date_device_is_paid'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Report', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.PositiveIntegerField(default=0)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('created_by', models.ForeignKey(blank=True, null=True, 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')), + ], + ), + ] diff --git a/core/apps/management/migrations/0022_alter_district_region.py b/core/apps/management/migrations/0022_alter_district_region.py new file mode 100644 index 0000000..91ba59d --- /dev/null +++ b/core/apps/management/migrations/0022_alter_district_region.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.7 on 2026-02-17 09:17 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('management', '0021_report'), + ] + + operations = [ + migrations.AlterField( + model_name='district', + name='region', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='districts', to='management.region'), + ), + ] diff --git a/core/apps/management/models/__init__.py b/core/apps/management/models/__init__.py index 36cdbc0..f8ec68e 100644 --- a/core/apps/management/models/__init__.py +++ b/core/apps/management/models/__init__.py @@ -5,4 +5,5 @@ from .district import * from .toyMovement import * from .warehouse import * from .expense import * -from .rent import * \ No newline at end of file +from .rent import * +from .report import * \ No newline at end of file diff --git a/core/apps/management/models/device.py b/core/apps/management/models/device.py index 47fdff8..b0b9fdb 100644 --- a/core/apps/management/models/device.py +++ b/core/apps/management/models/device.py @@ -1,10 +1,31 @@ from django.db import models from .district import District +from django.utils import timezone class Device(models.Model): address = models.CharField(max_length=100, unique=True) district = models.ForeignKey(District, on_delete=models.PROTECT) + due_date = models.DateField(null=True, blank=True) + amount = models.IntegerField(null=True, blank=True) + is_paid = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): - return self.address \ No newline at end of file + return self.address + + @property + def days_until_due(self): + """Calculate days remaining until payment is due""" + if not self.due_date: + return None + today = timezone.now().date() + delta = self.due_date - today + return delta.days + + @property + def is_overdue(self): + """Check if payment is overdue""" + if not self.due_date or self.is_paid: + return False + today = timezone.now().date() + return self.due_date < today \ No newline at end of file diff --git a/core/apps/management/models/district.py b/core/apps/management/models/district.py index 55f12ef..db97edb 100644 --- a/core/apps/management/models/district.py +++ b/core/apps/management/models/district.py @@ -4,7 +4,7 @@ from .region import Region class District(models.Model): region = models.ForeignKey( Region, - on_delete=models.CASCADE, + on_delete=models.PROTECT, related_name="districts" ) name = models.CharField(max_length=100) diff --git a/core/apps/management/models/expense.py b/core/apps/management/models/expense.py index 5a179f3..e63fee8 100644 --- a/core/apps/management/models/expense.py +++ b/core/apps/management/models/expense.py @@ -3,7 +3,6 @@ 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" @@ -28,7 +27,7 @@ class Expense(models.Model): "accounts.User", on_delete=models.PROTECT, null=True, blank=True, related_name="confirmed_expenses" ) - + comment = models.TextField(blank=True, null=True) is_confirmed = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) @@ -41,7 +40,6 @@ class Expense(models.Model): # 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."}) \ No newline at end of file diff --git a/core/apps/management/models/income.py b/core/apps/management/models/income.py index 0ec4b0a..0a9543f 100644 --- a/core/apps/management/models/income.py +++ b/core/apps/management/models/income.py @@ -3,8 +3,10 @@ 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") + warehouse = models.ForeignKey("management.Warehouse", on_delete=models.PROTECT, related_name="warehouse_incomes", null=True, blank=True) + 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) \ No newline at end of file + created_at = models.DateTimeField(auto_now_add=True) diff --git a/core/apps/management/models/rent.py b/core/apps/management/models/rent.py index 1391691..6e5ad61 100644 --- a/core/apps/management/models/rent.py +++ b/core/apps/management/models/rent.py @@ -1,6 +1,8 @@ from django.db import models from .district import District from .device import Device +from django.conf import settings + class Rent(models.Model): address = models.CharField(max_length=100, unique=True) @@ -9,6 +11,15 @@ class Rent(models.Model): due_date = models.DateField() amount = models.IntegerField() + is_paid = models.BooleanField(default=False) + paid_at = models.DateTimeField(null=True, blank=True) + created_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + related_name="created_rents", + on_delete=models.PROTECT, + null=True, + blank=True + ) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): diff --git a/core/apps/management/models/report.py b/core/apps/management/models/report.py new file mode 100644 index 0000000..8646d03 --- /dev/null +++ b/core/apps/management/models/report.py @@ -0,0 +1,9 @@ +from django.db import models + + +class Report(models.Model): + quantity = models.PositiveIntegerField(default=0) + device = models.ForeignKey("management.Device", on_delete=models.PROTECT, null=True, blank=True) + created_by = models.ForeignKey("accounts.User", on_delete=models.PROTECT, null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + diff --git a/core/apps/management/models/toyMovement.py b/core/apps/management/models/toyMovement.py index bb985c2..97de7c2 100644 --- a/core/apps/management/models/toyMovement.py +++ b/core/apps/management/models/toyMovement.py @@ -1,23 +1,39 @@ from django.db import models -from .device import Device + +from core.apps.management.choice import TOY_MOVEMENT_TYPE +from core.apps.management.models 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, + Warehouse, + on_delete=models.PROTECT, related_name="outgoing" ) + to_warehouse = models.ForeignKey( - Warehouse, on_delete=models.PROTECT, + Warehouse, + on_delete=models.PROTECT, related_name="incoming", - null=True, blank=True + null=True, + blank=True ) + device = models.ForeignKey( - Device, on_delete=models.PROTECT, - null=True, blank=True + 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) \ No newline at end of file + + created_by = models.ForeignKey( + "accounts.User", + on_delete=models.PROTECT + ) + + created_at = models.DateTimeField(auto_now_add=True) diff --git a/core/apps/management/templates/base.html b/core/apps/management/templates/base.html index fff04ef..47011c4 100644 --- a/core/apps/management/templates/base.html +++ b/core/apps/management/templates/base.html @@ -6,7 +6,9 @@
- + +
+ {% csrf_token %} + +
- -
{% block content %}{% endblock %}
- \ No newline at end of file diff --git a/core/apps/management/templates/businessman/businessman_dashboard.html b/core/apps/management/templates/businessman/businessman_dashboard.html index f32d0e0..3355314 100644 --- a/core/apps/management/templates/businessman/businessman_dashboard.html +++ b/core/apps/management/templates/businessman/businessman_dashboard.html @@ -1,117 +1,146 @@ {% extends "base.html" %} {% block content %} -
-

Businessman Paneli

+
+

Businessman Paneli

- + - + .dashboard-card.logout-card { + background: #fee2e2; + color: #dc2626; + } -{% endblock %} + .dashboard-card.logout-card:hover { + background: #dc2626; + color: #fff; + } + + .label { + font-size: 14px; + text-align: center; + } + + /* Mobile adjustments */ + @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; + } + } + + +{% endblock %} \ No newline at end of file diff --git a/core/apps/management/templates/common/create/device_create.html b/core/apps/management/templates/common/create/device_create.html index 5d4a638..04c4b6b 100644 --- a/core/apps/management/templates/common/create/device_create.html +++ b/core/apps/management/templates/common/create/device_create.html @@ -1,33 +1,58 @@ {% extends "base.html" %} -{% block title %}{{ title|default:"Yaratish" }}{% endblock %} +{% block title %}{{ title|default:"Aparat Yaratish" }}{% endblock %} {% block content %}
-

{{ title|default:"Yaratish" }}

+

{{ title|default:"Aparat Yaratish" }}

- - - + + + Orqaga
{% csrf_token %} - {% for field in form %} + +
- - {{ field }} - {% if field.help_text %} - {{ field.help_text }} - {% endif %} - {% for error in field.errors %} -
{{ error }}
+ + {{ form.address }} + {% for error in form.address.errors %} +
{{ error }}
+ {% endfor %} +
+ + +
+ + {{ form.district }} + {% for error in form.district.errors %} +
{{ error }}
+ {% endfor %} +
+ + +
+ + {{ form.amount }} + {% for error in form.amount.errors %} +
{{ error }}
+ {% endfor %} +
+ + +
+ + {{ form.due_date }} + {% for error in form.due_date.errors %} +
{{ error }}
{% endfor %}
- {% endfor %} @@ -36,7 +61,7 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/core/apps/management/templates/common/create/expense_create.html b/core/apps/management/templates/common/create/expense_create.html index 3995dd6..5a61454 100644 --- a/core/apps/management/templates/common/create/expense_create.html +++ b/core/apps/management/templates/common/create/expense_create.html @@ -6,7 +6,7 @@

{{ title|default:"Yaratish" }}

-
+ @@ -41,25 +41,42 @@ document.addEventListener("DOMContentLoaded", function() { const typeSelect = document.getElementById("id_expense_type"); const employeeGroup = document.getElementById("group-employee"); const deviceGroup = document.getElementById("group-device"); + const commentGroup = document.getElementById("group-comment"); + + function hideAllConditional() { + if (employeeGroup) employeeGroup.style.display = "none"; + if (deviceGroup) deviceGroup.style.display = "none"; + if (commentGroup) commentGroup.style.display = "none"; + } function toggleFields() { if (!typeSelect) return; const value = typeSelect.value; - // Hide both initially - if (employeeGroup) employeeGroup.style.display = "none"; - if (deviceGroup) deviceGroup.style.display = "none"; + // Hide all conditional fields first + hideAllConditional(); + // Show relevant fields based on expense type if (value === "salary") { if (employeeGroup) employeeGroup.style.display = "flex"; } else if (value === "rent" || value === "maintenance") { if (deviceGroup) deviceGroup.style.display = "flex"; + } else if (value === "other") { + if (commentGroup) commentGroup.style.display = "flex"; } } + // Initialize: hide all conditional fields on page load + hideAllConditional(); + + // Then show appropriate fields based on current selection toggleFields(); - if (typeSelect) typeSelect.addEventListener("change", toggleFields); + + // Listen for changes + if (typeSelect) { + typeSelect.addEventListener("change", toggleFields); + } }); @@ -114,7 +131,8 @@ document.addEventListener("DOMContentLoaded", function() { color: #374151; } .form-group input, -.form-group select { +.form-group select, +.form-group textarea { padding: 10px 14px; border: 1px solid #d1d5db; border-radius: 8px; @@ -123,7 +141,8 @@ document.addEventListener("DOMContentLoaded", function() { transition: border-color 0.2s, box-shadow 0.2s; } .form-group input:focus, -.form-group select:focus { +.form-group select:focus, +.form-group textarea:focus { border-color: #4f46e5; box-shadow: 0 0 0 2px rgba(79,70,229,0.2); } @@ -162,4 +181,4 @@ document.addEventListener("DOMContentLoaded", function() { .form-container { padding: 20px 16px; } } -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/core/apps/management/templates/common/create/income_create.html b/core/apps/management/templates/common/create/income_create.html index 601228e..afaea13 100644 --- a/core/apps/management/templates/common/create/income_create.html +++ b/core/apps/management/templates/common/create/income_create.html @@ -6,7 +6,7 @@

{{ title|default:"Yaratish" }}

-
+ diff --git a/core/apps/management/templates/common/create/rent_create.html b/core/apps/management/templates/common/create/rent_create.html new file mode 100644 index 0000000..eabb87d --- /dev/null +++ b/core/apps/management/templates/common/create/rent_create.html @@ -0,0 +1,148 @@ +{% extends "base.html" %} + +{% block title %}{{ title|default:"Arenda Yaratish" }}{% endblock %} + +{% block content %} +
+

{{ title|default:"Arenda Yaratish" }}

+ +
+ + + + Orqaga + + +
+ {% csrf_token %} + + {% for field in form %} +
+ + {{ field }} + {% if field.help_text %} + {{ field.help_text }} + {% endif %} + {% for error in field.errors %} +
{{ error }}
+ {% endfor %} +
+ {% endfor %} + + +
+
+ + +{% endblock %} \ No newline at end of file diff --git a/core/apps/management/templates/common/create/report_create.html b/core/apps/management/templates/common/create/report_create.html new file mode 100644 index 0000000..c9cbe88 --- /dev/null +++ b/core/apps/management/templates/common/create/report_create.html @@ -0,0 +1,284 @@ +{% extends "base.html" %} + +{% block title %}Yakuniy Hisobot{% endblock %} + +{% block content %} +
+
+

Yakuniy Hisobot

+

Kunni yakunlash uchun qurilma va oxirgi ko'rsatkichni tanlang

+
+ + {% if form.non_field_errors %} +
+ {% for error in form.non_field_errors %} +

⚠️ {{ error }}

+ {% endfor %} +
+ {% endif %} + +
+ {% csrf_token %} + + +
+ +
+ {{ form.device }} + {% if form.device.help_text %} + {{ form.device.help_text }} + {% endif %} + {% if form.device.errors %} +
+ {% for error in form.device.errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %} +
+
+ + +
+ +
+ {{ form.quantity }} + {% if form.quantity.errors %} +
+ {% for error in form.quantity.errors %} +

{{ error }}

+ {% endfor %} +
+ {% endif %} +
+
+ + + + + + Orqaga qaytish +
+
+ + + + + +{% endblock %} \ No newline at end of file diff --git a/core/apps/management/templates/common/create/toy_movement_create.html b/core/apps/management/templates/common/create/toy_movement_create.html index da5cd59..37a0e7b 100644 --- a/core/apps/management/templates/common/create/toy_movement_create.html +++ b/core/apps/management/templates/common/create/toy_movement_create.html @@ -4,31 +4,93 @@ {% block content %}
-

{{ title|default:"Yaratish" }}

+

{{ title|default:"O'yinchoq harakati yaratish" }}

- - + + Orqaga + {% if form.non_field_errors %} +
+ {% for error in form.non_field_errors %} +

⚠️ {{ error }}

+ {% endfor %} +
+ {% endif %} +
{% csrf_token %} - {% for field in form %} -
- - {{ field }} - {% if field.help_text %} - {{ field.help_text }} - {% endif %} - {% for error in field.errors %} -
{{ error }}
- {% endfor %} -
- {% endfor %} + + {% if request.user.role == "employee" %} + +
+ + {{ form.device }} + {% if form.device.errors %} +
{{ form.device.errors.0 }}
+ {% endif %} +
+ + +
+ + {{ form.quantity }} + {% if form.quantity.errors %} +
{{ form.quantity.errors.0 }}
+ {% endif %} +
+ + + {% else %} + +
+ + {{ form.movement_type }} + {% if form.movement_type.errors %} +
{{ form.movement_type.errors.0 }}
+ {% endif %} +
+ + +
+ + {{ form.from_warehouse }} + {% if form.from_warehouse.errors %} +
{{ form.from_warehouse.errors.0 }}
+ {% endif %} +
+ + + + + + + + +
+ + {{ form.quantity }} + {% if form.quantity.errors %} +
{{ form.quantity.errors.0 }}
+ {% endif %} +
+ {% endif %}
@@ -79,10 +141,29 @@ height: 16px; } +.alert { + padding: 12px 14px; + border-radius: 8px; + margin-bottom: 20px; + font-size: 14px; + line-height: 1.5; +} + +.alert-error { + background: #fee2e2; + border: 1px solid #fecaca; + color: #dc2626; +} + +.alert-error p { + margin: 0; +} + .form-group { margin-bottom: 16px; display: flex; flex-direction: column; + width: 100%; } .form-group label { @@ -94,11 +175,14 @@ .form-group input, .form-group select { padding: 10px 14px; - border: 1px solid #d1d5db; + border: 1.5px solid #d1d5db; border-radius: 8px; font-size: 14px; outline: none; transition: border-color 0.2s, box-shadow 0.2s; + font-family: inherit; + width: 100%; + box-sizing: border-box; } .form-group input:focus, @@ -117,6 +201,10 @@ font-size: 12px; color: #ef4444; margin-top: 4px; + padding: 6px 8px; + background: #fee2e2; + border-left: 3px solid #ef4444; + border-radius: 4px; } .submit-btn { @@ -146,49 +234,37 @@ diff --git a/core/apps/management/templates/common/create/user_create.html b/core/apps/management/templates/common/create/user_create.html index 804933e..aac876d 100644 --- a/core/apps/management/templates/common/create/user_create.html +++ b/core/apps/management/templates/common/create/user_create.html @@ -6,7 +6,7 @@
{% endfor %} @@ -35,120 +35,121 @@
- - - -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/core/apps/management/templates/common/create/warehouse_create.html b/core/apps/management/templates/common/create/warehouse_create.html index 566549b..2d247ba 100644 --- a/core/apps/management/templates/common/create/warehouse_create.html +++ b/core/apps/management/templates/common/create/warehouse_create.html @@ -1,10 +1,120 @@ - - - - - Title - - +{% extends "base.html" %} - - \ No newline at end of file +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+
+ + + ← Orqaga + + +

{{ title }}

+ +
+ {% csrf_token %} + +
+ + {{ form.name }} + {{ form.name.errors }} +
+ +
+ + {{ form.region }} + {{ form.region.errors }} +
+ +
+ + {{ form.toys_count }} + {{ form.toys_count.errors }} +
+ + +
+ +
+
+ + +{% endblock %} \ No newline at end of file diff --git a/core/apps/management/templates/common/edit/expense_edit.html b/core/apps/management/templates/common/edit/expense_edit.html index 5780bdc..f901f1f 100644 --- a/core/apps/management/templates/common/edit/expense_edit.html +++ b/core/apps/management/templates/common/edit/expense_edit.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% block title %}Create Expense{% endblock %} +{% block title %}Edit Expense{% endblock %} {% block content %}
-

Create Expense

+ + + + + Orqaga + + +

{% if title %}{{ title }}{% else %}Edit Expense{% endif %}

{% if form.non_field_errors %}
{{ form.non_field_errors.0 }}
{% endif %} -
+ {% csrf_token %}
{{ form.expense_type.label_tag }} {{ form.expense_type }} - {{ form.expense_type.errors }} + {% for error in form.expense_type.errors %} +
{{ error }}
+ {% endfor %}
{{ form.amount.label_tag }} {{ form.amount }} - {{ form.amount.errors }} + {% for error in form.amount.errors %} +
{{ error }}
+ {% endfor %}
+ +
@@ -113,27 +178,38 @@ const typeSelect = document.getElementById("id_expense_type"); const employeeField = document.getElementById("field-employee"); const deviceField = document.getElementById("field-device"); + const commentField = document.getElementById("field-comment"); + + function hideAllConditional() { + if (employeeField) employeeField.style.display = "none"; + if (deviceField) deviceField.style.display = "none"; + if (commentField) commentField.style.display = "none"; + } function toggleFields() { if (!typeSelect) return; const value = typeSelect.value; - // Reset both - employeeField.style.display = "none"; - deviceField.style.display = "none"; + // Hide all conditional fields first + hideAllConditional(); + // Show relevant fields based on expense type if (value === "salary") { - employeeField.style.display = "block"; + if (employeeField) employeeField.style.display = "block"; } else if (value === "rent" || value === "maintenance") { - deviceField.style.display = "block"; + if (deviceField) deviceField.style.display = "block"; + } else if (value === "other") { + if (commentField) commentField.style.display = "block"; } } if (typeSelect) { + // Initialize on page load toggleFields(); + // Listen for changes typeSelect.addEventListener("change", toggleFields); } }); -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/core/apps/management/templates/common/edit/income_edit.html b/core/apps/management/templates/common/edit/income_edit.html index cd916ce..3c9cf65 100644 --- a/core/apps/management/templates/common/edit/income_edit.html +++ b/core/apps/management/templates/common/edit/income_edit.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% block title %}Create Income{% endblock %} +{% block title %}Edit Income{% endblock %} {% block content %}
-

Create Income

+

Edit Income

{% if form.non_field_errors %}
{{ form.non_field_errors.0 }}
@@ -90,7 +90,7 @@ {{ form.amount.errors }}
- diff --git a/core/apps/management/templates/common/edit/warehouse_edit.html b/core/apps/management/templates/common/edit/warehouse_edit.html index 566549b..dc98470 100644 --- a/core/apps/management/templates/common/edit/warehouse_edit.html +++ b/core/apps/management/templates/common/edit/warehouse_edit.html @@ -1,10 +1,153 @@ - - - - - Title - - +{% extends "base.html" %} - - \ No newline at end of file +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+

{{ title }}

+ + + + + + Orqaga + + +
+ {% csrf_token %} + +
+ + {{ form.name }} + {% for error in form.name.errors %} +
{{ error }}
+ {% endfor %} +
+ +
+ + {{ form.region }} + {% for error in form.region.errors %} +
{{ error }}
+ {% endfor %} +
+ +
+ + {{ form.toys_count }} + {% for error in form.toys_count.errors %} +
{{ error }}
+ {% endfor %} +
+ + +
+
+ + +{% endblock %} \ No newline at end of file diff --git a/core/apps/management/templates/common/lists/device_list.html b/core/apps/management/templates/common/lists/device_list.html index f54162f..c6697ee 100644 --- a/core/apps/management/templates/common/lists/device_list.html +++ b/core/apps/management/templates/common/lists/device_list.html @@ -3,7 +3,20 @@ {% block title %}Aparatlar{% endblock %} {% block content %} -

{{ title|default:"Aparatlar" }}

+
+

{{ title|default:"Aparatlar" }}

+ {% if role == "manager" or role == "businessman" %} + + Yaratish + {% endif %} +
+ +
{% for device in devices %} diff --git a/core/apps/management/templates/common/lists/device_payment_list.html b/core/apps/management/templates/common/lists/device_payment_list.html new file mode 100644 index 0000000..4c146ab --- /dev/null +++ b/core/apps/management/templates/common/lists/device_payment_list.html @@ -0,0 +1,401 @@ +{% extends "base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+

Arendalar

+ + + + +
+ + + + + + + + + + + + + {% for device in devices %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
#AparatHududTo'lov muddatiSummaTo'landi
{{ forloop.counter }}{{ device.address }}{{ device.district.name }}{{ device.due_date|date:"d.m.Y" }}{{ device.amount }} + {% if not device.is_paid %} + + {% else %} + + {% endif %} +
+ Hech qanday arenda topilmadi +
+
+ + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/core/apps/management/templates/common/lists/expense_list.html b/core/apps/management/templates/common/lists/expense_list.html index 5277925..526c3dd 100644 --- a/core/apps/management/templates/common/lists/expense_list.html +++ b/core/apps/management/templates/common/lists/expense_list.html @@ -4,33 +4,256 @@ {% block title %}Xarajatlar{% endblock %} {% block content %} -

{{ title|default:"Xarajatlar" }}

+
+

{{ title|default:"Xarajatlar" }}

+ + Qo'shish +
- {% for obj in expenses %} -
-
Miqdor: {{ obj.amount }}
-
Tur: {{ obj|attr:"expense_type" }}
- {% if obj.device %}
Aparat: {{ obj.device.name }}
{% else %}{% endif %} - {% if obj.employee %}
Hodim: {{ obj.employee.get_full_name }}
{% else %}{% endif %} -
Yaratgan: {{ obj.created_by.get_full_name }}
-
Tasdiqlanganmi: {% if obj.confirmed_by %}{{ obj.confirmed_by.get_full_name }}{% else %}Yo'q{% endif %}
-
Yaratilgan sana: {{ obj.created_at|date:"d.m.Y H:i" }}
+ {% if expenses %} + {% for obj in expenses %} +
+
+
+ {% if obj.expense_type == 'other' and obj.comment %} + {{ obj.comment }} + {% else %} + {{ obj.get_expense_type_display }} + {% endif %} +
+
#{{ forloop.counter }}
+
- {% if role == "manager" or role == "businessman" %} -
- {% if not obj.is_confirmed %} - - - {% endif %} - {% if role == "businessman" %} - Tahrirlash +
+
+ Miqdor: + {{ obj.amount|floatformat:2 }} so'm +
+
+ Sana: + {{ obj.created_at|date:"d.m.Y H:i" }} +
+
+ Yaratgan: + {{ obj.created_by.get_full_name }} +
+
+ + {% if role == "manager" or role == "businessman" %} +
+ {% if not obj.is_confirmed %} + ✔ Tasdiqlash + ✖ Rad etish + {% else %} + ✅ Tasdiqlandi + {% endif %} + {% if role == "businessman" %} + ✎ Tahrirlash + {% endif %} +
{% endif %}
- {% endif %} + {% endfor %} + {% else %} +
+

Hech qanday xarajat topilmadi

- {% empty %} -

Hech narsa topilmadi

- {% endfor %} + {% endif %}
-{% endblock %} + + + +{% endblock %} \ No newline at end of file diff --git a/core/apps/management/templates/common/lists/report_list.html b/core/apps/management/templates/common/lists/report_list.html new file mode 100644 index 0000000..29852f3 --- /dev/null +++ b/core/apps/management/templates/common/lists/report_list.html @@ -0,0 +1,164 @@ +{% extends "base.html" %} + +{% block title %}Hisobotlar{% endblock %} + +{% block content %} +
+

{{ title|default:"Hisobotlar" }}

+
+ +
+ {% if reports %} + {% for report in reports %} +
+
+
+ {{ report.device.address }} +
+
#{{ forloop.counter }}
+
+ +
+
+ Miqdor: + {{ report.quantity }} dona +
+
+ Sana: + {{ report.created_at|date:"d.m.Y H:i" }} +
+
+ Yaratgan: + {{ report.created_by.get_full_name }} +
+
+
+ {% endfor %} + {% else %} +
+

Hech qanday hisobot topilmadi

+
+ {% endif %} +
+ + + +{% endblock %} \ No newline at end of file diff --git a/core/apps/management/templates/common/lists/toy_movement_list.html b/core/apps/management/templates/common/lists/toy_movement_list.html index 5bb1a8d..743bfa8 100644 --- a/core/apps/management/templates/common/lists/toy_movement_list.html +++ b/core/apps/management/templates/common/lists/toy_movement_list.html @@ -3,32 +3,184 @@ {% block title %}O'yinchoq harakatlari{% endblock %} {% block content %} -

{{ title|default:"O'yinchoq harakatlari" }}

+
+

{{ title|default:"O'yinchoq harakatlari" }}

-{% if role == "manager" or role == "businessman" %} -
- Yaratish + {% if role == "manager" or role == "businessman" %} + + + Yaratish + + + {% elif role == "employee" %} + + + Yaratish + + {% endif %}
-
-{% endif %} +
- {% for tm in toy_movements %} -
-
Tur: {{ tm.get_movement_type_display }}
-
From: {{ tm.from_warehouse.name }}
- {% if tm.to_warehouse %} -
To: {{ tm.to_warehouse.name }}
- {% endif %} - {% if tm.device %} -
Aparat: {{ tm.device.name }}
- {% endif %} -
Miqdor: {{ tm.quantity }}
-
Yaratgan: {{ tm.created_by.get_full_name }}
-
Sana: {{ tm.created_at|date:"d.m.Y H:i" }}
+ {% if toy_movements %} + {% for tm in toy_movements %} +
+
+
+ {% if tm.device %} + {{ tm.device.address }} + {% elif tm.to_warehouse %} + {{ tm.to_warehouse.name }} + {% else %} + {{ tm.from_warehouse.name }} + {% endif %} +
+
#{{ forloop.counter }}
+
+ +
+
+ Miqdor: + {{ tm.quantity }} dona +
+
+ Sana: + {{ tm.created_at|date:"d.m.Y H:i" }} +
+
+ Yaratgan: + {{ tm.created_by.get_full_name }} +
+
+ Turi: + {{ tm.get_movement_type_display }} +
+
+
+ {% endfor %} + {% else %} +
+

Hech qanday harakat topilmadi

- {% empty %} -

Hech narsa topilmadi

- {% endfor %} + {% endif %}
-{% endblock %} + + + +{% endblock %} \ No newline at end of file diff --git a/core/apps/management/templates/common/lists/user_list.html b/core/apps/management/templates/common/lists/user_list.html index 7548ffe..adad3ce 100644 --- a/core/apps/management/templates/common/lists/user_list.html +++ b/core/apps/management/templates/common/lists/user_list.html @@ -3,7 +3,20 @@ {% block title %}Foydalanuvchilar{% endblock %} {% block content %} -

{{ title|default:"Foydalanuvchilar" }}

+
+

{{ title|default:"Foydalanuvchilar" }}

+ {% if role == "manager" or role == "businessman" %} + + Yaratish + {% endif %} +
+ +
{% for user in users %} diff --git a/core/apps/management/templates/common/lists/warehouse_list.html b/core/apps/management/templates/common/lists/warehouse_list.html index 9b8fb2d..b8ce69e 100644 --- a/core/apps/management/templates/common/lists/warehouse_list.html +++ b/core/apps/management/templates/common/lists/warehouse_list.html @@ -3,7 +3,20 @@ {% block title %}Omborlar{% endblock %} {% block content %} -

{{ title|default:"Omborlar" }}

+
+

{{ title|default:"Omborlar" }}

+ {% if role == "businessman" %} + + Yaratish + {% endif %} +
+ +
{% for wh in warehouses %} diff --git a/core/apps/management/templates/employee/employee_dashboard.html b/core/apps/management/templates/employee/employee_dashboard.html index 941bdb7..e255d1d 100644 --- a/core/apps/management/templates/employee/employee_dashboard.html +++ b/core/apps/management/templates/employee/employee_dashboard.html @@ -1,105 +1,138 @@ {% extends "base.html" %} {% block content %} -

Hodim Paneli

+
+

Hodim Paneli

- + + -@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; - } -} - {% endblock %} \ No newline at end of file diff --git a/core/apps/management/templates/manager/manager_dashboard.html b/core/apps/management/templates/manager/manager_dashboard.html index 8adb945..3d934c2 100644 --- a/core/apps/management/templates/manager/manager_dashboard.html +++ b/core/apps/management/templates/manager/manager_dashboard.html @@ -1,112 +1,145 @@ {% extends "base.html" %} - {% block content %} -

Manager Paneli

+
+

Manager Paneli

- + - + .dashboard-card.logout-card { + background: #fee2e2; + color: #dc2626; + } -{% endblock %} + .dashboard-card.logout-card:hover { + background: #dc2626; + color: #fff; + } + + .label { + font-size: 14px; + text-align: center; + } + + /* Mobile adjustments */ + @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; + } + } + + +{% endblock %} \ No newline at end of file diff --git a/core/apps/management/urls.py b/core/apps/management/urls.py index 1d20de2..bd896bc 100644 --- a/core/apps/management/urls.py +++ b/core/apps/management/urls.py @@ -14,14 +14,18 @@ urlpatterns = [ 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"), + path("create/rent/", views.create_rent, name="create_rent"), + path("create/report/", views.create_report, name="create_report"), # # List + path("list/device-payments/", views.device_payment_list, name="device_payment_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"), + path("list/reports/", views.report_list, name="report_list"), # Edit path("edit/device//", views.edit_device, name="edit_device"), @@ -29,6 +33,8 @@ urlpatterns = [ path("edit/expense//", views.edit_expense, name="edit_expense"), path("edit/warehouse//", views.edit_warehouse, name="edit_warehouse"), path("edit/user//", views.edit_user, name="edit_user"), + path("edit/device-payments//", views.mark_device_paid, name="mark_device_paid"), + # path("edit/toy-movement//", views.edit_toy_movement, name="edit_toy_movement"), diff --git a/core/apps/management/views/common/create.py b/core/apps/management/views/common/create.py index 3f1b941..8d52ccd 100644 --- a/core/apps/management/views/common/create.py +++ b/core/apps/management/views/common/create.py @@ -1,27 +1,42 @@ +from django.db.models import F, Sum + from core.apps.management.forms import DeviceForm, IncomeForm, WarehouseForm, UserCreateForm, ExpenseFormEmployee, \ - ExpenseFormManager, ExpenseFormBusinessman + ExpenseFormManager, ExpenseFormBusinessman, ReportForm 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 +from core.apps.management.forms.RentForm import RentForm +from core.apps.management.models import ToyMovement, Warehouse, Report + @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" - }) + user = request.user + + if request.method == "POST": + form = DeviceForm(request.POST, user=user) + if form.is_valid(): + form.save() + return redirect("dashboard") + else: + form = DeviceForm(user=user) + + return render( + request, + "common/create/device_create.html", + { + "form": form, + "title": "Aparat Yaratish" + } + ) @login_required +@role_required(["employee"]) def create_income(request): if request.method == "POST": form = IncomeForm(request.POST, user=request.user) @@ -35,6 +50,24 @@ def create_income(request): form = IncomeForm(user=request.user) return render(request, "common/create/income_create.html", {"form": form}) +@login_required +@role_required(['manager', 'businessman']) +def create_income_manager_and_businessman(request): + if request.method == "POST": + form = IncomeForm(request.POST, user=request.user) + if form.is_valid(): + with transaction.atomic(): + income = form.save(commit=False) + income.created_by = request.user + income.save() + + warehouse = income.warehouse + warehouse.toys_count += income.amount + warehouse.save() + return redirect("common/create/income_create.html") + else: + form = IncomeForm(user=request.user) + return render(request=request, template_name="common/create/income_create.html", context={"form": form}) @login_required def create_expense(request): @@ -111,110 +144,239 @@ def create_user(request): "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 user.role == "employee": + form_class = ToyMovementFormEmployee + else: + form_class = 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} + quantity = form.cleaned_data["quantity"] + + if user.role == "employee": + # Auto determine warehouse by region + from_wh = Warehouse.objects.select_for_update().filter( + region=user.region + ).first() + + if not from_wh: + form.add_error(None, "No warehouse assigned to your region.") + return render(request, + "common/create/toy_movement_create.html", + {"form": form}) + + if from_wh.toys_count < quantity: + form.add_error("quantity", "Not enough toys in warehouse.") + return render(request, + "common/create/toy_movement_create.html", + {"form": form}) + + # Deduct stock + Warehouse.objects.filter(pk=from_wh.pk).update( + toys_count=F("toys_count") - quantity ) - # Deduct from source warehouse - from_wh.toys_count -= movement.quantity - from_wh.save() + movement = form.save(commit=False) + movement.movement_type = "from_warehouse" + movement.from_warehouse = from_wh + movement.to_warehouse = None + movement.created_by = user + movement.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() + else: + # Manager / Businessman normal logic + from_wh = form.cleaned_data["from_warehouse"] + movement_type = form.cleaned_data["movement_type"] + to_wh = form.cleaned_data.get("to_warehouse") - # Set creator - movement.created_by = user - movement.save() + from_wh = Warehouse.objects.select_for_update().get(pk=from_wh.pk) + + if from_wh.toys_count < quantity: + form.add_error("quantity", "Not enough toys in warehouse.") + return render(request, + "common/create/toy_movement_create.html", + {"form": form}) + + Warehouse.objects.filter(pk=from_wh.pk).update( + toys_count=F("toys_count") - quantity + ) + + if movement_type == "between_warehouses" and to_wh: + to_wh = Warehouse.objects.select_for_update().get(pk=to_wh.pk) + Warehouse.objects.filter(pk=to_wh.pk).update( + toys_count=F("toys_count") + quantity + ) + + movement = form.save(commit=False) + 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} + {"form": form} ) - - - @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 - ) + form = ToyMovementForm(request.POST, user=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." - } - ) + if form.is_valid(): + with transaction.atomic(): + # Get cleaned data + from_wh = form.cleaned_data.get("from_warehouse") + to_wh = form.cleaned_data.get("to_warehouse") + movement_type = form.cleaned_data.get("movement_type") + quantity = form.cleaned_data.get("quantity") - with transaction.atomic(): - # Update warehouse stock - from_wh.toys_count -= movement.quantity - from_wh.save() + # Validate warehouse exists + if not from_wh: + form.add_error("from_warehouse", "Source warehouse is required.") + return render(request, "common/create/toy_movement_create.html", {"form": form}) - # Save movement - movement.save() + # Lock and get fresh warehouse data + from_wh = Warehouse.objects.select_for_update().get(pk=from_wh.pk) - return redirect("dashboard") + # Check stock + if from_wh.toys_count < quantity: + form.add_error("quantity", "Not enough toys in warehouse.") + return render(request, "common/create/toy_movement_create.html", {"form": form}) - # 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 + # Deduct from source warehouse + Warehouse.objects.filter(pk=from_wh.pk).update( + toys_count=F("toys_count") - quantity + ) + + # Add to destination warehouse if between_warehouses + if movement_type == "between_warehouses" and to_wh: + to_wh = Warehouse.objects.select_for_update().get(pk=to_wh.pk) + Warehouse.objects.filter(pk=to_wh.pk).update( + toys_count=F("toys_count") + quantity + ) + + # Save movement + movement = form.save(commit=False) + movement.created_by = user + movement.save() + + return redirect("toy_movement_list") + else: + # Form has errors, display them + return render(request, "common/create/toy_movement_create.html", {"form": form}) + + else: + # GET request → render the create form + form = ToyMovementForm(user=user) return render( request, "common/create/toy_movement_create.html", - {"form": form, "user_role": user.role} + {"form": form} + ) + +@login_required +@role_required(["manager", "businessman"]) +def create_rent(request): + + if request.method == "POST": + form = RentForm(request.POST) + if form.is_valid(): + rent = form.save(commit=False) + rent.created_by = request.user + rent.save() + return redirect("dashboard") + else: + form = RentForm() + + return render(request, "common/create/rent_create.html", {"form":form, "title":"Create Rent"}) + +@login_required +@role_required(["employee"]) +def create_report(request): + + if request.method == "POST": + form = ReportForm(request.POST, user=request.user) + + if form.is_valid(): + + # 🔥 Employee MUST have warehouse + if not request.user.warehouse: + form.add_error(None, "Sizga ombor biriktirilmagan.") + return render( + request, + "common/create/report_create.html", + {"form": form, "title": "Yakuniy Hisobot"} + ) + + device = form.cleaned_data["device"] + entered_quantity = form.cleaned_data["quantity"] + + with transaction.atomic(): + + # ✅ 1. Save last entered quantity as ToyMovement + ToyMovement.objects.create( + movement_type="from_warehouse", + from_warehouse=request.user.warehouse, + device=device, + quantity=entered_quantity, + created_by=request.user, + ) + + # ✅ 2. Find last report for this device + last_report = Report.objects.filter( + device=device + ).order_by("-created_at").first() + + if last_report: + last_time = last_report.created_at + + total_since_last = ToyMovement.objects.filter( + device=device, + created_at__gt=last_time + ).aggregate( + total=Sum("quantity") + )["total"] or 0 + else: + total_since_last = ToyMovement.objects.filter( + device=device + ).aggregate( + total=Sum("quantity") + )["total"] or 0 + + # ✅ 3. Create Report + Report.objects.create( + device=device, + quantity=total_since_last, + created_by=request.user + ) + + return redirect("employee_dashboard") + + else: + form = ReportForm(user=request.user) + + return render( + request, + "common/create/report_create.html", + { + "form": form, + "title": "Yakuniy Hisobot" + } ) \ No newline at end of file diff --git a/core/apps/management/views/common/edit.py b/core/apps/management/views/common/edit.py index f47a8ea..077ef2a 100644 --- a/core/apps/management/views/common/edit.py +++ b/core/apps/management/views/common/edit.py @@ -2,7 +2,8 @@ from django.contrib.auth.decorators import login_required from core.apps.accounts.models import User from core.apps.management.forms import DeviceForm, IncomeForm, ExpenseForm, WarehouseForm, UserCreateForm, \ - ToyMovementEmployeeForm, ToyMovementForm, ExpenseFormEmployee, ExpenseFormManager, ExpenseFormBusinessman + ToyMovementEmployeeForm, ToyMovementForm, ExpenseFormEmployee, ExpenseFormManager, ExpenseFormBusinessman, \ + DevicePaymentForm from django.shortcuts import render, redirect, get_object_or_404 from core.apps.management.models import Device, Income, Expense, Warehouse, ToyMovement from django.db import transaction @@ -30,11 +31,11 @@ def edit_income(request, pk): form = IncomeForm(request.POST, instance=income) if form.is_valid(): form.save() - return redirect("income_list") + return redirect("common/create/income_create.html") else: form = IncomeForm(instance=income) - return render(request, "common/edit/income_edit.html", { + return render(request, "common/create/income_create.html", { "form": form, "title": "Kirimni tahrirlash" }) @@ -125,6 +126,22 @@ def edit_user(request, pk): "title": "Foydalanuvchini tahrirlash", }) + +@login_required +@role_required(["employee"]) +def mark_device_paid(request, pk): + device = get_object_or_404(Device, pk=pk) + + # Security check + if device.district.region != request.user.region: + return redirect("device_payment_list") + + if request.method == "POST": + device.is_paid = True + device.save() + + return redirect("device_payment_list") + # @role_required(["businessman"]) # @login_required # def edit_toy_movement(request, pk): diff --git a/core/apps/management/views/common/list.py b/core/apps/management/views/common/list.py index 57a6e27..3e4e7fe 100644 --- a/core/apps/management/views/common/list.py +++ b/core/apps/management/views/common/list.py @@ -1,6 +1,6 @@ from django.shortcuts import render from django.contrib.auth.decorators import login_required -from core.apps.management.models import Device, Income, Expense, Warehouse, ToyMovement +from core.apps.management.models import Device, Income, Expense, Warehouse, ToyMovement, Report from core.apps.accounts.models import User from core.apps.management.decorators import role_required @@ -39,14 +39,34 @@ def device_list(request): @login_required def expense_list(request): - expenses = Expense.objects.select_related("created_by", "confirmed_by").order_by("-created_at") + user = request.user + + expenses = Expense.objects.select_related( + "created_by", + "confirmed_by" + ).order_by("-created_at") + + # 🔐 ROLE-BASED FILTERING + if user.role == "employee": + expenses = expenses.filter(created_by=user) + + elif user.role == "manager": + # Optional: show only region expenses + expenses = expenses.filter( + created_by__region=user.region + ) + + # businessman sees everything (no filter) + context = { "expenses": expenses, - "role": request.user.role, + "role": user.role, } + return render(request, "common/lists/expense_list.html", context) + @login_required def income_list(request): incomes = Income.objects.select_related("device", "created_by", "created_by__region").order_by("-created_at") @@ -87,4 +107,63 @@ def toy_movement_list(request): "toy_movements": toy_movements, "role": request.user.role, } - return render(request, "common/lists/toy_movement_list.html", context) \ No newline at end of file + return render(request, "common/lists/toy_movement_list.html", context) + +@login_required +@role_required(["manager", "businessman"]) +def income_list(request): + + if request.user.role == "businessman": + incomes = Income.objects.all() + elif request.user.role == "manager": + incomes = Income.objects.filter(warehouse__region=request.user.region) + + return render(request, "common/create/income_create.html", {"incomes": incomes}) + +@login_required +@role_required(["employee"]) +def device_payment_list(request): + # Employee only sees devices in his region + devices = Device.objects.filter( + district__region=request.user.region + ).order_by("due_date") + + return render( + request, + "common/lists/device_payment_list.html", + { + "devices": devices, + "title": "Arendalar" + } + ) + +from django.contrib.auth.decorators import login_required +from django.shortcuts import render +from django.db.models import Q + +from core.apps.management.models import Report +from core.apps.management.decorators import role_required + + +@login_required +@role_required(['manager', 'businessman']) +def report_list(request): + reports = ( + Report.objects + .select_related("device", "device__district", "created_by") + .order_by("-created_at") + ) + + if request.user.role == "manager": + reports = reports.filter( + device__district__region=request.user.region + ) + + return render( + request, + "common/lists/report_list.html", + { + "reports": reports, + "title": "Hisobotlar" + } + ) diff --git a/docker/Dockerfile.web b/docker/Dockerfile.web index 24b1a6f..0288e96 100644 --- a/docker/Dockerfile.web +++ b/docker/Dockerfile.web @@ -13,8 +13,6 @@ COPY ./ /code COPY ./resources/scripts/$SCRIPT /code/$SCRIPT -RUN chmod +x /code/resources/scripts/$SCRIPT - CMD sh /code/resources/scripts/$SCRIPT diff --git a/resources/scripts/entrypoint.sh b/resources/scripts/entrypoint.sh old mode 100644 new mode 100755