Compare commits

...

10 Commits

Author SHA1 Message Date
behruz
f293cc7018 shared: district api unique_together field qoshildi 2025-12-11 18:19:29 +05:00
behruz
445ae78095 core: bot qoshildi 2025-12-10 17:53:23 +05:00
behruz
b924442631 dashboard: userlarga message yozish uchun api qoshildi 2025-12-10 16:39:28 +05:00
behruz
479d8b8faa orders: yangi api qoshildi 2025-12-10 14:16:55 +05:00
behruz-dev
07d7e8604f dis_product serializer uchun date field qoshildig 2025-12-05 17:26:51 +05:00
behruz-dev
ae8a14b83c fix 2025-12-05 15:06:15 +05:00
behruz-dev
78746e6071 permission ozgardi 2025-12-05 14:47:35 +05:00
behruz-dev
824ad25a17 yangi api qoshildi 2025-12-04 17:47:47 +05:00
behruz-dev
e26ab5eea6 sharedda user uchun support list qoshildi 2025-12-04 17:39:58 +05:00
behruz-dev
eb010cb6de supportda tuman requireddan olib tashlandi 2025-12-04 17:36:33 +05:00
26 changed files with 468 additions and 16 deletions

View File

@@ -12,7 +12,7 @@ class DistributedProductListSerializer(serializers.ModelSerializer):
class Meta:
model = DistributedProduct
fields = [
'id', 'product', 'quantity', 'employee_name', 'quantity', 'user', 'created_at'
'id', 'product', 'quantity', 'employee_name', 'quantity', 'user', 'created_at', 'date'
]
def get_user(self, obj):

View File

@@ -36,6 +36,8 @@ class DistrictCreateSerializer(serializers.Serializer):
if not user:
raise serializers.ValidationError({"user_id": "Foydalanuvchi topilmadi"})
data['user'] = user
if District.objects.filter(name=data['name'], user=user).exists():
raise serializers.ValidationError({'name': "District qo'shib bolmadi"})
return data
def create(self, validated_data):

View File

@@ -0,0 +1,7 @@
# rest framework
from rest_framework import serializers
class SendMessageSerializer(serializers.Serializer):
user_ids = serializers.ListField(child=serializers.IntegerField())
message = serializers.CharField()

View File

@@ -19,7 +19,7 @@ class SupportListSerializer(serializers.ModelSerializer):
return {
'id': obj.district.id,
'name': obj.district.name,
}
} if obj.district else None
def get_user(self, obj):
return {

View File

@@ -35,6 +35,8 @@ from core.apps.dashboard.views.location import LocationViewSet, UserLocationView
from core.apps.dashboard.views.support import SupportListApiView
# distibuted products
from core.apps.dashboard.views.dis_prod import DistributedProductListApiView
# send message
from core.apps.dashboard.views.send_message import SendMessageToEmployee
urlpatterns = [
@@ -82,7 +84,10 @@ urlpatterns = [
[
path('list/', DistributedProductListApiView.as_view(), name='distributed-product-list-api'),
]
))
)),
# -------------- send message --------------
path('send_message/', SendMessageToEmployee.as_view()),
]

View File

@@ -75,6 +75,13 @@ class DoctorViewSet(viewsets.GenericViewSet, ResponseMixin):
required=False,
type=openapi.TYPE_STRING,
),
openapi.Parameter(
in_=openapi.IN_QUERY,
type=openapi.TYPE_INTEGER,
name='user_id',
description="user id bo'yicha filter",
required=False,
)
],
operation_description="Shifokorlar ro'yxatini olish",
operation_summary="Shifokolar ro'yxati",
@@ -147,6 +154,8 @@ class DoctorViewSet(viewsets.GenericViewSet, ResponseMixin):
work_place = request.query_params.get('work_place', None)
sphere = request.query_params.get('sphere', None)
user_full_name = request.query_params.get('user', None)
user_id = request.query_params.get('user_id', None)
queryset = self.queryset.all()
@@ -177,6 +186,8 @@ class DoctorViewSet(viewsets.GenericViewSet, ResponseMixin):
Q(user__first_name__istartswith=user_full_name) |
Q(user__last_name__istartswith=user_full_name)
)
if not user_id is None:
queryset = queryset.filter(user__id=user_id)
page = self.paginate_queryset(queryset)
if page is not None:

View File

