initial commit

This commit is contained in:
2025-08-05 10:26:39 +05:00
commit b7412bbef6
298 changed files with 10533 additions and 0 deletions

0
core/__init__.py Normal file
View File

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

View File

View File

@@ -0,0 +1,2 @@
from .core import * # noqa
from .user import * # noqa

View File

@@ -0,0 +1,18 @@
"""
Admin panel register
"""
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.contrib.auth import models as db_models
from django_core.models import SmsConfirm
from ..admin import user
from .user import SmsConfirmAdmin
admin.site.unregister(db_models.Group)
admin.site.register(db_models.Group, user.GroupAdmin)
admin.site.register(db_models.Permission, user.PermissionAdmin)
admin.site.register(get_user_model(), user.CustomUserAdmin)
admin.site.register(SmsConfirm, SmsConfirmAdmin)

View File

@@ -0,0 +1,125 @@
from django.contrib.auth import admin
from django.utils.translation import gettext_lazy as _
from unfold.admin import ModelAdmin # type: ignore
from unfold.forms import AdminPasswordChangeForm # type: ignore # UserCreationForm,
from unfold.forms import UserChangeForm # type: ignore
class CustomUserAdmin(admin.UserAdmin, ModelAdmin):
change_password_form = AdminPasswordChangeForm
# add_form = UserCreationForm
form = UserChangeForm
list_display = (
"phone",
"full_name",
"role",
"validated_at",
)
search_fields = (
"first_name",
"last_name",
"phone",
"validated_at",
"inn_code",
)
list_display_links = (
"phone",
)
autocomplete_fields = (
"groups",
"user_permissions"
)
fieldsets = (
(
None,
{
"fields": (
"phone",
)
}
),
(
None,
{
"fields":
(
"username",
"password"
)
}
),
(
_("Personal info"),
{
"fields": (
"first_name",
"last_name",
"email"
)
}
),
(
_("Permissions"),
{
"fields": (
"is_active",
"is_staff",
"is_superuser",
"groups",
"user_permissions",
"role",
),
},
),
(
_("Important dates"),
{
"fields":
(
"last_login",
"date_joined"
)
}
),
)
class PermissionAdmin(ModelAdmin):
list_display = (
"name",
)
search_fields = (
"name",
)
list_display_links = (
"name",
)
class GroupAdmin(ModelAdmin):
list_display = [
"name"
]
search_fields = [
"name"
]
autocomplete_fields = (
"permissions",
)
class SmsConfirmAdmin(ModelAdmin):
list_display = [
"phone",
"code",
"resend_count",
"try_count"
]
search_fields = [
"phone",
"code"
]

View File

@@ -0,0 +1,9 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "core.apps.accounts"
def ready(self):
from core.apps.accounts import signals # noqa

View File

@@ -0,0 +1 @@
from .user import * # noqa

View File

@@ -0,0 +1,12 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
class RoleChoice(models.TextChoices):
"""
User Role Choice
"""
SUPERUSER = "superuser", _("Superuser")
ADMIN = "admin", _("Admin")
USER = "user", _("User")

View File

@@ -0,0 +1 @@
from .user import * # noqa

View File

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

View File

@@ -0,0 +1,141 @@
# Generated by Django 5.2.4 on 2025-08-01 09:53
import django.core.validators
import django.db.models.deletion
import django.utils.timezone
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("auth", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="User",
fields=[
("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",
),
),
(
"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")),
(
"id",
models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name="ID"
),
),
(
"phone",
models.CharField(
max_length=255,
unique=True,
validators=[
django.core.validators.RegexValidator(
message="Enter a valid international phone number (E.164 format, e.g., +14155552671).",
regex="^\\+?[1-9]\\d{1,14}$",
)
],
),
),
("username", models.CharField(blank=True, max_length=255, null=True)),
("validated_at", models.DateTimeField(blank=True, null=True)),
("inn_code", models.CharField(blank=True, max_length=12, null=True)),
("first_name", models.CharField(max_length=150, verbose_name="First Name")),
("last_name", models.CharField(max_length=150, verbose_name="Last Name")),
("email", models.EmailField(blank=True, max_length=254, verbose_name="Email Address")),
(
"role",
models.CharField(
choices=[("superuser", "Superuser"), ("admin", "Admin"), ("user", "User")],
default="user",
max_length=255,
verbose_name="Role",
),
),
("created_at", models.DateTimeField(auto_now_add=True, verbose_name="Created At")),
("updated_at", models.DateTimeField(auto_now=True, verbose_name="Updated At")),
(
"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",
),
),
(
"user_permissions",
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",
),
),
],
options={
"verbose_name": "User",
"verbose_name_plural": "Users",
"db_table": "users",
},
),
migrations.CreateModel(
name="ResetToken",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("token", models.CharField(max_length=255, unique=True)),
("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
"verbose_name": "Reset Token",
"verbose_name_plural": "Reset Tokens",
},
),
migrations.AddIndex(
model_name="user",
index=models.Index(fields=["phone"], name="users_phone_inx"),
),
migrations.AddIndex(
model_name="user",
index=models.Index(fields=["email"], name="users_email_inx"),
),
migrations.AddIndex(
model_name="user",
index=models.Index(fields=["inn_code"], name="users_inn_code_inx"),
),
]

View File

@@ -0,0 +1,3 @@
# isort: skip_file
from .user import * # noqa
from .reset_token import * # noqa

View File

@@ -0,0 +1,15 @@
from django.contrib.auth import get_user_model
from django.db import models
from django_core.models import AbstractBaseModel
class ResetToken(AbstractBaseModel):
token = models.CharField(max_length=255, unique=True)
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
def __str__(self):
return self.token
class Meta:
verbose_name = "Reset Token"
verbose_name_plural = "Reset Tokens"

View File

@@ -0,0 +1,133 @@
import uuid
from django.contrib.auth import models as auth_models
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.core.validators import RegexValidator
from ..choices import RoleChoice
from ..managers import UserManager
phone_validator = RegexValidator(
regex=r'^\+?[1-9]\d{1,14}$',
message=_(
"Enter a valid international phone number "
"(E.164 format, e.g., +14155552671)."
)
)
class User(auth_models.AbstractUser):
id = models.UUIDField(
_("ID"),
primary_key=True,
default=uuid.uuid4,
editable=False
)
phone = models.CharField(
max_length=255,
validators=[
phone_validator
],
unique=True
)
username = models.CharField(
max_length=255,
null=True,
blank=True
)
validated_at = models.DateTimeField(
null=True,
blank=True
)
inn_code = models.CharField(
max_length=12,
null=True,
blank=True
)
first_name = models.CharField(
_("First Name"),
max_length=150,
blank=False,
null=False,
)
last_name = models.CharField(
_("Last Name"),
max_length=150,
blank=False,
null=False
)
email = models.EmailField(
_("Email Address"),
blank=True,
)
role = models.CharField(
_("Role"),
max_length=255,
choices=RoleChoice,
default=RoleChoice.USER,
)
created_at = models.DateTimeField(
verbose_name=_("Created At"),
auto_now_add=True
)
updated_at = models.DateTimeField(
verbose_name=_("Updated At"),
auto_now=True
)
USERNAME_FIELD = "phone"
REQUIRED_FIELDS = [
"first_name",
"last_name",
*auth_models.AbstractUser.REQUIRED_FIELDS
]
objects = UserManager() # type: ignore
def save(self, *args: object, **kwargs: object):
"""
save method overwriten to make self.role updated
every time when user is made admin or superuser
"""
if self.is_staff:
self.role = RoleChoice.ADMIN
if self.is_superuser:
self.role = RoleChoice.SUPERUSER
else:
self.role = RoleChoice.USER
super().save(*args, **kwargs) # type: ignore
def __str__(self):
return self.phone
@property
def full_name(self) -> str:
return f"{self.first_name} {self.last_name}"
class Meta:
db_table = "users"
verbose_name = _("User")
verbose_name_plural = _("Users")
indexes = [
models.Index(
fields=["phone"],
name="users_phone_inx"
),
models.Index(
fields=["email"],
name="users_email_inx",
),
models.Index(
fields=["inn_code"],
name="users_inn_code_inx"
)
]

