Compare commits

..

85 Commits

Author SHA1 Message Date
858af1231b fix --> livestock type none on sync_livestock 2026-02-10 12:53:56 +03:30
2c87642f56 fix --> remove print qs on last lines in sync_livestocks mangement 2026-02-10 12:36:51 +03:30
1e773ef53d fix --> sync livestocks with table ExcelLivestocks 2026-02-10 12:30:41 +03:30
67fa1e23e7 import mrkazi livesock excel tablr 2026-02-09 14:46:06 +03:30
c02c165ff9 import mrkazi livesock excel tablr 2026-02-09 14:45:45 +03:30
f13851a30d fix --> org unique unit id edit 2026-02-09 11:26:23 +03:30
aa8e39c25b fix --> int type id on org filter 2026-02-09 11:00:01 +03:30
66554159ea import --> unique_unit_identity 2026-02-09 10:42:35 +03:30
13caa78087 fix --> bug of download uploadded file 2026-02-09 10:03:20 +03:30
6c291e2294 fix --> bug of download uploadded file 2026-02-09 09:48:56 +03:30
7618518dc7 fix --> bug of download uploadded file 2026-02-09 09:43:28 +03:30
8bea18e676 fix --> org addresses org.id bug 2026-02-09 09:02:47 +03:30
3b6deb0356 fix --> org addresses org.id bug 2026-02-09 08:58:06 +03:30
8449e2ef1b fix --> edit addresses org bug 2026-02-08 16:33:40 +03:30
627168b82a fix --> assign document 2026-02-08 15:01:45 +03:30
8296214401 import --> org multiple locations on create/edit/list & serialzier 2026-02-08 14:09:03 +03:30
84ea3b35d2 fix --> remove libraries gtk fromdockerfile 2026-02-08 12:39:22 +03:30
dbd0715c54 fix --> import libraries gtk fromdockerfile 2026-02-08 12:32:14 +03:30
241a3551b7 fix --> remove libraries gtk fromdockerfile 2026-02-08 11:54:13 +03:30
e00bd72a58 fix --> error of weasy priny 2026-02-08 11:52:54 +03:30
77f35fe350 fix --> docker file changing libraries for gtk weasyprint 2026-02-08 11:39:29 +03:30
bec3405454 import --> download & upload warehouise exit doc and that status 2026-02-08 11:19:36 +03:30
ce9e45667f fix --> otp check on tag distribution 2026-02-07 16:42:08 +03:30
3209dd2d31 import --> tag distribution child list 2026-02-07 12:50:47 +03:30
0a84ca6fe4 fix --> show tag distribution batches on diffrent situations for orgs 2026-02-07 12:11:08 +03:30
cc81bc8a3d import --> new fields in tag distribution batch 2026-02-07 10:51:33 +03:30
3258991014 fix --> soft delete distribution batch 2026-02-03 11:18:34 +03:30
fb58e6c3aa edit tag distribute from distribution 2026-02-03 08:46:38 +03:30
c5b87e8591 fix --> batch identity bug in create distribution from ditribute 2026-02-02 12:09:31 +03:30
0dd145131f fix --> bug of calculate remaining tags of distribution parent batch 2026-02-02 11:24:37 +03:30
74a870380e fix --> bug of transaction dashboard on free visibility scope 2026-02-02 10:40:54 +03:30
f798b72dbc fix --> bug of transaction dashboard on free visibility scope 2026-02-02 10:36:35 +03:30
93180edc0b revert development transactions dashboard commit 2026-02-02 10:31:33 +03:30
0b08107c14 fix --> bug of transactions dashboard with free visibility scope on queryset 2026-02-02 10:25:05 +03:30
56025d77b1 import --> distribute from distribution and calculate some values in model 2026-02-01 16:56:33 +03:30
e68485c4cc fix --> batch statistics 2026-02-01 11:55:18 +03:30
d643237a77 fix --> siganl of distribute to change some values in batch 2026-02-01 09:16:40 +03:30
8c9f7aca02 import --> distribute from distribution 2026-02-01 09:03:17 +03:30
9ed2a099e7 import --> detail of tag distribution batch 2026-01-28 11:59:10 +03:30
879e004e9b fix --> tag batch main dashboard 2026-01-28 11:34:27 +03:30
10c6eb9e79 fix --> remaining tags of tag batches 2026-01-27 16:00:04 +03:30
36df84da98 fix --> bug of calculated distribution of tag batch 2026-01-27 15:26:25 +03:30
915b0bf5a1 distribution of tags & tag batches 2026-01-27 14:52:50 +03:30
da15cb5b99 import --> filter tag batches by species code 2026-01-26 11:24:11 +03:30
e94d5e4d1b import --> tag batch inner dashboard 2026-01-26 11:03:44 +03:30
eaba79ee91 device validation by serial - DeviceException 2026-01-26 10:27:19 +03:30
e218c550e4 fix-->import distinct to tag batch count in dashboard 2026-01-26 09:56:39 +03:30
c173a1cd85 fix --> tags by batch 2026-01-25 11:41:37 +03:30
08468fe67c import --> tag batch main page dashboard 2026-01-25 11:06:02 +03:30
dd807f04be fix --> structure of species data in main dist batch dashboard 2026-01-25 09:59:37 +03:30
c0e62541c3 fix --> tag distribution dashboard by species 2026-01-24 16:35:54 +03:30
b93d9148f8 fix --> import fields in livestock specis 2026-01-24 15:15:00 +03:30
6d55f1cd3b fix --> livestock species 2026-01-24 15:03:04 +03:30
c3511590b7 fix --> recieved & sent dist tags 2026-01-24 14:09:07 +03:30
b4997da6b7 import --> total recieved tag count on main dashboerd 2026-01-24 14:02:10 +03:30
71726591e3 fix --> admin filter by is closed in dist batch dasjboard 2026-01-24 12:56:43 +03:30
afdb201b78 import --> parent to distribution batches 2026-01-24 12:44:26 +03:30
10a3572a8f import --> is closed distribution batches dashboard 2026-01-24 12:41:00 +03:30
bf4b3d4422 test 2026-01-24 12:11:38 +03:30
6311a23609 import --> activate canceled tag distribution batches 2026-01-24 12:01:08 +03:30
757ad85688 import --> tag distribution / tag disstribution batch / remove distribution / cancel distributions 2026-01-24 10:09:54 +03:30
f2aab5c6b6 fix --> batch_identity Nonetype in distribution batch 2026-01-21 14:52:41 +03:30
e44121b10b fix --> add batch_identity in distribution batch 2026-01-21 14:45:20 +03:30
0062d0c375 fix --> filter tag distributions/batch by is_closed/ edit tag distributiosn & batch 2026-01-21 14:18:29 +03:30
40e0daeb60 fix --> import serial to distributions in dist batch 2026-01-21 11:10:46 +03:30
4cabee3502 import --> add distribution detail in dist batch 2026-01-21 10:31:35 +03:30
d0cbe435e3 import --> add distribution detail in dist batch 2026-01-21 10:17:59 +03:30
853dc70396 import --> add distribution detail in dist batch 2026-01-21 10:12:18 +03:30
6e4fc46271 import --> add distribution type to distribution batch 2026-01-21 10:03:05 +03:30
eef768e360 fix --> non batch in tag distribution 2026-01-21 09:40:25 +03:30
975132d21e change dockeerfile 2026-01-21 09:11:48 +03:30
81c272766e import --> create tag distribution with/without batch 2026-01-20 15:06:40 +03:30
54047e625b import --> first steps of tag distribution 2026-01-19 14:04:41 +03:30
057943b37f fix --> development dockerFi;e 2026-01-19 11:49:42 +03:30
5a4fe87561 import & fix --> tagBatch / tagDistribution 2026-01-19 11:31:06 +03:30
0ffc474279 push dam project on mnpc new git repo 2026-01-18 13:16:17 +03:30
ebe746ae98 fix - scanned serial on 7 digits plus 0 2026-01-07 15:52:09 +03:30
c47c8054e4 import - add org ownership_code on profile 2026-01-07 15:39:53 +03:30
8fb3bf5dc9 fix - tag code generation in tags 2026-01-07 15:36:22 +03:30
1bf107d81d import - add tag/tag_batch --> edit tag_batch --> list of tag_batch 2026-01-07 14:21:27 +03:30
89c794bf81 import - tag batch / batch items 2026-01-06 16:55:33 +03:30
d1de26e002 fix - live stock model f string bug 2026-01-06 15:27:39 +03:30
87dc1f6eec import - pelak kobi system --> list, detail, create,.... 2026-01-06 15:15:10 +03:30
78adbfd3ce fix - some improves on new excel transactions 2026-01-04 16:35:16 +03:30
3062a0167d fix - some improves on new excel transactions 2026-01-04 16:21:44 +03:30
65 changed files with 2918 additions and 227 deletions

View File

@@ -7,7 +7,7 @@ ENV_NAME=DEV
# Database secrets # Database secrets
DB_HOST=31.7.78.133 DB_HOST=31.7.78.133
DB_PORT=14352 DB_PORT=14352
DB_NAME=Production DB_NAME=Development
DB_USERNAME=postgres DB_USERNAME=postgres
DB_PASSWORD=pfLIVXupbDetvFMt2gUvxLXUL9b4HIOHaPcKXsBEZ1i8zl0iLUjmhUfXlGfJKcTV DB_PASSWORD=pfLIVXupbDetvFMt2gUvxLXUL9b4HIOHaPcKXsBEZ1i8zl0iLUjmhUfXlGfJKcTV

View File

@@ -1,5 +1,5 @@
# pull official base image # pull official base image
FROM ghcr.io/seniorkian/python310-rasaddam:1.0.1 FROM registry.hamdocker.ir/seniorkian/python310-rasaddam:1.0.0
# Create the app directory # Create the app directory
RUN #mkdir /app RUN #mkdir /app
@@ -12,10 +12,22 @@ ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1 ENV PYTHONUNBUFFERED 1
# install dependencies # install dependencies
ENV TZ="Asia/Tehran"
RUN pip config --user set global.index https://mirror-pypi.runflare.com/simple
RUN pip config --user set global.index-url https://mirror-pypi.runflare.com/simple
RUN pip config --user set global.trusted-host mirror-pypi.runflare.com
RUN pip install --upgrade pip RUN pip install --upgrade pip
#RUN apt-get update && apt-get install -y \
# libcairo2 \
# libpango-1.0-0 \
# libpangocairo-1.0-0 \
# libgdk-pixbuf2.0-0 \
# libffi-dev \
# shared-mime-info \
# fonts-dejavu \
# && rm -rf /var/lib/apt/lists/*
COPY ./requirements.txt . COPY ./requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \ RUN pip install --no-cache-dir -r requirements.txt
pip install --no-cache-dir -r requirements.txt
# copy project # copy project
COPY . /app/ COPY . /app/

View File

@@ -30,7 +30,7 @@ from apps.authentication.models import (
Organization, Organization,
OrganizationType, OrganizationType,
BankAccountInformation, BankAccountInformation,
BlacklistedAccessToken BlacklistedAccessToken, OrganizationLocationInfo
) )
from apps.authentication.tools import get_token_jti from apps.authentication.tools import get_token_jti
from apps.authorization.api.v1 import api as authorize_view from apps.authorization.api.v1 import api as authorize_view
@@ -261,14 +261,21 @@ class OrganizationViewSet(BaseViewSet, ModelViewSet, DynamicSearchMixin):
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
""" all organization """ """ all organization """
org = get_organization_by_user(request.user) org = get_organization_by_user(request.user)
param = self.request.query_params # noqa
queryset = self.get_queryset( queryset = self.get_queryset(
visibility_by_org_scope=True visibility_by_org_scope=True
) if org.free_visibility_by_scope else self.get_queryset() ) if org.free_visibility_by_scope else self.get_queryset()
query = self.filter_query(queryset) # filter by organization type
if 'org_type' in param.keys():
queryset = queryset.filter(type__id=int(param.get('org_type', 0)))
page = self.paginate_queryset(query.order_by('-create_date')) # paginate queryset # filter on search
if 'search' in param.keys():
queryset = self.filter_query(queryset)
page = self.paginate_queryset(queryset.order_by('-create_date')) # paginate queryset
if page is not None: # noqa if page is not None: # noqa
serializer = self.serializer_class(page, many=True) serializer = self.serializer_class(page, many=True)
@@ -284,6 +291,18 @@ class OrganizationViewSet(BaseViewSet, ModelViewSet, DynamicSearchMixin):
if serializer.is_valid(): if serializer.is_valid():
organization = serializer.save() organization = serializer.save()
if 'addresses' in request.data.keys():
# import multiple addresses with postal_code to orgs
address_obj_list = []
for addr in request.data['addresses']:
addr.update({'org': organization})
address_obj_list.append(
OrganizationLocationInfo(**addr)
)
OrganizationLocationInfo.objects.bulk_create(address_obj_list)
if 'user_relations' in request.data.keys(): if 'user_relations' in request.data.keys():
user_relations = CustomOperations().custom_create( # create user relations user_relations = CustomOperations().custom_create( # create user relations
request=request, request=request,
@@ -315,6 +334,22 @@ class OrganizationViewSet(BaseViewSet, ModelViewSet, DynamicSearchMixin):
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
organization = serializer.save() organization = serializer.save()
if 'addresses' in request.data.keys():
# import multiple addresses with postal_code to orgs
locations = organization.locations.all()
locations.delete() # remove ex locations
# create new locations
address_obj_list = []
for addr in request.data['addresses']:
addr.update({'org': organization})
address_obj_list.append(
OrganizationLocationInfo(**addr)
)
OrganizationLocationInfo.objects.bulk_create(address_obj_list)
if 'user_relations' in request.data.keys(): if 'user_relations' in request.data.keys():
user_relations = CustomOperations().custom_update( # update user relations user_relations = CustomOperations().custom_update( # update user relations
request=request, request=request,

View File

@@ -258,6 +258,7 @@ class OrganizationSerializer(serializers.ModelSerializer):
'address', 'address',
'parent_organization', 'parent_organization',
'national_unique_id', 'national_unique_id',
'unique_unit_identity',
'company_code', 'company_code',
'field_of_activity', 'field_of_activity',
'free_visibility_by_scope', 'free_visibility_by_scope',
@@ -363,6 +364,13 @@ class OrganizationSerializer(serializers.ModelSerializer):
'name': city.name, 'name': city.name,
} for city in instance.service_area.all() } for city in instance.service_area.all()
] ]
representation['addresses'] = [
{
"postal_code": addr.postal_code,
"address": addr.address
} for addr in instance.locations.all()
]
return representation return representation
def update(self, instance, validated_data): def update(self, instance, validated_data):
@@ -374,6 +382,7 @@ class OrganizationSerializer(serializers.ModelSerializer):
instance.address = validated_data.get('address', instance.address) instance.address = validated_data.get('address', instance.address)
instance.parent_organization = validated_data.get('parent_organization', instance.parent_organization) 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.national_unique_id = validated_data.get('national_unique_id', instance.national_unique_id)
instance.unique_unit_identity = validated_data.get('unique_unit_identity', instance.unique_unit_identity)
instance.purchase_policy = validated_data.get('purchase_policy', instance.purchase_policy) instance.purchase_policy = validated_data.get('purchase_policy', instance.purchase_policy)
instance.free_visibility_by_scope = validated_data.get( instance.free_visibility_by_scope = validated_data.get(
'free_visibility_by_scope', 'free_visibility_by_scope',

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0 on 2026-01-05 07:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0057_organization_purchase_policy'),
]
operations = [
migrations.AddField(
model_name='organization',
name='ownership_code',
field=models.IntegerField(default=0),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.0 on 2026-01-05 07:39
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('authentication', '0058_organization_ownership_code'),
]
operations = [
migrations.RemoveField(
model_name='organization',
name='ownership_code',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0 on 2026-01-05 07:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0059_remove_organization_ownership_code'),
]
operations = [
migrations.AddField(
model_name='organization',
name='ownership_code',
field=models.IntegerField(default=0),
),
]

View File

@@ -0,0 +1,16 @@
# Generated by Django 5.0 on 2026-02-09 06:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0060_organization_ownership_code'),
]
operations = [
migrations.AddField(
model_name='organization',
name='unique_unit_identity',
field=models.CharField(default='0', max_length=150),
),
]

View File

@@ -0,0 +1,34 @@
# Generated by Django 5.0 on 2026-02-09 06:57
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0061_organization_unique_unit_identity_and_more'),
]
operations = [
migrations.CreateModel(
name='OrganizationLocationInfo',
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)),
('postal_code', models.CharField(blank=True, max_length=150, null=True)),
('address', models.TextField(blank=True, max_length=2000, null=True)),
('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)),
('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)),
('org', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='locations', to='authentication.organization')),
],
options={
'abstract': False,
},
),
]