@@ -60,6 +60,13 @@ class PharmacyViewSet(viewsets.GenericViewSet, ResponseMixin):
required=False,
type=openapi.TYPE_STRING,
),
openapi.Parameter(
in_=openapi.IN_QUERY,
type=openapi.TYPE_INTEGER,
name='user_id',
description="user id bo'yicha filter",
required=False,
)
],
)
@action(detail=False, methods=['get'], url_path="list")
@@ -70,6 +77,7 @@ class PharmacyViewSet(viewsets.GenericViewSet, ResponseMixin):
place_name = request.query_params.get('place', None)
district_name = request.query_params.get('district', None)
user_full_name = request.query_params.get('user', None)
user_id = request.query_params.get('user_id', None)
queryset = self.queryset.all()
@@ -88,6 +96,9 @@ class PharmacyViewSet(viewsets.GenericViewSet, ResponseMixin):
Q(user__first_name__istartswith=user_full_name) |
Q(user__last_name__istartswith=user_full_name)
)
if not user_id is None:
queryset = queryset.filter(user__id=user_id)
page = self.paginate_queryset(queryset)
if page is not None:

View File

@@ -53,6 +53,13 @@ class PlaceViewSet(viewsets.GenericViewSet, ResponseMixin):
required=False,
type=openapi.TYPE_STRING,
),
openapi.Parameter(
in_=openapi.IN_QUERY,
type=openapi.TYPE_INTEGER,
name='user_id',
description="user id bo'yicha filter",
required=False,
)
],
)
@action(detail=False, methods=['get'], url_path="list")
@@ -62,6 +69,7 @@ class PlaceViewSet(viewsets.GenericViewSet, ResponseMixin):
name = request.query_params.get('name', None)
district_name = request.query_params.get('district', None)
user_full_name = request.query_params.get('user', None)
user_id = request.query_params.get('user_id', None)
queryset = self.queryset.all()
@@ -77,6 +85,9 @@ class PlaceViewSet(viewsets.GenericViewSet, ResponseMixin):
Q(user__first_name__istartswith=user_full_name) |
Q(user__last_name__istartswith=user_full_name)
)
if not user_id is None:
queryset = queryset.filter(user__id=user_id)
page = self.paginate_queryset(queryset)
if page is not None:

View File

@@ -0,0 +1,38 @@
# rest framework
from rest_framework import generics, permissions
# services
from core.services.send_telegram_msg import send_message
# accounts
from core.apps.accounts.models import User
#shared
from core.apps.shared.utils.response_mixin import ResponseMixin
# dashboard
from core.apps.dashboard.serializers.send_message import SendMessageSerializer
class SendMessageToEmployee(generics.GenericAPIView, ResponseMixin):
serializer_class = SendMessageSerializer
permission_classes = [permissions.IsAdminUser]
queryset = User.objects.all()
def post(self, request):
try:
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
data = serializer.validated_data
message = data.get('message')
user_ids = data.get('user_ids')
users = User.objects.filter(id__in=user_ids)
for user in users:
send_message(chat_id=user.telegram_id, message=message)
return self.success_response(
data={},
message="Xabar yuborildi"
)
except Exception as e:
return self.error_response(
data=str(e),
message="xatolik, backend dasturchi bilan boglaning"
)

View File

@@ -19,6 +19,7 @@ urlpatterns = [
path('list/', order_view.OrderListApiView.as_view(), name='order-list-api'),
path('create/', order_view.OrderCreateApiView.as_view(), name='order-create-api'),
path('<int:id>/update/', order_view.OrderUpdateApiView.as_view(), name='order-update-api'),
path('<int:id>/send_pdf/', order_view.SendFileToTelegramApiView.as_view(), name='order-send-pdf-api'),
]
)),

View File

