diff --git a/apps/herd/migrations/0016_rancher_activity_rancher_heavy_livestock_number_and_more.py b/apps/herd/migrations/0016_rancher_activity_rancher_heavy_livestock_number_and_more.py new file mode 100644 index 0000000..2b7b6c9 --- /dev/null +++ b/apps/herd/migrations/0016_rancher_activity_rancher_heavy_livestock_number_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 5.0 on 2025-08-19 05:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('herd', '0015_alter_herd_postal'), + ] + + operations = [ + migrations.AddField( + model_name='rancher', + name='activity', + field=models.CharField(choices=[('I', 'Industrial'), ('V', 'Village'), ('N', 'Nomadic')], max_length=1, null=True), + ), + migrations.AddField( + model_name='rancher', + name='heavy_livestock_number', + field=models.BigIntegerField(default=0), + ), + migrations.AddField( + model_name='rancher', + name='light_livestock_number', + field=models.BigIntegerField(default=0), + ), + migrations.AddField( + model_name='rancher', + name='rancher_type', + field=models.CharField(choices=[('N', 'Natural'), ('L', 'Legal')], default='N', help_text='N is natural & L is legal', max_length=1), + ), + migrations.AddField( + model_name='rancher', + name='union_code', + field=models.CharField(max_length=50, null=True), + ), + migrations.AddField( + model_name='rancher', + name='union_name', + field=models.CharField(max_length=50, null=True), + ), + ] diff --git a/apps/herd/models.py b/apps/herd/models.py index 9f0e575..82befc1 100644 --- a/apps/herd/models.py +++ b/apps/herd/models.py @@ -82,6 +82,30 @@ class Herd(BaseModel): class Rancher(BaseModel): ranching_farm = models.CharField(max_length=150, null=True) + union_name = models.CharField(max_length=50, null=True) + union_code = models.CharField(max_length=50, null=True) + activity_types = ( + ("I", "Industrial"), + ("V", "Village"), + ("N", "Nomadic") + ) + activity = models.CharField( + choices=activity_types, + max_length=1, + null=True + ) + rancher_types = ( + ('N', 'Natural'), + ('L', 'Legal') + ) + rancher_type = models.CharField( + max_length=1, + choices=rancher_types, + default='N', + help_text="N is natural & L is legal" + ) + heavy_livestock_number = models.BigIntegerField(default=0) + light_livestock_number = models.BigIntegerField(default=0) herd_code = models.CharField(max_length=100, null=True) first_name = models.CharField(max_length=150, null=True) last_name = models.CharField(max_length=150, null=True) diff --git a/apps/herd/pos/api/v1/api.py b/apps/herd/pos/api/v1/api.py new file mode 100644 index 0000000..06fdb35 --- /dev/null +++ b/apps/herd/pos/api/v1/api.py @@ -0,0 +1,154 @@ +from apps.herd.web.api.v1.serializers import HerdSerializer, RancherSerializer +from apps.core.mixins.search_mixin import DynamicSearchMixin +from apps.livestock.web.api.v1.serializers import LiveStockSerializer +from rest_framework.exceptions import APIException +from rest_framework.permissions import AllowAny +from rest_framework.response import Response +from rest_framework.decorators import action +from common.tools import CustomOperations +from rest_framework import viewsets +from apps.herd.models import Herd, Rancher +from django.db import transaction +from rest_framework import status + + +class HerdViewSet(viewsets.ModelViewSet): + """ Herd ViewSet """ + + queryset = Herd.objects.all() + serializer_class = HerdSerializer + permission_classes = [AllowAny] + + @transaction.atomic + def create(self, request, *args, **kwargs): + """ create herd with user """ + + if 'rancher' in request.data.keys(): + rancher = CustomOperations().custom_create( + request=request, + view=RancherViewSet(), + data=request.data['rancher'] + ) + request.data.update({'rancher': rancher['id']}) + + serializer = self.serializer_class(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + else: + return Response(serializer.errors, status=status.HTTP_403_FORBIDDEN) + + @action( + methods=['get'], + detail=False, + url_name='my_herds', + url_path='my_herds', + name='my_herds' + ) + @transaction.atomic + def my_herds(self, request): + """ get current user herds """ + serializer = self.serializer_class(self.queryset.filter(owner=request.user.id), many=True) + if serializer.data: + return Response(serializer.data, status=status.HTTP_200_OK) + else: + return Response(status=status.HTTP_204_NO_CONTENT) + + @action( + methods=['post'], + detail=True, + url_path='trash', + url_name='trash', + name='trash' + ) + @transaction.atomic + def trash(self, request, pk=None): + """ Sent herd to trash """ + try: + herd = self.queryset.get(id=pk) + herd.trash = True + herd.save() + return Response(status=status.HTTP_200_OK) + except APIException as e: + return Response(e, status=status.HTTP_204_NO_CONTENT) + + @action( + methods=['post'], + detail=True, + url_path='delete', + url_name='delete', + name='delete' + ) + @transaction.atomic + def delete(self, request, pk=None): + """ full delete of herd """ + try: + herd = self.queryset.get(id=pk) + herd.delete() + return Response(status=status.HTTP_200_OK) + except APIException as e: + return Response(e, status=status.HTTP_204_NO_CONTENT) + + @action( + methods=['get'], + detail=True, + url_path='live_stocks', + url_name='live_stocks', + name='live_stocks' + ) + def live_stocks(self, request, pk=None): + """ list of herd live_stocks""" + + herd = self.get_object() + queryset = herd.live_stock_herd.all() # get herd live_stocks + + # paginate queryset + page = self.paginate_queryset(queryset) + if page is not None: + serializer = LiveStockSerializer(page, many=True) + return self.get_paginated_response(serializer.data) + + +class RancherViewSet(viewsets.ModelViewSet, DynamicSearchMixin): + queryset = Rancher.objects.all() + serializer_class = RancherSerializer + search_fields = [ + "ranching_farm", + "first_name", + "last_name", + "mobile", + "national_code", + "birthdate", + "nationality", + "address", + "province__name", + "city__name", + ] + + def list(self, request, *args, **kwargs): + """ list of ranchers """ + + search = self.filter_query(self.queryset.order_by('-modify_date')) # search & filter + page = self.paginate_queryset(search) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + + @action( + methods=['get'], + detail=True, + url_path='herds', + url_name='herds', + name='herds' + ) + def herds_by_rancher(self, request, pk=None): + """ list of rancher herds """ + + rancher = self.get_object() + queryset = rancher.herd.all().order_by('-modify_date') # get rancher herds + + # paginate queryset + page = self.paginate_queryset(queryset) + if page is not None: + serializer = HerdSerializer(page, many=True) + return self.get_paginated_response(serializer.data) diff --git a/apps/herd/pos/api/v1/serializers.py b/apps/herd/pos/api/v1/serializers.py index e69de29..88e0563 100644 --- a/apps/herd/pos/api/v1/serializers.py +++ b/apps/herd/pos/api/v1/serializers.py @@ -0,0 +1,53 @@ +from apps.authentication.api.v1.serializers.serializer import ( + UserSerializer, + OrganizationSerializer, + ProvinceSerializer, + CitySerializer +) +from rest_framework import serializers +from apps.herd.models import Herd, Rancher + + +class HerdSerializer(serializers.ModelSerializer): + """ Herd Serializer """ + class Meta: + model = Herd + fields = '__all__' + + def to_representation(self, instance): + """ Customize serializer output """ + representation = super().to_representation(instance) + if isinstance(instance, Herd): + if instance.owner: + representation['owner'] = instance.owner.id + representation['cooperative'] = OrganizationSerializer(instance.cooperative).data + representation['province'] = ProvinceSerializer(instance.province).data + representation['city'] = CitySerializer(instance.city).data + if instance.contractor: + representation['contractor'] = OrganizationSerializer(instance.contractor).data + representation['rancher'] = RancherSerializer(instance.rancher).data + + return representation + + +class RancherSerializer(serializers.ModelSerializer): + class Meta: + model = Rancher + fields = '__all__' + + def to_representation(self, instance): + """ customize output of serializer """ + + representation = super().to_representation(instance) + + representation['province'] = { + 'id': instance.province.id, + 'name': instance.province.name + } + + representation['city'] = { + 'id': instance.city.id, + 'name': instance.city.name + } + + return representation diff --git a/apps/herd/pos/api/v1/urls.py b/apps/herd/pos/api/v1/urls.py index e69de29..51628b9 100644 --- a/apps/herd/pos/api/v1/urls.py +++ b/apps/herd/pos/api/v1/urls.py @@ -0,0 +1,11 @@ +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from . import api + +router = DefaultRouter() +router.register(r'herd', api.HerdViewSet, basename='herd') +router.register(r'rancher', api.RancherViewSet, basename='rancher') + +urlpatterns = [ + path('api/v1/', include(router.urls)) +] diff --git a/apps/herd/urls.py b/apps/herd/urls.py index 3834512..110de13 100644 --- a/apps/herd/urls.py +++ b/apps/herd/urls.py @@ -5,5 +5,6 @@ from django.urls import path, include urlpatterns = [ path('web/', include('apps.herd.web.api.v1.urls')), + path('pos/', include('apps.herd.pos.api.v1.urls')), path('excel/', include('apps.herd.services.excel.urls')), ] diff --git a/apps/pos_device/migrations/0061_posfreeproducts.py b/apps/pos_device/migrations/0061_posfreeproducts.py new file mode 100644 index 0000000..b41125e --- /dev/null +++ b/apps/pos_device/migrations/0061_posfreeproducts.py @@ -0,0 +1,39 @@ +# Generated by Django 5.0 on 2025-08-18 11:13 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0035_bankaccountinformation_account_and_more'), + ('pos_device', '0060_alter_sessions_latitude_alter_sessions_longitude_and_more'), + ('product', '0069_quota_group'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='POSFreeProducts', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_date', models.DateTimeField(auto_now_add=True)), + ('modify_date', models.DateTimeField(auto_now=True)), + ('creator_info', models.CharField(max_length=100, null=True)), + ('modifier_info', models.CharField(max_length=100, null=True)), + ('trash', models.BooleanField(default=False)), + ('balance', models.PositiveIntegerField(default=0)), + ('price', models.PositiveIntegerField(default=0)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_createddby', to=settings.AUTH_USER_MODEL)), + ('device', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='free_products', to='pos_device.device')), + ('modified_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_modifiedby', to=settings.AUTH_USER_MODEL)), + ('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='free_products', to='authentication.organization')), + ('product', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='pos_free_products', to='product.product')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/apps/pos_device/mixins/__init__.py b/apps/pos_device/mixins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/pos_device/mixins/pos_device_mixin.py b/apps/pos_device/mixins/pos_device_mixin.py new file mode 100644 index 0000000..4fe60bc --- /dev/null +++ b/apps/pos_device/mixins/pos_device_mixin.py @@ -0,0 +1,34 @@ +import typing +from apps.pos_device import models as pos_models + + +class POSDeviceMixin: + """ get require objects with values in request header """ + + def get_pos_device(self): + """ get device object by device serial in request header """ + + device = pos_models.Device.objects.get( + serial=self.request.headers.get('device-serial') # noqa + ) + + return device + + def get_device_organization(self): + """ get device owner (organization) information """ + + organization = pos_models.DeviceAssignment.objects.filter( + device__serial=self.request.headers.get('device-serial') # noqa + ).first().organization + + return organization + + def get_provider_organization(self): + """ get pos provider organization """ + + provider = pos_models.Organization.objects.get( + en_name=self.request.headers.get('device-provider') # noqa + ) + + return provider + diff --git a/apps/pos_device/models.py b/apps/pos_device/models.py index 4f9f298..667cff9 100644 --- a/apps/pos_device/models.py +++ b/apps/pos_device/models.py @@ -1,8 +1,8 @@ -import datetime import random import string from apps.authentication.models import Organization +from apps.product.models import Product from django.contrib.postgres.fields import ArrayField from apps.authorization.models import UserRelations from apps.core.models import BaseModel @@ -55,7 +55,7 @@ class Device(BaseModel): def generate_device_identity(self): # noqa """ generate identity for every device """ - prefix = "POS" + # prefix = "POS" while True: number_part = ''.join(random.choices(string.digits, k=9)) code = f"{number_part}" @@ -264,3 +264,32 @@ class StakeHolders(BaseModel): def save(self, *args, **kwargs): return super(StakeHolders, self).save(*args, **kwargs) + + +class POSFreeProducts(BaseModel): + product = models.ForeignKey( + Product, + on_delete=models.CASCADE, + related_name='pos_free_products', + null=True + ) + organization = models.ForeignKey( + Organization, + on_delete=models.CASCADE, + related_name='free_products', + null=True + ) + device = models.ForeignKey( + Device, + on_delete=models.CASCADE, + related_name='free_products', + null=True + ) + balance = models.PositiveIntegerField(default=0) + price = models.PositiveIntegerField(default=0) + + def __str__(self): + return f'{self.product.name}-{self.organization.name}' + + def save(self, *args, **kwargs): + return super(POSFreeProducts, self).save(*args, **kwargs) diff --git a/apps/pos_device/pos/api/v1/viewsets/device.py b/apps/pos_device/pos/api/v1/viewsets/device.py index 90afbf3..d8f9b9f 100644 --- a/apps/pos_device/pos/api/v1/viewsets/device.py +++ b/apps/pos_device/pos/api/v1/viewsets/device.py @@ -68,6 +68,7 @@ class POSDeviceViewSet(viewsets.ModelViewSet): "device_identity": device.device_identity, "serial": device.serial }, status=status.HTTP_200_OK) + return Response({ "message": "device pre registered - unauthorized", "device_identity": device.device_identity, diff --git a/apps/product/pos/api/v1/serializers/product_serializers.py b/apps/product/pos/api/v1/serializers/product_serializers.py index 284cb56..63256fa 100644 --- a/apps/product/pos/api/v1/serializers/product_serializers.py +++ b/apps/product/pos/api/v1/serializers/product_serializers.py @@ -1,10 +1,27 @@ from rest_framework import serializers from apps.product import models as product_models +from apps.pos_device.models import POSFreeProducts from apps.authorization.api.v1 import serializers as authorize_serializers from apps.authentication.api.v1.serializers.serializer import OrganizationSerializer, OrganizationTypeSerializer -class ProductCategorySerializer(serializers.ModelSerializer): +class POSFreeProductSerializer(serializers.ModelSerializer): + class Meta: + model = POSFreeProducts + fields = '__all__' + + def to_representation(self, instance): + representation = super().to_representation(instance) + + representation['product'] = { + 'name': instance.product.name, + 'id': instance.product.id + } + + return representation + + +class ProductCategorySerializer(serializers.ModelSerializer): # noqa class Meta: model = product_models.ProductCategory fields = '__all__' diff --git a/apps/product/pos/api/v1/urls.py b/apps/product/pos/api/v1/urls.py index 69c3507..a73fec2 100644 --- a/apps/product/pos/api/v1/urls.py +++ b/apps/product/pos/api/v1/urls.py @@ -1,8 +1,11 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter +from .viewsets import product_api router = DefaultRouter() +router.register(r'product', product_api.ProductViewSet, basename='product') +router.register(r'pos_free_products', product_api.POSFreeProductsViewSet, basename='pos_free_products') urlpatterns = [ path('v1/', include(router.urls)) -] \ No newline at end of file +] diff --git a/apps/product/pos/api/v1/viewsets/product_api.py b/apps/product/pos/api/v1/viewsets/product_api.py index bae58ba..b3b4469 100644 --- a/apps/product/pos/api/v1/viewsets/product_api.py +++ b/apps/product/pos/api/v1/viewsets/product_api.py @@ -1,16 +1,15 @@ -import datetime from apps.product.pos.api.v1.serializers import product_serializers as product_serializers -from apps.product.pos.api.v1.serializers import quota_serializers -from common.helpers import get_organization_by_user +from apps.pos_device.mixins.pos_device_mixin import POSDeviceMixin +from apps.core.mixins.search_mixin import DynamicSearchMixin from rest_framework.exceptions import APIException from apps.product import models as product_models +from apps.pos_device import models as pos_models +from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.decorators import action -from rest_framework import viewsets, filters +from rest_framework import viewsets from rest_framework import status from django.db import transaction -from django.db.models import Q -from datetime import datetime def trash(queryset, pk): # noqa @@ -26,54 +25,15 @@ def delete(queryset, pk): obj.delete() -class ProductCategoryViewSet(viewsets.ModelViewSet): - queryset = product_models.ProductCategory.objects.all() - serializer_class = product_serializers.ProductCategorySerializer - filter_backends = [filters.SearchFilter] - search_fields = ['type', 'name'] - - @action( - methods=['put'], - detail=True, - url_path='trash', - url_name='trash', - name='trash', - ) - @transaction.atomic - def trash(self, request, pk=None): - """ Sent product to trash """ - try: - trash(self.queryset, pk) - except APIException as e: - return Response(e, status.HTTP_204_NO_CONTENT) - - @action( - methods=['post'], - detail=True, - url_name='delete', - url_path='delete', - name='delete' - ) - @transaction.atomic - def delete(self, request, pk=None): - """ Full delete of product object """ - try: - delete(self.queryset, pk) - return Response(status=status.HTTP_200_OK) - except APIException as e: - return Response(e, status=status.HTTP_204_NO_CONTENT) - - -class ProductViewSet(viewsets.ModelViewSet): - queryset = product_models.Product.objects.all() +class ProductViewSet(viewsets.ModelViewSet, DynamicSearchMixin, POSDeviceMixin): + queryset = product_models.Product.objects.all() # noqa serializer_class = product_serializers.ProductSerializer - filter_backends = [filters.SearchFilter] + permission_classes = [AllowAny] search_fields = ['type', 'name'] def list(self, request, *args, **kwargs): - """ custom list view """ # - queryset = self.filter_queryset(self.get_queryset().order_by('-create_date')) # noqa + queryset = self.filter_query(self.get_queryset().order_by('-create_date')) # noqa page = self.paginate_queryset(queryset) if page is not None: @@ -83,34 +43,6 @@ class ProductViewSet(viewsets.ModelViewSet): serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) - @action( - methods=['get'], - detail=True, - url_path='related_quotas', - url_name='related_quotas', - name='related_quotas' - ) - @transaction.atomic() - def my_related_quotas_by_product(self, request, pk=None): - """ quotas that related to my organization and product """ - - organization = get_organization_by_user(request.user) - quota = product_models.Quota.objects.filter( - Q( - distributions_assigned__in=product_models.QuotaDistribution.objects.filter( - Q(assigned_organization=organization) | - Q(assigner_organization=organization) - ) - ) | - Q(registerer_organization=organization), - product=self.get_object() - ).distinct() - - page = self.paginate_queryset(quota) - if page is not None: - serializer = quota_serializers.QuotaSerializer(page, many=True) - return self.get_paginated_response(serializer.data) - @action( methods=['put'], detail=True, @@ -124,7 +56,7 @@ class ProductViewSet(viewsets.ModelViewSet): try: trash(self.queryset, pk) except APIException as e: - return Response(e, status.HTTP_204_NO_CONTENT) + return Response(e, status.HTTP_204_NO_CONTENT) @action( methods=['post'], @@ -142,3 +74,38 @@ class ProductViewSet(viewsets.ModelViewSet): except APIException as e: return Response(e, status=status.HTTP_204_NO_CONTENT) + +class POSFreeProductsViewSet(viewsets.ModelViewSet, DynamicSearchMixin, POSDeviceMixin): + queryset = pos_models.POSFreeProducts.objects.all() + serializer_class = product_serializers.POSFreeProductSerializer + permission_classes = [AllowAny] + search_fields = ['product__name', 'organization__name', 'device__serial'] + + def list(self, request, *args, **kwargs): + + device = self.get_pos_device() + + queryset = self.filter_query( + self.get_queryset().filter(device=device).order_by('-create_date') + ) # noqa + + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + + @transaction.atomic + def create(self, request, *args, **kwargs): + """ create my free product from pos & set details """ + + organization = self.get_device_organization() + device = self.get_pos_device() + + request.data.update({'organization': organization.id, 'device': device.id}) + + serializer = product_serializers.POSFreeProductSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_403_FORBIDDEN) diff --git a/apps/product/urls.py b/apps/product/urls.py index a64041d..1a764e5 100644 --- a/apps/product/urls.py +++ b/apps/product/urls.py @@ -2,5 +2,6 @@ from django.urls import path, include urlpatterns = [ path('web/api/', include('apps.product.web.api.v1.urls')), + path('pos/api/', include('apps.product.pos.api.v1.urls')), path('excel/', include('apps.product.services.excel.urls')), ]