View File

View File

@@ -0,0 +1 @@
from .core import * # noqa

View File

@@ -0,0 +1,10 @@
"""
Create a new user/superuser
"""
from django.contrib.auth import get_user_model
class UserSeeder:
def run(self):
get_user_model().objects.create_superuser("998888112309", "2309")

View File

@@ -0,0 +1,4 @@
from .auth import * # noqa
from .change_password import * # noqa
from .set_password import * # noqa
from .user import * # noqa

View File

@@ -0,0 +1,59 @@
from django.contrib.auth import get_user_model
from django.utils.translation import gettext as _
from rest_framework import exceptions, serializers
class LoginSerializer(serializers.Serializer):
username = serializers.CharField(max_length=255)
password = serializers.CharField(max_length=255)
class RegisterSerializer(serializers.ModelSerializer):
phone = serializers.CharField(max_length=255)
def validate_phone(self, value):
user = get_user_model().objects.filter(phone=value, validated_at__isnull=False)
if user.exists():
raise exceptions.ValidationError(_("Phone number already registered."), code="unique")
return value
class Meta:
model = get_user_model()
fields = ["first_name", "last_name", "phone", "password"]
extra_kwargs = {
"first_name": {
"required": True,
},
"last_name": {"required": True},
}
class ConfirmSerializer(serializers.Serializer):
code = serializers.IntegerField(min_value=1000, max_value=9999)
phone = serializers.CharField(max_length=255)
class ResetPasswordSerializer(serializers.Serializer):
phone = serializers.CharField(max_length=255)
def validate_phone(self, value):
user = get_user_model().objects.filter(phone=value)
if user.exists():
return value
raise serializers.ValidationError(_("User does not exist"))
class ResetConfirmationSerializer(serializers.Serializer):
code = serializers.IntegerField(min_value=1000, max_value=9999)
phone = serializers.CharField(max_length=255)
def validate_phone(self, value):
user = get_user_model().objects.filter(phone=value)
if user.exists():
return value
raise serializers.ValidationError(_("User does not exist"))
class ResendSerializer(serializers.Serializer):
phone = serializers.CharField(max_length=255)

View File

@@ -0,0 +1,6 @@
from rest_framework import serializers
class ChangePasswordSerializer(serializers.Serializer):
old_password = serializers.CharField(required=True)
new_password = serializers.CharField(required=True, min_length=8)

View File

@@ -0,0 +1,6 @@
from rest_framework import serializers
class SetPasswordSerializer(serializers.Serializer):
password = serializers.CharField()
token = serializers.CharField(max_length=255)

View File

@@ -0,0 +1,23 @@
from django.contrib.auth import get_user_model
from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer):
class Meta:
exclude = [
"created_at",
"updated_at",
"password",
"groups",
"user_permissions"
]
model = get_user_model()
class UserUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = get_user_model()
fields = [
"first_name",
"last_name"
]

View File

@@ -0,0 +1 @@
from .user import * # noqa

View File

@@ -0,0 +1,10 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth import get_user_model
@receiver(post_save, sender=get_user_model())
def user_signal(sender, created, instance, **kwargs):
if created and instance.username is None:
instance.username = "U%(id)s" % {"id": str(instance.id)}
instance.save()

View File

View File

@@ -0,0 +1,116 @@
import logging
from unittest.mock import patch
from django.test import TestCase
from django.urls import reverse
from pydantic import BaseModel
from rest_framework import status
from rest_framework.test import APIClient
from core.apps.accounts.models import ResetToken
from django_core.models import SmsConfirm
from core.services import SmsService
from django.contrib.auth import get_user_model
class TokenModel(BaseModel):
access: str
refresh: str
class SmsViewTest(TestCase):
def setUp(self):
self.client = APIClient()
self.phone = "998999999999"
self.password = "password"
self.code = "1111"
self.token = "token"
self.user = get_user_model().objects.create_user(
phone=self.phone, first_name="John", last_name="Doe", password=self.password
)
SmsConfirm.objects.create(phone=self.phone, code=self.code)
def test_reg_view(self):
"""Test register view."""
data = {
"phone": "998999999991",
"first_name": "John",
"last_name": "Doe",
"password": "password",
}
with patch.object(SmsService, "send_confirm", return_value=True):
response = self.client.post(reverse("auth-register"), data=data)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(
response.data["data"]["detail"],
"Sms %(phone)s raqamiga yuborildi" % {"phone": data["phone"]},
)
def test_confirm_view(self):
"""Test confirm view."""
data = {"phone": self.phone, "code": self.code}
response = self.client.post(reverse("auth-confirm"), data=data)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
def test_invalid_confirm_view(self):
"""Test confirm view."""
data = {"phone": self.phone, "code": "1112"}
response = self.client.post(reverse("auth-confirm"), data=data)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_reset_confirmation_code_view(self):
"""Test reset confirmation code view."""
data = {"phone": self.phone, "code": self.code}
response = self.client.post(reverse("auth-confirm"), data=data)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertIn("token", response.data["data"])
def test_reset_confirmation_code_view_invalid_code(self):
"""Test reset confirmation code view with invalid code."""
data = {"phone": self.phone, "code": "123456"}
response = self.client.post(reverse("auth-confirm"), data=data)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_reset_set_password_view(self):
"""Test reset set password view."""
token = ResetToken.objects.create(user=self.user, token=self.token)
data = {"token": token.token, "password": "new_password"}
response = self.client.post(reverse("reset-password-reset-password-set"), data=data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_reset_set_password_view_invalid_token(self):
"""Test reset set password view with invalid token."""
token = "test_token"
data = {"token": token, "password": "new_password"}
with patch.object(get_user_model().objects, "filter", return_value=get_user_model().objects.none()):
response = self.client.post(reverse("reset-password-reset-password-set"), data=data)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.data["data"]["detail"], "Invalid token")
def test_resend_view(self):
"""Test resend view."""
data = {"phone": self.phone}
response = self.client.post(reverse("auth-resend"), data=data)
logging.error(response.json())
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_reset_password_view(self):
"""Test reset password view."""
data = {"phone": self.phone}
response = self.client.post(reverse("reset-password-reset-password"), data=data)
logging.error(response.json())
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_me_view(self):
"""Test me view."""
self.client.force_authenticate(user=self.user)
response = self.client.get(reverse("me-me"))
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_me_update_view(self):
"""Test me update view."""
self.client.force_authenticate(user=self.user)
data = {"first_name": "Updated"}
response = self.client.patch(reverse("me-user-update"), data=data)
logging.error(response.json())
self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

