diff --git a/core/apps/finance/serializers/expence.py b/core/apps/finance/serializers/expence.py index 82200d6..ff77c46 100644 --- a/core/apps/finance/serializers/expence.py +++ b/core/apps/finance/serializers/expence.py @@ -72,7 +72,8 @@ class ExpenceCreateSerializer(serializers.ModelSerializer): expence.counterparty.save() body = f"""{user.full_name} {expence.price} {expence.currency.upper()}... \n screen: /monitoring""" data = { - "screen": "/monitoring" + "screen": "/monitoring", + "type": "expence", } notify_user(user=user, title="Tasdiqlang yoki rad eting", body=body, data=data) cash_transaction.save() diff --git a/core/apps/notifications/admin/__init__.py b/core/apps/notifications/admin/__init__.py index b70bf7e..ca4adba 100644 --- a/core/apps/notifications/admin/__init__.py +++ b/core/apps/notifications/admin/__init__.py @@ -1 +1,2 @@ -from .notification import * \ No newline at end of file +from .notification import * +from .notification_history import * \ No newline at end of file diff --git a/core/apps/notifications/admin/notification_history.py b/core/apps/notifications/admin/notification_history.py new file mode 100644 index 0000000..7d55c90 --- /dev/null +++ b/core/apps/notifications/admin/notification_history.py @@ -0,0 +1,6 @@ +from django.contrib import admin + +from core.apps.notifications.models.notification_history import NotificationHistory + + +admin.site.register(NotificationHistory) \ No newline at end of file diff --git a/core/apps/notifications/migrations/0003_notificationhistory.py b/core/apps/notifications/migrations/0003_notificationhistory.py new file mode 100644 index 0000000..717622a --- /dev/null +++ b/core/apps/notifications/migrations/0003_notificationhistory.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.4 on 2025-10-30 16:16 + +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('notifications', '0002_notification_type_alter_notification_unique_together'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='NotificationHistory', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('title', models.CharField(max_length=200)), + ('body', models.TextField()), + ('is_read', models.BooleanField(default=False)), + ('data', models.JSONField(blank=True, null=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notification_histories', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/core/apps/notifications/models/__init__.py b/core/apps/notifications/models/__init__.py index b70bf7e..ca4adba 100644 --- a/core/apps/notifications/models/__init__.py +++ b/core/apps/notifications/models/__init__.py @@ -1 +1,2 @@ -from .notification import * \ No newline at end of file +from .notification import * +from .notification_history import * \ No newline at end of file diff --git a/core/apps/notifications/models/notification_history.py b/core/apps/notifications/models/notification_history.py new file mode 100644 index 0000000..24768e8 --- /dev/null +++ b/core/apps/notifications/models/notification_history.py @@ -0,0 +1,17 @@ +from django.db import models + +from core.apps.shared.models import BaseModel +from core.apps.accounts.models import User + + +class NotificationHistory(BaseModel): + title = models.CharField(max_length=200) + body = models.TextField() + user = models.ForeignKey( + User, on_delete=models.CASCADE, related_name='notification_histories' + ) + is_read = models.BooleanField(default=False) + data = models.JSONField(null=True, blank=True) + + def __str__(self): + return f'Notification to {self.user}: {self.title}' \ No newline at end of file diff --git a/core/apps/notifications/serializers/notification_history.py b/core/apps/notifications/serializers/notification_history.py new file mode 100644 index 0000000..e3d8305 --- /dev/null +++ b/core/apps/notifications/serializers/notification_history.py @@ -0,0 +1,11 @@ +from rest_framework import serializers + +from core.apps.notifications.models import NotificationHistory + + +class NotificationHistorySerializer(serializers.ModelSerializer): + class Meta: + model = NotificationHistory + fields = [ + 'id', 'title', 'body', 'data', 'is_read', 'created_at' + ] \ No newline at end of file diff --git a/core/apps/notifications/tasks/create_notification_history.py b/core/apps/notifications/tasks/create_notification_history.py new file mode 100644 index 0000000..3e759b9 --- /dev/null +++ b/core/apps/notifications/tasks/create_notification_history.py @@ -0,0 +1,22 @@ +from celery import shared_task + +from django.shortcuts import get_object_or_404 + +from core.apps.notifications.models import NotificationHistory +from core.apps.accounts.models import User + + +@shared_task +def create_history(user_ids, title, body, data=None): + histories = [] + for user_id in user_ids: + user = get_object_or_404(User, id=user_id) + histories.append(NotificationHistory( + title=title, + user=user, + body=body, + data=data, + is_read=False + )) + + NotificationHistory.objects.bulk_create(histories) diff --git a/core/apps/notifications/urls.py b/core/apps/notifications/urls.py index 2bdceed..9fea8cb 100644 --- a/core/apps/notifications/urls.py +++ b/core/apps/notifications/urls.py @@ -1,7 +1,11 @@ from django.urls import path from core.apps.notifications.views import notification +from core.apps.notifications.views import notification_history + urlpatterns = [ path('device/register/', notification.RegisterExpoPushToken.as_view()), + path('history/', notification_history.NotificationHistoryListApiView.as_view()), + path('history//read/', notification_history.NotificationHistoryUpdateApiView.as_view()), ] \ No newline at end of file diff --git a/core/apps/notifications/utils/send_notification.py b/core/apps/notifications/utils/send_notification.py index da9df89..f4d5cf9 100644 --- a/core/apps/notifications/utils/send_notification.py +++ b/core/apps/notifications/utils/send_notification.py @@ -1,18 +1,20 @@ import requests from firebase_admin import messaging -from django.conf import settings - from core.apps.notifications.models import Notification +from core.apps.notifications.tasks.create_notification_history import create_history + EXPO_API_URL = "https://exp.host/--/api/v2/push/send" def send_notification(token, title, body, data=None): tokens = list(Notification.objects.exclude(token=token).values_list("token", flat=True)) + users = list(Notification.objects.exclude(token=token).values_list("user", flat=True)) + create_history.delay(users, title, body, data) if not tokens: return {"error": "No tokens found"} - + messages = [ { "to": token, @@ -38,6 +40,8 @@ def send_notification(token, title, body, data=None): def send_web_notification(token, title, body, data=None): tokens = list(Notification.objects.exclude(token=token).values_list('token', flat=True)) + users = list(Notification.objects.exclude(token=token).values_list("user", flat=True)) + create_history.delay(users, title, body, data) if not tokens: return diff --git a/core/apps/notifications/views/notification_history.py b/core/apps/notifications/views/notification_history.py new file mode 100644 index 0000000..2be30c8 --- /dev/null +++ b/core/apps/notifications/views/notification_history.py @@ -0,0 +1,43 @@ +from django.shortcuts import get_object_or_404 + +from rest_framework import generics, views +from rest_framework.response import Response + +from core.apps.accounts.permissions.permissions import HasRolePermission +from core.apps.notifications.models import NotificationHistory +from core.apps.notifications.serializers.notification_history import NotificationHistorySerializer + + + +class NotificationHistoryListApiView(generics.GenericAPIView): + serializer_class = NotificationHistorySerializer + permission_classes = [HasRolePermission] + + def get_queryset(self): + return NotificationHistory.objects.filter(user=self.request.user).order_by('-created_at') + + def get(self, request): + queryset = self.get_queryset() + is_read = request.query_params.get('is_read') + if is_read: + if is_read.lower() == 'true': + queryset = queryset.filter(is_read=True) + if is_read.lower() == 'false': + queryset = queryset.filter(is_read=False) + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.serializer_class(page, many=True) + return self.get_paginated_response(serializer.data) + return None + + +class NotificationHistoryUpdateApiView(views.APIView): + permission_classes = [HasRolePermission] + + def post(self, request, id): + obj = get_object_or_404(NotificationHistory, id=id) + if obj.is_read: + return Response({'success': False, "message": 'already readed'}, status=400) + obj.is_read = True + obj.save() + return Response({'success': True, "message": 'readed'}, status=200) diff --git a/core/apps/orders/serializers/order.py b/core/apps/orders/serializers/order.py index 6d3899c..c0c4c37 100644 --- a/core/apps/orders/serializers/order.py +++ b/core/apps/orders/serializers/order.py @@ -88,7 +88,8 @@ class MultipleOrderCreateSerializer(serializers.Serializer): body = f"""{user.full_name} {order.project_folder.name} uchun {order.wherehouse.name} ombor ga {order.quantity} {order.unity.value} {order.product.name} ga buyurtma berdi. Buyurtma yetkazish sanasi {common_date} """ data = { - "screen": "/supply" + "screen": "/supply", + "type": "order" } notify_user(user=user, title="Ta'minot",body=body, data=data) diff --git a/core/apps/wherehouse/migrations/0018_alter_invalidproduct_project_folder.py b/core/apps/wherehouse/migrations/0018_alter_invalidproduct_project_folder.py new file mode 100644 index 0000000..860cfe9 --- /dev/null +++ b/core/apps/wherehouse/migrations/0018_alter_invalidproduct_project_folder.py @@ -0,0 +1,20 @@ +# Generated by Django 5.2.4 on 2025-10-30 16:19 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0016_estimatework_employee_estimatework_end_date_and_more'), + ('wherehouse', '0017_wherehouse_users'), + ] + + operations = [ + migrations.AlterField( + model_name='invalidproduct', + name='project_folder', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='invalid_products', to='projects.projectfolder'), + ), + ] diff --git a/core/apps/wherehouse/models/invalid_product.py b/core/apps/wherehouse/models/invalid_product.py index 1a030d3..5e8903e 100644 --- a/core/apps/wherehouse/models/invalid_product.py +++ b/core/apps/wherehouse/models/invalid_product.py @@ -26,7 +26,8 @@ class InvalidProduct(BaseModel): # relationship inventory = models.ForeignKey(Inventory, on_delete=models.CASCADE, related_name='invalid_products') project_folder = models.ForeignKey( - ProjectFolder, on_delete=models.CASCADE, related_name='invalid_products' + ProjectFolder, on_delete=models.CASCADE, related_name='invalid_products', + null=True, blank=True ) witnesses = models.ManyToManyField(User, related_name='invalid_products') work = models.ForeignKey( diff --git a/core/apps/wherehouse/serializers/invalid_product.py b/core/apps/wherehouse/serializers/invalid_product.py index 6477f92..020e2da 100644 --- a/core/apps/wherehouse/serializers/invalid_product.py +++ b/core/apps/wherehouse/serializers/invalid_product.py @@ -10,7 +10,7 @@ from core.apps.accounts.serializers.user import UserListSerializer class InvalidProductCreateSerializer(serializers.Serializer): inventory_id = serializers.UUIDField() - project_folder_id = serializers.UUIDField() + project_folder_id = serializers.UUIDField(required=False) witnesses_ids = serializers.ListField(child=serializers.UUIDField()) work_id = serializers.UUIDField(required=False) amount = serializers.IntegerField() @@ -24,16 +24,17 @@ class InvalidProductCreateSerializer(serializers.Serializer): inventory = Inventory.objects.filter(id=attrs['inventory_id']).first() if not inventory: raise serializers.ValidationError("Inventory not found") - project_folder = ProjectFolder.objects.filter(id=attrs['project_folder_id']).first() - if not project_folder: - raise serializers.ValidationError("Project Folder not found") + if attrs.get('project_folder_id'): + project_folder = ProjectFolder.objects.filter(id=attrs['project_folder_id']).first() + if not project_folder: + raise serializers.ValidationError("Project Folder not found") + attrs['project_folder'] = project_folder if attrs.get('work_id'): work = EstimateWork.objects.filter(id=attrs['work_id']).first() if not work: raise serializers.ValidationError("Work not found") attrs['work'] = work attrs['inventory'] = inventory - attrs['project_folder'] = project_folder return super().validate(attrs) def create(self, validated_data): @@ -75,7 +76,7 @@ class InvliadProductListSerializer(serializers.ModelSerializer): return { 'id': obj.project_folder.id, 'name': obj.project_folder.name, - } + } if obj.project_folder else None def get_wherehouse(self, obj): return {