diff --git a/.gitignore b/.gitignore index f8dd262..8469dc6 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ __pycache__/ # C extensions *.so +*.xlsx + # Distribution / packaging .Python build/ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..a9d7db9 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# GitHub Copilot persisted chat sessions +/copilot/chatSessions diff --git a/.idea/aws.xml b/.idea/aws.xml new file mode 100644 index 0000000..3b7efc8 --- /dev/null +++ b/.idea/aws.xml @@ -0,0 +1,20 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/crmApi.iml b/.idea/crmApi.iml index bf29436..9fb8a3e 100644 --- a/.idea/crmApi.iml +++ b/.idea/crmApi.iml @@ -13,8 +13,11 @@ - - + + + + + diff --git a/.idea/misc.xml b/.idea/misc.xml index 6b0bed8..1dbe7d0 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,7 @@ - + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 309a9d2..8ff1770 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -1,16 +1,36 @@ + + + + + + + + + + + + + - + + - + + + + + + + + + + + file://$PROJECT_DIR$/apps/shop/views/excel_import.py + 16 + + + file://$PROJECT_DIR$/apps/shop/views/excel_import.py + 19 + + + file://$PROJECT_DIR$/apps/shop/views/excel_import.py + 21 + + + file://$PROJECT_DIR$/apps/shop/views/excel_import.py + 23 + + + file://$PROJECT_DIR$/apps/shop/views/excel_import.py + 29 + + + file://$PROJECT_DIR$/apps/shop/views/excel_import.py + 30 + + + file://$PROJECT_DIR$/apps/shop/views/excel_import.py + 39 + + + file://$PROJECT_DIR$/apps/jwt_auth/services.py + 14 + + + file://$PROJECT_DIR$/apps/jwt_auth/services.py + 17 + + + file://$PROJECT_DIR$/apps/jwt_auth/services.py + 19 + + + file://$PROJECT_DIR$/apps/jwt_auth/services.py + 20 + + + file://$PROJECT_DIR$/apps/jwt_auth/services.py + 37 + + + file://$PROJECT_DIR$/apps/jwt_auth/services.py + 38 + + + file://$PROJECT_DIR$/apps/jwt_auth/views.py + 116 + + + file://$PROJECT_DIR$/apps/jwt_auth/views.py + 118 + + + + + + + + + + \ No newline at end of file diff --git a/apps/cuser/migrations/0012_cuserrole_is_admin.py b/apps/cuser/migrations/0012_cuserrole_is_admin.py new file mode 100644 index 0000000..eaad38c --- /dev/null +++ b/apps/cuser/migrations/0012_cuserrole_is_admin.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2024-04-16 06:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cuser', '0011_cuserrole_customer_panel'), + ] + + operations = [ + migrations.AddField( + model_name='cuserrole', + name='is_admin', + field=models.BooleanField(blank=True, default=None, null=True), + ), + ] diff --git a/apps/cuser/models.py b/apps/cuser/models.py index ddfd217..4c07096 100644 --- a/apps/cuser/models.py +++ b/apps/cuser/models.py @@ -26,6 +26,7 @@ class CuserRole(BaseModel): mobile = models.PositiveBigIntegerField(null=False, blank=False) mobile_verify = models.BooleanField(default=False) customer_panel = models.BooleanField(default=None, blank=True, null=True) + is_admin = models.BooleanField(default=None, blank=True, null=True) sys_ws = models.ForeignKey(WorkspaceModel, on_delete=models.CASCADE, null=True, blank=True, default=None) uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) diff --git a/apps/customer/selectors.py b/apps/customer/selectors.py index 7c3b2cd..213379a 100644 --- a/apps/customer/selectors.py +++ b/apps/customer/selectors.py @@ -101,7 +101,6 @@ def customer_form_item(request): 'shahr': shahr, 'responsible_groups': responsible_groups } - return data diff --git a/apps/jwt_auth/migrations/0082_alter_cuserroleloginopt_uuid.py b/apps/jwt_auth/migrations/0082_alter_cuserroleloginopt_uuid.py new file mode 100644 index 0000000..20cfcde --- /dev/null +++ b/apps/jwt_auth/migrations/0082_alter_cuserroleloginopt_uuid.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.1 on 2024-02-10 09:59 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('jwt_auth', '0081_alter_cuserroleloginopt_uuid'), + ] + + operations = [ + migrations.AlterField( + model_name='cuserroleloginopt', + name='uuid', + field=models.UUIDField(default=uuid.UUID('6537ef3d-8ef9-486d-a1b6-7892e021dc19'), unique=True), + ), + ] diff --git a/apps/jwt_auth/migrations/0083_alter_cuserroleloginopt_uuid.py b/apps/jwt_auth/migrations/0083_alter_cuserroleloginopt_uuid.py new file mode 100644 index 0000000..7fe0a9b --- /dev/null +++ b/apps/jwt_auth/migrations/0083_alter_cuserroleloginopt_uuid.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.1 on 2024-02-10 11:06 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('jwt_auth', '0082_alter_cuserroleloginopt_uuid'), + ] + + operations = [ + migrations.AlterField( + model_name='cuserroleloginopt', + name='uuid', + field=models.UUIDField(default=uuid.UUID('745db968-c736-42cd-ad3d-4de41e8e5b98'), unique=True), + ), + ] diff --git a/apps/jwt_auth/migrations/0084_alter_cuserroleloginopt_uuid.py b/apps/jwt_auth/migrations/0084_alter_cuserroleloginopt_uuid.py new file mode 100644 index 0000000..2fd76d8 --- /dev/null +++ b/apps/jwt_auth/migrations/0084_alter_cuserroleloginopt_uuid.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.1 on 2024-02-18 09:44 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('jwt_auth', '0083_alter_cuserroleloginopt_uuid'), + ] + + operations = [ + migrations.AlterField( + model_name='cuserroleloginopt', + name='uuid', + field=models.UUIDField(default=uuid.UUID('96211808-5203-451b-a6ce-eee394e02afc'), unique=True), + ), + ] diff --git a/apps/jwt_auth/migrations/0085_alter_cuserroleloginopt_uuid.py b/apps/jwt_auth/migrations/0085_alter_cuserroleloginopt_uuid.py new file mode 100644 index 0000000..472063a --- /dev/null +++ b/apps/jwt_auth/migrations/0085_alter_cuserroleloginopt_uuid.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.1 on 2024-02-27 09:59 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('jwt_auth', '0084_alter_cuserroleloginopt_uuid'), + ] + + operations = [ + migrations.AlterField( + model_name='cuserroleloginopt', + name='uuid', + field=models.UUIDField(default=uuid.UUID('561d07a5-fff4-436f-bab3-4e3b16e05fba'), unique=True), + ), + ] diff --git a/apps/jwt_auth/migrations/0086_alter_cuserroleloginopt_uuid.py b/apps/jwt_auth/migrations/0086_alter_cuserroleloginopt_uuid.py new file mode 100644 index 0000000..2b3c998 --- /dev/null +++ b/apps/jwt_auth/migrations/0086_alter_cuserroleloginopt_uuid.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.1 on 2024-04-16 06:28 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('jwt_auth', '0085_alter_cuserroleloginopt_uuid'), + ] + + operations = [ + migrations.AlterField( + model_name='cuserroleloginopt', + name='uuid', + field=models.UUIDField(default=uuid.UUID('b5346fad-8d28-482d-8ed9-2807850b6a4d'), unique=True), + ), + ] diff --git a/apps/jwt_auth/services.py b/apps/jwt_auth/services.py index 24c375b..7ec9ace 100644 --- a/apps/jwt_auth/services.py +++ b/apps/jwt_auth/services.py @@ -5,14 +5,16 @@ from apps.jwt_auth.models import CUserRoleLoginOpt from apps.cuser.models import CuserRole as CuserRoleModel, SmsOpt as SmsOptModel from django.utils import timezone from rest_framework import exceptions -CUser = get_user_model() +import requests +from datetime import timedelta +CUser = get_user_model() def login_by_username(username: str, password: str) -> CUserRoleLoginOpt: try: print(username) user = CUser.objects.get(username=username) - print(user) + if user and user.check_password(password): cuser_roles = CuserRoleModel.objects.filter(cuser=user) @@ -52,21 +54,35 @@ def login_by_mobile(mobile: int): random_number = random.randint(100000, 999999) + try: SmsOptModel.objects.filter(mobile=mobile).delete() except Exception as exp: pass - opt_code = SmsOptModel(mobile=mobile, opt=123456) + expire_time = timezone.now() + timedelta(minutes=3) + opt_code = SmsOptModel(mobile=mobile, opt=random_number, expire=expire_time) opt_code.save() + sms_text = f' کد ورود شما: {random_number}' + + payload = { + 'USERNAME': 'vsms7937', + 'PASSWORD': '335577775533', + 'FROM': '30005367', + 'TO': int(mobile), + 'TEXT': f'{sms_text}' + } + r = requests.get('http://www.0098sms.com/sendsmslink.aspx', params=payload) + except Exception as exp: raise exceptions.ValidationError({'error': "mobile not found"}) def verify_mobile_and_get_token(mobile: int, opt: int) -> CUserRoleLoginOpt: try: - opt_obj = SmsOptModel.objects.filter(mobile=mobile, opt=opt)[0] + time_now = timezone.now() + opt_obj = SmsOptModel.objects.filter(mobile=mobile, opt=opt, expire__gte=time_now)[0] if opt_obj: cuser_roles = CuserRoleModel.objects.filter(cuser__mobile=mobile, is_active=True) @@ -87,4 +103,8 @@ def verify_mobile_and_get_token(mobile: int, opt: int) -> CUserRoleLoginOpt: SmsOptModel.objects.filter(mobile=mobile).delete() return login_opts except Exception as exp: + try: + SmsOptModel.objects.filter(mobile=mobile).delete() + except Exception as exp: + pass raise exceptions.ValidationError({'error': "cannot verify this mobile and opt code:"+str(exp)}) diff --git a/apps/lead/migrations/0036_alter_lead_status.py b/apps/lead/migrations/0036_alter_lead_status.py new file mode 100644 index 0000000..c35c80e --- /dev/null +++ b/apps/lead/migrations/0036_alter_lead_status.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2024-02-10 09:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('lead', '0035_alter_lead_status'), + ] + + operations = [ + migrations.AlterField( + model_name='lead', + name='status', + field=models.CharField(blank=True, choices=[('SU', 'OK'), ('UN', 'unknown'), ('FA', 'FAIL')], default='UN', max_length=2, null=True), + ), + ] diff --git a/apps/lead/migrations/0037_alter_lead_status.py b/apps/lead/migrations/0037_alter_lead_status.py new file mode 100644 index 0000000..7d54b15 --- /dev/null +++ b/apps/lead/migrations/0037_alter_lead_status.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2024-02-10 11:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('lead', '0036_alter_lead_status'), + ] + + operations = [ + migrations.AlterField( + model_name='lead', + name='status', + field=models.CharField(blank=True, choices=[('UN', 'unknown'), ('SU', 'OK'), ('FA', 'FAIL')], default='UN', max_length=2, null=True), + ), + ] diff --git a/apps/lead/migrations/0038_alter_lead_status.py b/apps/lead/migrations/0038_alter_lead_status.py new file mode 100644 index 0000000..0388295 --- /dev/null +++ b/apps/lead/migrations/0038_alter_lead_status.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2024-02-18 09:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('lead', '0037_alter_lead_status'), + ] + + operations = [ + migrations.AlterField( + model_name='lead', + name='status', + field=models.CharField(blank=True, choices=[('FA', 'FAIL'), ('SU', 'OK'), ('UN', 'unknown')], default='UN', max_length=2, null=True), + ), + ] diff --git a/apps/lead/migrations/0039_alter_lead_status.py b/apps/lead/migrations/0039_alter_lead_status.py new file mode 100644 index 0000000..e64727d --- /dev/null +++ b/apps/lead/migrations/0039_alter_lead_status.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2024-02-27 09:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('lead', '0038_alter_lead_status'), + ] + + operations = [ + migrations.AlterField( + model_name='lead', + name='status', + field=models.CharField(blank=True, choices=[('SU', 'OK'), ('UN', 'unknown'), ('FA', 'FAIL')], default='UN', max_length=2, null=True), + ), + ] diff --git a/apps/lead/migrations/0040_alter_lead_status.py b/apps/lead/migrations/0040_alter_lead_status.py new file mode 100644 index 0000000..8cacb80 --- /dev/null +++ b/apps/lead/migrations/0040_alter_lead_status.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2024-04-16 06:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('lead', '0039_alter_lead_status'), + ] + + operations = [ + migrations.AlterField( + model_name='lead', + name='status', + field=models.CharField(blank=True, choices=[('FA', 'FAIL'), ('SU', 'OK'), ('UN', 'unknown')], default='UN', max_length=2, null=True), + ), + ] diff --git a/apps/lead/selectors.py b/apps/lead/selectors.py index 36eba2b..4e594da 100644 --- a/apps/lead/selectors.py +++ b/apps/lead/selectors.py @@ -3,7 +3,7 @@ from apps.lead.models import Lead as LeadModel, LeadType as LeadTypeModel,\ from apps.shop.models import ProductType as ProductTypeModel from apps.workspace.models import City as CityModel from utils.get_user_workspace import get_user_workspace_uuid -from utils.get_cuser_role import get_cuser_role_uuid +from utils.get_cuser_role import get_cuser_role_uuid, get_cuser_role from rest_framework import exceptions @@ -12,7 +12,16 @@ def list_leads(request) -> LeadModel: try: workspace_uuid = get_user_workspace_uuid(request) cuser_role_uuid = get_cuser_role_uuid(request) - leads = LeadModel.objects.filter(sys_ws__uuid=workspace_uuid, cuser_role__uuid=cuser_role_uuid).order_by('-created_at') + cuser_role = get_cuser_role(request) + + base_filter = { + 'sys_ws__uuid': workspace_uuid, + } + + if not cuser_role.is_admin: + base_filter['cuser_role__uuid'] = cuser_role_uuid + + leads = LeadModel.objects.filter(**base_filter).order_by('-created_at') return leads except Exception as exp: @@ -25,7 +34,16 @@ def retrieve_lead(request, id: int) -> LeadModel: try: workspace_uuid = get_user_workspace_uuid(request) cuser_role_uuid = get_cuser_role_uuid(request) - lead = LeadModel.objects.get(id=id, sys_ws__uuid=workspace_uuid, cuser_role__uuid=cuser_role_uuid) + + cuser_role = get_cuser_role(request) + base_filter = { + 'sys_ws__uuid': workspace_uuid, + } + + if not cuser_role.is_admin: + base_filter['cuser_role__uuid'] = cuser_role_uuid + + lead = LeadModel.objects.get(id=id, **base_filter) return lead except Exception as exp: message = f'Error retrieve Leads: {exp}' diff --git a/apps/lead/views.py b/apps/lead/views.py index ec33d90..0cb27ed 100644 --- a/apps/lead/views.py +++ b/apps/lead/views.py @@ -139,6 +139,11 @@ class LeadListApi(ListAPIView): 'ostan': inline_serializer(many=False, fields={'name': serializers.CharField() }) }) + cuser_role = inline_serializer(many=False, fields={ + 'id': serializers.IntegerField(), + 'name_family': serializers.CharField() + }) + class Meta: model = LeadModel fields = '__all__' diff --git a/apps/reminder/migrations/0030_alter_reminder_type.py b/apps/reminder/migrations/0030_alter_reminder_type.py new file mode 100644 index 0000000..5aa0e2c --- /dev/null +++ b/apps/reminder/migrations/0030_alter_reminder_type.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2024-02-10 09:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reminder', '0029_alter_reminder_type'), + ] + + operations = [ + migrations.AlterField( + model_name='reminder', + name='type', + field=models.CharField(choices=[('LE', 'LEAD'), ('NO', 'NOTE'), ('UN', 'UNKNOWN'), ('CU', 'CUSTOMER')], default='UN', max_length=2), + ), + ] diff --git a/apps/reminder/migrations/0031_alter_reminder_type.py b/apps/reminder/migrations/0031_alter_reminder_type.py new file mode 100644 index 0000000..e8082d2 --- /dev/null +++ b/apps/reminder/migrations/0031_alter_reminder_type.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2024-02-10 11:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reminder', '0030_alter_reminder_type'), + ] + + operations = [ + migrations.AlterField( + model_name='reminder', + name='type', + field=models.CharField(choices=[('CU', 'CUSTOMER'), ('NO', 'NOTE'), ('LE', 'LEAD'), ('UN', 'UNKNOWN')], default='UN', max_length=2), + ), + ] diff --git a/apps/reminder/migrations/0032_alter_reminder_type.py b/apps/reminder/migrations/0032_alter_reminder_type.py new file mode 100644 index 0000000..d4d82ea --- /dev/null +++ b/apps/reminder/migrations/0032_alter_reminder_type.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2024-02-18 09:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reminder', '0031_alter_reminder_type'), + ] + + operations = [ + migrations.AlterField( + model_name='reminder', + name='type', + field=models.CharField(choices=[('UN', 'UNKNOWN'), ('CU', 'CUSTOMER'), ('LE', 'LEAD'), ('NO', 'NOTE')], default='UN', max_length=2), + ), + ] diff --git a/apps/reminder/migrations/0033_alter_reminder_type.py b/apps/reminder/migrations/0033_alter_reminder_type.py new file mode 100644 index 0000000..259d448 --- /dev/null +++ b/apps/reminder/migrations/0033_alter_reminder_type.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2024-02-27 09:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reminder', '0032_alter_reminder_type'), + ] + + operations = [ + migrations.AlterField( + model_name='reminder', + name='type', + field=models.CharField(choices=[('LE', 'LEAD'), ('UN', 'UNKNOWN'), ('CU', 'CUSTOMER'), ('NO', 'NOTE')], default='UN', max_length=2), + ), + ] diff --git a/apps/reminder/migrations/0034_alter_reminder_type.py b/apps/reminder/migrations/0034_alter_reminder_type.py new file mode 100644 index 0000000..61846d4 --- /dev/null +++ b/apps/reminder/migrations/0034_alter_reminder_type.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2024-04-16 06:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('reminder', '0033_alter_reminder_type'), + ] + + operations = [ + migrations.AlterField( + model_name='reminder', + name='type', + field=models.CharField(choices=[('CU', 'CUSTOMER'), ('NO', 'NOTE'), ('UN', 'UNKNOWN'), ('LE', 'LEAD')], default='UN', max_length=2), + ), + ] diff --git a/apps/report/__init__.py b/apps/report/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/report/selector.py b/apps/report/selector.py new file mode 100644 index 0000000..963bff6 --- /dev/null +++ b/apps/report/selector.py @@ -0,0 +1,41 @@ +from apps.lead.models import Lead as LeadModel +from apps.cuser.models import CuserRole as CuserRoleModel, RoleType as RoleTypeModel +from rest_framework import serializers +from django.db.models import Count +from django.db.models import Count +from rest_framework import exceptions + + +def list_leads_api() -> LeadModel: + + try: + base_filter = { + 'sys_ws__uuid': 'ca918836-ddf2-4d88-8d76-033d285062e4', + } + + leads = LeadModel.objects.filter(**base_filter).order_by('-created_at') + + return leads + + except Exception as exp: + message = f'Error Fetching List Leads: {exp}' + raise exceptions.ValidationError({'error': message}) + + + + +def user_lead_report_count_by_date(start_datetime, end_datetime, status: str | None = None,): + + base_filer = { + 'cuser_role__role_type__role_name_en': 'workspace_operator', + 'created_at__range': [start_datetime, end_datetime], + } + if status: + base_filer['status'] = status + + return LeadModel.objects.filter(**base_filer).values('cuser_role__name_family').annotate( + count=Count('cuser_role__name_family')).order_by('-count') + + +def operators_cuser_role(): + return CuserRoleModel.objects.filter(sys_ws_id=2, role_type__role_name_en='workspace_operator') \ No newline at end of file diff --git a/apps/report/services.py b/apps/report/services.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/apps/report/services.py @@ -0,0 +1 @@ + diff --git a/apps/report/urls.py b/apps/report/urls.py new file mode 100644 index 0000000..e440054 --- /dev/null +++ b/apps/report/urls.py @@ -0,0 +1,8 @@ +from django.urls import path +from apps.report.views import ReportApiView, LeadListApi + + +urlpatterns = [ + path('lead/', ReportApiView.as_view(), name='report-lead'), + path('lead/list/loolegostar-x89i20/', LeadListApi.as_view(), name='report-lead'), +] diff --git a/apps/report/views.py b/apps/report/views.py new file mode 100644 index 0000000..c26b33b --- /dev/null +++ b/apps/report/views.py @@ -0,0 +1,115 @@ +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import serializers +from rest_framework.generics import ListAPIView +from rest_framework.views import APIView +from apps.lead.models import Lead as LeadModel +from rest_framework.response import Response +from rest_framework import status +from apps.report.selector import user_lead_report_count_by_date +from apps.cuser.models import CuserRole as CuserRoleModel +from datetime import datetime +from apps.report.selector import list_leads_api +from utils.pagination import CustomPagination +from utils.tools import inline_serializer + + +class ReportApiView(APIView): + + class ResultSerializer(serializers.Serializer): + + class OperatorSerializer(serializers.Serializer): + cuser_role__name_family = serializers.CharField() + count = serializers.IntegerField() + + su = OperatorSerializer(many=True) + un = OperatorSerializer(many=True) + fa = OperatorSerializer(many=True) + all = OperatorSerializer(many=True) + + def get(self, request): + date_format = "%Y-%m-%d" + start_datetime = request.query_params.get('start_datetime', None) + if start_datetime: + start_datetime = start_datetime.replace(" ", "") + + end_datetime = request.query_params.get('end_datetime', None) + if end_datetime: + end_datetime = end_datetime.replace(" ", "") + + #example start_datetime : 2024-03-20T14:31:16 03:30 + if start_datetime and end_datetime: + start_dt = datetime.strptime(start_datetime, date_format) + end_dt = datetime.strptime(end_datetime, date_format) + else: + start_dt = datetime.now() + end_dt = datetime.now() + + # status = request.query_params.get('status') + + #همه + users_all_lead_count = user_lead_report_count_by_date(start_datetime=start_dt, end_datetime=end_dt) + #موفق + users_su_lead_count = user_lead_report_count_by_date(status='SU', start_datetime=start_dt, end_datetime=end_dt) + #شکست + users_fa_lead_count = user_lead_report_count_by_date(status='FA', start_datetime=start_dt, end_datetime=end_dt) + #نا معلوم + users_un_lead_count = user_lead_report_count_by_date(status='UN', start_datetime=start_dt, end_datetime=end_dt) + + result = { + 'su': users_su_lead_count, + 'fa': users_fa_lead_count, + 'un': users_un_lead_count, + 'all': users_all_lead_count, + } + + output_serializer = self.ResultSerializer(result) + + return Response(output_serializer.data) + + +class LeadListApi(ListAPIView): + + #permission_classes = (IsAuthenticated, custom_permissions.WorkspaceOperatorPermission) + + class Pagination(CustomPagination): + page_size = 10 + page_size_query_param = 'page_size' + max_page_size = 20 + + class OutputSerializer(serializers.ModelSerializer): + + customer = inline_serializer(many=False, fields={'name_family': serializers.CharField(), }) + + product_type = inline_serializer(many=False, fields={'name': serializers.CharField(), }) + + type = inline_serializer(many=False, fields={'title': serializers.CharField(), }) + + source = inline_serializer(many=False, fields={'title': serializers.CharField(), }) + + step = inline_serializer(many=False, fields={ + 'id': serializers.IntegerField(), + 'name': serializers.CharField(), + }) + + city = inline_serializer(many=False, fields={ + 'name': serializers.CharField(), + 'ostan': inline_serializer(many=False, fields={'name': serializers.CharField() }) + }) + + cuser_role = inline_serializer(many=False, fields={ + 'id': serializers.IntegerField(), + 'name_family': serializers.CharField() + }) + + class Meta: + model = LeadModel + fields = '__all__' + + serializer_class = OutputSerializer + pagination_class = Pagination + + filter_backends = [DjangoFilterBackend] + filterset_fields = ['step', 'status', ] + + def get_queryset(self): + return list_leads_api() diff --git a/apps/shop/admin.py b/apps/shop/admin.py index 0c8095c..e211f1d 100644 --- a/apps/shop/admin.py +++ b/apps/shop/admin.py @@ -22,7 +22,7 @@ class ProductCategoryAdmin(admin.ModelAdmin): @admin.register(ProductModel) class ProductAdmin(admin.ModelAdmin): - pass + search_fields = ("name", "category__name", "acc_code") @admin.register(OrderModel) diff --git a/apps/shop/migrations/0020_product_view_only.py b/apps/shop/migrations/0020_product_view_only.py new file mode 100644 index 0000000..3b82dc1 --- /dev/null +++ b/apps/shop/migrations/0020_product_view_only.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2024-02-27 09:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shop', '0019_alter_product_price'), + ] + + operations = [ + migrations.AddField( + model_name='product', + name='view_only', + field=models.BooleanField(blank=True, default=False, null=True), + ), + ] diff --git a/apps/shop/models.py b/apps/shop/models.py index 5dfef47..b5777c5 100644 --- a/apps/shop/models.py +++ b/apps/shop/models.py @@ -34,6 +34,7 @@ class Product(BaseModel): acc_code = models.CharField(null=True, blank=True) order_prd = models.PositiveIntegerField(null=True, blank=True, default=None) attr = models.JSONField(null=True, blank=True) + view_only = models.BooleanField(null=True, blank=True, default=False) sys_ws = models.ForeignKey(WorkspaceModel, on_delete=models.CASCADE, null=False, blank=False) diff --git a/apps/shop/selectors/order.py b/apps/shop/selectors/order.py index ca2c2ed..ac8d96b 100644 --- a/apps/shop/selectors/order.py +++ b/apps/shop/selectors/order.py @@ -55,11 +55,18 @@ def list_order_operators(request) -> OrderModel: try: workspace_uuid = get_user_workspace_uuid(request) cuser_role_uuid = get_cuser_role_uuid(request) + cuser_role = get_cuser_role(request) customer_responsibles = CustomerResponsibleModel.objects.filter(cuser_role__uuid=cuser_role_uuid, sys_ws__uuid=workspace_uuid) responsible_group_ids = customer_responsibles.values_list('responsible_group__id', flat=True) - orders = OrderModel.objects.filter(sys_ws__uuid=workspace_uuid, - responsible_group_id__in=responsible_group_ids).order_by('-created_at')[:10] + base_filter = { + 'sys_ws__uuid': workspace_uuid, + } + + if not cuser_role.is_admin: + base_filter['responsible_group_id__in'] = responsible_group_ids + + orders = OrderModel.objects.filter(**base_filter).order_by('-created_at')[:50] return orders except Exception as exp: message = f'Error Fetching List Order: {exp}' @@ -70,13 +77,23 @@ def retrieve_order_operators(request, order_id) -> OrderModel: try: workspace_uuid = get_user_workspace_uuid(request) cuser_role_uuid = get_cuser_role_uuid(request) + cuser_role = get_cuser_role(request) customer_responsibles = CustomerResponsibleModel.objects.filter(cuser_role__uuid=cuser_role_uuid, sys_ws__uuid=workspace_uuid) responsible_group_ids = customer_responsibles.values_list('responsible_group__id', flat=True) - order = OrderModel.objects.get(id=order_id, sys_ws__uuid=workspace_uuid, - responsible_group_id__in=responsible_group_ids) - order.view_at = timezone.now() - order.save() + base_filter = { + 'sys_ws__uuid': workspace_uuid, + 'id': order_id, + } + + if not cuser_role.is_admin: + base_filter['responsible_group_id__in'] = responsible_group_ids + + order = OrderModel.objects.get(**base_filter) + + if not cuser_role.is_admin: + order.view_at = timezone.now() + order.save() return order except Exception as exp: diff --git a/apps/shop/selectors/product.py b/apps/shop/selectors/product.py index 87f442f..1d560ca 100644 --- a/apps/shop/selectors/product.py +++ b/apps/shop/selectors/product.py @@ -22,13 +22,24 @@ def list_product(workspace_uuid: str) -> ProductModel: def list_product_by_category(workspace_uuid: str, category_id: int) -> ProductModel: try: - product_category_obj = ProductModel.objects.filter(sys_ws__uuid=workspace_uuid, category_id=category_id) + product_category_obj = ProductModel.objects.filter(sys_ws__uuid=workspace_uuid, category_id=category_id, + view_only=False) return product_category_obj except Exception as exp: message = f'Error Fetching List Product by category: {exp}' raise exceptions.ValidationError({'error': message}) +def list_view_only_product_by_category(workspace_uuid: str, category_id: int) -> ProductModel: + try: + view_only_product_category_obj = ProductModel.objects.filter(sys_ws__uuid=workspace_uuid, category_id=category_id, + view_only=True) + return view_only_product_category_obj + except Exception as exp: + message = f'Error Fetching View Only List Product by category: {exp}' + raise exceptions.ValidationError({'error': message}) + + def count_product_in_category(workspace_uuid: str, category_id: int) -> int: try: count_product = ProductModel.objects.filter(sys_ws__uuid=workspace_uuid, category_id=category_id).count() diff --git a/apps/shop/urls.py b/apps/shop/urls.py index f30f27f..d3b2ad6 100644 --- a/apps/shop/urls.py +++ b/apps/shop/urls.py @@ -34,5 +34,7 @@ urlpatterns = [ path('cart/add/', CartCreateApi.as_view(), name='add_cart'), path('cart/delete//', CartDeleteApi.as_view(), name='delete_cart'), - #path('import/', ImportExcelView.as_view(), name='import_excel'), + path('import/', ImportExcelView.as_view(), name='import_excel'), + path('import/customer/', ImportExcelCustomersView.as_view(), name='import_excel_customer'), + #path('import/city/', ImportExcelCitysView.as_view(), name='import_excel_city'), ] diff --git a/apps/shop/views/__init__.py b/apps/shop/views/__init__.py index 6d4909b..acd4a86 100644 --- a/apps/shop/views/__init__.py +++ b/apps/shop/views/__init__.py @@ -16,4 +16,4 @@ from .cart import (CartListApi, CartCreateApi, CartDeleteApi) from .order_item import (OrderItemsListApi, ) -from .excel_import import ImportExcelView +from .excel_import import ImportExcelView, ImportExcelCitysView, ImportExcelCustomersView diff --git a/apps/shop/views/excel_import.py b/apps/shop/views/excel_import.py index 7511237..3cbe937 100644 --- a/apps/shop/views/excel_import.py +++ b/apps/shop/views/excel_import.py @@ -2,7 +2,9 @@ from rest_framework.views import APIView from rest_framework.response import Response import pandas as pd from apps.shop.models import Product as ProductModel, ProductCategory as ProductCategoryModel -from apps.workspace.models import Workspace as WorkspaceModel +from apps.workspace.models import Workspace as WorkspaceModel, City as CityModel +from apps.customer.models import Customer as CustomerModel +from apps.customer.models import ResponsibleGroup as ResponsibleGroupModel import os @@ -13,27 +15,100 @@ class ImportExcelView(APIView): df = pd.read_excel('pe.xlsx') for index, row in df.iterrows(): cate_name = row['cate_name'] - order_cat = row['order_cat'] + #order_cat = row['order_cat'] acc_code = row['acc_code'] - prd_name = row['prd_name'] - order_prd = row['order_prd'] + prd_name = row['name'] + order_prd = row['size'] vahed = row['vahed'] workspace = WorkspaceModel.objects.get(id=2) try: - prd_ct = ProductCategoryModel.objects.get(name=cate_name, sys_ws=workspace, order_cat=order_cat) + prd_ct = ProductCategoryModel.objects.get(name=cate_name, sys_ws=workspace) except ProductCategoryModel.DoesNotExist as exp: - prd_ct = ProductCategoryModel.objects.create(name=cate_name, sys_ws=workspace, order_cat=order_cat) + prd_ct = ProductCategoryModel.objects.create(name=cate_name, sys_ws=workspace) - new_product = ProductModel() - new_product.name = prd_name - new_product.price = 0 - new_product.category = ProductCategoryModel.objects.get(id=prd_ct.id) - new_product.acc_code = acc_code - new_product.order_prd = order_prd - new_product.attr = {"acc_vahed": vahed} - new_product.sys_ws = workspace - new_product.save() + if ProductModel.objects.filter(acc_code=acc_code).exists() == False: + new_product = ProductModel() + new_product.name = prd_name + new_product.price = 0 + new_product.category = ProductCategoryModel.objects.get(id=prd_ct.id) + new_product.acc_code = acc_code + new_product.order_prd = order_prd + new_product.view_only = True + new_product.attr = {"acc_vahed": vahed} + new_product.sys_ws = workspace + new_product.save() + else: + up_product = ProductModel.objects.get(acc_code=acc_code) + up_product.name = prd_name + up_product.save() return Response(data={"result": 'ok'}) + +class ImportExcelCitysView(APIView): + + def get(self, request): + + + df = pd.read_excel('cities.xlsx') + for index, row in df.iterrows(): + city_name = row['name'] + pv_id = row['province_id'] + + try: + ostan = CityModel.objects.get(pv_id=pv_id, is_ostan=True) + except Exception as exp: + print(city_name) + print(pv_id) + + city_obj = CityModel() + city_obj.name = city_name + city_obj.is_ostan = False + city_obj.ostan = ostan + city_obj.save() + + + return Response(data={"result": 'ok'}) + + +class ImportExcelCustomersView(APIView): + + def get(self, request): + + df = pd.read_excel('customersData.xlsx') + for index, row in df.iterrows(): + name = row['name'] + mobile = row['mobile'] + + if name and mobile: + + phone = row['phone'] + address = row['address'] + ostan = row['ostan'] + shahr = row['shar'] + mantaghe = row['id_mantaghe'] + + try: + ostan_obj = CityModel.objects.get(name=ostan, is_ostan=True) + shahr_obj = CityModel.objects.get(name=shahr, ostan=ostan_obj) + except Exception as exp: + shahr_obj = CityModel.objects.get(id=1229) + + try: + cs = CustomerModel() + cs.name_family = name + cs.mobile = mobile + if phone: + cs.phone = phone + + cs.city = shahr_obj + if address: + cs.address = address + cs.responsible_group = ResponsibleGroupModel.objects.get(id=mantaghe) + cs.sys_ws = WorkspaceModel.objects.get(id=2) + cs.save() + except Exception as exp: + print(name) + + return Response(data={"result": 'ok'}) diff --git a/apps/shop/views/order.py b/apps/shop/views/order.py index ba925d7..ab30b07 100644 --- a/apps/shop/views/order.py +++ b/apps/shop/views/order.py @@ -124,7 +124,7 @@ class OrderUpdateApi(APIView): class OrderDeleteApi(APIView): - + permission_classes = (IsAuthenticated,) def delete(self, request, order_id: int): user_workspace_uuid = get_user_workspace_uuid(request=request) @@ -139,7 +139,7 @@ class OrderDeleteApi(APIView): class OrderListApi(generics.ListAPIView): - + permission_classes = (IsAuthenticated,) class OutputSerializer(serializers.ModelSerializer): class Meta: model = OrderModel @@ -155,6 +155,7 @@ class OrderListApi(generics.ListAPIView): class OrderListOperatorApi(generics.ListAPIView): + permission_classes = (IsAuthenticated,) class OutputSerializer(serializers.ModelSerializer): customer = inline_serializer(fields={ 'id': serializers.IntegerField(), @@ -172,6 +173,7 @@ class OrderListOperatorApi(generics.ListAPIView): class OrderRetrieveOperatorApi(APIView): + permission_classes = (IsAuthenticated,) class OutputSerializer(serializers.ModelSerializer): customer = inline_serializer(fields={ @@ -210,6 +212,7 @@ class OrderRetrieveOperatorApi(APIView): class OrderRetrieveOperatorExcelApi(APIView): + permission_classes = (IsAuthenticated, ) def get(self, request, order_id): order_df = retrieve_order_item_excel(request=request, order_id=order_id) diff --git a/apps/shop/views/product.py b/apps/shop/views/product.py index 12e03ea..1b16ec5 100644 --- a/apps/shop/views/product.py +++ b/apps/shop/views/product.py @@ -134,6 +134,7 @@ class ProductListCategoryApi(generics.ListAPIView): id = serializers.IntegerField() name = serializers.CharField() price = serializers.FloatField() + view_only = serializers.BooleanField() class Meta: exclude = ('sys_ws', ) @@ -144,6 +145,8 @@ class ProductListCategoryApi(generics.ListAPIView): serializer = self.get_serializer(queryset_data, many=True) workspace_uuid = get_user_workspace_uuid(request=request) category = retrieve_product_category(category_id=category_id, workspace_uuid=workspace_uuid) + view_only_products = list_view_only_product_by_category(category_id=category_id, workspace_uuid=workspace_uuid) + view_only_products_serializer = self.get_serializer(view_only_products, many=True) if len(serializer.data) == 0: return Response( @@ -152,9 +155,14 @@ class ProductListCategoryApi(generics.ListAPIView): data = { "category_name": category.name, "category_img": category.image.url, - "products": serializer.data + "products": serializer.data, } + if len(view_only_products_serializer.data) >=1: + data['view_only_products'] = view_only_products_serializer.data + else: + data['view_only_products'] = None + return Response(data) def get_queryset(self): diff --git a/apps/workspace/migrations/0014_city_pv_id.py b/apps/workspace/migrations/0014_city_pv_id.py new file mode 100644 index 0000000..f0ef277 --- /dev/null +++ b/apps/workspace/migrations/0014_city_pv_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2024-02-10 09:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('workspace', '0013_alter_workspace_cuser_owner'), + ] + + operations = [ + migrations.AddField( + model_name='city', + name='pv_id', + field=models.PositiveSmallIntegerField(blank=True, null=True), + ), + ] diff --git a/apps/workspace/migrations/0015_alter_city_name.py b/apps/workspace/migrations/0015_alter_city_name.py new file mode 100644 index 0000000..1ce6098 --- /dev/null +++ b/apps/workspace/migrations/0015_alter_city_name.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.1 on 2024-02-10 11:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('workspace', '0014_city_pv_id'), + ] + + operations = [ + migrations.AlterField( + model_name='city', + name='name', + field=models.CharField(blank=True, max_length=30, null=True), + ), + ] diff --git a/apps/workspace/migrations/0016_remove_workspace_cuser_owner.py b/apps/workspace/migrations/0016_remove_workspace_cuser_owner.py new file mode 100644 index 0000000..4263224 --- /dev/null +++ b/apps/workspace/migrations/0016_remove_workspace_cuser_owner.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.1 on 2024-02-18 09:44 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('workspace', '0015_alter_city_name'), + ] + + operations = [ + migrations.RemoveField( + model_name='workspace', + name='cuser_owner', + ), + ] diff --git a/apps/workspace/models.py b/apps/workspace/models.py index a3853ce..0a2e54a 100644 --- a/apps/workspace/models.py +++ b/apps/workspace/models.py @@ -11,7 +11,7 @@ class Workspace(BaseModel): #cuser_role_owner = models.OneToOneField(CuserRoleModel, on_delete=models.CASCADE, # related_name='workspace', null=True, blank=True) - cuser_owner = models.ForeignKey(CUser, on_delete=models.SET_NULL, null=True, blank=True, default=None) + #cuser_owner = models.ForeignKey(CUser, on_delete=models.SET_NULL, null=True, blank=True, default=None) name = models.CharField(max_length=15, null=False, blank=False) username = models.CharField(max_length=30, null=False, blank=False, unique=True) expire = models.DateTimeField(null=True, blank=True) @@ -27,9 +27,10 @@ class Workspace(BaseModel): class City(BaseModel): - name = models.CharField(max_length=15, null=True, blank=True) + name = models.CharField(max_length=30, null=True, blank=True) is_ostan = models.BooleanField(default=False, null=False, blank=False) ostan = models.ForeignKey('self', models.CASCADE, null=True, blank=True) + pv_id = models.PositiveSmallIntegerField(null=True, blank=True) def __str__(self): return str(self.name) diff --git a/config/asgi.py b/config/asgi.py index 072ae9e..4427c6d 100644 --- a/config/asgi.py +++ b/config/asgi.py @@ -11,6 +11,6 @@ import os from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'crmApi.settings') +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') application = get_asgi_application() diff --git a/config/settings/base.py b/config/settings/base.py index e33c440..8dae0fb 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -19,8 +19,6 @@ BASE_DIR = Path(__file__).resolve().parent.parent MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR.parent /'media' -print(MEDIA_ROOT) - # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ @@ -33,8 +31,22 @@ DEBUG = True ALLOWED_HOSTS = ['*'] CORS_ALLOWED_ORIGINS = [ + "http://192.168.138.94:3000", + "http://192.168.138.94:3001", "http://localhost:3001", + "http://localhost:3002", "http://localhost:3000", + "https://loolegostar.liara.run", + "https://loolegostar-crm.liara.run", +] + +CORS_ALLOW_METHODS = [ + "DELETE", + "GET", + "OPTIONS", + "PATCH", + "POST", + "PUT", ] AUTH_USER_MODEL = 'cuser.CUser' diff --git a/config/settings/production.py b/config/settings/production.py index 9b5ed21..b386768 100644 --- a/config/settings/production.py +++ b/config/settings/production.py @@ -1 +1,12 @@ from .base import * +DEBUG = False +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'crm-api', + 'USER': 'root', + 'PASSWORD': '1yI0Iw2Fpjm2BXhEnwUKVdDg', + 'HOST': 'loolegostar-db', + 'PORT': '5432' + } +} diff --git a/config/urls.py b/config/urls.py index 5401752..e5abe07 100644 --- a/config/urls.py +++ b/config/urls.py @@ -23,6 +23,7 @@ urlpatterns = [ path('admin/', admin.site.urls), path('auth/', include('apps.jwt_auth.urls')), path('cuser/', include('apps.cuser.urls')), + path('report/', include('apps.report.urls')), path('customer/', include('apps.customer.urls')), path('shop/', include('apps.shop.urls')), path('ticket/', include('apps.ticket.urls')), diff --git a/config/wsgi.py b/config/wsgi.py index de5557e..fd9f5ac 100644 --- a/config/wsgi.py +++ b/config/wsgi.py @@ -11,6 +11,6 @@ import os from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'crmApi.settings') +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') application = get_wsgi_application() diff --git a/liara.json b/liara.json new file mode 100644 index 0000000..3579ef3 --- /dev/null +++ b/liara.json @@ -0,0 +1,10 @@ +{ + "platform": "django", + "app": "loolegostar-api", + "disks": [ + { + "name": "media", + "mountTo": "media" + } + ] +} \ No newline at end of file diff --git a/liara_nginx.conf b/liara_nginx.conf new file mode 100644 index 0000000..d5a7402 --- /dev/null +++ b/liara_nginx.conf @@ -0,0 +1,29 @@ +gzip on; +gzip_disable "msie6"; +gzip_vary on; +gzip_proxied any; +gzip_comp_level 6; +gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml; + +client_max_body_size 250M; + +location /media { + alias /usr/src/app/media; +} + +location /static { + alias /usr/src/app/config/staticfiles; +} + +location / { + try_files /dev/null @django_app; +} + +location ~\.sqlite3$ { + deny all; + error_page 403 =404 /; +} + +location ~ /\.well-known { + allow all; +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..20ac44f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,14 @@ +Django==4.2.1 +djangorestframework==3.14.0 +Markdown==3.4.3 +requests==2.31.0 +django-filter==23.2 +drf-spectacular==0.26.2 +psycopg==3.1.9 +psycopg-binary==3.1.9 +djangorestframework-simplejwt==5.2.2 +django-cors-headers==4.3.1 +openpyxl==3.1.2 +XlsxWriter==3.1.9 +pandas==2.2.0 +pillow==10.2.0 \ No newline at end of file diff --git a/requirements/base.txt b/requirements/base.txt index 46927b3..20ac44f 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,9 +1,11 @@ Django==4.2.1 djangorestframework==3.14.0 Markdown==3.4.3 +requests==2.31.0 django-filter==23.2 drf-spectacular==0.26.2 psycopg==3.1.9 +psycopg-binary==3.1.9 djangorestframework-simplejwt==5.2.2 django-cors-headers==4.3.1 openpyxl==3.1.2