@@ -0,0 +1,58 @@
from core.apps.accounts.serializers import ChangePasswordSerializer
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient
class ChangePasswordViewTest(TestCase):
def setUp(self):
self.client = APIClient()
self.phone = "9981111111"
self.password = "12345670"
self.path = reverse("change-password-change-password")
self.user = get_user_model().objects.create_user(
phone=self.phone, password=self.password, email="test@example.com"
)
self.client.force_authenticate(user=self.user)
def test_change_password_success(self):
data = {
"old_password": self.password,
"new_password": "newpassword",
}
response = self.client.post(self.path, data=data, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['data']["detail"], "password changed successfully")
self.assertTrue(self.user.check_password("newpassword"))
def test_change_password_invalid_old_password(self):
data = {
"old_password": "wrongpassword",
"new_password": "newpassword",
}
response = self.client.post(self.path, data=data, format="json")
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.data['data']["detail"], "invalida password")
def test_change_password_serializer_validation(self):
data = {
"old_password": self.password,
"new_password": "newpassword",
}
serializer = ChangePasswordSerializer(data=data)
self.assertTrue(serializer.is_valid())
data = {
"old_password": self.password,
"new_password": "123",
}
serializer = ChangePasswordSerializer(data=data)
self.assertFalse(serializer.is_valid())
def test_change_password_view_permissions(self):
self.client.force_authenticate(user=None)
response = self.client.post(self.path, data={}, format="json")
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

View File