View File

@@ -116,6 +116,7 @@ class Organization(BaseModel):
null=True null=True
) )
national_unique_id = models.CharField(max_length=30, default="0") national_unique_id = models.CharField(max_length=30, default="0")
unique_unit_identity = models.CharField(max_length=150, default="0")
activity_fields = ( activity_fields = (
('CO', 'Country'), ('CO', 'Country'),
('PR', 'Province'), ('PR', 'Province'),
@@ -147,6 +148,7 @@ class Organization(BaseModel):
has_pos = models.BooleanField(default=False) has_pos = models.BooleanField(default=False)
additional_data = models.JSONField(default=dict) additional_data = models.JSONField(default=dict)
service_area = models.ManyToManyField(City, related_name='service_area') service_area = models.ManyToManyField(City, related_name='service_area')
ownership_code = models.IntegerField(default=0)
PURCHASE_POLICIES = ( PURCHASE_POLICIES = (
('INTERNAL_ONLY', 'Internal Only'), ('INTERNAL_ONLY', 'Internal Only'),
@@ -166,6 +168,23 @@ class Organization(BaseModel):
super(Organization, self).save(*args, **kwargs) super(Organization, self).save(*args, **kwargs)
class OrganizationLocationInfo(BaseModel):
org = models.ForeignKey(
Organization,
on_delete=models.CASCADE,
related_name='locations',
null=True
)
postal_code = models.CharField(max_length=150, null=True, blank=True)
address = models.TextField(max_length=2000, null=True, blank=True)
def __str__(self):
return f'{self.org.name}-{self.postal_code}-{self.address}'
def save(self, *args, **kwargs):
super(OrganizationLocationInfo, self).save(*args, **kwargs)
class OrganizationStats(BaseModel): class OrganizationStats(BaseModel):
organization = models.OneToOneField( organization = models.OneToOneField(
Organization, Organization,

View File

@@ -156,7 +156,11 @@ class UserRelationSerializer(serializers.ModelSerializer):
if instance.user: if instance.user:
representation['user'] = auth_serializer.UserSerializer(instance.user).data representation['user'] = auth_serializer.UserSerializer(instance.user).data
if instance.organization: if instance.organization:
representation['organization'] = {"id": instance.organization.id, "name": instance.organization.name} representation['organization'] = {
"id": instance.organization.id,
"name": instance.organization.name,
"ownership_code": instance.organization.ownership_code
}
if instance.role: if instance.role:
representation['role'] = { representation['role'] = {
"id": instance.role.id, "role_name": instance.role.role_name, 'type': { "id": instance.role.id, "role_name": instance.role.role_name, 'type': {

View File

@@ -10,6 +10,8 @@ VISIBILITY_MAP = {
'device': 'assignment__client__organization', 'device': 'assignment__client__organization',
'rancher': 'organization', 'rancher': 'organization',
'rancherorganizationlink': 'organization', # noqa 'rancherorganizationlink': 'organization', # noqa
'tagbatch': 'organization', # noqa
'tagdistribution': ['assigner_org', 'assigned_org']
# 'deviceactivationcode': 'organization', # 'deviceactivationcode': 'organization',
# 'deviceversion': 'organization', # 'deviceversion': 'organization',

View File

@@ -42,7 +42,6 @@ class HerdRancherSyncService:
seen_in_batch = set() seen_in_batch = set()
for temp in queryset.iterator(chunk_size=batch_size): for temp in queryset.iterator(chunk_size=batch_size):
rancher = rancher_map.get(temp.rancher_national_code) rancher = rancher_map.get(temp.rancher_national_code)
if not rancher: if not rancher:
@@ -52,7 +51,7 @@ class HerdRancherSyncService:
national_code=temp.rancher_national_code, national_code=temp.rancher_national_code,
rancher_type='N', rancher_type='N',
city_id=city_map.get(temp.city.strip()), city_id=city_map.get(temp.city.strip()),
province_id=30 province_id=28
) )
new_ranchers.append(rancher) new_ranchers.append(rancher)
rancher_map[temp.rancher_national_code] = rancher rancher_map[temp.rancher_national_code] = rancher
@@ -78,7 +77,7 @@ class HerdRancherSyncService:
postal=temp.postal_code, postal=temp.postal_code,
unit_unique_id=temp.unit_unique_id, unit_unique_id=temp.unit_unique_id,
city_id=city_map.get(temp.city.strip()), city_id=city_map.get(temp.city.strip()),
province_id=30 province_id=28
) )
} }
) )

View File

@@ -30,7 +30,6 @@ class HerdSerializer(serializers.ModelSerializer):
if isinstance(instance, Herd): if isinstance(instance, Herd):
if instance.owner: if instance.owner:
representation['owner'] = instance.owner.id representation['owner'] = instance.owner.id
representation['cooperative'] = OrganizationSerializer(instance.cooperative).data
representation['province'] = ProvinceSerializer(instance.province).data representation['province'] = ProvinceSerializer(instance.province).data
representation['city'] = CitySerializer(instance.city).data representation['city'] = CitySerializer(instance.city).data
if instance.contractor: if instance.contractor:

View File

@@ -1,8 +1,9 @@
import pandas as pd import pandas as pd
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.utils.dateparse import parse_datetime from django.utils.dateparse import parse_datetime
from apps.livestock.models import LiveStock, LiveStockType, LiveStockSpecies
from apps.herd.models import Herd from apps.herd.models import Herd
from apps.livestock.models import LiveStock, LiveStockType, LiveStockSpecies
from apps.tag.models import Tag from apps.tag.models import Tag
@@ -16,6 +17,7 @@ class Command(BaseCommand):
path = options['excel_path'] path = options['excel_path']
df = pd.read_excel(path) df = pd.read_excel(path)
records = df.to_dict(orient='records') records = df.to_dict(orient='records')
print(records[1])
self.stdout.write(self.style.SUCCESS(f"{len(records)} records loaded.")) self.stdout.write(self.style.SUCCESS(f"{len(records)} records loaded."))
@@ -46,7 +48,7 @@ class Command(BaseCommand):
herd_cache[herd_code] = herd herd_cache[herd_code] = herd
tag_code = r.get('national_id_livestock_code') tag_code = r.get('national_id_livestock_code')
tag = Tag.objects.filter(code=tag_code).first() tag = Tag.objects.filter(tag_code=tag_code).first()
if not tag: if not tag:
skipped += 1 skipped += 1
continue continue

View File

@@ -0,0 +1,144 @@
from datetime import datetime
import jdatetime
from django.core.management.base import BaseCommand
from django.db import transaction
from django.utils import timezone
from apps.herd.models import Herd
from apps.livestock.models import (
LiveStock,
LiveStockSpecies,
ExcelLiveStocks
)
BATCH_SIZE = 100
class Command(BaseCommand):
help = "Import livestock from ExcelLiveStocks into LiveStock using bulk_create"
def normalize_herd_code(self, value, length=10):
if value is None:
return None
return str(value).strip().zfill(length)
def parse_jalali_datetime(self, date_str: str):
if not date_str:
return None
year, month, day = map(int, date_str.split('/'))
# jalali → gregorian (date)
g_date = jdatetime.date(year, month, day).togregorian()
# date → naive datetime
naive_dt = datetime.combine(g_date, datetime.min.time())
# naive → aware (VERY IMPORTANT)
return timezone.make_aware(naive_dt)
def handle(self, *args, **options):
qs = ExcelLiveStocks.objects.all()
if not qs.exists():
self.stdout.write(self.style.WARNING("No records to import"))
return
# ---------- preload lookups ----------
herd_map = {
h.code: h
for h in Herd.objects.all()
}
species_map = {
s.name.strip(): s
for s in LiveStockSpecies.objects.all()
}
livestocks_to_create = []
processed_ids = []
created_count = 0
skipped = 0
self.stdout.write("Starting import...")
with transaction.atomic():
for row in qs.iterator(chunk_size=BATCH_SIZE):
herd = herd_map.get(self.normalize_herd_code(row.herd_code))
# print(self.normalize_herd_code(row.herd_code))
if not herd:
# print("herd")
skipped += 1
continue
# species cache / create
species_name = (row.species or "").strip()
if not species_name:
# print("species")
skipped += 1
continue
species = species_map.get(species_name)
if not species:
species = LiveStockSpecies.objects.create(
name=species_name
)
species_map[species_name] = species
livestocks_to_create.append(
LiveStock(
herd=herd,
species=species,
gender=self.map_gender(row.gender),
birthdate=self.parse_jalali_datetime(row.birthdate),
)
)
processed_ids.append(row.id)
if len(livestocks_to_create) >= BATCH_SIZE:
print("-----------------------------CREATE------------------------------------")
print(livestocks_to_create)
LiveStock.objects.bulk_create(
livestocks_to_create,
batch_size=BATCH_SIZE
)
created_count += len(livestocks_to_create)
livestocks_to_create.clear()
break
# flush remaining
if livestocks_to_create:
LiveStock.objects.bulk_create(
livestocks_to_create,
batch_size=BATCH_SIZE
)
created_count += len(livestocks_to_create)
# mark excel rows as archived
# ExcelLiveStocks.objects.filter(
# id__in=processed_ids
# ).update(archive=True)
self.stdout.write(self.style.SUCCESS(
f"Import finished. Created: {created_count}, Skipped: {skipped}"
))
@staticmethod
def map_gender(value):
if not value:
return 1
value = value.strip().lower()
if value in ['female', 'f', 'ماده']:
return 2
return 1
@staticmethod
def parse_date(value):
if not value:
return None
try:
return datetime.strptime(value, '%Y/%m/%d')
except Exception:
return None

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0 on 2026-01-24 11:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('livestock', '0016_temporarylivestock'),
]
operations = [
migrations.AddField(
model_name='livestockspecies',
name='value',
field=models.IntegerField(default=0),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.0 on 2026-01-24 11:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('livestock', '0017_livestockspecies_value'),
]
operations = [
migrations.AddField(
model_name='livestockspecies',
name='en_name',
field=models.CharField(max_length=50, null=True),
),
migrations.AlterField(
model_name='livestockspecies',
name='name',
field=models.CharField(max_length=50, null=True),
),
]

View File

@@ -0,0 +1,38 @@
# Generated by Django 5.0 on 2026-02-09 10:58
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('livestock', '0018_livestockspecies_en_name_alter_livestockspecies_name'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ExcelLiveStocks',
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)),
('national_id', models.CharField(max_length=250, null=True)),
('herd_code', models.CharField(max_length=150, null=True)),
('species', models.CharField(max_length=250, null=True)),
('birthdate', models.CharField(max_length=150, null=True)),
('gender', models.CharField(max_length=150, null=True)),
('agent_code', models.CharField(max_length=150, null=True)),
('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)),
('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)),
],
options={
'abstract': False,
},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0 on 2026-02-10 08:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('livestock', '0019_excellivestocks'),
]
operations = [
migrations.AddField(
model_name='excellivestocks',
name='sync_status',
field=models.CharField(max_length=50, null=True),
),
]

View File

@@ -7,7 +7,9 @@ from apps.tag import models as tag_models
class LiveStockSpecies(BaseModel): # noqa class LiveStockSpecies(BaseModel): # noqa
""" species of live stocks like Kurdi, Luri, etc """ # noqa """ species of live stocks like Kurdi, Luri, etc """ # noqa
name = models.CharField(max_length=50) name = models.CharField(max_length=50, null=True)
en_name = models.CharField(max_length=50, null=True)
value = models.IntegerField(default=0)
def __str__(self): def __str__(self):
return f'{self.name}' return f'{self.name}'
@@ -95,12 +97,22 @@ class LiveStock(BaseModel):
archive = models.BooleanField(default=False) archive = models.BooleanField(default=False)
def __str__(self): def __str__(self):
return f'{self.type.name}-{self.species.name}' return f"{self.type.name if self.type else ''}-{self.species.name if self.species else ''}"
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
return super(LiveStock, self).save(*args, **kwargs) return super(LiveStock, self).save(*args, **kwargs)
class ExcelLiveStocks(BaseModel):
national_id = models.CharField(max_length=250, null=True)
herd_code = models.CharField(max_length=150, null=True)
species = models.CharField(max_length=250, null=True)
birthdate = models.CharField(max_length=150, null=True)
gender = models.CharField(max_length=150, null=True)
agent_code = models.CharField(max_length=150, null=True)
sync_status = models.CharField(max_length=50, null=True)
class TemporaryLiveStock(BaseModel): class TemporaryLiveStock(BaseModel):
rancher = models.ForeignKey( rancher = models.ForeignKey(
herd_models.Rancher, herd_models.Rancher,

View File

@@ -1,6 +1,7 @@
from rest_framework import serializers from rest_framework import serializers
from apps.livestock import models as livestock_models
from apps.herd.web.api.v1.serializers import HerdSerializer from apps.herd.web.api.v1.serializers import HerdSerializer
from apps.livestock import models as livestock_models
from apps.tag.web.api.v1.serializers import TagSerializer from apps.tag.web.api.v1.serializers import TagSerializer
@@ -28,7 +29,9 @@ class LiveStockSpeciesSerializer(serializers.ModelSerializer):
model = livestock_models.LiveStockSpecies model = livestock_models.LiveStockSpecies
fields = [ fields = [
'id', 'id',
'name' 'name',
'en_name',
'value',
] ]

View File

@@ -2,6 +2,23 @@ from rest_framework import status
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
class DeviceException(APIException):
status_code = status.HTTP_400_BAD_REQUEST
default_detail = "خطا در اطلاعات پلاک" # noqa
default_code = 'error'
def __init__(self, message=None, status_code=None, code=None):
if status_code is not None:
self.status_code = status_code
detail = {
"message": message,
"status_code": status_code
}
super().__init__(detail)
class DeviceAlreadyAssigned(APIException): class DeviceAlreadyAssigned(APIException):
status_code = status.HTTP_403_FORBIDDEN status_code = status.HTTP_403_FORBIDDEN
default_detail = "این دستگاه قبلا به این کلاینت تخصیص داده شده است" # noqa default_detail = "این دستگاه قبلا به این کلاینت تخصیص داده شده است" # noqa

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0 on 2026-01-26 06:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pos_device', '0080_bankaccountdevicelink'),
]
operations = [
migrations.AlterField(
model_name='device',
name='serial',
field=models.TextField(null=True),
),
]

View File

@@ -36,7 +36,7 @@ class Device(BaseModel):
acceptor = models.CharField(max_length=50, null=True) acceptor = models.CharField(max_length=50, null=True)
terminal = models.CharField(max_length=50, null=True) terminal = models.CharField(max_length=50, null=True)
mac = models.CharField(max_length=50, null=True) mac = models.CharField(max_length=50, null=True)
serial = models.TextField(null=True, unique=True) serial = models.TextField(null=True)
password = models.CharField(max_length=25, null=True) password = models.CharField(max_length=25, null=True)
multi_device = models.BooleanField(default=False) multi_device = models.BooleanField(default=False)
server_in = models.BooleanField(default=False) server_in = models.BooleanField(default=False)

View File

@@ -3,6 +3,7 @@ from rest_framework.serializers import ModelSerializer
from apps.authentication.api.v1.serializers.serializer import BankAccountSerializer from apps.authentication.api.v1.serializers.serializer import BankAccountSerializer
from apps.pos_device import exceptions as pos_exceptions from apps.pos_device import exceptions as pos_exceptions
from apps.pos_device import models as pos_models from apps.pos_device import models as pos_models
from apps.pos_device.exceptions import DeviceException
from apps.pos_device.web.api.v1.serilaizers import client as client_serializer from apps.pos_device.web.api.v1.serilaizers import client as client_serializer
from apps.product.web.api.v1.serializers.quota_distribution_serializers import QuotaDistributionSerializer from apps.product.web.api.v1.serializers.quota_distribution_serializers import QuotaDistributionSerializer
@@ -18,6 +19,19 @@ class DeviceSerializer(ModelSerializer):
model = pos_models.Device model = pos_models.Device
fields = '__all__' fields = '__all__'
def validate(self, attrs):
serial = attrs['serial']
if not self.instance:
if self.Meta.model.objects.filter(serial=serial).exists():
raise DeviceException("دستگاه یا این شماره سریال از قبل ثبت شده است.", status_code=403) # noqa
if self.instance:
if serial != self.instance.serial and self.Meta.model.objects.filter(serial=serial).exists():
raise DeviceException("دستگاهی با این شماره سریال وجود دارد.", status_code=403) # noqa
return attrs
def to_representation(self, instance): def to_representation(self, instance):
""" custom output of serializer """ """ custom output of serializer """
representation = super().to_representation(instance) representation = super().to_representation(instance)

View File

@@ -4,3 +4,6 @@ from django.apps import AppConfig
class TagConfig(AppConfig): class TagConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.tag' name = 'apps.tag'
def ready(self):
import apps.tag.signals.tag_distribution_signals # noqa

View File

@@ -1,8 +1,25 @@
from rest_framework.exceptions import APIException
from rest_framework import status from rest_framework import status
from rest_framework.exceptions import APIException
class SpeciesNumberCheckException(APIException): class SpeciesNumberCheckException(APIException):
status_code = status.HTTP_403_FORBIDDEN status_code = status.HTTP_403_FORBIDDEN
default_detail = 'Entered species number is more than user free tags' default_detail = 'Entered species number is more than user free tags'
default_code = 'more than free tags' default_code = 'more than free tags'
class TagException(APIException):
status_code = status.HTTP_400_BAD_REQUEST
default_detail = "خطا در اطلاعات پلاک" # noqa
default_code = 'error'
def __init__(self, message=None, status_code=None, code=None):
if status_code is not None:
self.status_code = status_code
detail = {
"message": message,
"status_code": status_code
}
super().__init__(detail)

View File

@@ -4,12 +4,12 @@ from django.core.management.base import BaseCommand
from django.db import transaction from django.db import transaction
from apps.herd.models import Herd from apps.herd.models import Herd
from apps.livestock.models import LiveStock, LiveStockType from apps.livestock.models import LiveStock, LiveStockType, ExcelLiveStocks
from apps.tag.models import Tag, TemporaryTags from apps.tag.models import Tag
from common.generics import parse_birthdate from common.generics import parse_birthdate
BATCH_SIZE = 5000 BATCH_SIZE = 1000
CHUNK_SIZE = 10000 CHUNK_SIZE = 1000
class Command(BaseCommand): class Command(BaseCommand):
@@ -22,16 +22,16 @@ class Command(BaseCommand):
) )
qs = ( qs = (
TemporaryTags.objects ExcelLiveStocks.objects
.filter(sync_status__isnull=True) .filter(sync_status__isnull=True)
.only('herd_code', 'birthdate', 'gender', 'tag') .only('herd_code', 'birthdate', 'gender', 'national_id')
) )
total = qs.count() total = qs.count()
processed = 0 processed = 0
start_time = time.time() start_time = time.time()
LOG_EVERY = 10000 LOG_EVERY = 1000
buffer = [] buffer = []
for temp in qs.iterator(chunk_size=CHUNK_SIZE): for temp in qs.iterator(chunk_size=CHUNK_SIZE):
@@ -64,7 +64,7 @@ class Command(BaseCommand):
self.stdout.write(self.style.SUCCESS("DONE ✅")) self.stdout.write(self.style.SUCCESS("DONE ✅"))
def process_batch(self, temps): def process_batch(self, temps):
herd_codes = {t.herd_code for t in temps if t.herd_code} herd_codes = {self.normalize_herd_code(t.herd_code) for t in temps if t.herd_code}
herds = { herds = {
h.code: h h.code: h
@@ -90,7 +90,7 @@ class Command(BaseCommand):
existing_tags = { existing_tags = {
t.tag_code: t t.tag_code: t
for t in Tag.objects.filter( for t in Tag.objects.filter(
tag_code__in=[t.tag for t in temps if t.tag] tag_code__in=[t.national_id for t in temps if t.national_id]
) )
} }
@@ -99,28 +99,28 @@ class Command(BaseCommand):
new_tags = [] new_tags = []
for temp in temps: for temp in temps:
herd = herds.get(temp.herd_code) herd = herds.get(self.normalize_herd_code(temp.herd_code))
if not herd: if not herd:
continue continue
birthdate = parse_birthdate(temp.birthdate) birthdate = parse_birthdate(temp.birthdate)
gender = 1 if temp.gender == 'M' else 2 gender = 1 if temp.gender == 'M' else 2
livestock_type = livestock_types.get(temp.type) livestock_type = livestock_types.get(temp.species)
weight_type = livestock_type.weight_type weight_type = livestock_type.weight_type if livestock_type else 'L'
key = (temp.herd_code, birthdate, gender) key = (self.normalize_herd_code(temp.herd_code), birthdate, gender)
livestock = livestock_map.get(key) livestock = livestock_map.get(key)
if not livestock: if not livestock:
if not temp.tag: if not temp.national_id:
continue continue
tag = existing_tags.get(temp.tag) tag = existing_tags.get(temp.national_id)
if not tag: if not tag:
tag = Tag(tag_code=temp.tag, status='A') tag = Tag(tag_code=temp.national_id, status='A')
new_tags.append(tag) new_tags.append(tag)
existing_tags[temp.tag] = tag existing_tags[temp.national_id] = tag
livestock = LiveStock( livestock = LiveStock(
herd=herd, herd=herd,
@@ -136,13 +136,13 @@ class Command(BaseCommand):
temp.sync_status = 'S' temp.sync_status = 'S'
continue continue
if livestock.tag is None and temp.tag: if livestock.tag is None and temp.national_id:
tag = existing_tags.get(temp.tag) tag = existing_tags.get(temp.national_id)
if not tag: if not tag:
tag = Tag(tag_code=temp.tag, status='A') tag = Tag(tag_code=temp.national_id, status='A')
new_tags.append(tag) new_tags.append(tag)
existing_tags[temp.tag] = tag existing_tags[temp.national_id] = tag
livestock.tag = tag livestock.tag = tag
updated_livestock.append(livestock) updated_livestock.append(livestock)
@@ -161,8 +161,13 @@ class Command(BaseCommand):
['tag'], ['tag'],
batch_size=BATCH_SIZE batch_size=BATCH_SIZE
) )
TemporaryTags.objects.bulk_update( ExcelLiveStocks.objects.bulk_update(
temps, temps,
['sync_status'], ['sync_status'],
batch_size=BATCH_SIZE batch_size=BATCH_SIZE
) )
def normalize_herd_code(self, value, length=10):
if value is None:
return None
return str(value).strip().zfill(length)

View File

@@ -0,0 +1,38 @@
# Generated by Django 5.0 on 2026-01-05 07:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tag', '0026_temporarytags_sync_status'),
]
operations = [
migrations.AlterField(
model_name='tag',
name='country_code',
field=models.IntegerField(default=364, help_text='it is static 364, iran country code'),
),
migrations.AlterField(
model_name='tag',
name='ownership_code',
field=models.IntegerField(default=0, help_text='ownership code of organizations like: 51, 52, etc'),
),
migrations.AlterField(
model_name='tag',
name='serial',
field=models.CharField(help_text='7 digit number as serial on tag code', max_length=8),
),
migrations.AlterField(
model_name='tag',
name='species_code',
field=models.IntegerField(default=0, help_text='code of livestock type like: 1->sheep'),
),
migrations.AlterField(
model_name='tag',
name='static_code',
field=models.IntegerField(default=0, help_text='always a static number'),
),
]

View File

@@ -0,0 +1,16 @@
# Generated by Django 5.0 on 2026-01-07 07:13
from django.conf import settings
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('authentication', '0060_organization_ownership_code'),
('tag', '0027_alter_tag_country_code_alter_tag_ownership_code_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
]

View File

@@ -0,0 +1,46 @@
# Generated by Django 5.0 on 2026-01-07 11:55
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0060_organization_ownership_code'),
('tag', '0028_tagbatch'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='tag',
name='tag_code',
field=models.CharField(max_length=20, null=True),
),
migrations.CreateModel(
name='TagBatch',
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)),
('request_number', models.CharField(default='0', max_length=50, null=True)),
('species_code', models.IntegerField(default=0)),
('serial_from', models.PositiveBigIntegerField(default=0)),
('serial_to', models.PositiveBigIntegerField(default=0)),
('status', models.CharField(choices=[('created', 'CREATED'), ('distributed', 'DISTRIBUTED')], default='created', max_length=20, null=True)),
('description', models.TextField(blank=True, null=True)),
('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)),
('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='tag_batches', to='authentication.organization')),
('tag', models.ManyToManyField(related_name='tags', to='tag.tag')),
],
options={
'abstract': False,
},
),
]