@@ -2,7 +2,7 @@
from django.shortcuts import get_object_or_404
# rest framework
from rest_framework import generics, permissions
from rest_framework import generics, permissions, views
# drf yasg
from drf_yasg.utils import swagger_auto_schema
@@ -14,6 +14,9 @@ from core.apps.orders.serializers.order import OrderCreateSerializer, OrderListS
from core.apps.shared.utils.response_mixin import ResponseMixin
from core.apps.shared.serializers.base import BaseResponseSerializer, SuccessResponseSerializer
# services
from core.services.send_telegram_msg import send_to_telegram
class OrderCreateApiView(generics.GenericAPIView, ResponseMixin):
serializer_class = OrderCreateSerializer
@@ -93,4 +96,28 @@ class OrderUpdateApiView(generics.GenericAPIView, ResponseMixin):
)
except Exception as e:
return self.error_response(data=str(e), message='xatolik')
return self.error_response(data=str(e), message='xatolik')
class SendFileToTelegramApiView(views.APIView, ResponseMixin):
permission_classes = [permissions.IsAuthenticated]
def get(self, request, id):
try:
order = Order.objects.filter(id=id).first()
if not order:
return self.failure_response(
data={},
message="Order not found"
)
send_to_telegram(request.user.telegram_id, order.id)
return self.success_response(
data={},
message='Succefully send!'
)
except Exception as e:
return self.error_response(
data=str(e),
message="xatolik, backend dasturchiga murojaat qiling"
)

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.2 on 2025-12-04 12:35
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shared', '0016_plan_comment_plan_doctor_plan_extra_location_and_more'),
]
operations = [
migrations.AlterField(
model_name='support',
name='district',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='supports', to='shared.district'),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.2 on 2025-12-11 13:15
from django.conf import settings
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('shared', '0017_alter_support_district'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterUniqueTogether(
name='district',
unique_together={('user', 'name')},
),
]

View File

@@ -14,3 +14,6 @@ class District(BaseModel):
def __str__(self):
return self.name
class Meta:
unique_together = ('user', 'name')

View File

@@ -13,7 +13,13 @@ class Support(BaseModel):
('HELP', 'yordam'),
)
district = models.ForeignKey(District, on_delete=models.CASCADE, related_name='supports')
district = models.ForeignKey(
District,
on_delete=models.SET_NULL,
related_name='supports',
blank=True,
null=True
)
problem = models.TextField()
date = models.DateField()
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='supports')

View File

@@ -0,0 +1,24 @@
# rest framework
from rest_framework import serializers
# orders
from core.apps.orders.models import DistributedProduct
class DistributedProductListSerializer(serializers.ModelSerializer):
product = serializers.SerializerMethodField(method_name='get_product')
class Meta:
model = DistributedProduct
fields = [
'id', 'product', 'quantity', 'employee_name', 'quantity', 'created_at', 'date'
]
ref_name = "DisProductListSerializer"
def get_product(self, obj):
return {
"id": obj.product.id,
"name": obj.product.name,
"price": obj.product.price,
}

View File

@@ -9,16 +9,17 @@ from core.apps.shared.models import Support, District
class SupportCreateSerializer(serializers.Serializer):
district_id = serializers.IntegerField()
district_id = serializers.IntegerField(required=False)
problem = serializers.CharField()
date = serializers.DateField()
type = serializers.ChoiceField(choices=Support.TYPE)
def validate(self, data):
district = District.objects.filter(id=data['district_id']).first()
if not district:
raise serializers.ValidationError({'district': "district not found"})
data['district'] = district
if data.get('district_id'):
district = District.objects.filter(id=data['district_id']).first()
if not district:
raise serializers.ValidationError({'district': "district not found"})
data['district'] = district
return data
def create(self, validated_data):
@@ -30,3 +31,21 @@ class SupportCreateSerializer(serializers.Serializer):
type=validated_data.get('type'),
problem=validated_data.get('problem' )
)
class SupportListSerializer(serializers.ModelSerializer):
district = serializers.SerializerMethodField(method_name='get_district')
class Meta:
model = Support
fields = [
'id', 'problem', 'date', 'type', 'district', 'created_at'
]
ref_name = "SupportListSerializerForUser"
def get_district(self, obj):
return {
'id': obj.district.id,
'name': obj.district.name,
} if obj.district else None

View File

@@ -20,6 +20,8 @@ from core.apps.shared.views import tour_plan as tp_view
from core.apps.shared.views import factory as factory_view
# shared support view
from core.apps.shared.views import support as support_view
# shared dis product
from core.apps.shared.views import dis_product as dp_view
urlpatterns = [
@@ -88,6 +90,12 @@ urlpatterns = [
path('support/', include(
[
path('send/', support_view.SupportCreateApiView.as_view(), name='support-create-api'),
path('list/', support_view.SupportListApiView.as_view(), name='support-list-api'),
]
))
)),
path('distributed_product/', include(
[
path('list/', dp_view.DistributedProductListApiView.as_view()),
]
)),
]

View File