@@ -0,0 +1,35 @@
"""
Accounts app urls
"""
from django.urls import path, include
from rest_framework_simplejwt import views as jwt_views
from .views import (
RegisterView,
ResetPasswordView,
MeView,
ChangePasswordView,
MeCompanyView,
)
from rest_framework.routers import DefaultRouter # type: ignore
router = DefaultRouter()
router.register("auth", RegisterView, basename="auth") # type: ignore
router.register("auth", ResetPasswordView, basename="reset-password") # type: ignore
router.register("auth", MeView, basename="me") # type: ignore
router.register("auth", ChangePasswordView, basename="change-password") # type: ignore
router.register(r"me/companies", MeCompanyView, "me-company") # type: ignore
urlpatterns = [ # type: ignore
path("", include(router.urls)), # type: ignore
path("auth/token/", jwt_views.TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("auth/token/verify/", jwt_views.TokenVerifyView.as_view(), name="token_verify"),
path(
"auth/token/refresh/",
jwt_views.TokenRefreshView.as_view(),
name="token_refresh",
),
]

View File

@@ -0,0 +1,3 @@
from .auth import * # noqa
from .users import * # type: ignore
from .me import * # type: ignore

View File

@@ -0,0 +1,209 @@
import uuid
from typing import Type
from core.services import UserService, SmsService
from django.contrib.auth import get_user_model
from django.utils.translation import gettext_lazy as _
from django_core import exceptions
from drf_spectacular.utils import extend_schema
from rest_framework import status, throttling, request
from rest_framework.response import Response
from rest_framework.exceptions import PermissionDenied
from rest_framework.viewsets import GenericViewSet
from django_core.mixins import BaseViewSetMixin
from rest_framework.decorators import action
from ..serializers import (
RegisterSerializer,
ConfirmSerializer,
ResendSerializer,
ResetPasswordSerializer,
ResetConfirmationSerializer,
SetPasswordSerializer,
UserSerializer,
UserUpdateSerializer,
)
from rest_framework.permissions import AllowAny
from django.contrib.auth.hashers import make_password
from drf_spectacular.utils import OpenApiResponse
from rest_framework.permissions import IsAuthenticated
from ..serializers import ChangePasswordSerializer
from .. import models
@extend_schema(tags=["register"])
class RegisterView(BaseViewSetMixin, GenericViewSet, UserService):
throttle_classes = [throttling.UserRateThrottle]
permission_classes = [AllowAny]
def get_serializer_class(self):
match self.action:
case "register":
return RegisterSerializer
case "confirm":
return ConfirmSerializer
case "resend":
return ResendSerializer
case _:
return RegisterSerializer
@action(methods=["POST"], detail=False, url_path="register")
def register(self, request):
ser = self.get_serializer(data=request.data)
ser.is_valid(raise_exception=True)
data = ser.data
phone = data.get("phone")
# Create pending user
self.create_user(phone, data.get("first_name"), data.get("last_name"), data.get("password"))
self.send_confirmation(phone) # Send confirmation code for sms eskiz.uz
return Response(
{"detail": _("Sms %(phone)s raqamiga yuborildi") % {"phone": phone}},
status=status.HTTP_202_ACCEPTED,
)
@extend_schema(summary="Auth confirm.", description="Auth confirm user.")
@action(methods=["POST"], detail=False, url_path="confirm")
def confirm(self, request):
ser = self.get_serializer(data=request.data)
ser.is_valid(raise_exception=True)
data = ser.data
phone, code = data.get("phone"), data.get("code")
try:
if SmsService.check_confirm(phone, code=code):
token = self.validate_user(get_user_model().objects.filter(phone=phone).first())
return Response(
data={
"detail": _("Tasdiqlash ko'di qabul qilindi"),
"token": token,
},
status=status.HTTP_202_ACCEPTED,
)
except exceptions.SmsException as e:
raise PermissionDenied(e) # Response exception for APIException
except Exception as e:
raise PermissionDenied(e) # Api exception for APIException
@action(methods=["POST"], detail=False, url_path="resend")
def resend(self, rq: Type[request.Request]):
ser = self.get_serializer(data=rq.data)
ser.is_valid(raise_exception=True)
phone = ser.data.get("phone")
self.send_confirmation(phone)
return Response({"detail": _("Sms %(phone)s raqamiga yuborildi") % {"phone": phone}})
@extend_schema(tags=["reset-password"])
class ResetPasswordView(BaseViewSetMixin, GenericViewSet, UserService):
permission_classes = [AllowAny]
def get_serializer_class(self):
match self.action:
case "reset_password":
return ResetPasswordSerializer
case "reset_confirm":
return ResetConfirmationSerializer
case "reset_password_set":
return SetPasswordSerializer
case _:
return None
@action(methods=["POST"], detail=False, url_path="reset-password")
def reset_password(self, request):
ser = self.get_serializer(data=request.data)
ser.is_valid(raise_exception=True)
phone = ser.data.get("phone")
self.send_confirmation(phone)
return Response({"detail": _("Sms %(phone)s raqamiga yuborildi") % {"phone": phone}})
@action(methods=["POST"], detail=False, url_path="reset-password-confirm")
def reset_confirm(self, request):
ser = self.get_serializer(data=request.data)
ser.is_valid(raise_exception=True)
data = ser.data
code, phone = data.get("code"), data.get("phone")
try:
SmsService.check_confirm(phone, code)
token = models.ResetToken.objects.create(
user=get_user_model().objects.filter(phone=phone).first(),
token=str(uuid.uuid4()),
)
return Response(
data={
"token": token.token,
"created_at": token.created_at,
"updated_at": token.updated_at,
},
status=status.HTTP_200_OK,
)
except exceptions.SmsException as e:
raise PermissionDenied(str(e))
except Exception as e:
raise PermissionDenied(str(e))
@action(methods=["POST"], detail=False, url_path="reset-password-set")
def reset_password_set(self, request):
ser = self.get_serializer(data=request.data)
ser.is_valid(raise_exception=True)
data = ser.data
token = data.get("token")
password = data.get("password")
token = models.ResetToken.objects.filter(token=token)
if not token.exists():
raise PermissionDenied(_("Invalid token"))
phone = token.first().user.phone
token.delete()
self.change_password(phone, password)
return Response({"detail": _("password updated")}, status=status.HTTP_200_OK)
@extend_schema(tags=["me"])
class MeView(BaseViewSetMixin, GenericViewSet, UserService):
permission_classes = [IsAuthenticated]
def get_serializer_class(self):
match self.action:
case "me":
return UserSerializer
case "user_update":
return UserUpdateSerializer
case _:
return None
@action(methods=["GET", "OPTIONS"], detail=False, url_path="me")
def me(self, request):
return Response(self.get_serializer(request.user).data)
@action(methods=["PATCH", "PUT"], detail=False, url_path="user-update")
def user_update(self, request):
ser = self.get_serializer(instance=request.user, data=request.data, partial=True)
ser.is_valid(raise_exception=True)
ser.save()
return Response({"detail": _("Malumotlar yangilandi")})
@extend_schema(tags=["change-password"], description="Parolni o'zgartirish uchun")
class ChangePasswordView(BaseViewSetMixin, GenericViewSet):
serializer_class = ChangePasswordSerializer
permission_classes = (IsAuthenticated,)
@extend_schema(
request=serializer_class,
responses={200: OpenApiResponse(ChangePasswordSerializer)},
summary="Change user password.",
description="Change password of the authenticated user.",
)
@action(methods=["POST"], detail=False, url_path="change-password")
def change_password(self, request, *args, **kwargs):
user = self.request.user
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
if user.check_password(request.data["old_password"]):
user.password = make_password(request.data["new_password"])
user.save()
return Response(
data={"detail": "password changed successfully"},
status=status.HTTP_200_OK,
)
raise PermissionDenied(_("invalida password"))

View File

@@ -0,0 +1,70 @@
from rest_framework.viewsets import GenericViewSet # type: ignore
from rest_framework.decorators import action # type: ignore
from rest_framework import status # type: ignore
from rest_framework.request import HttpRequest # type: ignore
from rest_framework.response import Response # type: ignore
from rest_framework.permissions import ( # type: ignore
IsAuthenticated
)
from django_core.mixins import BaseViewSetMixin # type: ignore
from core.apps.companies.serializers import (
RetrieveCompanySerializer,
CreateCompanySerializer,
)
from core.apps.companies.models import (
CompanyModel,
CompanyAccountModel
)
from django.db import transaction
class MeCompanyView(BaseViewSetMixin, GenericViewSet):
permission_classes = [IsAuthenticated]
action_permission_classes = {}
action_serializer_class = {
"create": CreateCompanySerializer,
"list": RetrieveCompanySerializer,
}
def list(
self,
request: HttpRequest,
*args: object,
**kwargs: object
) -> Response:
companies = CompanyModel.objects.filter(
accounts__user=request.user
)
return Response(
RetrieveCompanySerializer(instance=companies, many=True).data,
status=status.HTTP_200_OK
)
def create(
self,
request: HttpRequest,
*args: object,
**kwargs: object
) -> Response:
with transaction.atomic():
serializer = CreateCompanySerializer(data=request.data) # type: ignore
serializer.is_valid(raise_exception=True)
company = serializer.save() # type: ignore
account = CompanyAccountModel(
company=company,
user=request.user
)
account.save()
return Response(
data=serializer.data,
status=status.HTTP_201_CREATED
)

View File

@@ -0,0 +1,84 @@
import uuid
from drf_spectacular.utils import extend_schema
from rest_framework.viewsets import GenericViewSet # type: ignore
from rest_framework.decorators import action # type: ignore
from rest_framework import status # type: ignore
from rest_framework.request import HttpRequest # type: ignore
from rest_framework.response import Response # type: ignore
from rest_framework.permissions import ( # type: ignore
IsAdminUser,
)
from django_core.mixins import BaseViewSetMixin
from rest_framework.generics import get_object_or_404 # type: ignore
from django.contrib.auth import get_user_model
from django.db import transaction
from core.apps.companies.serializers import (
CreateCompanySerializer,
RetrieveCompanySerializer
)
from core.apps.companies.models import (
CompanyModel,
CompanyAccountModel,
)
UserModel = get_user_model()
class UserCompaniesView(BaseViewSetMixin, GenericViewSet):
permission_classes = [IsAdminUser]
action_permission_classes = {}
action_permission_classes = {
"list_company": RetrieveCompanySerializer,
"create_company": CreateCompanySerializer,
}
@extend_schema(
summary="Get list of companies",
description="Get list of companies",
)
@action(url_path="companies", detail=True, methods=["GET"])
def list_company(
self,
request: HttpRequest,
pk: uuid.UUID,
*args: object,
**kwargs: object,
) -> Response:
companies = CompanyModel.objects.filter(accounts__user__pk=pk)
return Response(
data=RetrieveCompanySerializer(instance=companies, many=True),
status=status.HTTP_200_OK
)
@extend_schema(
summary="Create Company",
description="Create Company",
)
@action(url_path="companies", detail=True, methods=["POST"])
def create_company(
self,
request: HttpRequest,
pk: uuid.UUID,
*args: object,
**kwargs: object,
) -> Response:
with transaction.atomic():
ser = CreateCompanySerializer(data=request.data) # type: ignore
ser.is_valid(raise_exception=True)
company = ser.save() # type: ignore
user = get_object_or_404(UserModel, pk=pk)
account = CompanyAccountModel(company=company, user=user)
account.save()
return Response(data=ser.data, status=status.HTTP_201_CREATED)

View File

View File

@@ -0,0 +1 @@
from .banks import * # noqa

View File

@@ -0,0 +1,23 @@
from django.contrib import admin
from unfold.admin import ModelAdmin # type: ignore
from core.apps.banks.models import BankModel
@admin.register(BankModel)
class BankAdmin(ModelAdmin):
list_display = (
"name",
"bic_code",
"created_at",
"updated_at",
)
search_fields = (
"name",
"bic_code",
"created_at",
"updated_at"
)
list_display_links = (
"name",
)

6
core/apps/banks/apps.py Normal file
View File

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

View File

@@ -0,0 +1 @@
from .banks import * # noqa

View File

@@ -0,0 +1,13 @@
from django_filters import rest_framework as filters
from core.apps.banks.models import BanksModel
class BanksFilter(filters.FilterSet):
# name = filters.CharFilter(field_name="name", lookup_expr="icontains")
class Meta:
model = BanksModel
fields = [
"name",
]

View File

@@ -0,0 +1 @@
from .banks import * # noqa

View File

@@ -0,0 +1,10 @@
from django import forms
from core.apps.banks.models import BanksModel
class BanksForm(forms.ModelForm):
class Meta:
model = BanksModel
fields = "__all__"

View File

@@ -0,0 +1,65 @@
# Generated by Django 5.2.4 on 2025-08-01 09:53
import django.core.validators
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="BankModel",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name="ID"
),
),
("created_at", models.DateTimeField(auto_now_add=True, verbose_name="Created At")),
("updated_at", models.DateTimeField(auto_now=True, verbose_name="Updated At")),
(
"name",
models.CharField(
max_length=255,
unique=True,
validators=[
django.core.validators.RegexValidator(
message="Enter a valid bank name. Only letters, numbers, spaces, hyphens, ampersands, commas, periods, apostrophes, and parentheses are allowed. Length must be 2100 characters.",
regex="^[A-Za-z0-9À-ÿ&'\\-.,() ]{2,100}$",
)
],
verbose_name="Name",
),
),
(
"bic_code",
models.CharField(
unique=True,
validators=[
django.core.validators.RegexValidator(
code="Invalid BIC/SWIFT code",
message="Enter a valid BIC/SWIFT code (8 or 11 uppercase letters/numbers).",
regex="^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$",
)
],
verbose_name="BIC code",
),
),
],
options={
"verbose_name": "Bank",
"verbose_name_plural": "Banks",
"db_table": "banks",
"indexes": [
models.Index(fields=["bic_code"], name="banks_bic_code_inx"),
models.Index(fields=["name"], name="banks_name_inx"),
],
},
),
]

