From 20e4bfad75803e0e35afe6a5a32812d6a4663290 Mon Sep 17 00:00:00 2001 From: Mojtaba-z Date: Wed, 20 Aug 2025 14:44:43 +0330 Subject: [PATCH] some apis of pos --- apps/herd/pos/api/v1/api.py | 54 ++++++----- apps/herd/services/services.py | 74 +++++++++++++++ apps/pos_device/mixins/pos_device_mixin.py | 2 +- apps/pos_device/pos/api/v1/viewsets/device.py | 9 +- ...uotasaletransaction_pos_device_and_more.py | 26 ++++++ apps/warehouse/models.py | 14 +++ apps/warehouse/pos/api/v1/api.py | 55 +++++++++++ apps/warehouse/pos/api/v1/serializers.py | 93 +++++++++++++++++++ apps/warehouse/pos/api/v1/urls.py | 11 +++ apps/warehouse/urls.py | 1 + apps/warehouse/web/api/v1/api.py | 5 +- apps/warehouse/web/api/v1/serializers.py | 1 + 12 files changed, 316 insertions(+), 29 deletions(-) create mode 100644 apps/herd/services/services.py create mode 100644 apps/warehouse/migrations/0013_inventoryquotasaletransaction_pos_device_and_more.py create mode 100644 apps/warehouse/pos/api/v1/api.py diff --git a/apps/herd/pos/api/v1/api.py b/apps/herd/pos/api/v1/api.py index 06fdb35..82388f8 100644 --- a/apps/herd/pos/api/v1/api.py +++ b/apps/herd/pos/api/v1/api.py @@ -1,6 +1,11 @@ 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 apps.herd.services.services import ( + get_rancher_statistics, + rancher_quota_weight +) +from apps.warehouse.models import InventoryEntry +from apps.core.mixins.search_mixin import DynamicSearchMixin from rest_framework.exceptions import APIException from rest_framework.permissions import AllowAny from rest_framework.response import Response @@ -110,8 +115,9 @@ class HerdViewSet(viewsets.ModelViewSet): class RancherViewSet(viewsets.ModelViewSet, DynamicSearchMixin): - queryset = Rancher.objects.all() + queryset = Rancher.objects.all() # noqa serializer_class = RancherSerializer + permission_classes = [AllowAny] search_fields = [ "ranching_farm", "first_name", @@ -125,30 +131,28 @@ class RancherViewSet(viewsets.ModelViewSet, DynamicSearchMixin): "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' + methods=['post'], + detail=False, + url_name='check_national_code', + url_path='check_national_code', + name='check_national_code' ) - def herds_by_rancher(self, request, pk=None): - """ list of rancher herds """ + @transaction.atomic + def check_national_code(self, request): + """ check national code & existence of rancher """ - rancher = self.get_object() - queryset = rancher.herd.all().order_by('-modify_date') # get rancher herds + rancher = self.queryset.filter(national_code=request.data['national_code']) + inventory = InventoryEntry.objects.get(id=43) - # paginate queryset - page = self.paginate_queryset(queryset) - if page is not None: - serializer = HerdSerializer(page, many=True) - return self.get_paginated_response(serializer.data) + # get rancher live stocks information ant total quota for rancher + rancher_quota_by_live_stock = rancher_quota_weight(rancher.first(), inventory) + + if rancher.exists(): + serializer = self.serializer_class(rancher.first()) + + return Response(serializer.data, status=status.HTTP_200_OK) + else: + return Response({ + "message": "rancher has not existence" + }, status=status.HTTP_204_NO_CONTENT) diff --git a/apps/herd/services/services.py b/apps/herd/services/services.py new file mode 100644 index 0000000..59cccc8 --- /dev/null +++ b/apps/herd/services/services.py @@ -0,0 +1,74 @@ +from decimal import Decimal +from apps.herd.models import Rancher +from apps.livestock.models import LiveStock +from apps.warehouse.models import InventoryEntry +from apps.product.models import Quota +import typing + + +def get_rancher_statistics(rancher: Rancher = None) -> typing.Any: + """ get statistics of a rancher """ # noqa + + herds = rancher.herd.all() # noqa + herd_count = herds.count() + + livestocks = LiveStock.objects.filter(herd__in=herds) # noqa + + light_count = livestocks.filter(weight_type='L').count() + heavy_count = livestocks.filter(weight_type='H').count() + + sheep_count = livestocks.filter(type__name="گوسفند").count() # noqa + goat_count = livestocks.filter(type__name="بز").count() + cow_count = livestocks.filter(type__name="گاو").count() + camel_count = livestocks.filter(type__name="شتر").count() + horse_count = livestocks.filter(type__name="اسب").count() + + return { + "herd_count": herd_count, + "light_count": light_count, + "heavy_count": heavy_count, + "sheep_count": sheep_count, + "goat_count": goat_count, + "cow_count": cow_count, + "camel_count": camel_count, + "horse_count": horse_count, + } + + +def rancher_quota_weight(rancher, inventory_entry: InventoryEntry): + """ + :param rancher: Rancher instance + :param inventory_entry: InventoryEntry instance + :return: dict {total, by_type} + """ + + live_stock_meta = { + "گوسفند": "sheep_count", # noqa + "بز": "goat_count", + "گاو": "cow_count", + "شتر": "camel_count", + "اسب": "horse_count" + } + + quota: Quota = inventory_entry.distribution.quota + allocations = quota.livestock_allocations.all() + + livestock_counts = get_rancher_statistics(rancher) + + total_weight = Decimal(0) + details = {} + + for alloc in allocations: + animal_type = alloc.livestock_type.name + per_head = Decimal(alloc.quantity_kg) + count = livestock_counts.get(live_stock_meta.get(animal_type), 0) + + weight = per_head * Decimal(count) + print(weight) + details[animal_type] = weight + total_weight += weight + + return { + "total": total_weight, + "by_type": details + } diff --git a/apps/pos_device/mixins/pos_device_mixin.py b/apps/pos_device/mixins/pos_device_mixin.py index 4fe60bc..cff539a 100644 --- a/apps/pos_device/mixins/pos_device_mixin.py +++ b/apps/pos_device/mixins/pos_device_mixin.py @@ -19,7 +19,7 @@ class POSDeviceMixin: organization = pos_models.DeviceAssignment.objects.filter( device__serial=self.request.headers.get('device-serial') # noqa - ).first().organization + ).first().client.organization return organization diff --git a/apps/pos_device/pos/api/v1/viewsets/device.py b/apps/pos_device/pos/api/v1/viewsets/device.py index d8f9b9f..3b37432 100644 --- a/apps/pos_device/pos/api/v1/viewsets/device.py +++ b/apps/pos_device/pos/api/v1/viewsets/device.py @@ -1,4 +1,5 @@ from apps.pos_device.pos.api.v1.serializers.device import DeviceSerializer +from apps.pos_device.mixins.pos_device_mixin import POSDeviceMixin from apps.pos_device import models as pos_models from rest_framework.permissions import AllowAny from rest_framework.decorators import action @@ -9,7 +10,7 @@ from django.db import transaction from rest_framework import status -class POSDeviceViewSet(viewsets.ModelViewSet): +class POSDeviceViewSet(viewsets.ModelViewSet, POSDeviceMixin): device_queryset = pos_models.Device.objects.all() session_queryset = pos_models.Sessions.objects.all() serializer_class = DeviceSerializer @@ -32,6 +33,9 @@ class POSDeviceViewSet(viewsets.ModelViewSet): def login(self, request): """ login of pos device """ + # get device owner (organization) + organization = self.get_device_organization() + # convert headers to dictionary headers_data = {key: request.headers.get(key) for key in self.HEADERS} @@ -66,7 +70,8 @@ class POSDeviceViewSet(viewsets.ModelViewSet): return Response({ "message": "login success - session activated", "device_identity": device.device_identity, - "serial": device.serial + "serial": device.serial, + "device_owner": organization.name }, status=status.HTTP_200_OK) return Response({ diff --git a/apps/warehouse/migrations/0013_inventoryquotasaletransaction_pos_device_and_more.py b/apps/warehouse/migrations/0013_inventoryquotasaletransaction_pos_device_and_more.py new file mode 100644 index 0000000..fd71e82 --- /dev/null +++ b/apps/warehouse/migrations/0013_inventoryquotasaletransaction_pos_device_and_more.py @@ -0,0 +1,26 @@ +# Generated by Django 5.0 on 2025-08-20 11:14 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('herd', '0016_rancher_activity_rancher_heavy_livestock_number_and_more'), + ('pos_device', '0061_posfreeproducts'), + ('warehouse', '0012_inventoryentry_balance'), + ] + + operations = [ + migrations.AddField( + model_name='inventoryquotasaletransaction', + name='pos_device', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='transactions', to='pos_device.device'), + ), + migrations.AddField( + model_name='inventoryquotasaletransaction', + name='rancher', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='transactions', to='herd.rancher'), + ), + ] diff --git a/apps/warehouse/models.py b/apps/warehouse/models.py index 1f6a568..26cfa78 100644 --- a/apps/warehouse/models.py +++ b/apps/warehouse/models.py @@ -1,5 +1,7 @@ from apps.product import models as product_models from apps.authentication.models import User +from apps.pos_device.models import Device +from apps.herd.models import Rancher from apps.core.models import BaseModel from django.db import models @@ -41,6 +43,18 @@ class InventoryEntry(BaseModel): class InventoryQuotaSaleTransaction(BaseModel): + rancher = models.ForeignKey( + Rancher, + on_delete=models.CASCADE, + related_name='transactions', + null=True + ) + pos_device = models.ForeignKey( + Device, + on_delete=models.CASCADE, + related_name='transactions', + null=True + ) transaction_id = models.CharField(max_length=50, null=True) seller_organization = models.ForeignKey( product_models.Organization, diff --git a/apps/warehouse/pos/api/v1/api.py b/apps/warehouse/pos/api/v1/api.py new file mode 100644 index 0000000..a49a938 --- /dev/null +++ b/apps/warehouse/pos/api/v1/api.py @@ -0,0 +1,55 @@ +from apps.warehouse.pos.api.v1 import serializers as warehouse_serializers +from apps.pos_device.mixins.pos_device_mixin import POSDeviceMixin +from apps.core.mixins.search_mixin import DynamicSearchMixin +from apps.warehouse import models as warehouse_models +from common.helpers import get_organization_by_user +from rest_framework.permissions import AllowAny +from rest_framework.decorators import action +from rest_framework.response import Response +from rest_framework import viewsets, filters +from django.db import transaction +from rest_framework import status +import typing + + +class InventoryEntryViewSet(viewsets.ModelViewSet, DynamicSearchMixin, POSDeviceMixin): + queryset = warehouse_models.InventoryEntry.objects.all() + serializer_class = warehouse_serializers.InventoryEntrySerializer + permission_classes = [AllowAny] + search_fields = [ + "distribution__distribution_id", + "organization__name", + "weight", + "balance", + "lading_number", + "is_confirmed", + ] + date_field = "create_date" + + @action( + methods=['get'], + detail=False, + url_path='my_entries', + url_name='my_entries', + name='my_entries' + ) + def inventory_entries(self, request): + """ list of pos inventory entries """ + + organization = self.get_device_organization() + + entries = self.queryset.filter(organization=organization) + queryset = self.filter_query(entries) # return by search param or all objects + + # paginate & response + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + + +class InventoryQuotaSaleTransactionViewSet(viewsets.ModelViewSet): + queryset = warehouse_models.InventoryQuotaSaleTransaction.objects.all() + serializer_class = warehouse_serializers.InventoryQuotaSaleTransactionSerializer + filter_backends = [filters.SearchFilter] + search_fields = [''] diff --git a/apps/warehouse/pos/api/v1/serializers.py b/apps/warehouse/pos/api/v1/serializers.py index e69de29..ae5c579 100644 --- a/apps/warehouse/pos/api/v1/serializers.py +++ b/apps/warehouse/pos/api/v1/serializers.py @@ -0,0 +1,93 @@ +from apps.warehouse.exceptions import ( + InventoryEntryWeightException, + TotalInventorySaleException +) +from apps.product.exceptions import QuotaExpiredTimeException +from apps.warehouse import models as warehouse_models +from apps.authorization.models import UserRelations +from rest_framework import serializers +from django.db import models + + +class InventoryEntrySerializer(serializers.ModelSerializer): + class Meta: + model = warehouse_models.InventoryEntry + fields = [ + "id", + "create_date", + "modify_date", + "organization", + "distribution", + "weight", + "balance", + "lading_number", + "delivery_address", + "is_confirmed", + "notes", + ] + + def to_representation(self, instance): + """ custom output of inventory entry serializer """ + + representation = super().to_representation(instance) + if instance.document: + representation['document'] = instance.document + if instance.distribution: + # distribution data + representation['distribution'] = { + 'distribution_identity': instance.distribution.distribution_id, + 'sale_unit': instance.distribution.quota.sale_unit.unit, + 'id': instance.distribution.id + } + representation['quota'] = { + 'quota_identity': instance.distribution.quota.quota_id, + 'quota_weight': instance.distribution.quota.quota_weight, + } + representation['product'] = { + 'name': instance.distribution.quota.product.name, + 'id': instance.distribution.quota.product.id, + } + + return representation + + +class InventoryQuotaSaleTransactionSerializer(serializers.ModelSerializer): + class Meta: + model = warehouse_models.InventoryQuotaSaleTransaction + fields = '__all__' + depth = 0 + + def validate(self, attrs): + """ + validate total inventory sale should be fewer than + inventory entry from distribution + """ + inventory_entry = attrs['inventory_entry'] + distribution = attrs['quota_distribution'] + + total_sale_weight = inventory_entry.inventory_sales.aggregate( + total=models.Sum('weight') + )['total'] or 0 + + if total_sale_weight + attrs['weight'] > distribution.warehouse_balance: + raise TotalInventorySaleException() + + return attrs + + def create(self, validated_data): + """ Custom create & set some parameters like seller & buyer """ + + distribution = validated_data['quota_distribution'] + seller_organization = distribution.assigned_organization + + user = self.context['request'].user + buyer_user = user + seller_user = validated_data['inventory_entry'].created_by + + return warehouse_models.InventoryQuotaSaleTransaction.objects.create( + seller_organization=seller_organization, + seller_user=seller_user, + buyer_user=buyer_user, + **validated_data + ) + diff --git a/apps/warehouse/pos/api/v1/urls.py b/apps/warehouse/pos/api/v1/urls.py index e69de29..ee798de 100644 --- a/apps/warehouse/pos/api/v1/urls.py +++ b/apps/warehouse/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'inventory_entry', api.InventoryEntryViewSet, basename='inventory_entry') + +urlpatterns = [ + path('v1/', include(router.urls)) +] diff --git a/apps/warehouse/urls.py b/apps/warehouse/urls.py index b3c94c9..455a79a 100644 --- a/apps/warehouse/urls.py +++ b/apps/warehouse/urls.py @@ -4,6 +4,7 @@ from django.urls import path, include urlpatterns = [ path('web/api/', include('apps.warehouse.web.api.v1.urls')), + path('pos/api/', include('apps.warehouse.pos.api.v1.urls')), path('excel/', include('apps.warehouse.services.excel.urls')), ] diff --git a/apps/warehouse/web/api/v1/api.py b/apps/warehouse/web/api/v1/api.py index 78d9677..e65804f 100644 --- a/apps/warehouse/web/api/v1/api.py +++ b/apps/warehouse/web/api/v1/api.py @@ -53,8 +53,11 @@ class InventoryEntryViewSet(viewsets.ModelViewSet, DynamicSearchMixin): """ custom create of inventory entry """ # create inventory entry + inventory_balance = request.data['weight'] + request.data.update({ - 'organization': (get_organization_by_user(request.user)).id + 'organization': (get_organization_by_user(request.user)).id, + 'balance': inventory_balance }) serializer = self.serializer_class(data=request.data) if serializer.is_valid(): diff --git a/apps/warehouse/web/api/v1/serializers.py b/apps/warehouse/web/api/v1/serializers.py index 655ddb0..d6907b5 100644 --- a/apps/warehouse/web/api/v1/serializers.py +++ b/apps/warehouse/web/api/v1/serializers.py @@ -19,6 +19,7 @@ class InventoryEntrySerializer(serializers.ModelSerializer): "organization", "distribution", "weight", + "balance", "lading_number", "delivery_address", "is_confirmed",