View File

@@ -0,0 +1,38 @@
# Generated by Django 5.0 on 2026-01-18 10:44
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0060_organization_ownership_code'),
('tag', '0029_alter_tag_tag_code_tagbatch'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='TagDistribution',
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)),
('species_code', models.IntegerField(default=0)),
('distributed_number', models.IntegerField(default=0)),
('batch', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tag_distributions', to='tag.tagbatch')),
('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)),
('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='tag_distributions', to='authentication.organization')),
('tag', models.ManyToManyField(related_name='distributions', to='tag.tag')),
],
options={
'abstract': False,
},
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 5.0 on 2026-01-19 06:19
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0060_organization_ownership_code'),
('tag', '0030_tagdistribution'),
]
operations = [
migrations.AddField(
model_name='tagbatch',
name='batch_identity',
field=models.PositiveBigIntegerField(default=0),
),
migrations.AlterField(
model_name='tagbatch',
name='organization',
field=models.ForeignKey(help_text='creator org of tag batch', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tag_batches', to='authentication.organization'),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.0 on 2026-01-19 06:25
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('tag', '0031_tagbatch_batch_identity_alter_tagbatch_organization'),
]
operations = [
migrations.RemoveField(
model_name='tagbatch',
name='batch_identity',
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0 on 2026-01-19 06:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tag', '0032_remove_tagbatch_batch_identity'),
]
operations = [
migrations.AddField(
model_name='tagbatch',
name='batch_identity',
field=models.CharField(max_length=50, null=True, unique=True),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.0 on 2026-01-19 06:41
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('tag', '0033_tagbatch_batch_identity'),
]
operations = [
migrations.RemoveField(
model_name='tagdistribution',
name='organization',
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 5.0 on 2026-01-19 06:42
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0060_organization_ownership_code'),
('tag', '0034_remove_tagdistribution_organization'),
]
operations = [
migrations.AddField(
model_name='tagdistribution',
name='assigned_org',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tag_distributions', to='authentication.organization'),
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 5.0 on 2026-01-19 06:46
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0060_organization_ownership_code'),
('tag', '0035_tagdistribution_assigned_org'),
]
operations = [
migrations.AddField(
model_name='tagdistribution',
name='assigner_org',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='assigner_tag_dist', to='authentication.organization'),
),
migrations.AlterField(
model_name='tagdistribution',
name='assigned_org',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='assigned_tag_dist', to='authentication.organization'),
),
]

View File

@@ -0,0 +1,54 @@
# Generated by Django 5.0 on 2026-01-20 07:31
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0060_organization_ownership_code'),
('tag', '0036_tagdistribution_assigner_org_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='tagdistribution',
name='dist_identity',
field=models.CharField(default='0', max_length=20, null=True, unique=True),
),
migrations.AddField(
model_name='tagdistribution',
name='is_closed',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='tagbatch',
name='tag',
field=models.ManyToManyField(related_name='batches', to='tag.tag'),
),
migrations.CreateModel(
name='TagDistributionBatch',
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)),
('dist_batch_identity', models.CharField(default='0', max_length=20, null=True, unique=True)),
('total_tag_count', models.IntegerField(default=0)),
('is_closed', models.BooleanField(default=False)),
('assigned_org', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='to_tag_distribution_batch', to='authentication.organization')),
('assigner_org', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='from_tag_distribution_batch', to='authentication.organization')),
('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)),
('distributions', models.ManyToManyField(related_name='tag_distribution_batch', to='tag.tagdistribution')),
('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)),
],
options={
'abstract': False,
},
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 5.0 on 2026-01-24 05:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tag', '0037_tagdistribution_dist_identity_and_more'),
]
operations = [
migrations.AddField(
model_name='tagdistributionbatch',
name='distribution_type',
field=models.CharField(choices=[('random', 'RANDOM'), ('batch', 'BATCH')], default='batch', max_length=20,
null=True),
),
migrations.AddField(
model_name='tagdistributionbatch',
name='remaining_tag_count',
field=models.PositiveBigIntegerField(default=0),
),
migrations.AddField(
model_name='tagdistributionbatch',
name='total_distributed_tag_count',
field=models.PositiveBigIntegerField(default=0),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0 on 2026-01-24 06:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tag', '0038_tagdistributionbatch_distribution_type_and_more'),
]
operations = [
migrations.AddField(
model_name='tagbatch',
name='total_distributed_tags',
field=models.PositiveBigIntegerField(default=0),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.0 on 2026-01-24 09:13
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tag', '0039_tagbatch_total_distributed_tags'),
]
operations = [
migrations.AddField(
model_name='tagdistributionbatch',
name='parent',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='tag.tagdistributionbatch'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0 on 2026-01-24 10:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tag', '0040_tagdistributionbatch_parent'),
]
operations = [
migrations.AddField(
model_name='tagbatch',
name='total_remaining_tags',
field=models.PositiveBigIntegerField(default=0),
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 5.0 on 2026-01-27 09:18
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tag', '0041_tagbatch_total_remaining_tags'),
]
operations = [
migrations.AddField(
model_name='tagdistribution',
name='parent',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='child', to='tag.tagdistribution'),
),
migrations.AddField(
model_name='tagdistribution',
name='remaining_number',
field=models.IntegerField(default=0),
),
migrations.AddField(
model_name='tagdistribution',
name='total_tag_count',
field=models.IntegerField(default=0),
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 5.0 on 2026-02-07 07:29
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0060_organization_ownership_code'),
('tag', '0042_tagdistribution_parent_and_more'),
]
operations = [
migrations.AddField(
model_name='tagdistributionbatch',
name='owner_org',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tag_distribution_batch', to='authentication.organization'),
),
migrations.AddField(
model_name='tagdistributionbatch',
name='top_root_distribution',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.0 on 2026-02-08 07:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tag', '0043_tagdistributionbatch_owner_org_and_more'),
]
operations = [
migrations.AddField(
model_name='tagdistributionbatch',
name='exit_doc_status',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='tagdistributionbatch',
name='warehouse_exit_doc',
field=models.CharField(max_length=350, null=True),
),
]

View File

@@ -1,22 +1,21 @@
import random import random
from crum import get_current_user
from django.db import models from django.db import models
from jdatetime import datetime from jdatetime import datetime
from apps.authentication import models as auth_models from apps.authentication import models as auth_models
from apps.authorization import models as authoriz_models from apps.authentication.models import Organization
from apps.core.models import BaseModel from apps.core.models import BaseModel
from apps.tag.tools import tag_code_serial_scanning from apps.tag.tools import tag_code_serial_scanning
class Tag(BaseModel): class Tag(BaseModel):
country_code = models.IntegerField(default=364) country_code = models.IntegerField(default=364, help_text='it is static 364, iran country code')
static_code = models.IntegerField(default=0) static_code = models.IntegerField(default=0, help_text='always a static number')
ownership_code = models.IntegerField(default=0) ownership_code = models.IntegerField(default=0, help_text='ownership code of organizations like: 51, 52, etc')
species_code = models.IntegerField(default=0) species_code = models.IntegerField(default=0, help_text='code of livestock type like: 1->sheep')
serial = models.CharField(max_length=8) serial = models.CharField(max_length=8, help_text='7 digit number as serial on tag code')
tag_code = models.CharField(max_length=20, unique=True, null=True) tag_code = models.CharField(max_length=20, null=True)
organization = models.ForeignKey( organization = models.ForeignKey(
auth_models.Organization, auth_models.Organization,
on_delete=models.CASCADE, on_delete=models.CASCADE,
@@ -25,7 +24,8 @@ class Tag(BaseModel):
) )
status_choices = ( status_choices = (
('F', 'Free'), ('F', 'Free'),
('A', 'Assigned') ('R', 'Reserved'),
('A', 'Assigned'),
) )
status = models.CharField(max_length=20, default="F") status = models.CharField(max_length=20, default="F")
@@ -43,19 +43,144 @@ class Tag(BaseModel):
f"{self.ownership_code}" \ f"{self.ownership_code}" \
f"{self.species_code}" \ f"{self.species_code}" \
f"{self.serial}" f"{self.serial}"
if not self.organization:
# set user organization for tag
user = get_current_user()
self.organization = (
authoriz_models.UserRelations.objects.select_related(
'organization'
).get(
user=user
)).organization
super(Tag, self).save(*args, **kwargs) super(Tag, self).save(*args, **kwargs)
class TagBatch(BaseModel):
batch_identity = models.CharField(
unique=True,
max_length=50,
null=True
)
organization = models.ForeignKey(
Organization,
on_delete=models.CASCADE,
related_name='tag_batches',
null=True,
help_text="creator org of tag batch"
)
request_number = models.CharField(
max_length=50,
default="0",
null=True
)
tag = models.ManyToManyField(Tag, related_name='batches')
species_code = models.IntegerField(default=0)
serial_from = models.PositiveBigIntegerField(default=0)
serial_to = models.PositiveBigIntegerField(default=0)
total_distributed_tags = models.PositiveBigIntegerField(default=0)
total_remaining_tags = models.PositiveBigIntegerField(default=0)
status = models.CharField(
max_length=20,
choices=[
('created', 'CREATED'),
('distributed', 'DISTRIBUTED'),
],
null=True,
default='created'
)
description = models.TextField(null=True, blank=True)
def __str__(self):
return f'{self.id}-{self.request_number}-{self.organization.name}'
def save(self, *args, **kwargs):
return super(TagBatch, self).save(*args, **kwargs)
class TagDistribution(BaseModel):
parent = models.ForeignKey(
'self',
on_delete=models.CASCADE,
related_name='child',
null=True
)
dist_identity = models.CharField(max_length=20, default="0", unique=True, null=True)
batch = models.ForeignKey(
TagBatch,
on_delete=models.CASCADE,
related_name='tag_distributions',
null=True
)
tag = models.ManyToManyField(Tag, related_name='distributions')
assigner_org = models.ForeignKey(
Organization,
on_delete=models.CASCADE,
related_name='assigner_tag_dist',
null=True
)
assigned_org = models.ForeignKey(
Organization,
on_delete=models.CASCADE,
related_name='assigned_tag_dist',
null=True
)
species_code = models.IntegerField(default=0)
total_tag_count = models.IntegerField(default=0)
distributed_number = models.IntegerField(default=0)
remaining_number = models.IntegerField(default=0)
is_closed = models.BooleanField(default=False)
def __str__(self):
return f'{self.id}-{self.distributed_number}-{self.assigned_org.name}'
def save(self, *args, **kwargs):
return super(TagDistribution, self).save(*args, **kwargs)
class TagDistributionBatch(BaseModel):
parent = models.ForeignKey(
'self',
on_delete=models.CASCADE,
related_name='children',
null=True
)
dist_batch_identity = models.CharField(max_length=20, default="0", unique=True, null=True)
owner_org = models.ForeignKey(
Organization,
on_delete=models.CASCADE,
related_name='tag_distribution_batch',
null=True
)
assigner_org = models.ForeignKey(
Organization,
on_delete=models.CASCADE,
related_name='from_tag_distribution_batch',
null=True
)
assigned_org = models.ForeignKey(
Organization,
on_delete=models.CASCADE,
related_name="to_tag_distribution_batch",
null=True
)
distribution_type = models.CharField(
choices=(
('random', 'RANDOM'),
('batch', 'BATCH'),
),
max_length=20,
null=True,
default='batch'
)
distributions = models.ManyToManyField(TagDistribution, related_name='tag_distribution_batch')
total_tag_count = models.IntegerField(default=0)
total_distributed_tag_count = models.PositiveBigIntegerField(default=0)
remaining_tag_count = models.PositiveBigIntegerField(default=0)
top_root_distribution = models.BooleanField(default=False)
warehouse_exit_doc = models.CharField(max_length=350, null=True)
exit_doc_status = models.BooleanField(default=False)
is_closed = models.BooleanField(default=False)
def __str__(self):
return f'{self.id}'
def save(self, *args, **kwargs):
return super(TagDistributionBatch, self).save(*args, **kwargs)
class TagAssignment(BaseModel): class TagAssignment(BaseModel):
organization = models.ForeignKey( organization = models.ForeignKey(
auth_models.Organization, auth_models.Organization,

View File

View File

@@ -0,0 +1,69 @@
from django.db.models import Sum, Q, Count, QuerySet, OuterRef, Subquery, IntegerField
from django.db.models.functions import Coalesce
from apps.authentication.models import Organization
from apps.authentication.services.service import get_all_org_child
from apps.tag.models import TagBatch
class TagBatchService:
"""
services of tag batch
"""
def tag_batch_main_dashboard(self, org: Organization = None, batch: QuerySet[TagBatch] = None):
"""
dashboard data of batch main page
"""
qs = TagBatch.objects.select_related('organization') if not batch else batch
if org.type.key != 'ADM':
child_orgs = get_all_org_child(org) # noqa
child_orgs.append(org)
qs = qs.filter(organization__in=child_orgs)
base_data = qs.aggregate(
batch_count=Count('id', distinct=True),
total_distributed_tags=Coalesce(Sum('total_distributed_tags'), 0),
total_remaining_tags=Coalesce(Sum('total_remaining_tags'), 0),
has_distributed_batches_number=Count(
'id',
distinct=True,
filter=Q(status__in=[
'distributed',
])
)
)
base_data.update(qs.aggregate(tag_count_created_by_batch=Count('tag')))
tag_count_subquery = (
TagBatch.objects
.filter(id=OuterRef('id'))
.annotate(cnt=Count('tag'))
.values('cnt')
)
species_data = (
qs
.annotate(
tag_count=Subquery(tag_count_subquery, output_field=IntegerField())
)
.values('species_code')
.annotate(
batch_count=Count('id', distinct=True),
total_distributed_tags=Coalesce(Sum('total_distributed_tags'), 0),
total_remaining_tags=Coalesce(Sum('total_remaining_tags'), 0),
tag_count_created_by_batch=Coalesce(Sum('tag_count'), 0),
has_distributed_batches_number=Count(
'id',
distinct=True,
filter=Q(status='distributed')
)
)
.order_by('species_code')
)
base_data['batch_data_by_species'] = list(species_data)
return base_data

View File

@@ -0,0 +1,377 @@
import random
from django.db import transaction
from django.db.models import Sum, Q
from django.db.models.aggregates import Count
from django.db.models.functions import Coalesce
from rest_framework.exceptions import PermissionDenied
from apps.authentication.models import Organization
from apps.livestock.models import LiveStockSpecies
from apps.tag.exceptions import TagException
from apps.tag.models import Tag, TagBatch, TagDistribution, TagDistributionBatch
from common.generics import generate_unique_code
class TagDistributionService:
"""
service of distribute tags in organizations
"""
def create_distribution_from_batch(self, org: Organization = None, data: dict = None):
"""
distribute tags with batch
"""
with transaction.atomic():
distributions = []
total_counted_tags = 0
assigned_org = Organization.objects.get(id=data.get('assigned_org'))
for distribution in data.get('dists'):
batch_identity = distribution.get('batch_identity', None)
# if batch identity exists distribute tags of batch
if batch_identity:
batch = TagBatch.objects.get(batch_identity=batch_identity)
tags = Tag.objects.filter(
batches__batch_identity=batch_identity,
species_code=distribution.get('species_code'),
status='F'
)
distribution_type = 'batch'
else:
batch = None
# get tags without batch and only with species code
tags = Tag.objects.filter(
species_code=distribution.get('species_code'),
status='F'
)
distribution_type = 'random'
if tags.count() < distribution.get('count'):
raise TagException(
"تعداد وارد شده از تعداد موجودی این گونه بیشتر میباشد.", # noqa
403
)
dist = TagDistribution.objects.create(
batch=batch,
assigner_org=org,
assigned_org=assigned_org,
species_code=distribution.get('species_code'),
total_tag_count=distribution.get('count'),
remaining_number=distribution.get('count'),
dist_identity=generate_unique_code(f"{random.randint(1000, 9999)}"),
)
# get counted tag ids and filter by them to update status To Reserve
counted_tags_obj = tags.order_by('create_date')[:int(distribution.get('count'))]
counted_tag_ids = [tag.id for tag in counted_tags_obj]
tags.filter(id__in=counted_tag_ids).update(status='R')
dist.tag.add(*counted_tags_obj)
distributions.append(dist)
total_counted_tags += distribution.get('count')
# create distribution batch
distributions_batch = TagDistributionBatch.objects.create(
parent=TagDistributionBatch.objects.get(id=data.get('parent')) if data.get('parent') else None,
owner_org=assigned_org,
assigner_org=org,
assigned_org=assigned_org,
total_tag_count=total_counted_tags,
dist_batch_identity=generate_unique_code(f"{random.randint(1000, 9999)}"),
distribution_type=distribution_type,
top_root_distribution=True
)
distributions_batch.distributions.add(*distributions)
return {'tag_distributions': distributions, 'distributions_batch': distributions_batch}
def edit_distribution_from_batch(
self, dist_batch: TagDistributionBatch = None,
data: dict = None,
org: Organization = None
):
"""
edit record of distributed tags
"""
# clear and hard delete of distributions
dist_batch_distributions = dist_batch.distributions.all()
for dist in dist_batch_distributions: # free distributed tags from reserve
dist.tag.all().update(status='F')
dist_batch_distributions.delete()
# create new distributions and update batch
total_counted_tags = 0
distributions = []
assigned_org = Organization.objects.get(id=data.get('assigned_org'))
for distribution in data.get('dists'):
batch_identity = distribution.get('batch_identity', None)
# if batch identity exists distribute tags of batch
if batch_identity:
batch = TagBatch.objects.get(batch_identity=batch_identity)
tags = Tag.objects.filter(
batches__batch_identity=batch_identity,
species_code=distribution.get('species_code'),
status='F'
)
distribution_type = 'batch'
else:
batch = None
# get tags without batch and only with species code
tags = Tag.objects.filter(
species_code=distribution.get('species_code'),
status='F'
)
distribution_type = 'random'
if tags.count() < distribution.get('count'):
raise TagException(
"تعداد وارد شده از تعداد موجودی این گونه بیشتر میباشد.", # noqa
403
)
dist = TagDistribution.objects.create(
batch=batch,
assigner_org=org,
assigned_org=assigned_org,
species_code=distribution.get('species_code'),
total_tag_count=distribution.get('count'),
remaining_number=distribution.get('count'),
dist_identity=generate_unique_code(f"{random.randint(1000, 9999)}"),
)
# get counted tag ids and filter by them to update status To Reserve
counted_tags_obj = tags.order_by('create_date')[:int(distribution.get('count'))]
counted_tag_ids = [tag.id for tag in counted_tags_obj]
tags.filter(id__in=counted_tag_ids).update(status='R')
dist.tag.add(*counted_tags_obj)
distributions.append(dist)
total_counted_tags += distribution.get('count')
# update distribution batch
dist_batch.assigned_org = assigned_org
dist_batch.total_tag_count = total_counted_tags
dist_batch.distribution_type = distribution_type # noqa
dist_batch.save(update_fields=['assigned_org', 'total_tag_count', 'distribution_type'])
dist_batch.distributions.add(*distributions)
return {'tag_distributions': distributions, 'distributions_batch': dist_batch}
def create_distribution_from_distribution(
self, org: Organization = None,
tag_batch: TagDistributionBatch = None,
data: dict = None
):
"""
create a distribution from distribution to target organization
"""
with transaction.atomic():
distributions = []
total_counted_tags = 0
assigned_org = Organization.objects.get(id=data['assigned_org'])
parent_batch = TagDistributionBatch.objects.get(
id=data['parent_distribution_batch']
)
if parent_batch.assigned_org != org and org.type.key != 'ADM':
raise PermissionDenied("دسترسی غیرمجاز") # noqa
for dist_data in data['dists']:
species = dist_data['species_code']
count = dist_data['count']
parent_tag_distribution = TagDistribution.objects.get(
id=dist_data['parent_tag_distribution']
)
batch = TagBatch.objects.get(
batch_identity=dist_data.get('batch_identity')
) if dist_data.get('batch_identity') else None
tags = Tag.objects.filter(
distributions__tag_distribution_batch=parent_batch,
species_code=species,
status='R',
)
if tags.count() < count:
raise TagException("پلاک کافی برای این گونه وجود ندارد", 403) # noqa
dist = TagDistribution.objects.create(
parent=parent_tag_distribution,
batch=batch,
assigner_org=org,
assigned_org=assigned_org,
species_code=species,
total_tag_count=count,
remaining_number=count,
dist_identity=generate_unique_code(
f"{random.randint(1000, 9999)}"
),
)
selected_tags = tags.order_by('create_date')[:count]
dist.tag.add(*selected_tags)
distributions.append(dist)
total_counted_tags += count
dist_batch = TagDistributionBatch.objects.create(
parent=parent_batch,
owner_org=assigned_org,
assigner_org=org,
assigned_org=assigned_org,
total_tag_count=total_counted_tags,
distribution_type=parent_batch.distribution_type,
dist_batch_identity=generate_unique_code(
f"{random.randint(1000, 9999)}"
)
)
dist_batch.distributions.add(*distributions)
return {
'tag_distributions': distributions,
'distributions_batch': dist_batch
}
def edit_distribution_from_distribution(
self, org: Organization = None,
tag_batch: TagDistributionBatch = None,
data: dict = None
):
with transaction.atomic():
if tag_batch.assigner_org != org:
raise PermissionDenied("اجازه ویرایش این توزیع را ندارید") # noqa
for dist in tag_batch.distributions.all():
dist.tag.all().update(
status='R',
organization=org
)
old_distributions = tag_batch.distributions.all()
tag_batch.distributions.clear()
old_distributions.delete()
assigned_org = Organization.objects.get(id=data['assigned_org'])
parent_batch = tag_batch.parent
distributions = []
total_counted_tags = 0
for dist_data in data['dists']:
species = dist_data['species_code']
parent_tag_distribution = TagDistribution.objects.get(
id=dist_data['parent_tag_distribution']
)
batch = TagBatch.objects.get(
batch_identity=dist_data.get('batch_identity')
) if dist_data.get('batch_identity') else None
count = dist_data['count']
tags = Tag.objects.filter(
distributions__tag_distribution_batch=parent_batch,
species_code=species,
status='R',
)
if tags.count() < count:
raise TagException(
"پلاک کافی برای این گونه وجود ندارد", # noqa
403
)
dist = TagDistribution.objects.create(
parent=parent_tag_distribution,
batch=batch,
assigner_org=org,
assigned_org=assigned_org,
species_code=species,
total_tag_count=count,
remaining_number=count,
dist_identity=generate_unique_code(
f"{random.randint(1000, 9999)}"
),
)
selected_tags = tags.order_by('create_date')[:count]
dist.tag.add(*selected_tags)
distributions.append(dist)
total_counted_tags += count
# 5⃣ update distribution batch
tag_batch.assigned_org = assigned_org
tag_batch.total_tag_count = total_counted_tags
tag_batch.is_closed = False
tag_batch.save(update_fields=[
'assigned_org',
'total_tag_count',
'is_closed'
])
tag_batch.distributions.add(*distributions)
return {
'tag_distributions': distributions,
'distributions_batch': tag_batch
}
def distribution_batch_main_dashboard(self, org: Organization, is_closed: str = 'false'):
"""
distribution batch main page dashboard detail
"""
is_closed = False if is_closed == 'false' else True
if org.type.key == 'ADM':
distribution_query = (Q(is_closed=is_closed))
else:
distribution_query = (
Q(assigner_org=org) |
Q(assigned_org=org),
Q(is_closed=is_closed)
)
distributions_batch = TagDistributionBatch.objects.prefetch_related(
'distributions'
).filter(distribution_query)
data = distributions_batch.aggregate(
count=Count('id'),
total_sent_tag_count=Coalesce(Sum('total_tag_count', filter=Q(assigner_org=org)), 0),
total_recieved_tag_count=Coalesce(Sum('total_tag_count', filter=Q(assigned_org=org)), 0),
total_recieved_distributions=Count('id', filter=Q(assigned_org=org)),
total_sent_distributions=Count('id', filter=Q(assigner_org=org)),
total_distributed_tag_count=Sum('total_distributed_tag_count'),
remaining_tag_count=Sum('remaining_tag_count'),
)
# distributions item list detail
items_list = []
distributions = TagDistribution.objects.filter(
distribution_query
)
species = LiveStockSpecies.objects.values('value')
for spec in species:
dist_data = distributions.aggregate(
dist_count=Count('id', filter=Q(species_code=spec.get('value'))),
tag_count=Coalesce(
Sum('distributed_number', filter=Q(species_code=spec.get('value'))), 0
)
)
dist_data.update({'species_code': spec.get('value')}) # add species code to data
items_list.append(dist_data)
data.update({'items': items_list})
return data

View File

@@ -0,0 +1,175 @@
import random
from django.db.models import Q
from django.db.models.aggregates import Count
from apps.authentication.models import Organization
from apps.livestock.web.api.v1.serializers import LiveStockSerializer
from apps.tag.exceptions import TagException
from apps.tag.models import Tag, TagBatch
from apps.tag.tools import tag_code_serial_scanning
SPECIES_MAP = {
'cow': 1,
'buffalo': 2,
'camel': 3,
'sheep': 4,
'goat': 5,
}
class TagService:
"""
Different Services of Livestock Tags
"""
def tag_code_compelition(self, tag_data: dict, serial, ownership_code, organization):
"""
complete data of tag
"""
tag_data.update({
'serial': serial,
'ownership_code': ownership_code,
'organization': organization,
'tag_code': f"{tag_data.get('country_code')}"
f"{tag_data.get('static_code')}"
f"{ownership_code}"
f"{tag_data.get('species_code')}"
f"{tag_code_serial_scanning(str(serial))}"
})
return tag_data
def create_tag(
self,
serial_start_range: int = None,
serial_end_range: int = None,
org: Organization = None,
data: dict = None
):
"""
create livestock tag with batch / batch item
"""
# create tag batch
request_number = (serial_end_range - serial_start_range) + 1
batch_identity = f'{serial_start_range}{serial_end_range}{data.get("species_code")}{random.randint(1000, 9999)}'
batch = TagBatch.objects.create(
batch_identity=batch_identity,
organization=org,
request_number=request_number if request_number > 0 else 1,
species_code=data.get('species_code'),
serial_from=serial_start_range,
serial_to=serial_end_range,
total_remaining_tags=request_number if request_number > 0 else 1,
status='created',
)
tag_list = []
while serial_start_range <= serial_end_range:
tag_data = self.tag_code_compelition(
tag_data=data,
serial=serial_start_range,
organization=org,
ownership_code=org.ownership_code
)
tag_list.append(Tag(**tag_data))
if Tag.objects.filter(serial=serial_start_range, species_code=data.get('species_code')).exists():
raise TagException(f' پلاک با مشخصات مورد نظر {serial_start_range} وجود دارد ', status_code=403) # noqa
serial_start_range += 1
created_tags = Tag.objects.bulk_create(tag_list)
batch.tag.add(*created_tags)
return created_tags
def update_batch_tag(
self,
serial_start_range: int = None,
serial_end_range: int = None,
org: Organization = None,
data: dict = None,
batch_id: int = None,
):
"""
update livestock tag with batch / batch item
"""
# update tag batch
request_number = (serial_end_range - serial_start_range) + 1
batch = TagBatch.objects.get(id=batch_id)
batch.request_number = request_number
batch.species_code = data.get('species_code')
batch.serial_from = serial_start_range
batch.serial_to = serial_end_range
batch.total_remaining_tags = request_number
batch.save(update_fields=['request_number', 'species_code', 'serial_from', 'serial_to'])
# recreate tags for batch
tag_list = []
while serial_start_range <= serial_end_range:
tag_data = self.tag_code_compelition(
tag_data=data,
serial=serial_start_range,
organization=org,
ownership_code=org.ownership_code
)
tag_list.append(Tag(**tag_data))
if Tag.objects.filter(
serial=serial_start_range,
species_code=data.get('species_code')
).exists() and not batch.tag.filter(
serial=serial_start_range
).exists():
raise TagException(f' پلاک با مشخصات مورد نظر {serial_start_range} وجود دارد ', status_code=403) # noqa
serial_start_range += 1
created_tags = Tag.objects.bulk_create(tag_list)
# hard delete of created tags for batch
batch.tag.all().delete()
batch.tag.add(*created_tags)
return created_tags
def tag_detail(self, by_id: int = None, by_queryset: bool = False):
"""
get detail of a tag like: livestock, rancher, herd, etc
"""
if by_id:
tag = Tag.objects.prefetch_related(
'livestock_tag'
).get(id=by_id)
livestock = tag.livestock_tag.first()
result = LiveStockSerializer(livestock).data
else:
result = dict()
return result
def tag_dashboard(self):
"""
dashboard of tags page
"""
queryset = Tag.objects.all()
response = queryset.aggregate(
count=Count('id'),
cow_count=Count('id', filter=Q(species_code=1)),
buffalo_count=Count('id', filter=Q(species_code=2)),
camel_count=Count('id', filter=Q(species_code=3)),
sheep_count=Count('id', filter=Q(species_code=4)),
goat_count=Count('id', filter=Q(species_code=5)),
free_count=Count('id', filter=Q(status='F')),
assign_count=Count('id', filter=Q(status='A')),
)
return response

View File

View File

@@ -0,0 +1,75 @@
from django.db.models import Sum
from django.db.models.functions import Coalesce
from django.db.models.signals import m2m_changed
from django.db.models.signals import post_save
from django.dispatch import receiver
from apps.tag.models import TagDistribution, TagDistributionBatch, Tag
@receiver(m2m_changed, sender=TagDistribution.tag.through)
def update_batch_on_distribution_change(
sender, instance: TagDistribution, action, **kwargs
):
if action not in ['post_add', 'post_remove', 'post_clear']:
return
if not instance.batch:
return
if instance.parent:
return
batch = instance.batch
distributions = TagDistribution.objects.filter(batch=batch)
distributed_tags = Tag.objects.filter(
distributions__batch=batch,
status__in=['R', 'A'],
).distinct().count()
print("distributed_tags", distributed_tags)
batch.total_distributed_tags = distributed_tags
batch.total_remaining_tags = (
int(batch.request_number) - distributed_tags
)
batch.status = (
'distributed'
if batch.total_remaining_tags == 0
else 'created'
)
batch.save(update_fields=[
'total_distributed_tags',
'total_remaining_tags',
'status'
])
@receiver(post_save, sender=TagDistributionBatch)
def calculate_tag_distribution_detail(sender, instance: TagDistributionBatch, **kwargs):
"""
calculate distribution & remaining distributed tags
"""
if getattr(instance, 'flag', False):
return
tag_dist_batch = instance
parent = tag_dist_batch.parent
if parent:
parent.total_distributed_tag_count = parent.children.aggregate(
total=Coalesce(Sum('total_tag_count'), 0)
)['total']
parent.remaining_tag_count = (
parent.total_tag_count - parent.total_distributed_tag_count
)
parent.parent_flag = True
parent.save(update_fields=['remaining_tag_count', 'total_distributed_tag_count'])
if not getattr(instance, 'parent_flag', False):
tag_dist_batch.remaining_tag_count = tag_dist_batch.total_tag_count
instance.flag = True
tag_dist_batch.save(update_fields=['remaining_tag_count'])

View File

@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<style>
@page {
size: A4;
margin: 2cm;
}
body {
font-family: DejaVu Sans;
font-size: 12px;
}
h1 {
text-align: center;
margin-bottom: 20px;
}
.meta {
margin-bottom: 20px;
}
.meta div {
margin-bottom: 5px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #333;
padding: 6px;
text-align: center;
}
th {
background: #f0f0f0;
}
.footer {
margin-top: 30px;
font-size: 10px;
text-align: center;
color: #666;
}
</style>
</head>
<body>
<h1>سند توزیع پلاک دام</h1>
<div class="meta">
<div><strong>شناسه توزیع:</strong> {{ batch.dist_batch_identity }}</div>
<div><strong>سازمان تخصیص‌دهنده:</strong> {{ batch.assigner_org.name }}</div>
<div><strong>سازمان دریافت‌کننده:</strong> {{ batch.assigned_org.name }}</div>
<div><strong>تاریخ ایجاد:</strong> {{ batch.create_date }}</div>
<div><strong>تعداد کل پلاک:</strong> {{ batch.total_tag_count }}</div>
</div>
<table>
<thead>
<tr>
<th>کد گونه</th>
<th>از سریال</th>
<th>تا سریال</th>
<th>تعداد کل</th>
<th>باقی‌مانده</th>
</tr>
</thead>
<tbody>
{% for dist in batch.distributions.all %}
<tr>
<td>{{ dist.species_code }}</td>
<td>{{ dist.serial_from }}</td>
<td>{{ dist.serial_to }}</td>
<td>{{ dist.total_tag_count }}</td>
<td>{{ dist.remaining_number }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="footer">
این سند به صورت سیستمی تولید شده و معتبر می‌باشد.
</div>
</body>
</html>

View File

@@ -6,15 +6,20 @@ def tag_code_serial_scanning(serial: str = None) -> typing.AnyStr:
serial code is 8 number serial, serial code is 8 number serial,
set 4 first numbers to 0 set 4 first numbers to 0
""" """
if len(str(serial)) == 4: if len(str(serial)) == 1:
scanned_serial = "000000" + str(serial)
if len(str(serial)) == 2:
scanned_serial = "00000" + str(serial)
if len(str(serial)) == 3:
scanned_serial = "0000" + str(serial) scanned_serial = "0000" + str(serial)
if len(str(serial)) == 5: if len(str(serial)) == 4:
scanned_serial = "000" + str(serial) scanned_serial = "000" + str(serial)
if len(str(serial)) == 6: if len(str(serial)) == 5:
scanned_serial = "00" + str(serial) scanned_serial = "00" + str(serial)
if len(str(serial)) == 7: if len(str(serial)) == 6:
scanned_serial = "0" + str(serial) scanned_serial = "0" + str(serial)
if len(str(serial)) == 7:
scanned_serial = str(serial)
else: else:
pass pass
return scanned_serial return scanned_serial

View File

@@ -1,64 +1,161 @@
from apps.core.mixins.soft_delete_mixin import SoftDeleteMixin import random
from rest_framework import viewsets import typing
from apps.tag import models as tag_models
from django.db import transaction
from rest_framework import status from rest_framework import status
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import APIException
from rest_framework.filters import SearchFilter
from rest_framework.response import Response from rest_framework.response import Response
from apps.authentication.api.v1.api import GeneralOTPViewSet
from apps.authorization import models as authorize_models
from apps.core.api import BaseViewSet
from apps.core.mixins.search_mixin import DynamicSearchMixin
from apps.core.mixins.soft_delete_mixin import SoftDeleteMixin
from apps.tag import exceptions as tag_exceptions
from apps.tag import models as tag_models
from apps.tag.models import TagBatch
from apps.tag.services.tag_batch_service import TagBatchService
from apps.tag.services.tag_distribution_services import TagDistributionService
from apps.tag.services.tag_services import TagService
from common.helpers import get_organization_by_user
from common.liara_tools import upload_to_liara
from common.storage import upload_to_storage
from .serializers import ( from .serializers import (
TagSerializer, TagSerializer,
TagAssignmentSerializer, TagAssignmentSerializer,
AllocatedTagsSerializer AllocatedTagsSerializer, TagBatchSerializer, TagDistributionSerializer, TagDistributionBatchSerializer
) )
from rest_framework.decorators import action
from django.db import transaction
from rest_framework.exceptions import APIException
from apps.tag import permissions as tag_permissions
from apps.authorization import models as authorize_models
from apps.authentication.api.v1.api import GeneralOTPViewSet
from apps.tag.tools import tag_code_serial_scanning
from apps.tag import exceptions as tag_exceptions
from common.helpers import detect_file_extension
from common.liara_tools import upload_to_liara
from django.db import IntegrityError
import typing
def trash(queryset, pk): # from weasyprint import HTML
""" sent object to trash """
obj = queryset.get(id=pk)
obj.trash = True
obj.save()
def delete(queryset, pk): class TagViewSet(BaseViewSet, TagService, SoftDeleteMixin, DynamicSearchMixin, viewsets.ModelViewSet):
""" full delete object """
obj = queryset.get(id=pk)
obj.delete()
class TagViewSet(SoftDeleteMixin, viewsets.ModelViewSet):
""" Tag View Set """ """ Tag View Set """
queryset = tag_models.Tag.objects.all() queryset = tag_models.Tag.objects.all()
serializer_class = TagSerializer serializer_class = TagSerializer
filter_backends = [SearchFilter]
search_fields = [
'serial',
'status',
'tag_code',
'organization__name',
'organization__type__key',
'livestock_tag__herd__rancher__ranching_farm',
'livestock_tag__herd__rancher__first_name',
'livestock_tag__herd__rancher__last_name',
'livestock_tag__herd__rancher__mobile',
'livestock_tag__herd__rancher__national_code',
]
def list(self, request, *args, **kwargs):
"""
list of tags with filter & search
"""
param = self.request.query_params # noqa
queryset = self.get_queryset(visibility_by_org_scope=True).order_by('-create_date')
if 'status' in param.keys():
queryset = queryset.filter(status=param.get('status', None))
# filter queryset
queryset = self.filter_query(queryset)
page = self.paginate_queryset(queryset)
if page is not None: # noqa
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
return Response(self.serializer_class(queryset).data)
@transaction.atomic @transaction.atomic
def create(self, request: object, *args: list, **kwargs: dict) -> typing.Any: def create(self, request: object, *args: list, **kwargs: dict) -> typing.Any:
""" Create tag for livestocks """ # noqa """ Create tag for livestocks """ # noqa
tag_objects = []
serial_start_range, serial_end_range = request.data['serial_range'] # serial_range is like [500, 550]
while serial_start_range <= serial_end_range:
try:
request.data.update({'serial': str(serial_start_range)})
serializer = self.serializer_class(data=request.data)
if serializer.is_valid(raise_exception=True):
tag_objects.append(serializer.save())
except IntegrityError as e: # if tag exists before
if 'unique constraint' in e.args[0]:
return Response("tag exists", status.HTTP_406_NOT_ACCEPTABLE)
serial_start_range += 1
serializer = self.serializer_class(tag_objects, many=True) org = get_organization_by_user(request.user) # noqa
serial_start_range, serial_end_range = request.data.pop('serial_range') # serial_range is like [500, 550]
data = request.data.copy()
# create tag & batch
created_tags = self.create_tag(
serial_start_range=serial_start_range,
serial_end_range=serial_end_range,
data=data,
org=org
)
serializer = self.serializer_class(created_tags, many=True)
return Response(serializer.data, status.HTTP_201_CREATED) return Response(serializer.data, status.HTTP_201_CREATED)
def update(self, request, pk=None, *args, **kwargs):
""" update tag for livestocks """ # noqa
org = get_organization_by_user(request.user) # noqa
serial_start_range, serial_end_range = request.data.pop('serial_range') # serial_range is like [500, 550]
data = request.data.copy()
# create tag & batch
created_tags = self.update_batch_tag(
serial_start_range=serial_start_range,
serial_end_range=serial_end_range,
data=data,
org=org,
batch_id=int(pk)
)
serializer = self.serializer_class(created_tags, many=True)
return Response(serializer.data, status.HTTP_201_CREATED)
@action(
methods=['get'],
detail=True,
url_path='tag_detail',
url_name='tag_detail',
name='tag_detail',
)
def get_tag_detail(self, request, pk=None):
"""
show detail of a tag
"""
response = self.tag_detail(by_id=pk)
return Response(response)
@action(
methods=['get'],
detail=True,
url_name='tags_by_batch',
url_path='tags_by_batch',
name='tags_by_batch',
)
def get_tags_by_batch_id(self, request, pk=None):
"""
get tags by batch id
"""
tags = self.queryset.filter(batches__id=pk)
page = self.paginate_queryset(tags)
if page is not None: # noqa
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
return Response(self.serializer_class(tags).data)
@action(
methods=['get'],
detail=False,
url_path='tag_dashboard',
url_name='tag_dashboard',
name='tag_dashboard'
)
def get_tag_dashboard(self, request):
"""
dashboard of tags page
"""
response = self.tag_dashboard()
return Response(response)
@action( @action(
methods=['get'], methods=['get'],
detail=False, detail=False,
@@ -77,39 +174,8 @@ class TagViewSet(SoftDeleteMixin, viewsets.ModelViewSet):
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(
methods=['put'],
detail=True,
url_path='trash',
url_name='trash',
name='trash',
)
@transaction.atomic
def trash(self, request, pk=None):
""" Sent Tag to trash """
try:
trash(self.queryset, pk)
except APIException as e:
return Response(e, status.HTTP_204_NO_CONTENT)
@action( class TagAssignmentViewSet(BaseViewSet, SoftDeleteMixin, DynamicSearchMixin, viewsets.ModelViewSet):
methods=['post'],
detail=True,
url_name='delete',
url_path='delete',
name='delete'
)
@transaction.atomic
def delete(self, request, pk=None):
""" Full delete of Tag 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 TagAssignmentViewSet(SoftDeleteMixin, viewsets.ModelViewSet):
""" assignment of tags """ """ assignment of tags """
queryset = tag_models.TagAssignment.objects.all() queryset = tag_models.TagAssignment.objects.all()
user_relations_queryset = authorize_models.UserRelations.objects.all() user_relations_queryset = authorize_models.UserRelations.objects.all()
@@ -236,72 +302,503 @@ class TagAssignmentViewSet(SoftDeleteMixin, viewsets.ModelViewSet):
check_response = GeneralOTPViewSet().check_otp(request) check_response = GeneralOTPViewSet().check_otp(request)
if check_response.status_code == 200: if check_response.status_code == 200:
return Response(check_response.status_code, status=status.HTTP_200_OK) return Response(check_response.status_code, status=status.HTTP_200_OK)
else:
return Response(check_response.status_code, status=status.HTTP_403_FORBIDDEN) return Response(check_response.status_code, status=status.HTTP_403_FORBIDDEN)
@action(
methods=['put'],
detail=True,
url_path='trash',
url_name='trash',
name='trash',
)
@transaction.atomic
def trash(self, request, pk=None):
""" Sent TagAssigment 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 TagAssignment object """
try:
delete(self.queryset, pk)
return Response(status=status.HTTP_200_OK) return Response(status=status.HTTP_200_OK)
except APIException as e:
return Response(e, status=status.HTTP_204_NO_CONTENT)
class AllocatedTagsViewSet(SoftDeleteMixin, viewsets.ModelViewSet): class AllocatedTagsViewSet(SoftDeleteMixin, viewsets.ModelViewSet):
queryset = tag_models.AllocatedTags.objects.all() queryset = tag_models.AllocatedTags.objects.all()
serializer_class = AllocatedTagsSerializer serializer_class = AllocatedTagsSerializer
class TagBatchViewSet(BaseViewSet, SoftDeleteMixin, DynamicSearchMixin, TagBatchService, viewsets.ModelViewSet):
queryset = TagBatch.objects.all()
serializer_class = TagBatchSerializer
filter_backends = [SearchFilter]
search_fields = [
"organization__name"
"request_number"
"tag__tag_code"
"species_code"
]
def list(self, request, *args, **kwargs):
"""
list of tag batches
"""
queryset = self.get_queryset(visibility_by_org_scope=True).order_by('-create_date')
params = self.request.query_params # noqa
if params.get('species_code'):
queryset = queryset.filter(species_code=int(params.get('species_code')))
# filter queryset
queryset = self.filter_query(self.filter_queryset(queryset))
page = self.paginate_queryset(queryset)
if page is not None: # noqa
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
return Response(self.serializer_class(queryset).data)
@action( @action(
methods=['put'], methods=['get'],
detail=True, detail=False,
url_path='trash', url_name='main_dashboard',
url_name='trash', url_path='main_dashboard',
name='trash', name='main_dashboard',
) )
@transaction.atomic def main_dashboard(self, request):
def trash(self, request, pk=None): """
""" Sent AllocatedTag to trash """ dashboard of tag batches main page
try: """
trash(self.queryset, pk) org = get_organization_by_user(request.user)
except APIException as e:
return Response(e, status.HTTP_204_NO_CONTENT) dashboard_data = self.tag_batch_main_dashboard(org=org)
return Response(dashboard_data, status=status.HTTP_200_OK)
@action(
methods=['get'],
detail=True,
url_name='inner_dashboard',
url_path='inner_dashboard',
name='inner_dashboard',
)
def inner_dashboard(self, request, pk=None):
"""
dashboard of tag batches inner page by id
"""
org = get_organization_by_user(request.user)
dashboard_data = self.tag_batch_main_dashboard(
org=org,
batch=self.queryset.filter(id=self.get_object().id)
)
return Response(dashboard_data, status=status.HTTP_200_OK)
def destroy(self, request, pk=None, *args, **kwargs):
"""
soft delete batch with tag items
"""
batch = self.get_object()
batch.soft_delete()
for tag in batch.tag.all():
tag.soft_delete()
return Response(status=status.HTTP_200_OK)
class TagDistributionViewSet(
BaseViewSet,
SoftDeleteMixin,
DynamicSearchMixin,
viewsets.ModelViewSet,
TagDistributionService
):
queryset = tag_models.TagDistribution.objects.all()
serializer_class = TagDistributionSerializer
filter_backends = [SearchFilter]
search_fields = [
'batch__batch_identity',
'tag__tag_code',
'assigner_org__name',
'assigned_org__name',
'species_code',
]
def list(self, request, *args, **kwargs):
"""
list of tag distributions
"""
queryset = self.get_queryset(visibility_by_org_scope=True).filter(is_closed=False).order_by('-create_date')
queryset = self.filter_queryset(self.filter_query(queryset))
page = self.paginate_queryset(queryset)
if page is not None: # noqa
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
return Response(self.serializer_class(queryset).data)
def create(self, request, *args, **kwargs):
"""
create tag distributions with batch or without batch in random
"""
org = get_organization_by_user(request.user)
data = request.data.copy()
distribution_data = self.create_distribution_from_batch(
org=org,
data=data
)
serializer = self.serializer_class(distribution_data.get('tag_distributions'), many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
def update(self, request, pk=None, *args, **kwargs):
"""
edit tag distribution with/without batch in random
"""
org = get_organization_by_user(request.user)
data = request.data.copy()
dist_batch = tag_models.TagDistributionBatch.objects.get(id=pk)
distribution_data = self.edit_distribution_from_batch(org=org, data=data, dist_batch=dist_batch)
serializer = self.serializer_class(distribution_data.get('tag_distributions'), many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
@action( @action(
methods=['post'], methods=['post'],
detail=True, detail=True,
url_name='delete', url_path='distribute_distribution',
url_path='delete', url_name='distribute_distribution',
name='delete' name='distribute_distribution',
)
def create_distribute_from_distribution(self, request, pk=None):
"""
distribute from a tag distribution
"""
data = request.data.copy()
org = get_organization_by_user(request.user)
dist_batch = tag_models.TagDistributionBatch.objects.get(id=pk)
distribution_data = self.create_distribution_from_distribution(
org=org,
tag_batch=dist_batch,
data=data
)
serializer = self.serializer_class(distribution_data.get('tag_distributions'), many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
@action(
methods=['put'],
detail=True,
url_path='edit_distribute_distribution',
url_name='edit_distribute_distribution',
name='edit_distribute_distribution',
)
def update_distribute_from_distribution(self, request, pk=None):
"""
update created distribution from distribution
"""
data = request.data.copy()
org = get_organization_by_user(request.user)
dist_batch = tag_models.TagDistributionBatch.objects.get(id=pk)
distribution_data = self.edit_distribution_from_distribution(
org=org,
tag_batch=dist_batch,
data=data
)
serializer = self.serializer_class(distribution_data.get('tag_distributions'), many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
@action(
methods=['get'],
detail=True,
url_name='dist_by_batch',
url_path='dist_by_batch',
name='dist_by_batch'
)
def get_dist_by_batch(self, request, pk=None):
"""
get distributions by batch
"""
batch = tag_models.TagDistributionBatch.objects.get(id=pk)
distributions = batch.distributions.all()
page = self.paginate_queryset(distributions)
if page is not None: # noqa
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
return Response(self.serializer_class(distributions).data)
@action(
methods=['post'],
detail=True,
url_name='close_distribution',
url_path='close_distribution',
name='close_distribution',
)
def close_tag_distribution(self, request, pk=None):
distribution = self.get_object()
distribution.is_closed = True
distribution.save()
return Response(status=status.HTTP_200_OK)
@action(
methods=['get'],
detail=False,
url_path='close_distributions_list',
url_name='close_distributions_list',
name='close_distributions_list',
)
def close_distributions_list(self, request):
"""
list of closed distributions
"""
queryset = self.get_queryset(visibility_by_org_scope=True).filter(is_closed=True).order_by('-create_date')
queryset = self.filter_query(queryset)
page = self.paginate_queryset(queryset)
if page is not None: # noqa
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
return Response(self.serializer_class(queryset).data)
class TagDistributionBatchViewSet(
BaseViewSet,
viewsets.ModelViewSet,
SoftDeleteMixin,
DynamicSearchMixin,
TagDistributionService
):
queryset = tag_models.TagDistributionBatch.objects.all()
serializer_class = TagDistributionBatchSerializer
filter_backends = [SearchFilter]
search_filter = [
'dist_batch_identity',
'assigner_org__name',
'assigned_org__name',
'total_tag_count',
'is_closed',
]
def list(self, request, *args, **kwargs):
"""
list of tag distribution batches
"""
org = get_organization_by_user(request.user)
queryset = self.get_queryset(
visibility_by_org_scope=True
).filter(
is_closed=False,
top_root_distribution=True,
).order_by('-create_date')
if not queryset:
queryset = self.get_queryset(
visibility_by_org_scope=True
).filter(
is_closed=False,
owner_org=org,
top_root_distribution=False,
).order_by('-create_date')
queryset = self.filter_query(self.filter_queryset(queryset))
page = self.paginate_queryset(queryset)
if page is not None: # noqa
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
return Response(self.serializer_class(queryset).data)
def retrieve(self, request, pk=None, *args, **kwargs):
"""
detail of distribution batch
"""
distribution_batch = self.get_object()
serializer = self.serializer_class(distribution_batch)
return Response(serializer.data, status=status.HTTP_200_OK)
@action(
methods=['get'],
detail=True,
url_path='child_list',
url_name='child_list',
name='child_list'
)
def child_list(self, request, pk=None):
"""
list of all child from a tag distribution batch
"""
dist_batch = self.get_object()
queryset = dist_batch.children.all()
page = self.paginate_queryset(queryset)
if page is not None: # noqa
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
return Response(self.serializer_class(queryset).data)
@action(
methods=['post'],
detail=True,
url_name='close_dist_batch',
url_path='close_dist_batch',
name='close_dist_batch',
)
def close_tag_dist_batch(self, request, pk=None):
dist_batch = self.get_object()
# close distribution batch
dist_batch.is_closed = True
dist_batch.save()
dist_batch.distributions.all().update(is_closed=True) # close distributions of batch
for distribute in dist_batch.distributions.all():
distribute.tag.all().update(status='F')
return Response(status=status.HTTP_200_OK)
@action(
methods=['get'],
detail=False,
url_path='closed_tag_dist_batch_list',
url_name='closed_tag_dist_batch_list',
name='closed_tag_dist_batch_list',
)
def close_tag_dist_batch_list(self, request):
"""
list of closed tag distributions batch
"""
queryset = self.get_queryset(visibility_by_org_scope=True).filter(is_closed=True).order_by('create_date')
queryset = self.filter_query(self.filter_queryset(queryset))
page = self.paginate_queryset(queryset)
if page is not None: # noqa
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
return Response(self.serializer_class(queryset).data)
@action(
methods=['post'],
detail=True,
url_name='reactivate_tag_dist_batch',
url_path='reactivate_tag_dist_batch',
name='reactivate_tag_dist_batch',
)
def reactivate_tag_dist_batch(self, request, pk=None):
"""
reactivate canceled distribution batch
"""
dist_batch = self.get_object()
dist_batch.is_closed = False
dist_batch.save(update_fields=['is_closed']) #
return Response(status=status.HTTP_200_OK)
@action(
methods=['get'],
detail=False,
url_path='main_dashboard',
url_name='main_dashboard',
name='main_dashboard'
)
def main_dashboard(self, request):
"""
dashboard of main page
"""
org = get_organization_by_user(request.user)
params = self.request.query_params # noqa
dashboard_data = self.distribution_batch_main_dashboard(org=org, is_closed=params.get('is_closed'))
return Response(dashboard_data, status=status.HTTP_200_OK)
# @action(
# methods=['get'],
# detail=True,
# url_path='distribution_pdf_view',
# url_name='distribution_pdf_view',
# name='distribution_pdf_view',
# )
# def distribution_pdf_view(self, request, pk=None):
# batch = tag_models.TagDistributionBatch.objects.select_related(
# 'assigner_org', 'assigned_org'
# ).prefetch_related('distributions').get(id=pk)
#
# html_string = render_to_string(
# 'pdf/tag_distribution.html', # noqa
# {'batch': batch}
# )
#
# html = HTML(
# string=html_string,
# base_url=request.build_absolute_uri('/')
# )
#
# pdf = html.write_pdf()
#
# response = HttpResponse(pdf, content_type='application/pdf')
# response['Content-Disposition'] = (
# f'inline; filename="distribution_{batch.dist_batch_identity}.pdf"'
# )
#
# return response
@action(
methods=['post', ],
detail=True,
url_name='assign_document',
url_path='assign_document',
name='assign_document'
) )
@transaction.atomic @transaction.atomic
def delete(self, request, pk=None): def assign_document(self, request, pk=None):
""" Full delete of AllocatedTag object """ """ set document for tag assignment """
try:
delete(self.queryset, pk) # get tag assignment object & set document url
dist_batch = self.queryset.get(id=pk)
# upload document file to liara storage
document = request.FILES.get('dist_exit_document')
document_url = upload_to_storage(
document,
f'{random.randint(1000, 9999)}_distribution_batch_document.{str(document).split(".")[1]}'
)
dist_batch.warehouse_exit_doc = document_url
dist_batch.save(update_fields=['warehouse_exit_doc'])
serializer = self.serializer_class(dist_batch)
return Response(serializer.data, status=status.HTTP_200_OK)
@action(
methods=['post'],
detail=True,
url_path='accept_exit_doc',
url_name='accept_exit_doc',
name='accept_exit_doc',
)
def accept_exit_doc(self, request, pk=None):
"""
accept exit document from warehouse on distribution batch
"""
dist_batch = self.get_object()
dist_batch.exit_doc_status = True
dist_batch.save(update_fields=['exit_doc_status'])
return Response(status=status.HTTP_200_OK)
def destroy(self, request, pk=None, *args, **kwargs):
"""
delete tag distribution batch and free their tag from distribute
"""
dist_batch = self.get_object()
for distribute in dist_batch.distributions.all():
distribute.tag.all().update(status='F')
distribute.tag.clear()
distribute.soft_delete()
dist_batch.soft_delete()
return Response(status=status.HTTP_200_OK) return Response(status=status.HTTP_200_OK)
except APIException as e:
return Response(e, status=status.HTTP_204_NO_CONTENT)

View File

@@ -1,6 +1,9 @@
from apps.authentication.api.v1.serializers import serializer as auth_serializers
from rest_framework import serializers from rest_framework import serializers
from apps.authentication.api.v1.serializers import serializer as auth_serializers
from apps.tag import models as tag_models from apps.tag import models as tag_models
from apps.tag.exceptions import TagException
from apps.tag.models import TagBatch
class TagSerializer(serializers.ModelSerializer): class TagSerializer(serializers.ModelSerializer):
@@ -25,6 +28,18 @@ class TagSerializer(serializers.ModelSerializer):
} }
} }
def validate(self, attrs):
"""
Some validation on tags serialization
"""
species_code = attrs.get('species_code', None)
serial_range = attrs.get('serial_range', None)
if self.Meta.model.objects.filter(species_code=species_code, serial__in=serial_range).exists():
raise TagException(f'پلاک با سریال مد نظر از قبل وجود دارد', status_code=403) # noqa
return attrs
def update(self, instance, validated_data): def update(self, instance, validated_data):
""" update tag information """ """ update tag information """
@@ -58,7 +73,11 @@ class TagSerializer(serializers.ModelSerializer):
""" Customize output of serializer """ """ Customize output of serializer """
representation = super().to_representation(instance) representation = super().to_representation(instance)
if isinstance(instance, tag_models.Tag): if isinstance(instance, tag_models.Tag):
representation['organization'] = auth_serializers.OrganizationSerializer(instance.organization).data if instance.organization:
representation['organization'] = {
'name': instance.organization.name,
'id': instance.organization.id
}
return representation return representation
@@ -107,3 +126,89 @@ class AllocatedTagsSerializer(serializers.ModelSerializer):
representation['status'] = instance.status representation['status'] = instance.status
return representation return representation
class TagBatchSerializer(serializers.ModelSerializer):
class Meta:
model = TagBatch
fields = '__all__'
def to_representation(self, instance):
representation = super().to_representation(instance)
representation['organization'] = {
'id': instance.organization.id,
'name': instance.organization.name
}
representation['tag'] = [{
'tag_code': tag.tag_code,
'species_code': tag.species_code,
'status': tag.status
} for tag in instance.tag.all()]
return representation
class TagDistributionSerializer(serializers.ModelSerializer):
class Meta:
model = tag_models.TagDistribution
fields = '__all__'
def to_representation(self, instance):
"""
customize output of serializer
"""
representation = super().to_representation(instance)
if instance.batch:
representation['batch'] = {
'id': instance.batch.id,
'batch_creator': instance.batch.organization.name,
'batch_identity': instance.batch.batch_identity
}
representation['assigner_org'] = {
'id': instance.assigner_org.id,
'name': instance.assigner_org.name
}
representation['assigned_org'] = {
'id': instance.assigned_org.id,
'name': instance.assigned_org.name
}
return representation
class TagDistributionBatchSerializer(serializers.ModelSerializer):
class Meta:
model = tag_models.TagDistributionBatch
fields = '__all__'
def to_representation(self, instance):
representation = super().to_representation(instance)
representation['assigner_org'] = {
'id': instance.assigner_org.id,
'name': instance.assigner_org.name,
}
representation['assigned_org'] = {
'id': instance.assigned_org.id,
'name': instance.assigned_org.name
}
representation['distributions'] = [{
'id': dist.id,
'dist_identity': dist.dist_identity,
'batch_identity': dist.batch.batch_identity if dist.batch else None,
'species_code': dist.species_code,
'distributed_number': dist.distributed_number,
'total_tag_count': dist.total_tag_count,
'remaining_number': dist.remaining_number,
'serial_from': dist.batch.serial_from if dist.batch else None,
'serial_to': dist.batch.serial_to if dist.batch else None,
} for dist in instance.distributions.all()]
return representation

View File

@@ -1,15 +1,19 @@
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 .api import ( from .api import (
TagViewSet, TagViewSet,
TagAssignmentViewSet, TagAssignmentViewSet,
AllocatedTagsViewSet AllocatedTagsViewSet, TagBatchViewSet, TagDistributionViewSet, TagDistributionBatchViewSet
) )
router = DefaultRouter() router = DefaultRouter()
router.register(r'tag', TagViewSet, basename='tag') router.register(r'tag', TagViewSet, basename='tag')
router.register(r'tag_assignment', TagAssignmentViewSet, basename='tag_assignment') router.register(r'tag_assignment', TagAssignmentViewSet, basename='tag_assignment')
router.register(r'allocated_tag', AllocatedTagsViewSet, basename='allocated_tag') router.register(r'allocated_tag', AllocatedTagsViewSet, basename='allocated_tag')
router.register(r'tag_batch', TagBatchViewSet, basename='tag_batch')
router.register(r'tag_distribution', TagDistributionViewSet, basename='tag_distribution')
router.register(r'tag_distribution_batch', TagDistributionBatchViewSet, basename='tag_distribution_batch')
urlpatterns = [ urlpatterns = [
path('v1/', include(router.urls)) path('v1/', include(router.urls))

View File

@@ -3,11 +3,14 @@ from io import BytesIO
from django.http import HttpResponse from django.http import HttpResponse
from openpyxl import Workbook from openpyxl import Workbook
from openpyxl.styles import Font
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.decorators import action from rest_framework.decorators import action
from apps.authentication.services.visibility_services import apply_visibility_filter from apps.authentication.services.visibility_services import apply_visibility_filter
from apps.core.mixins.search_mixin import ExcelDynamicSearchMixin
from apps.warehouse import models as warehouse_models from apps.warehouse import models as warehouse_models
from apps.warehouse.web.api.v1 import serializers as warehouse_serializers
from common.helper_excel import create_header, excel_description, create_header_freez, create_value, shamsi_date, \ from common.helper_excel import create_header, excel_description, create_header_freez, create_value, shamsi_date, \
convert_str_to_date convert_str_to_date
from common.helpers import get_organization_by_user from common.helpers import get_organization_by_user
@@ -403,10 +406,118 @@ def calculate_share_totals(items):
# return response # return response
class WareHouseExcelViewSet(viewsets.ViewSet): class WareHouseExcelViewSet(viewsets.ModelViewSet, ExcelDynamicSearchMixin):
""" queryset = warehouse_models.InventoryEntry.objects.all()
بهینه شده: اکسل تراکنش‌ها serializer_class = warehouse_serializers.InventoryEntrySerializer
""" search_fields = [
"distribution__distribution_id",
"organization__name",
"weight",
"balance",
"lading_number",
"is_confirmed",
]
date_field = "create_date"
@action(
methods=['get'],
detail=False,
url_path='warehouse_excel',
url_name='warehouse_excel',
name='warehouse_excel'
)
def warehouse_excel(self, request):
output = BytesIO()
workbook = Workbook()
worksheet = workbook.active
worksheet.sheet_view.rightToLeft = True
worksheet.insert_rows(1)
queryset = self.filter_query(self.queryset)
entries = queryset.filter(organization=get_organization_by_user(request.user))
ser_data = self.serializer_class(entries, many=True).data
excel_options = [
"ردیف",
"تاریخ ورود به انبار",
"شماره سهمیه",
"وزن",
"بارنامه",
"محل دریافت",
"سند",
"توضیحات",
]
header_list = [
"وزن",
]
create_header(worksheet, header_list, 5, 2, height=25, border_style='thin')
excel_description(worksheet, 'B1', f'ورودی به انبار', row2='C3')
create_header_freez(worksheet, excel_options, 1, 6, 7, height=25, width=20)
l = 6
m = 1
if ser_data:
for data in ser_data:
document = data.get('document')
if document:
if str(document).startswith(('http://', 'https://')):
document_value = f'=HYPERLINK("{document}", "دانلود")'
else:
full_path = f"https://yourdomain.com/{document}"
document_value = f'=HYPERLINK("{full_path}", "دانلود")'
else:
document_value = 'ندارد'
list1 = [
m,
str(shamsi_date(convert_str_to_date(data['create_date']), in_value=True)) if data.get(
'create_date') else '',
str(data[
'distribution'].get('distribution')) or '-',
data.get('weight') or 0,
data.get('lading_number') or '-',
data.get('delivery_address') or '-',
document_value,
data.get('notes') or '',
]
create_value(worksheet, list1, l + 1, 1, height=23, m=m)
if document:
worksheet.cell(row=l + 1, column=7).font = Font(color="0563C1", underline='single', bold=True)
m += 1
l += 1
weight = sum((data['weight'] or 0) for data in ser_data)
value_list = [
weight
]
create_value(worksheet, value_list, 3, 5, border_style='thin')
list2 = [
'مجموع==>',
'',
'',
weight,
'',
'',
'',
''
]
create_value(worksheet, list2, l + 3, 1, color='gray', height=23)
workbook.save(output)
output.seek(0)
response = HttpResponse(
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
response[
'Content-Disposition'] = f'attachment; filename="ورودی به انبار.xlsx"'.encode(
'utf-8')
response.write(output.getvalue())
return response
@action( @action(
methods=['get'], methods=['get'],
@@ -415,6 +526,14 @@ class WareHouseExcelViewSet(viewsets.ViewSet):
url_name='inventory_sale_transaction_excel', url_name='inventory_sale_transaction_excel',
) )
def inventory_sale_transaction_excel(self, request): def inventory_sale_transaction_excel(self, request):
self.search_fields = [
'rancher_fullname', 'rancher_mobile', 'pos_device__device_identity',
'pos_device__acceptor', 'pos_device__terminal', 'pos_device__serial',
'transaction_id', 'seller_organization__name',
'quota_distribution__distribution_id', 'weight', 'delivery_address', 'transaction_price',
'price_paid', 'price_type', 'product_type', 'transactions_number', 'transaction_status',
'transaction_status_code', 'ref_num', 'terminal', 'payer_cart', 'transaction_date',
]
output = BytesIO() output = BytesIO()
workbook = Workbook() workbook = Workbook()
worksheet = workbook.active worksheet = workbook.active
@@ -425,13 +544,13 @@ class WareHouseExcelViewSet(viewsets.ViewSet):
queryset = warehouse_models.InventoryQuotaSaleTransaction.objects.all() queryset = warehouse_models.InventoryQuotaSaleTransaction.objects.all()
queryset = apply_visibility_filter(queryset, org) queryset = apply_visibility_filter(queryset, org)
# فیلتر وضعیت
status_param = request.query_params.get('status') status_param = request.query_params.get('status')
if status_param in ['waiting', 'success', 'failed']: if status_param in ['waiting', 'success', 'failed']:
queryset = queryset.filter(transaction_status=status_param) queryset = queryset.filter(transaction_status=status_param)
queryset = queryset.order_by('-create_date') queryset = queryset.order_by('-create_date')
# فقط فیلدهای مورد نیاز مستقیم از DB queryset = self.filter_query(queryset).order_by('create_date')
qs_values = queryset.values( qs_values = queryset.values(
'id', 'id',
'transaction_id', 'transaction_id',
@@ -447,7 +566,6 @@ class WareHouseExcelViewSet(viewsets.ViewSet):
transaction_ids = [t['id'] for t in qs_values] transaction_ids = [t['id'] for t in qs_values]
# همه آیتم‌ها برای aggregate share totals
items_qs = warehouse_models.InventoryQuotaSaleItem.objects.filter( items_qs = warehouse_models.InventoryQuotaSaleItem.objects.filter(
transaction_id__in=transaction_ids transaction_id__in=transaction_ids
).values( ).values(
@@ -459,7 +577,6 @@ class WareHouseExcelViewSet(viewsets.ViewSet):
'unit' 'unit'
) )
# Aggregate share totals per transaction
transaction_shares_map = defaultdict(list) transaction_shares_map = defaultdict(list)
share_names_set = set() share_names_set = set()
for item in items_qs: for item in items_qs:
@@ -469,7 +586,6 @@ class WareHouseExcelViewSet(viewsets.ViewSet):
share_names = list(share_names_set) share_names = list(share_names_set)
# تنظیمات اکسل
excel_options = [ excel_options = [
"ردیف", "ردیف",
"تعاونی دامدار", "تعاونی دامدار",
@@ -520,19 +636,16 @@ class WareHouseExcelViewSet(viewsets.ViewSet):
t_items = transaction_shares_map.get(t['id'], []) t_items = transaction_shares_map.get(t['id'], [])
products_str = '، '.join([i['name'] for i in t_items]) if t_items else '-' products_str = '، '.join([i['name'] for i in t_items]) if t_items else '-'
# جمع share totals هر تراکنش
share_values = [] share_values = []
for share_name in share_names: for share_name in share_names:
# print(t_items, '\n', share_name)
for i in t_items: for i in t_items:
share_total = sum( share_total = sum(
share['price'] for share in i['item_share'] if share['name'] == share_name) share['price'] for share in i['item_share'] if share['name'] == share_name)
share_values.append(share_total) share_values.append(share_total)
share_column_totals[share_name] += share_total share_column_totals[share_name] += share_total
# جمع کل
total_price += t['price_paid'] or 0 total_price += t['price_paid'] or 0
total_weight += sum(i['unit_price'] or 0 for i in t_items) # یا وزن واقعی اگر موجود باشه total_weight += sum(i['unit_price'] or 0 for i in t_items)
all_weight += total_weight all_weight += total_weight
list1 = [ list1 = [
@@ -545,9 +658,9 @@ class WareHouseExcelViewSet(viewsets.ViewSet):
str(shamsi_date(t['transaction_date'], in_value=True)) if t.get('transaction_date') else '', str(shamsi_date(t['transaction_date'], in_value=True)) if t.get('transaction_date') else '',
products_str, products_str,
t.get('transaction_id', '-'), t.get('transaction_id', '-'),
'-', # شماره کارت اگر موجود باشه '-', # card
t.get('price_paid', 0), t.get('price_paid', 0),
f'{total_weight}کیلوگرم', # ماهیت وزن f'{total_weight}کیلوگرم',
TRANSACTION_STATUS_MAP.get(t.get('transaction_status'), '-'), TRANSACTION_STATUS_MAP.get(t.get('transaction_status'), '-'),
] ]
list1.extend(share_values) list1.extend(share_values)

View File

@@ -12,7 +12,14 @@ from apps.warehouse.models import InventoryQuotaSaleTransaction, InventoryQuotaS
class TransactionDashboardService: class TransactionDashboardService:
@staticmethod @staticmethod
def get_dashboard(org: Organization, start_date: str = None, end_date: str = None, status: str = None): def get_dashboard(
org: Organization,
free_visibility_tr_objects=None,
free_visibility_tr_item_objects=None,
start_date: str = None,
end_date: str = None,
status: str = None
):
orgs_child = get_all_org_child(org=org) orgs_child = get_all_org_child(org=org)
orgs_child.append(org) orgs_child.append(org)
@@ -22,11 +29,16 @@ class TransactionDashboardService:
items = InventoryQuotaSaleItem.objects.all().select_related("gov_product", "free_product") items = InventoryQuotaSaleItem.objects.all().select_related("gov_product", "free_product")
else:
if free_visibility_tr_objects:
transactions = free_visibility_tr_objects
items = InventoryQuotaSaleItem.objects.filter(
transaction__in=transactions
).select_related("gov_product", "free_product")
else: else:
transactions = InventoryQuotaSaleTransaction.objects.filter( transactions = InventoryQuotaSaleTransaction.objects.filter(
seller_organization__in=orgs_child seller_organization__in=orgs_child
) )
items = InventoryQuotaSaleItem.objects.filter( items = InventoryQuotaSaleItem.objects.filter(
transaction__seller_organization__in=orgs_child transaction__seller_organization__in=orgs_child
).select_related("gov_product", "free_product") ).select_related("gov_product", "free_product")

View File

@@ -280,12 +280,30 @@ class InventoryQuotaSaleTransactionViewSet(
transaction_status = query_param.get('status') if 'status' in query_param.keys() else None transaction_status = query_param.get('status') if 'status' in query_param.keys() else None
org = get_organization_by_user(request.user) org = get_organization_by_user(request.user)
if org.free_visibility_by_scope:
tr_objects = self.get_queryset(visibility_by_org_scope=True)
tr_item_view = InventoryQuotaSaleItemViewSet()
tr_item_view.request = request
tr_item_view.kwargs = {'pk': None}
tr_item_objects = tr_item_view.get_queryset(visibility_by_org_scope=True)
transaction_dashboard_data = self.get_dashboard(
org,
free_visibility_tr_objects=tr_objects,
free_visibility_tr_item_objects=tr_item_objects,
start_date=start_date,
end_date=end_date,
status=transaction_status,
)
else:
# filer by date & transaction status # filer by date & transaction status
transaction_dashboard_data = self.get_dashboard( transaction_dashboard_data = self.get_dashboard(
org, org,
start_date=start_date, start_date=start_date,
end_date=end_date, end_date=end_date,
status=transaction_status status=transaction_status,
) )
return Response(transaction_dashboard_data, status=status.HTTP_200_OK) return Response(transaction_dashboard_data, status=status.HTTP_200_OK)

View File

@@ -1,4 +1,5 @@
import base64 import base64
import random
from datetime import datetime from datetime import datetime
from functools import lru_cache from functools import lru_cache
@@ -56,3 +57,10 @@ def parse_birthdate(jalali_str):
gregorian_dt, gregorian_dt,
timezone.get_current_timezone() timezone.get_current_timezone()
) )
def generate_unique_code(prefix: str):
now = timezone.now()
date_part = now.strftime("%Y%m%d")
rand_part = random.randint(100000, 999999)
return f"{prefix}{date_part}{rand_part}"

View File

@@ -1,4 +1,31 @@
import boto3
from botocore.exceptions import NoCredentialsError
STORAGE_ENDPOINT = 'https://s3.rasadyar.com/rasaddam' STORAGE_ENDPOINT = 'https://s3.rasadyar.com/rasaddam'
STORAGE_BUCKET_NAME = 'ticket-rasadyar' STORAGE_BUCKET_NAME = 'ticket-rasadyar'
STORAGE_ACCESS_KEY = "zG3ewsbYsTqCmuws" STORAGE_ACCESS_KEY = "zG3ewsbYsTqCmuws"
STORAGE_SECRET_KEY = 'RInUMB78zlQZp6CNf8+sRoSh2cNDHcGQhXrLnTJ1AuI=' STORAGE_SECRET_KEY = 'RInUMB78zlQZp6CNf8+sRoSh2cNDHcGQhXrLnTJ1AuI='
def upload_to_storage(file_obj, file_name):
try:
s3 = boto3.client(
's3',
endpoint_url=STORAGE_ENDPOINT,
aws_access_key_id=STORAGE_ACCESS_KEY,
aws_secret_access_key=STORAGE_SECRET_KEY
)
s3.upload_fileobj(
file_obj,
STORAGE_BUCKET_NAME,
file_name,
ExtraArgs={'ACL': 'public-read'} # دسترسی عمومی
)
return f"{STORAGE_ENDPOINT}/{STORAGE_BUCKET_NAME}/{file_name}"
except NoCredentialsError:
raise Exception("اعتبارنامه‌های AWS معتبر نیستند")
except Exception as e:
raise Exception(f"خطا در آپلود فایل: {e}")

View File

@@ -84,3 +84,4 @@ channels_redis
daphne daphne
django-jazzmin django-jazzmin
python-dotenv python-dotenv
weasyprint