View File

View File

@@ -0,0 +1 @@
from .banks import * # noqa

View File

@@ -0,0 +1,68 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from core.utils.base_model import UUIDPrimaryKeyBaseModel
from core.apps.banks.validators.banks import (
BankValidator, # used with BankModel.clean
bic_validator, # validates bic
name_validator, # validates bank name
)
class BankModel(UUIDPrimaryKeyBaseModel):
name = models.CharField(
_("Name"),
max_length=255,
validators=[
name_validator,
],
null=False,
blank=False,
unique=True,
)
bic_code = models.CharField(
_("BIC code"),
validators=[
bic_validator,
],
null=False,
blank=False,
unique=True,
)
def __str__(self):
return self.name
@classmethod
def _create_fake(cls):
return cls.objects.create(
name="Mock TBC Bank",
bic_code="MOCKUZ22"
)
def clean(self) -> None:
super().clean()
validator = BankValidator(self)
validator()
class Meta: # type: ignore
db_table = "banks"
verbose_name = _("Bank")
verbose_name_plural = _("Banks")
indexes = [
models.Index(
fields=["bic_code"],
name="banks_bic_code_inx"
),
models.Index(
fields=["name"],
name="banks_name_inx"
),
]

View File

@@ -0,0 +1 @@
from .banks import * # noqa

View File

@@ -0,0 +1,12 @@
from rest_framework import permissions
class BanksPermission(permissions.BasePermission):
def __init__(self) -> None: ...
def __call__(self, *args, **kwargs):
return self
def has_permission(self, request, view):
return True

View File

@@ -0,0 +1 @@
from .banks import * # noqa

View File

@@ -0,0 +1 @@
from .banks import * # noqa

View File

@@ -0,0 +1,35 @@
from rest_framework import serializers # type: ignore
from core.apps.banks.models import BankModel
class BaseBankSerializer(serializers.ModelSerializer):
class Meta:
model = BankModel
fields = "__all__"
read_only_fields = (
"id",
"created_at",
"updated_at"
)
class ListBankSerializer(BaseBankSerializer):
class Meta(BaseBankSerializer.Meta): ...
class RetrieveBankSerializer(BaseBankSerializer):
class Meta(BaseBankSerializer.Meta): ...
class CreateBankSerializer(BaseBankSerializer):
class Meta(BaseBankSerializer.Meta): ...
class UpdateBankSerializer(BaseBankSerializer):
class Meta(BaseBankSerializer.Meta): ...
class DestroyBankSerializer(BaseBankSerializer):
class Meta(BaseBankSerializer.Meta): ...

View File

@@ -0,0 +1 @@
from .banks import * # noqa

View File

@@ -0,0 +1,8 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from core.apps.banks.models import BanksModel
@receiver(post_save, sender=BanksModel)
def BanksSignal(sender, instance, created, **kwargs): ...

View File

@@ -0,0 +1 @@
from .test_banks import * # noqa

View File

@@ -0,0 +1,47 @@
from django.test import TestCase
from django.urls import reverse
from rest_framework.test import APIClient
from core.apps.banks.models import BanksModel
class BanksTest(TestCase):
def _create_data(self):
return BanksModel._create_fake()
def setUp(self):
self.client = APIClient()
self.instance = self._create_data()
self.urls = {
"list": reverse("Banks-list"),
"retrieve": reverse("Banks-detail", kwargs={"pk": self.instance.pk}),
"retrieve-not-found": reverse("Banks-detail", kwargs={"pk": 1000}),
}
def test_create(self):
self.assertTrue(True)
def test_update(self):
self.assertTrue(True)
def test_partial_update(self):
self.assertTrue(True)
def test_destroy(self):
self.assertTrue(True)
def test_list(self):
response = self.client.get(self.urls["list"])
self.assertTrue(response.json()["status"])
self.assertEqual(response.status_code, 200)
def test_retrieve(self):
response = self.client.get(self.urls["retrieve"])
self.assertTrue(response.json()["status"])
self.assertEqual(response.status_code, 200)
def test_retrieve_not_found(self):
response = self.client.get(self.urls["retrieve-not-found"])
self.assertFalse(response.json()["status"])
self.assertEqual(response.status_code, 404)

View File

@@ -0,0 +1 @@
from .banks import * # noqa

View File

@@ -0,0 +1,8 @@
from modeltranslation.translator import TranslationOptions, register
from core.apps.banks.models import BanksModel
@register(BanksModel)
class BanksTranslation(TranslationOptions):
fields = []

11
core/apps/banks/urls.py Normal file
View File

@@ -0,0 +1,11 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views
router = DefaultRouter()
router.register(r"banks", views.BankView, "banks")
urlpatterns = [
path("", include(router.urls)),
]

View File

@@ -0,0 +1 @@
from .banks import * # noqa

View File

@@ -0,0 +1,33 @@
# from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.utils.translation import gettext_lazy as _
from django_core.models.base import AbstractBaseModel # type: ignore
bic_validator = RegexValidator(
regex=r'^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$',
message=_(
"Enter a valid BIC/SWIFT code "
"(8 or 11 uppercase letters/numbers)."
),
code='Invalid BIC/SWIFT code'
)
name_validator = RegexValidator(
regex=r"^[A-Za-z0-9À-ÿ&'\-.,() ]{2,100}$",
message=_(
"Enter a valid bank name. "
"Only letters, numbers, spaces, hyphens, ampersands, "
"commas, periods, apostrophes, and parentheses are "
"allowed. Length must be 2100 characters."
)
)
class BankValidator:
def __init__(self, instance: AbstractBaseModel):
self.instance = instance
def __call__(self):
return

View File

@@ -0,0 +1 @@
from .banks import * # noqa

View File

@@ -0,0 +1,29 @@
from django_core.mixins import BaseViewSetMixin
from drf_spectacular.utils import extend_schema
from rest_framework.permissions import IsAdminUser
from rest_framework.viewsets import ModelViewSet
from core.apps.banks.models import BankModel
from core.apps.banks.serializers.banks import (
CreateBankSerializer,
ListBankSerializer,
RetrieveBankSerializer,
UpdateBankSerializer,
DestroyBankSerializer,
)
@extend_schema(tags=["Banks"])
class BankView(BaseViewSetMixin, ModelViewSet):
queryset = BankModel.objects.all()
serializer_class = ListBankSerializer
permission_classes = [IsAdminUser]
action_permission_classes = {}
action_serializer_class = {
"list": ListBankSerializer,
"retrieve": RetrieveBankSerializer,
"create": CreateBankSerializer,
"update": UpdateBankSerializer,
"destroy": DestroyBankSerializer,
}

View File

View File

@@ -0,0 +1,3 @@
from .accounts import * # noqa
from .companies import * # noqa
from .folders import * # noqa

View File

