add - device owner province in login pos device
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.db.models import Count, Min, Max
|
|
||||||
|
|
||||||
logger = logging.getLogger("merge_duplicate_ranchers")
|
logger = logging.getLogger("merge_duplicate_ranchers")
|
||||||
handler = logging.StreamHandler()
|
handler = logging.StreamHandler()
|
||||||
@@ -12,177 +11,6 @@ logger.setLevel(logging.INFO)
|
|||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = "Merge duplicate ranchers by national_code, reassign their herds and delete duplicates."
|
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
|
||||||
parser.add_argument(
|
|
||||||
'--dry-run',
|
|
||||||
action='store_true',
|
|
||||||
help='Run without making DB changes (just report what would change).'
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--apply',
|
|
||||||
action='store_true',
|
|
||||||
help='Apply changes to the database (perform updates and deletes).'
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--keep-strategy',
|
|
||||||
choices=['min_id', 'max_id', 'latest_updated'],
|
|
||||||
default='min_id',
|
|
||||||
help='Which rancher record to keep among duplicates (default: min_id).'
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--min-duplicates',
|
|
||||||
type=int,
|
|
||||||
default=2,
|
|
||||||
help='Only consider national_code groups with at least this many records (default=2).'
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--batch-size',
|
|
||||||
type=int,
|
|
||||||
default=200,
|
|
||||||
help='Process this many duplicate groups per DB transaction (default=200).'
|
|
||||||
)
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
dry_run = options['dry_run']
|
pass
|
||||||
apply_changes = options['apply']
|
|
||||||
keep_strategy = options['keep_strategy']
|
|
||||||
min_duplicates = options['min_duplicates']
|
|
||||||
batch_size = options['batch_size']
|
|
||||||
|
|
||||||
if not dry_run and not apply_changes:
|
|
||||||
self.stdout.write(self.style.ERROR(
|
|
||||||
"Specify --dry-run to preview or --apply to actually perform changes."
|
|
||||||
))
|
|
||||||
return
|
|
||||||
|
|
||||||
self.stdout.write(self.style.NOTICE("Collecting duplicate ranchers by national_code..."))
|
|
||||||
|
|
||||||
# Import مدلها لوکالی تا جلوگیری از circular imports
|
|
||||||
from apps.herd.models import Rancher as RancherModel # adjust import path
|
|
||||||
from apps.herd.models import Herd as HerdModel # adjust import path
|
|
||||||
|
|
||||||
# 1) پیدا کردن national_code هایی که duplicate هستن
|
|
||||||
dup_qs = RancherModel.objects.values('national_code').annotate(
|
|
||||||
cnt=Count('id'),
|
|
||||||
min_id=Min('id'),
|
|
||||||
max_id=Max('id'),
|
|
||||||
).filter(cnt__gte=min_duplicates)
|
|
||||||
|
|
||||||
total_groups = dup_qs.count()
|
|
||||||
self.stdout.write(self.style.SUCCESS(f"Found {total_groups} duplicated national_code groups."))
|
|
||||||
|
|
||||||
groups = list(dup_qs)
|
|
||||||
processed = 0
|
|
||||||
errors = 0
|
|
||||||
|
|
||||||
def choose_keep_id(group):
|
|
||||||
if keep_strategy == 'min_id':
|
|
||||||
return group['min_id']
|
|
||||||
elif keep_strategy == 'max_id':
|
|
||||||
return group['max_id']
|
|
||||||
elif keep_strategy == 'latest_updated':
|
|
||||||
# we'll fetch it explicitly later (fallback to min_id)
|
|
||||||
return None
|
|
||||||
return group['min_id']
|
|
||||||
|
|
||||||
# پردازش گروهها به صورت batch
|
|
||||||
for i in range(0, len(groups), batch_size):
|
|
||||||
batch = groups[i:i + batch_size]
|
|
||||||
if dry_run:
|
|
||||||
self.stdout.write(
|
|
||||||
self.style.WARNING(f"Dry-run: processing batch {i // batch_size + 1} ({len(batch)} groups)"))
|
|
||||||
else:
|
|
||||||
self.stdout.write(self.style.WARNING(f"Applying batch {i // batch_size + 1} ({len(batch)} groups)"))
|
|
||||||
|
|
||||||
# هر batch رو توی یک تراکنش میکنیم
|
|
||||||
try:
|
|
||||||
with transaction.atomic():
|
|
||||||
for g in batch:
|
|
||||||
national_code = g['national_code']
|
|
||||||
self.stdout.write(f"-- handling national_code={national_code}")
|
|
||||||
|
|
||||||
# fetch all ranchers for this national_code
|
|
||||||
ranchers = list(RancherModel.objects.filter(national_code=national_code).order_by('id'))
|
|
||||||
if len(ranchers) < min_duplicates:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# انتخاب رکورد نگهداری شده
|
|
||||||
keep_id = choose_keep_id(g)
|
|
||||||
if keep_id is None and keep_strategy == 'latest_updated':
|
|
||||||
# پیدا کردن رکورد با آخرین updated_at یا created_at
|
|
||||||
ordered = RancherModel.objects.filter(national_code=national_code).order_by('-updated_at',
|
|
||||||
'-id')
|
|
||||||
keep = ordered.first()
|
|
||||||
if not keep:
|
|
||||||
self.stderr.write(f"Couldn't determine keep record for {national_code}")
|
|
||||||
continue
|
|
||||||
keep_id = keep.id
|
|
||||||
else:
|
|
||||||
# اگه choose_keep_id برگشت None (نباید) از min_id استفاده میکنیم
|
|
||||||
if keep_id is None:
|
|
||||||
keep_id = g['min_id']
|
|
||||||
|
|
||||||
# آمادهسازی لیست ids برای حذف (به جز keep_id)
|
|
||||||
all_ids = [r.id for r in ranchers]
|
|
||||||
remove_ids = [rid for rid in all_ids if rid != keep_id]
|
|
||||||
|
|
||||||
self.stdout.write(f" keep_id={keep_id} remove_ids={remove_ids}")
|
|
||||||
|
|
||||||
# اگر dry-run، فقط گزارش میدیم
|
|
||||||
if dry_run:
|
|
||||||
# آماری از گلهها
|
|
||||||
herd_count_keep = HerdModel.objects.filter(rancher_id=keep_id).count()
|
|
||||||
herd_count_remove = HerdModel.objects.filter(rancher_id__in=remove_ids).count()
|
|
||||||
self.stdout.write(
|
|
||||||
f" [DRY] keep_has_herds={herd_count_keep} remove_has_herds={herd_count_remove}"
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# --- حالت apply: lock رکوردها و انجام تغییرات ---
|
|
||||||
# قفل رکورد(rancher)ها برای جلوگیری از race condition
|
|
||||||
RancherModel.objects.select_for_update().filter(id__in=all_ids)
|
|
||||||
|
|
||||||
# 1) انتقال herds از رکوردهای remove_ids به keep_id
|
|
||||||
updated = HerdModel.objects.filter(rancher_id__in=remove_ids).update(rancher_id=keep_id)
|
|
||||||
self.stdout.write(self.style.SUCCESS(f" moved {updated} herds to rancher {keep_id}"))
|
|
||||||
|
|
||||||
# 2) در صورت نیاز: اگر بخواهیم فیلدهای عددی در rancher را جمع کنیم
|
|
||||||
# مثال: total_weight و animal_count (در صورت وجود)
|
|
||||||
numeric_fields = []
|
|
||||||
for f in ['total_weight', 'animal_count', 'some_other_numeric_field']:
|
|
||||||
if hasattr(RancherModel, f):
|
|
||||||
numeric_fields.append(f)
|
|
||||||
|
|
||||||
if numeric_fields:
|
|
||||||
# Aggregate sums from all involved ranchers (including keep)
|
|
||||||
agg = RancherModel.objects.filter(id__in=all_ids).aggregate(
|
|
||||||
**{f: Sum(F(f)) for f in numeric_fields}
|
|
||||||
)
|
|
||||||
# prepare update dict
|
|
||||||
update_data = {f: agg[f] or 0 for f in numeric_fields}
|
|
||||||
|
|
||||||
# update keep record
|
|
||||||
RancherModel.objects.filter(id=keep_id).update(**update_data)
|
|
||||||
self.stdout.write(
|
|
||||||
self.style.SUCCESS(f" aggregated numeric fields on keep {keep_id}: {update_data}"))
|
|
||||||
|
|
||||||
# 3) حذف رکوردهای remove_ids
|
|
||||||
del_q = RancherModel.objects.filter(id__in=remove_ids)
|
|
||||||
count_del = del_q.count()
|
|
||||||
del_q.delete()
|
|
||||||
self.stdout.write(self.style.SUCCESS(f" deleted {count_del} duplicate rancher records"))
|
|
||||||
|
|
||||||
processed += 1
|
|
||||||
|
|
||||||
# پایان تراکنش برای batch
|
|
||||||
except Exception as e:
|
|
||||||
errors += 1
|
|
||||||
logger.exception(f"Error processing batch starting at index {i}: {e}")
|
|
||||||
# در حالت apply اگر خطا داشتیم تراکنش roll back شده و ادامه میدیم به batch بعدی
|
|
||||||
continue
|
|
||||||
|
|
||||||
self.stdout.write(self.style.SUCCESS(f"Done. Processed groups: {processed}, errors: {errors}"))
|
|
||||||
self.stdout.write(
|
|
||||||
self.style.NOTICE("IMPORTANT: If you ran with --apply, verify data integrity and related FK constraints."))
|
|
||||||
|
|||||||
@@ -105,7 +105,8 @@ class POSDeviceViewSet(viewsets.ModelViewSet, POSDeviceMixin):
|
|||||||
'en_name': device_owner_org.en_name,
|
'en_name': device_owner_org.en_name,
|
||||||
'phone': device_owner_org.phone,
|
'phone': device_owner_org.phone,
|
||||||
'type': device_owner_org.type.name,
|
'type': device_owner_org.type.name,
|
||||||
'shaba': device_owner_org.bank_information.all().first().sheba # noqa
|
'shaba': device_owner_org.bank_information.all().first().sheba, # noqa
|
||||||
|
'province': device_owner_org.province.name,
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response({
|
return Response({
|
||||||
|
|||||||
Reference in New Issue
Block a user