import - sync_livestock/herd_rancher_sync/purchase_policy & service_area in organization

This commit is contained in:
2025-12-30 11:54:09 +03:30
parent fe68b2db78
commit b94484eaaf
11 changed files with 982 additions and 29 deletions

View File

@@ -261,11 +261,15 @@ class OrganizationSerializer(serializers.ModelSerializer):
'company_code',
'field_of_activity',
'free_visibility_by_scope',
'service_area'
'service_area', # noqa
'purchase_policy'
]
extra_kwargs = {
'service_area': {
'service_area': { # noqa
'required': False
},
'purchase_policy': {
'purchase_policy': False
}
}
@@ -370,6 +374,7 @@ class OrganizationSerializer(serializers.ModelSerializer):
instance.address = validated_data.get('address', instance.address)
instance.parent_organization = validated_data.get('parent_organization', instance.parent_organization)
instance.national_unique_id = validated_data.get('national_unique_id', instance.national_unique_id)
instance.purchase_policy = validated_data.get('purchase_policy', instance.purchase_policy)
instance.free_visibility_by_scope = validated_data.get(
'free_visibility_by_scope',
instance.free_visibility_by_scope

View File

@@ -147,6 +147,16 @@ class Organization(BaseModel):
has_pos = models.BooleanField(default=False)
additional_data = models.JSONField(default=dict)
service_area = models.ManyToManyField(City, related_name='service_area')
PURCHASE_POLICIES = (
('INTERNAL_ONLY', 'Internal Only'),
('CROSS_COOP', 'Cross Cooperative Allowed'),
)
purchase_policy = models.CharField(
max_length=20,
choices=PURCHASE_POLICIES,
default='INTERNAL_ONLY',
help_text='defines where ranchers can purchase from'
)
def __str__(self):
return f'{self.name}-{self.type}'

View File

@@ -12,6 +12,11 @@ class DuplicateRancherException(APIException):
default_detail = "اطلاعات دامدار استعلام شده دارای مشکل میباشد با پشتیبانی تماس بگیرید" # noqa
class RancherOrganizationLinkException(APIException):
status_code = status.HTTP_403_FORBIDDEN
default_detail = "این دامدار دسترسی خرید از سازمان (تعاونی) مرتبط با این دستگاه را ندارد" # noqa
class HerdCapacityException(APIException):
status_code = status.HTTP_403_FORBIDDEN
default_detail = "مقدار حجم سبک و سنگین وارد شده از ظرفیت گله بیشتر میباشد" # noqa

View File

@@ -0,0 +1,39 @@
from multiprocessing import cpu_count, Pool
from django.core.management.base import BaseCommand
from django.db import connection
from apps.herd.models import Rancher
from .link_ranchers_parallel_worker import process_city
class Command(BaseCommand):
help = "Parallel link ranchers to cooperative by city"
def add_arguments(self, parser):
parser.add_argument(
'--worker',
type=int,
default=cpu_count() // 2
)
def handle(self, *args, **options):
workers = options['workers']
city_ids = (
Rancher.objects.filter(city__isnull=False)
.values_list('city_id', flat=True)
.distinct()
)
self.stdout.write(
f"Starting parallel sync for {len(city_ids)} cities "
f"with {workers} workers"
)
connection.close()
with Pool(processes=workers) as pool:
pool.map(process_city, city_ids)
self.stdout.write(self.style.SUCCESS("DONE ✅"))

View File

@@ -0,0 +1,55 @@
from django.db import connection, transaction
from apps.herd.models import Organization, Rancher, RancherOrganizationLink
BATCH_SIZE = 2000
def process_city(city_id):
connection.close()
orgs = Organization.objects.filter(
city_id=city_id,
type__key='CO'
)
if not orgs.exists() or orgs.count() > 1:
return
coop = orgs.first()
ranchers = (
Rancher.objects.filter(city_id=city_id)
.only('id')
)
buffer = []
for rancher in ranchers.iterator(chunk_size=BATCH_SIZE):
if RancherOrganizationLink.objects.filter(
rancher_id=rancher.id
).exists():
continue
buffer.append(
RancherOrganizationLink(
rancher_id=rancher.id,
organization_id=coop.id
)
)
if len(buffer) >= BATCH_SIZE:
bulk_insert(buffer)
buffer.clear()
if buffer:
bulk_insert(buffer)
@transaction.atomic
def bulk_insert(objs):
RancherOrganizationLink.objects.bulk_create(
objs,
ignore_conflicts=True
)

View File

@@ -11,6 +11,7 @@ from apps.herd.exception import DuplicateRancherException
from apps.herd.models import Herd, Rancher
from apps.herd.pos.api.v1.serializers import HerdSerializer, RancherSerializer
from apps.livestock.web.api.v1.serializers import LiveStockSerializer
from apps.pos_device.mixins.pos_device_mixin import POSDeviceMixin
from common.tools import CustomOperations
@@ -111,7 +112,7 @@ class HerdViewSet(viewsets.ModelViewSet):
return self.get_paginated_response(serializer.data)
class RancherViewSet(viewsets.ModelViewSet, DynamicSearchMixin):
class RancherViewSet(viewsets.ModelViewSet, DynamicSearchMixin, POSDeviceMixin):
queryset = Rancher.objects.all() # noqa
serializer_class = RancherSerializer
permission_classes = [AllowAny]
@@ -140,11 +141,17 @@ class RancherViewSet(viewsets.ModelViewSet, DynamicSearchMixin):
""" check national code & existence of rancher """
rancher = self.queryset.filter(national_code=request.data['national_code'])
org = self.get_device_organization()
if len(rancher) > 1:
raise DuplicateRancherException()
if rancher.exists():
# if not RancherOrganizationLink.objects.filter(organization=org, rancher=rancher).exists():
# if org.purchase_policy == 'INTERNAL_ONLY':
# raise RancherOrganizationLinkException()
serializer = self.serializer_class(rancher.first())
return Response(serializer.data, status=status.HTTP_200_OK)
else:

View File

@@ -12,17 +12,11 @@ class HerdRancherSyncService:
optimized bulk sync for large datasets
"""
# -------------------------
# Cache Cities
# -------------------------
city_map = {
name.strip(): id
for id, name in City.objects.all().values_list('id', 'name')
}
# -------------------------
# Cache existing ranchers
# -------------------------
rancher_map = {
r.national_code: r
for r in Rancher.objects.filter(
@@ -49,9 +43,6 @@ class HerdRancherSyncService:
for temp in queryset.iterator(chunk_size=batch_size):
# -------------------------
# Rancher
# -------------------------
rancher = rancher_map.get(temp.rancher_national_code)
if not rancher:
@@ -66,9 +57,6 @@ class HerdRancherSyncService:
new_ranchers.append(rancher)
rancher_map[temp.rancher_national_code] = rancher
# -------------------------
# Herd
# -------------------------
herd_key = (temp.rancher_national_code, temp.herd_code)
if herd_key in existing_herds:
@@ -95,9 +83,6 @@ class HerdRancherSyncService:
}
)
# -------------------------
# Bulk DB Operations
# -------------------------
with transaction.atomic():
Rancher.objects.bulk_create(
new_ranchers,
@@ -119,7 +104,7 @@ class HerdRancherSyncService:
rancher = rancher_map.get(item["rancher_code"])
if not rancher:
continue # یا raise error
continue
herd = item["herd"]
herd.rancher = rancher

View File

@@ -4,7 +4,7 @@ from datetime import timedelta
from django.db import transaction
from django.utils.timezone import now
from rest_framework import status
from rest_framework import status, filters
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import APIException
@@ -70,7 +70,7 @@ class ProviderCompanyViewSet(SoftDeleteMixin, viewsets.ModelViewSet): # noqa
class DeviceViewSet(BaseViewSet, SoftDeleteMixin, viewsets.ModelViewSet, AdminFilterMixin):
queryset = pos_models.Device.objects.all()
serializer_class = device_serializer.DeviceSerializer
# filter_backends = [filters.SearchFilter]
filter_backends = [filters.SearchFilter]
search_fields = [
'device_identity',
'acceptor',
@@ -176,7 +176,7 @@ class DeviceViewSet(BaseViewSet, SoftDeleteMixin, viewsets.ModelViewSet, AdminFi
def devices_by_psp(self, request, pk=None):
""" list of devices by their psp """
devices = self.queryset.filter(organization__id=pk).order_by('-create_date')
devices = self.filter_queryset(self.queryset.filter(organization__id=pk).order_by('-create_date'))
# paginate devices
page = self.paginate_queryset(devices)

View File

@@ -31,7 +31,7 @@ class Command(BaseCommand):
processed = 0
start_time = time.time()
LOG_EVERY = 10_000 # هر چند رکورد لاگ بده
LOG_EVERY = 10000
buffer = []
for temp in qs.iterator(chunk_size=CHUNK_SIZE):
@@ -63,8 +63,6 @@ class Command(BaseCommand):
self.stdout.write(self.style.SUCCESS("DONE ✅"))
# ----------------------------------------------------
def process_batch(self, temps):
herd_codes = {t.herd_code for t in temps if t.herd_code}
@@ -103,7 +101,7 @@ class Command(BaseCommand):
for temp in temps:
herd = herds.get(temp.herd_code)
if not herd:
continue # گله باید وجود داشته باشد
continue
birthdate = parse_birthdate(temp.birthdate)
gender = 1 if temp.gender == 'M' else 2
@@ -113,7 +111,6 @@ class Command(BaseCommand):
key = (temp.herd_code, birthdate, gender)
livestock = livestock_map.get(key)
# ---------- دام وجود ندارد ----------
if not livestock:
if not temp.tag:
continue
@@ -139,7 +136,6 @@ class Command(BaseCommand):
temp.sync_status = 'S'
continue
# ---------- دام وجود دارد ولی پلاک ندارد ----------
if livestock.tag is None and temp.tag:
tag = existing_tags.get(temp.tag)
@@ -153,7 +149,6 @@ class Command(BaseCommand):
temp.sync_status = 'S'
# ---------- BULK ----------
with transaction.atomic():
Tag.objects.bulk_create(new_tags, batch_size=BATCH_SIZE)
LiveStock.objects.bulk_create(