@@ -0,0 +1,27 @@
from django.contrib import admin
from unfold.admin import ModelAdmin # type: ignore
from core.apps.companies.models import CompanyAccountModel
@admin.register(CompanyAccountModel)
class DirectorAdmin(ModelAdmin):
list_display = (
"user_name",
"role",
"company",
"created_at",
)
list_display_links = (
"user_name",
"role",
)
search_fields = (
"user",
"role",
"company",
"created_at",
"updated_at",
)

View File

@@ -0,0 +1,32 @@
from django.contrib import admin
from unfold.admin import ModelAdmin # type: ignore
from core.apps.companies.models import CompanyModel
@admin.register(CompanyModel)
class CompanyAdmin(ModelAdmin):
list_display = (
"name",
"company_code",
"phone",
"email",
"iik_code",
"legal_address",
"real_address",
"created_at",
)
search_fields = (
"name",
"company_code",
"phone",
"email",
"iik_code",
"legal_address",
"real_address",
"created_at",
"updated_at"
)
list_display_links = (
"name",
)

View File

@@ -0,0 +1,24 @@
from django.contrib import admin
from unfold.admin import ModelAdmin # type: ignore
from core.apps.companies.models import CompanyFolderModel
@admin.register(CompanyFolderModel)
class FolderAdmin(ModelAdmin):
list_display = (
"name",
"company",
"created_at"
)
search_fields = (
"name",
"company",
"created_at",
"updated_at",
)
list_display_links = (
"name",
)

View File

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

View File

@@ -0,0 +1,3 @@
from .accounts import * # noqa
from .companies import * # noqa
from .folders import * # noqa

View File

@@ -0,0 +1,13 @@
from django_filters import rest_framework as filters
from core.apps.companies.models import CompanyaccountModel
class CompanyaccountFilter(filters.FilterSet):
# name = filters.CharFilter(field_name="name", lookup_expr="icontains")
class Meta:
model = CompanyaccountModel
fields = [
"name",
]

View File

@@ -0,0 +1,13 @@
from django_filters import rest_framework as filters
from core.apps.companies.models import CompanyModel
class CompanyFilter(filters.FilterSet):
# name = filters.CharFilter(field_name="name", lookup_expr="icontains")
class Meta:
model = CompanyModel
fields = [
"name",
]

View File

@@ -0,0 +1,13 @@
from django_filters import rest_framework as filters
from core.apps.companies.models import CompanyfolderModel
class CompanyfolderFilter(filters.FilterSet):
# name = filters.CharFilter(field_name="name", lookup_expr="icontains")
class Meta:
model = CompanyfolderModel
fields = [
"name",
]

View File

@@ -0,0 +1,3 @@
from .accounts import * # noqa
from .companies import * # noqa
from .folders import * # noqa

View File

@@ -0,0 +1,10 @@
from django import forms
from core.apps.companies.models import CompanyaccountModel
class CompanyaccountForm(forms.ModelForm):
class Meta:
model = CompanyaccountModel
fields = "__all__"

View File

@@ -0,0 +1,10 @@
from django import forms
from core.apps.companies.models import CompanyModel
class CompanyForm(forms.ModelForm):
class Meta:
model = CompanyModel
fields = "__all__"

View File

@@ -0,0 +1,10 @@
from django import forms
from core.apps.companies.models import CompanyfolderModel
class CompanyfolderForm(forms.ModelForm):
class Meta:
model = CompanyfolderModel
fields = "__all__"

View File