@@ -0,0 +1,75 @@
# django
from django.db.models import Q
# rest framework
from rest_framework import generics, permissions
# drf yasg
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
# orders
from core.apps.orders.models import DistributedProduct
# shared
from core.apps.shared.utils.response_mixin import ResponseMixin
# dashboard
from core.apps.shared.serializers.dis_product import DistributedProductListSerializer
class DistributedProductListApiView(generics.GenericAPIView, ResponseMixin):
serializer_class = DistributedProductListSerializer
queryset = DistributedProduct.objects.all()
permission_classes = [permissions.IsAuthenticated]
@swagger_auto_schema(
manual_parameters=[
openapi.Parameter(
in_=openapi.IN_QUERY,
name="product",
description="product name bo'yicha filter",
required=False,
type=openapi.TYPE_STRING,
),
openapi.Parameter(
in_=openapi.IN_QUERY,
name="date",
description="date bo'yicha qidirish",
required=False,
type=openapi.FORMAT_DATE,
),
],
)
def get(self, request):
try:
product_name = request.query_params.get('product', None)
date = request.query_params.get('date', None)
queryset = self.queryset.filter(user=request.user)
# filters
if product_name is not None:
queryset = queryset.filter(product__name__istartswith=product_name)
if date is not None:
queryset = queryset.filter(date=date)
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.serializer_class(page, many=True)
return self.success_response(
data=self.get_paginated_response(serializer.data).data,
message="Malumotlar fetch qilindi",
)
serializer = self.serializer_class(queryset, many=True)
return self.success_response(
data=serializer.data,
message='Malumotlar fetch qilindi'
)
except Exception as e:
return self.error_response(
data=str(e),
message='xatolik, iltimos backend dasturchiga murojaat qiling'
)

View File

@@ -53,6 +53,9 @@ class DistrictCreateApiView(generics.CreateAPIView, ResponseMixin):
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
name = serializer.validated_data.get('name')
if District.objects.filter(name=name, user=request.user).exists():
return self.failure_response(message="District qo'shib bolmadi")
obj = District.objects.create(name=name, user=request.user)
return self.success_response(
data=district_serializers.DistrictSerializer(obj).data,

View File

@@ -8,7 +8,7 @@ from drf_yasg.utils import swagger_auto_schema
# shared
from core.apps.shared.models import Support
from core.apps.shared.utils.response_mixin import ResponseMixin
from core.apps.shared.serializers.support import SupportCreateSerializer
from core.apps.shared.serializers.support import SupportCreateSerializer, SupportListSerializer
class SupportCreateApiView(generics.GenericAPIView, ResponseMixin):
@@ -34,4 +34,72 @@ class SupportCreateApiView(generics.GenericAPIView, ResponseMixin):
return self.error_response(
data=str(e),
message='xatolik, backend dastruchiga murojaat qiling iltimos'
)
class SupportListApiView(generics.GenericAPIView, ResponseMixin):
serializer_class = SupportListSerializer
queryset = Support.objects.all()
permission_classes = [permissions.IsAuthenticated]
@swagger_auto_schema(
manual_parameters=[
openapi.Parameter(
in_=openapi.IN_QUERY,
name="problem",
description="problem text bo'yicha filter",
required=False,
type=openapi.TYPE_STRING,
),
openapi.Parameter(
in_=openapi.IN_QUERY,
name="district",
description="tuman name bo'yicha filter",
required=False,
type=openapi.TYPE_STRING,
),
openapi.Parameter(
in_=openapi.IN_QUERY,
name="date",
description="date bo'yicha qidirish",
required=False,
type=openapi.FORMAT_DATE,
),
],
)
def get(self, request):
try:
problem = request.query_params.get('problem', None)
district_name = request.query_params.get('district', None)
date = request.query_params.get('date', None)
queryset = self.queryset.filter(user=request.user)
# filters
if problem is not None:
queryset = queryset.filter(problem__istartswith=problem)
if district_name is not None:
queryset = queryset.filter(district__name__istartswith=district_name)
if date is not None:
queryset = queryset.filter(date=date)
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.serializer_class(page, many=True)
return self.success_response(
data=self.get_paginated_response(serializer.data).data,
message="Malumotlar fetch qilindi",
)
serializer = self.serializer_class(queryset, many=True)
return self.success_response(
data=serializer.data,
message='Malumotlar fetch qilindi'
)
except Exception as e:
return self.error_response(
data=str(e),
message='xatolik, iltimos backend dasturchiga murojaat qiling'
)

58
core/bot/main.py Normal file
View File

@@ -0,0 +1,58 @@
# python
import asyncio, logging, sys, os
import django
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.base')
django.setup()
# aiogram
from aiogram import Bot, Dispatcher, types, filters
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton, WebAppInfo
# django
from django.conf import settings
TOKEN = settings.BOT_TOKEN
bot = Bot(token=TOKEN)
dp = Dispatcher()
@dp.message(filters.CommandStart)
async def start_handler(message: types.Message):
keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(
text="Tizimga kirish",
web_app=WebAppInfo(url="https://bot.meridynpharma.com")
)]
],
)
text = """
🔐 MeridynPharma ish tizimiga kirish
Hurmatli xodim,
MeridynPharmaning ichki ish jarayonlarini avtomatlashtirish va kunlik faoliyatni samarali boshqarish uchun moljallangan Rasmiy Xodimlar Mini-Ilovasiga xush kelibsiz.
Ushbu platforma orqali sizga biriktirilgan vazifalar, hisobotlar, inventarizatsiya jarayonlari va ichki eslatmalar yagona tizim orqali boshqariladi.
▶️ Tizimga kirish tartibi
Ish faoliyatini boshlash uchun quyidagi bosqichni bajaring:
1. Quyida joylashgan “Tizimga kirish” tugmasini bosing.
yoki
2. Mini-Ilova ochilgandan song, mini ilova pastki qismida joylashgan tizimga kirish degan tugmani bosing
Agar mini-app avtomatik ochilmasa, iltimos, tugmani yana bir bor bosing.
"""
await message.answer(text, reply_markup=keyboard)
async def main():
await dp.start_polling(bot)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
asyncio.run(main())

