Files
crmApi/apps/customer/management/commands/audit_mobile_duplicates.py
T
2026-10-08 18:09:12 +03:30

101 lines
4.4 KiB
Python

"""Read-only diagnostics for the legacy mobile uniqueness migration."""
import json
from collections import defaultdict
from importlib import import_module
from django.core.management.base import BaseCommand, CommandError
from django.db import connections
from apps.cuser.cuser_model import CUser
from apps.customer.models import Customer
canonical_mobile = import_module(
'apps.customer.migrations.0008_customer_mobile_unique'
).canonical_mobile
def _duplicate_groups(model, alias, per_workspace=False):
"""Use the migration's exact comparison, including country-prefix collisions."""
groups = defaultdict(list)
fields = ['pk', 'mobile'] + (['sys_ws_id'] if per_workspace else [])
for row in model.objects.using(alias).order_by('pk').values(*fields).iterator():
mobile = canonical_mobile(row['mobile'])
if mobile is None:
continue
key = (row['sys_ws_id'], mobile) if per_workspace else (None, mobile)
groups[key].append(row['pk'])
return [
{'workspace_id': workspace, 'canonical_mobile': mobile, 'record_ids': ids}
for (workspace, mobile), ids in groups.items() if len(ids) > 1
]
def _related_counts(record, alias):
"""Count direct foreign-key dependents without reading their private contents."""
counts = {}
for relation in record._meta.related_objects:
if relation.one_to_many or relation.one_to_one:
label = f'{relation.related_model._meta.label}.{relation.field.name}'
counts[label] = relation.related_model._default_manager.using(alias).filter(
**{relation.field.attname: record.pk}
).count()
return counts
def _customer_details(ids, alias):
rows = Customer.objects.using(alias).filter(pk__in=ids).select_related(
'sys_ws', 'cuser_role__cuser', 'cuser_role__role_type'
).order_by('pk')
details = []
for row in rows:
role = row.cuser_role
details.append({
'id': row.pk, 'name_family': row.name_family,
'mobile': row.mobile, 'canonical_mobile': canonical_mobile(row.mobile),
'workspace_id': row.sys_ws_id, 'workspace_name': row.sys_ws.name,
'panel_role': None if role is None else {
'id': role.pk, 'user_id': role.cuser_id, 'workspace_id': role.sys_ws_id,
'role': role.role_type.role_name_en, 'mobile': role.mobile,
'user_mobile': role.cuser.mobile if role.cuser_id else None,
'is_active': role.is_active, 'customer_panel': role.customer_panel,
},
'related_counts': _related_counts(row, alias),
})
return details
class Command(BaseCommand):
help = 'Report legacy Customer/CUser mobile conflicts; never modify or delete records.'
# Must work before migrations, without checking unrelated model declarations.
requires_system_checks = []
def add_arguments(self, parser):
parser.add_argument('--database', default='default')
parser.add_argument('--customer-ids', nargs='+', type=int, default=[])
def handle(self, *args, **options):
alias = options['database']
if alias not in connections:
raise CommandError(f'Unknown database alias: {alias}')
customer_groups = _duplicate_groups(Customer, alias, per_workspace=True)
user_groups = _duplicate_groups(CUser, alias)
customer_ids = set(options['customer_ids'])
customer_ids.update(pk for group in customer_groups for pk in group['record_ids'])
user_ids = [pk for group in user_groups for pk in group['record_ids']]
report = {
'read_only': True, 'database': alias,
'duplicate_group_count': len(customer_groups) + len(user_groups),
'customer_duplicate_groups': customer_groups,
'user_duplicate_groups': user_groups,
'customers': _customer_details(customer_ids, alias),
'missing_requested_customer_ids': sorted(customer_ids - set(
Customer.objects.using(alias).filter(pk__in=customer_ids).values_list('pk', flat=True)
)),
'users': [dict(row, canonical_mobile=canonical_mobile(row['mobile'])) for row in
CUser.objects.using(alias).filter(pk__in=user_ids).order_by('pk').values(
'id', 'username', 'name_family', 'mobile', 'is_active'
)],
}
self.stdout.write(json.dumps(report, ensure_ascii=False, indent=2))