@@ -0,0 +1,257 @@
# Generated by Django 5.2.4 on 2025-08-01 09:53
import django.core.validators
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("banks", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="CompanyModel",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name="ID"
),
),
("created_at", models.DateTimeField(auto_now_add=True, verbose_name="Created At")),
("updated_at", models.DateTimeField(auto_now=True, verbose_name="Updated At")),
(
"name",
models.CharField(
max_length=512,
validators=[
django.core.validators.RegexValidator(
message="Company name contains invalid characters.",
regex="^[A-Za-z0-9\\s\\.\\-\\,\\&]+$",
),
django.core.validators.MinLengthValidator(3),
],
verbose_name="name",
),
),
(
"pinfl_code",
models.CharField(
blank=True,
null=True,
validators=[
django.core.validators.RegexValidator(
message="PINFL code must be exactly 14 digits.", regex="^\\d{14}$"
)
],
verbose_name="PINFL code",
),
),
(
"iin_code",
models.CharField(
blank=True,
null=True,
validators=[
django.core.validators.RegexValidator(
message="IIN code must be exactly 14 digits.", regex="^\\d{14}$"
)
],
verbose_name="IIN code",
),
),
(
"phone",
models.CharField(
max_length=25,
unique=True,
validators=[
django.core.validators.RegexValidator(
message="Enter a valid international phone number (E.164 format, e.g., +14155552671).",
regex="^\\+?[1-9]\\d{1,14}$",
)
],
verbose_name="Phone Number",
),
),
(
"email",
models.CharField(
max_length=255,
unique=True,
validators=[django.core.validators.EmailValidator()],
verbose_name="Email Address",
),
),
(
"iik_code",
models.CharField(
blank=True,
max_length=30,
null=True,
validators=[
django.core.validators.RegexValidator(
message="IIK code must be alphanumeric and between 10 and 30 characters.",
regex="^[A-Z0-9]{10,30}$",
)
],
verbose_name="IIK code",
),
),
(
"legal_address",
models.CharField(
blank=True,
max_length=512,
null=True,
validators=[django.core.validators.MinLengthValidator(10)],
verbose_name="Legal Address",
),
),
(
"real_address",
models.CharField(
blank=True,
max_length=512,
null=True,
validators=[django.core.validators.MinLengthValidator(10)],
verbose_name="Real Address",
),
),
("logo", models.ImageField(upload_to="", verbose_name="Logo")),
(
"signature_authority_document",
models.FileField(upload_to="", verbose_name="Signature Authority Document"),
),
(
"bank",
models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to="banks.bankmodel"
),
),
],
options={
"verbose_name": "Company",
"verbose_name_plural": "Companies",
"db_table": "companies",
},
),
migrations.CreateModel(
name="CompanyFolderModel",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name="ID"
),
),
("created_at", models.DateTimeField(auto_now_add=True, verbose_name="Created At")),
("updated_at", models.DateTimeField(auto_now=True, verbose_name="Updated At")),
(
"name",
models.CharField(
max_length=150,
validators=[
django.core.validators.MaxLengthValidator(150),
django.core.validators.MinLengthValidator(3),
],
verbose_name="name",
),
),
(
"company",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, related_name="folders", to="companies.companymodel"
),
),
],
options={
"verbose_name": "Company Folder",
"verbose_name_plural": "Company Folders",
"db_table": "company_folders",
},
),
migrations.CreateModel(
name="CompanyAccountModel",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name="ID"
),
),
("created_at", models.DateTimeField(auto_now_add=True, verbose_name="Created At")),
("updated_at", models.DateTimeField(auto_now=True, verbose_name="Updated At")),
("role", models.CharField(blank=True, max_length=255, null=True, verbose_name="Role")),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="companies",
to=settings.AUTH_USER_MODEL,
verbose_name="User",
),
),
(
"company",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="accounts",
to="companies.companymodel",
verbose_name="Company",
),
),
],
options={
"verbose_name": "Company Account",
"verbose_name_plural": "Company Accounts",
"db_table": "company_accounts",
},
),
migrations.AddIndex(
model_name="companymodel",
index=models.Index(fields=["phone"], name="companies_phone_inx"),
),
migrations.AddIndex(
model_name="companymodel",
index=models.Index(fields=["email"], name="companies_email_inx"),
),
migrations.AddIndex(
model_name="companymodel",
index=models.Index(fields=["iin_code"], name="companies_iin_code_inx"),
),
migrations.AddIndex(
model_name="companymodel",
index=models.Index(fields=["pinfl_code"], name="companies_pinfl_code_inx"),
),
migrations.AddIndex(
model_name="companymodel",
index=models.Index(fields=["name"], name="companies_name_inx"),
),
migrations.AddConstraint(
model_name="companyfoldermodel",
constraint=models.UniqueConstraint(
fields=("name", "company"), name="company_folders_name_company_unique_constraint"
),
),
migrations.AddIndex(
model_name="companyaccountmodel",
index=models.Index(fields=["user"], name="company_accounts_user_inx"),
),
migrations.AddIndex(
model_name="companyaccountmodel",
index=models.Index(fields=["company"], name="company_accounts_company_inx"),
),
migrations.AlterUniqueTogether(
name="companyaccountmodel",
unique_together={("user", "company")},
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.2.4 on 2025-08-01 10:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("companies", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="companymodel",
name="logo",
field=models.ImageField(blank=True, null=True, upload_to="", verbose_name="Logo"),
),
migrations.AlterField(
model_name="companymodel",
name="signature_authority_document",
field=models.FileField(blank=True, null=True, upload_to="", verbose_name="Signature Authority Document"),
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 5.2.4 on 2025-08-01 12:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("companies", "0002_alter_companymodel_logo_and_more"),
("contracts", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="companyfoldermodel",
name="contracts",
field=models.ManyToManyField(
related_name="folders", to="contracts.contractmodel", verbose_name="Contracts"
),
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 5.2.4 on 2025-08-04 09:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("companies", "0003_companyfoldermodel_contracts"),
]
operations = [
migrations.AlterField(
model_name="companymodel",
name="logo",
field=models.ImageField(blank=True, max_length=1024, null=True, upload_to="", verbose_name="Logo"),
),
migrations.AlterField(
model_name="companymodel",
name="signature_authority_document",
field=models.FileField(
blank=True, max_length=1024, null=True, upload_to="", verbose_name="Signature Authority Document"
),
),
]

View File

@@ -0,0 +1,3 @@
from .accounts import * # noqa
from .companies import * # noqa
from .folders import * # noqa

View File

@@ -0,0 +1,77 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.contrib.auth import get_user_model
from core.utils.base_model import UUIDPrimaryKeyBaseModel
from core.apps.companies.models.companies import CompanyModel
from core.apps.companies.validators.accounts import (
CompanyAccountValidator
)
UserModel = get_user_model()
class CompanyAccountModel(UUIDPrimaryKeyBaseModel):
role = models.CharField(
_("Role"),
max_length=255,
null=True,
blank=True,
)
user = models.ForeignKey( # type: ignore
UserModel,
on_delete=models.CASCADE,
verbose_name=_("User"),
null=False,
blank=False,
related_name="companies"
)
company = models.ForeignKey( # type: ignore
CompanyModel,
on_delete=models.CASCADE,
verbose_name=_("Company"),
null=False,
blank=False,
related_name="accounts",
)
def __str__(self):
return (
f"{self.user!s} "
f"{self.company!s} "
f"{self.role}"
)
def clean(self) -> None:
super().clean()
validator = CompanyAccountValidator(self)
validator()
@property
def user_name(self) -> str: # type: ignore
return self.user.full_name # type: ignore
@classmethod
def _create_fake(cls):
return cls.objects.create(
name="Mock CompanyAccount",
user=UserModel._create_fake(), # type: ignore
company=CompanyModel._create_fake() # type: ignore
)
class Meta: # type: ignore
db_table = "company_accounts"
verbose_name = _("Company Account")
verbose_name_plural = _("Company Accounts")
indexes = [
models.Index(fields=["user"], name="company_accounts_user_inx"),
models.Index(fields=["company"], name="company_accounts_company_inx")
]
unique_together = ["user", "company"]

View File

@@ -0,0 +1,184 @@
from typing import Self
from django.db import models
from django.core.validators import (
EmailValidator,
MinLengthValidator,
)
from django.utils.translation import gettext_lazy as _
from django.contrib.auth import get_user_model
from core.utils.base_model import UUIDPrimaryKeyBaseModel
from core.apps.banks.models import BankModel
from core.apps.companies.validators.companies import (
iik_validator,
iin_validator,
name_validator,
phone_validator,
pinfl_validator,
CompanyValidator,
)
UserModel = get_user_model()
class CompanyModel(UUIDPrimaryKeyBaseModel):
name = models.CharField(
_("name"),
max_length=512,
validators=[
name_validator,
MinLengthValidator(3)
],
null=False,
blank=False
)
pinfl_code = models.CharField(
_("PINFL code"),
validators=[
pinfl_validator,
],
null=True,
blank=True,
)
iin_code = models.CharField(
_("IIN code"),
validators=[iin_validator],
null=True,
blank=True
)
phone = models.CharField(
_("Phone Number"),
max_length=25,
unique=True,
validators=[
phone_validator,
],
null=False,
blank=False,
)
email = models.CharField(
_("Email Address"),
max_length=255,
validators=[
EmailValidator()
],
unique=True,
null=False,
blank=False,
)
iik_code = models.CharField(
_("IIK code"),
max_length=30,
validators=[
iik_validator
],
null=True, blank=True
)
bank = models.ForeignKey(
BankModel,
on_delete=models.SET_NULL,
null=True,
blank=True
)
legal_address = models.CharField(
_("Legal Address"),
max_length=512,
validators=[
MinLengthValidator(10)
],
null=True,
blank=True
)
real_address = models.CharField(
_("Real Address"),
max_length=512,
validators=[
MinLengthValidator(10)
],
null=True,
blank=True
)
logo = models.ImageField(
_("Logo"),
max_length=1024,
null=True,
blank=True
)
signature_authority_document = models.FileField(
_("Signature Authority Document"),
max_length=1024,
null=True,
blank=True
)
def __str__(self):
return self.name
@property
def company_code(self) -> str:
if self.pinfl_code is not None:
return f"PINFL code: {self.pinfl_code}"
else:
return f"IIN code: {self.iin_code}"
def clean(self):
super().clean()
validator = CompanyValidator(self)
validator()
@classmethod
def _create_fake(cls) -> Self:
return cls.objects.create(
name="mock LLC",
pinfl_code="12345678901234",
iin_code="12345678901234",
phone="+998901234567",
email="mock@example.com",
iik_code="UZS1234567890",
legal_address="Some legal address, City, Country",
real_address="Same as above",
bank=BankModel._create_fake() # type: ignore
)
class Meta: # type: ignore
db_table = "companies"
verbose_name = _("Company")
verbose_name_plural = _("Companies")
indexes = [
models.Index(
fields=["phone"],
name="companies_phone_inx"
),
models.Index(
fields=["email"],
name="companies_email_inx"
),
models.Index(
fields=["iin_code"],
name="companies_iin_code_inx"
),
models.Index(
fields=["pinfl_code"],
name="companies_pinfl_code_inx"
),
models.Index(
fields=["name"],
name="companies_name_inx"
),
]

View File

@@ -0,0 +1,68 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.core.validators import (
MaxLengthValidator,
MinLengthValidator,
)
from core.utils.base_model import UUIDPrimaryKeyBaseModel
from core.apps.companies.models.companies import CompanyModel
from core.apps.companies.validators.folders import (
CompanyFolderValidator
)
from core.apps.contracts.models import ContractModel
class CompanyFolderModel(UUIDPrimaryKeyBaseModel):
name = models.CharField(
_("name"),
max_length=150,
validators=[
MaxLengthValidator(150),
MinLengthValidator(3),
]
)
company = models.ForeignKey(
CompanyModel,
on_delete=models.PROTECT,
related_name="folders",
null=False,
blank=False
)
contracts = models.ManyToManyField( # type: ignore
ContractModel,
verbose_name=_("Contracts"),
related_name="folders",
)
def __str__(self):
return f"{self.name} {self.company!s}"
@classmethod
def _create_fake(cls):
return cls.objects.create(
name="mock",
company=CompanyModel._create_fake() # type: ignore
)
def clean(self) -> None:
super().clean()
validator = CompanyFolderValidator(self)
validator()
class Meta: # type: ignore
db_table = "company_folders"
verbose_name = _("Company Folder")
verbose_name_plural = _("Company Folders")
constraints = [
models.UniqueConstraint(
fields=["name", "company"],
name="company_folders_name_company_unique_constraint"
)
]

View File

@@ -0,0 +1,3 @@
from .accounts import * # noqa
from .companies import * # noqa
from .folders import * # noqa

View File

@@ -0,0 +1,12 @@
from rest_framework import permissions
class CompanyaccountPermission(permissions.BasePermission):
def __init__(self) -> None: ...
def __call__(self, *args, **kwargs):
return self
def has_permission(self, request, view):
return True

View File

@@ -0,0 +1,23 @@
from rest_framework import permissions # type: ignore
from rest_framework.views import APIView # type: ignore
from rest_framework.request import HttpRequest # type: ignore
from core.apps.companies.models import (
CompanyAccountModel,
CompanyModel
)
class IsCompanyAccount(permissions.IsAuthenticated):
def has_object_permission( # type: ignore
self,
request: HttpRequest,
view: APIView,
obj: CompanyModel
) -> bool:
if request.user.is_staff:
return True
return CompanyAccountModel.objects.filter(
company=obj, user=request.user,
).exists()

View File

@@ -0,0 +1,12 @@
from rest_framework import permissions
class CompanyfolderPermission(permissions.BasePermission):
def __init__(self) -> None: ...
def __call__(self, *args, **kwargs):
return self
def has_permission(self, request, view):
return True

View File

@@ -0,0 +1,3 @@
from .accounts import * # noqa
from .companies import * # noqa
from .folders import * # noqa

View File

@@ -0,0 +1 @@
from .company_accounts import * # noqa

View File

@@ -0,0 +1,36 @@
from rest_framework import serializers
from core.apps.companies.models import CompanyAccountModel
class BaseCompanyAccountSerializer(serializers.ModelSerializer):
class Meta:
model = CompanyAccountModel
fields = "__all__"
read_only_fields = (
"id",
"created_at",
"updated_at"
)
class ListCompanyAccountSerializer(BaseCompanyAccountSerializer):
class Meta(BaseCompanyAccountSerializer.Meta): ...
class RetrieveCompanyAccountSerializer(BaseCompanyAccountSerializer):
class Meta(BaseCompanyAccountSerializer.Meta): ...
class CreateCompanyAccountSerializer(BaseCompanyAccountSerializer):
class Meta(BaseCompanyAccountSerializer.Meta): ...
class UpdateCompanyAccountSerializer(BaseCompanyAccountSerializer):
class Meta(BaseCompanyAccountSerializer.Meta): ...
class DestroyCompanyAccountSerializer(BaseCompanyAccountSerializer):
class Meta(BaseCompanyAccountSerializer.Meta):
fields = ["id"]

View File

@@ -0,0 +1 @@
from .companies import * # noqa

View File

@@ -0,0 +1,51 @@
from rest_framework import serializers
from core.apps.companies.models import CompanyModel
from core.apps.companies.serializers.accounts import (
CreateCompanyAccountSerializer,
)
from core.apps.companies.serializers.folders import (
CreateCompanyFolderSerializer
)
class BaseCompanySerializer(serializers.ModelSerializer):
class Meta:
model = CompanyModel
fields = "__all__"
read_only_fields = (
"id",
"created_at",
"updated_at"
)
class ListCompanySerializer(BaseCompanySerializer):
class Meta(BaseCompanySerializer.Meta):
fields = (
"id",
"name",
"phone",
"email",
"created_at",
"updated_at",
)
class RetrieveCompanySerializer(BaseCompanySerializer):
class Meta(BaseCompanySerializer.Meta): ...
class CreateCompanySerializer(BaseCompanySerializer):
class Meta(BaseCompanySerializer.Meta): ...
class UpdateCompanySerializer(BaseCompanySerializer):
class Meta(BaseCompanySerializer.Meta): ...
class DestroyCompanySerializer(BaseCompanySerializer):
class Meta(BaseCompanySerializer.Meta):
fields = ["id"]

View File

@@ -0,0 +1 @@
from .company_folders import * # noqa

View File

@@ -0,0 +1,48 @@
from rest_framework import serializers # type: ignore
from core.apps.companies.models import CompanyFolderModel
class BaseCompanyFolderSerializer(serializers.ModelSerializer):
class Meta:
model = CompanyFolderModel
fields = "__all__"
read_only_fields = (
"id",
"created_at",
"updated_at"
)
class ListCompanyFolderSerializer(BaseCompanyFolderSerializer):
class Meta(BaseCompanyFolderSerializer.Meta): ...
class RetrieveCompanyFolderSerializer(BaseCompanyFolderSerializer):
class Meta(BaseCompanyFolderSerializer.Meta): ...
class CreateCompanyFolderSerializer(BaseCompanyFolderSerializer):
class Meta(BaseCompanyFolderSerializer.Meta): ...
class UpdateCompanyFolderSerializer(BaseCompanyFolderSerializer):
class Meta(BaseCompanyFolderSerializer.Meta): ...
class DestroyCompanyFolderSerializer(BaseCompanyFolderSerializer):
class Meta(BaseCompanyFolderSerializer.Meta):
fields = ["id"]
class CreateCompanyFolderFromCompanySerializer(CreateCompanyFolderSerializer):
class Meta(CreateCompanyFolderSerializer.Meta):
read_only_fields = (
*CreateCompanyFolderSerializer.Meta.read_only_fields,
"company",
)
def create(self, validated_data: dict[str, object]) -> Meta.model:
validated_data["company_id"] = self.context["company_id"]
return super().create(validated_data) # type: ignore

View File

@@ -0,0 +1,3 @@
from .accounts import * # noqa
from .companies import * # noqa
from .folders import * # noqa

View File

@@ -0,0 +1,8 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from core.apps.companies.models import CompanyaccountModel
@receiver(post_save, sender=CompanyaccountModel)
def CompanyaccountSignal(sender, instance, created, **kwargs): ...

View File

@@ -0,0 +1,8 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from core.apps.companies.models import CompanyModel
@receiver(post_save, sender=CompanyModel)
def CompanySignal(sender, instance, created, **kwargs): ...

View File

@@ -0,0 +1,8 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from core.apps.companies.models import CompanyfolderModel
@receiver(post_save, sender=CompanyfolderModel)
def CompanyfolderSignal(sender, instance, created, **kwargs): ...

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