View File

@@ -30,4 +30,21 @@ def send_to_telegram(chat_id, order_id):
except Exception as e:
print(f"Telegram xatolik: {e}")
return False
def send_message(chat_id, message):
bot_token = settings.BOT_TOKEN
try:
url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
data = {
"chat_id": chat_id,
"text": message
}
response = requests.post(url, data=data)
print(response.json())
return True
except Exception as e:
print(f"Telegram xatosi: {e}")
return False

View File

@@ -1,3 +1,9 @@
aiofiles==25.1.0
aiogram==3.23.0
aiohappyeyeballs==2.6.1
aiohttp==3.13.2
aiosignal==1.4.0
annotated-types==0.7.0
asgiref==3.11.0
attrs==25.4.0
autobahn==25.11.1
@@ -22,20 +28,26 @@ djangorestframework==3.16.1
djangorestframework_simplejwt==5.5.1
drf-yasg==1.21.11
fonttools==4.60.1
frozenlist==1.8.0
gunicorn==23.0.0
h11==0.16.0
hyperlink==21.0.0
idna==3.11
Incremental==24.11.0
inflection==0.5.1
magic-filter==1.0.12
msgpack==1.1.2
multidict==6.7.0
packaging==25.0
pillow==12.0.0
propcache==0.4.1
psycopg2-binary==2.9.11
py-ubjson==0.16.1
pyasn1==0.6.1
pyasn1_modules==0.4.2
pycparser==2.23
pydantic==2.12.5
pydantic_core==2.41.5
pydyf==0.11.0
PyJWT==2.10.1
pyOpenSSL==25.3.0
@@ -51,6 +63,7 @@ tinycss2==1.5.1
tinyhtml5==2.0.0
Twisted==25.5.0
txaio==25.9.2
typing-inspection==0.4.2
typing_extensions==4.15.0
ujson==5.11.0
uritemplate==4.2.0
@@ -58,6 +71,6 @@ urllib3==2.5.0
uvicorn==0.38.0
weasyprint==66.0
webencodings==0.5.1
yarl==1.22.0
zope.interface==8.1.1
zopfli==0.4.0
websockets

View File

@@ -2,7 +2,10 @@
python3 manage.py collectstatic --noinput
python3 manage.py migrate --noinput
gunicorn config.wsgi:application -b 0.0.0.0:8000 --workers $(($(nproc) * 2 + 1))
gunicorn config.wsgi:application -b 0.0.0.0:8000 --workers $(($(nproc) * 2 + 1)) &
python3 core/bot/main.py &
wait
exit $?

View File

@@ -2,6 +2,10 @@
python3 manage.py collectstatic --noinput
python3 manage.py migrate --noinput
uvicorn config.asgi:application --host 0.0.0.0 --port 8000 --reload --reload-dir core --reload-dir config
uvicorn config.asgi:application --host 0.0.0.0 --port 8000 --reload --reload-dir core --reload-dir config &
python3 core/bot/main.py &
wait
exit $?