add new fields to rancher

This commit is contained in:
2025-08-19 09:03:41 +03:30
parent b64c28b6d1
commit 14cd349a7d
15 changed files with 459 additions and 82 deletions

View File

@@ -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),
),
]

View File

@@ -82,6 +82,30 @@ class Herd(BaseModel):
class Rancher(BaseModel): class Rancher(BaseModel):
ranching_farm = models.CharField(max_length=150, null=True) 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) herd_code = models.CharField(max_length=100, null=True)
first_name = models.CharField(max_length=150, null=True) first_name = models.CharField(max_length=150, null=True)
last_name = models.CharField(max_length=150, null=True) last_name = models.CharField(max_length=150, null=True)

154
apps/herd/pos/api/v1/api.py Normal file
View File

@@ -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)

View File

@@ -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

View File

@@ -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))
]

View File

@@ -5,5 +5,6 @@ from django.urls import path, include
urlpatterns = [ urlpatterns = [
path('web/', include('apps.herd.web.api.v1.urls')), 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')), path('excel/', include('apps.herd.services.excel.urls')),
] ]

View File

@@ -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,
},
),
]

View File

View File

@@ -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

View File

@@ -1,8 +1,8 @@
import datetime
import random import random
import string import string
from apps.authentication.models import Organization from apps.authentication.models import Organization
from apps.product.models import Product
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
from apps.authorization.models import UserRelations from apps.authorization.models import UserRelations
from apps.core.models import BaseModel from apps.core.models import BaseModel
@@ -55,7 +55,7 @@ class Device(BaseModel):
def generate_device_identity(self): # noqa def generate_device_identity(self): # noqa
""" generate identity for every device """ """ generate identity for every device """
prefix = "POS" # prefix = "POS"
while True: while True:
number_part = ''.join(random.choices(string.digits, k=9)) number_part = ''.join(random.choices(string.digits, k=9))
code = f"{number_part}" code = f"{number_part}"
@@ -264,3 +264,32 @@ class StakeHolders(BaseModel):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
return super(StakeHolders, self).save(*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)

View File

@@ -68,6 +68,7 @@ class POSDeviceViewSet(viewsets.ModelViewSet):
"device_identity": device.device_identity, "device_identity": device.device_identity,
"serial": device.serial "serial": device.serial
}, status=status.HTTP_200_OK) }, status=status.HTTP_200_OK)
return Response({ return Response({
"message": "device pre registered - unauthorized", "message": "device pre registered - unauthorized",
"device_identity": device.device_identity, "device_identity": device.device_identity,

View File

@@ -1,10 +1,27 @@
from rest_framework import serializers from rest_framework import serializers
from apps.product import models as product_models 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.authorization.api.v1 import serializers as authorize_serializers
from apps.authentication.api.v1.serializers.serializer import OrganizationSerializer, OrganizationTypeSerializer 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: class Meta:
model = product_models.ProductCategory model = product_models.ProductCategory
fields = '__all__' fields = '__all__'

View File

@@ -1,8 +1,11 @@
from django.urls import path, include from django.urls import path, include
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from .viewsets import product_api
router = DefaultRouter() 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 = [ urlpatterns = [
path('v1/', include(router.urls)) path('v1/', include(router.urls))
] ]

View File

@@ -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 product_serializers as product_serializers
from apps.product.pos.api.v1.serializers import quota_serializers from apps.pos_device.mixins.pos_device_mixin import POSDeviceMixin
from common.helpers import get_organization_by_user from apps.core.mixins.search_mixin import DynamicSearchMixin
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
from apps.product import models as product_models 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.response import Response
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework import viewsets, filters from rest_framework import viewsets
from rest_framework import status from rest_framework import status
from django.db import transaction from django.db import transaction
from django.db.models import Q
from datetime import datetime
def trash(queryset, pk): # noqa def trash(queryset, pk): # noqa
@@ -26,54 +25,15 @@ def delete(queryset, pk):
obj.delete() obj.delete()
class ProductCategoryViewSet(viewsets.ModelViewSet): class ProductViewSet(viewsets.ModelViewSet, DynamicSearchMixin, POSDeviceMixin):
queryset = product_models.ProductCategory.objects.all() queryset = product_models.Product.objects.all() # noqa
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()
serializer_class = product_serializers.ProductSerializer serializer_class = product_serializers.ProductSerializer
filter_backends = [filters.SearchFilter] permission_classes = [AllowAny]
search_fields = ['type', 'name'] search_fields = ['type', 'name']
def list(self, request, *args, **kwargs): 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) page = self.paginate_queryset(queryset)
if page is not None: if page is not None:
@@ -83,34 +43,6 @@ class ProductViewSet(viewsets.ModelViewSet):
serializer = self.get_serializer(queryset, many=True) serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data) 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( @action(
methods=['put'], methods=['put'],
detail=True, detail=True,
@@ -124,7 +56,7 @@ class ProductViewSet(viewsets.ModelViewSet):
try: try:
trash(self.queryset, pk) trash(self.queryset, pk)
except APIException as e: except APIException as e:
return Response(e, status.HTTP_204_NO_CONTENT) return Response(e, status.HTTP_204_NO_CONTENT)
@action( @action(
methods=['post'], methods=['post'],
@@ -142,3 +74,38 @@ class ProductViewSet(viewsets.ModelViewSet):
except APIException as e: except APIException as e:
return Response(e, status=status.HTTP_204_NO_CONTENT) 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)

View File

@@ -2,5 +2,6 @@ from django.urls import path, include
urlpatterns = [ urlpatterns = [
path('web/api/', include('apps.product.web.api.v1.urls')), 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')), path('excel/', include('apps.product.services.excel.urls')),
] ]