"""日结模块统计服务函数""" import logging from datetime import datetime, date, time, timedelta from decimal import Decimal, InvalidOperation from django.db.models import Prefetch from django.db.models import Q, Sum, Case, When, Value, CharField, IntegerField, Count from django.db.models.fields.json import KeyTextTransform from django.db.models.functions import Concat from django.utils.dateparse import parse_datetime from django.utils import timezone from printing.models import PlateOrder from stateflow.models import StateFlowRecord, StateLogParameterRecord logger = logging.getLogger(__name__) DEFAULT_DESIGNER_SUMMARY_STATE_NAMES = ("画图完成",) DEFAULT_DESIGNER_TASK_STATE_NAME_SUFFIX = "完成" DEFAULT_DESIGNER_PARAM_KEY = "设计师名称" DEFAULT_DESIGNER_TASK_QUANTITY_PARAM_KEY = "完成数量" DEFAULT_DESIGNER_TASK_TIME_PARAM_KEY = "完成时间" DEFAULT_EMPTY_TASK_QUANTITY = Decimal("1") def get_plate_order_summary_by_customer( merchant_id: int, settlement_date: date, user=None ) -> list[dict]: """ 获取按客户分组的开版订单统计 Args: merchant_id: 商户ID settlement_date: 统计日期 user: 当前用户(用于客户可见性过滤) Returns: list[dict]: 客户统计列表 """ logger.info( f'[settlement.services] 获取开版订单统计: ' f'merchant_id={merchant_id}, date={settlement_date}' ) settlement_date = _validate_and_normalize_summary_args( merchant_id=merchant_id, settlement_date=settlement_date, ) queryset = _get_plate_order_queryset(merchant_id, user) aggregated_data = _aggregate_plate_orders_by_customer_and_type( queryset, settlement_date ) return _format_plate_order_summary(aggregated_data) def get_plate_order_summary_by_designer( merchant_id: int, settlement_date: date, user=None, state_names: list[str] | tuple[str, ...] | None = None, designer_param_key: str = DEFAULT_DESIGNER_PARAM_KEY, ) -> list[dict]: """ 获取按设计师分组的开版订单统计。 设计师来源于 stateflow 参数记录,默认只统计“画图完成”节点下的 ``parameters["设计师名称"]``。同一设计师在同一开版单同一类型下出现多次只计 1 单。 """ logger.info( f'[settlement.services] 获取开版订单设计师统计: ' f'merchant_id={merchant_id}, date={settlement_date}, states={state_names}' ) settlement_date = _validate_and_normalize_summary_args( merchant_id=merchant_id, settlement_date=settlement_date, ) normalized_state_names = _normalize_designer_summary_state_names(state_names) if not designer_param_key or not str(designer_param_key).strip(): raise ValueError('designer_param_key 不能为空') month_start_dt, next_day_dt, today_start_dt = _get_summary_datetime_ranges( settlement_date ) queryset = _get_designer_plate_order_queryset( merchant_id=merchant_id, user=user, state_names=normalized_state_names, designer_param_key=designer_param_key, month_start_dt=month_start_dt, next_day_dt=next_day_dt, ) aggregated_data = _aggregate_plate_orders_by_designer_and_type( queryset=queryset, today_start_dt=today_start_dt, next_day_dt=next_day_dt, ) return _format_plate_order_designer_summary(aggregated_data) def get_designer_workflow_task_summary( merchant_id: int, settlement_date: date, user=None, state_names: list[str] | tuple[str, ...] | None = None, designer_names: list[str] | tuple[str, ...] | None = None, designer_param_key: str = DEFAULT_DESIGNER_PARAM_KEY, quantity_param_key: str = DEFAULT_DESIGNER_TASK_QUANTITY_PARAM_KEY, time_param_key: str = DEFAULT_DESIGNER_TASK_TIME_PARAM_KEY, ) -> list[dict]: """ 获取设计师工序任务量统计。 统计单位是未撤销的 StateFlowRecord。每条状态流转记录最多计入一次, 即使其下存在多条 StateLogParameterRecord。 """ logger.info( f'[settlement.services] 获取设计师工序任务量统计: ' f'merchant_id={merchant_id}, date={settlement_date}, states={state_names}' ) settlement_date = _validate_and_normalize_summary_args( merchant_id=merchant_id, settlement_date=settlement_date, ) normalized_state_names = _normalize_optional_state_names(state_names) normalized_designer_names = _normalize_optional_names(designer_names) for key, label in [ (designer_param_key, 'designer_param_key'), (quantity_param_key, 'quantity_param_key'), (time_param_key, 'time_param_key'), ]: if not key or not str(key).strip(): raise ValueError(f'{label} 不能为空') month_start_dt, next_day_dt, today_start_dt = _get_summary_datetime_ranges( settlement_date ) queryset = _get_designer_workflow_task_queryset( merchant_id=merchant_id, user=user, state_names=normalized_state_names, designer_param_key=designer_param_key, ) summary = _aggregate_designer_workflow_tasks( queryset=queryset, month_start_dt=month_start_dt, today_start_dt=today_start_dt, next_day_dt=next_day_dt, designer_names=normalized_designer_names, designer_param_key=designer_param_key, quantity_param_key=quantity_param_key, time_param_key=time_param_key, ) return _format_designer_workflow_task_summary(summary) def _normalize_optional_names( names: list[str] | tuple[str, ...] | None, ) -> tuple[str, ...] | None: if names is None: return None normalized = tuple(name.strip() for name in names if isinstance(name, str) and name.strip()) if not normalized: raise ValueError('designer_names 不能为空') return normalized def _normalize_optional_state_names( state_names: list[str] | tuple[str, ...] | None, ) -> tuple[str, ...] | None: if state_names is None: return None normalized = tuple( name.strip() for name in state_names if isinstance(name, str) and name.strip() ) if not normalized: raise ValueError('state_names 不能为空') return normalized def _normalize_designer_summary_state_names( state_names: list[str] | tuple[str, ...] | None, ) -> tuple[str, ...]: """标准化设计师统计节点名。""" if state_names is None: return DEFAULT_DESIGNER_SUMMARY_STATE_NAMES normalized = tuple( name.strip() for name in state_names if isinstance(name, str) and name.strip() ) if not normalized: raise ValueError('state_names 不能为空') return normalized def _get_summary_datetime_ranges(settlement_date: date): """返回月初、次日、当天起始的当前时区 aware datetime。""" current_tz = timezone.get_current_timezone() month_start = settlement_date.replace(day=1) month_start_dt = timezone.make_aware( datetime.combine(month_start, time.min), current_tz ) today_start_dt = timezone.make_aware( datetime.combine(settlement_date, time.min), current_tz ) next_day_dt = timezone.make_aware( datetime.combine(settlement_date + timedelta(days=1), time.min), current_tz ) return month_start_dt, next_day_dt, today_start_dt def _get_designer_workflow_task_queryset( *, merchant_id: int, user=None, state_names: tuple[str, ...] | None, designer_param_key: str, ): queryset = StateFlowRecord.objects.filter( is_cancelled=False, business_object__plate_order__merchant_id=merchant_id, business_object__plate_order__plate_type__isnull=False, business_object__plate_order__production_method__isnull=False, parameter_records__parameters__has_key=designer_param_key, ).select_related( 'state', 'business_object__plate_order', 'business_object__plate_order__customer', ).prefetch_related( Prefetch( 'parameter_records', queryset=StateLogParameterRecord.objects.order_by('created_at', 'id'), to_attr='_prefetched_parameter_records', ) ).distinct() if state_names is None: queryset = queryset.filter(state__name__endswith=DEFAULT_DESIGNER_TASK_STATE_NAME_SUFFIX) else: queryset = queryset.filter(state__name__in=state_names) if user and not user.is_superuser: emp = getattr(user, 'employee', None) if emp: queryset = queryset.filter( Q(business_object__plate_order__customer__created_by=emp) | Q(business_object__plate_order__customer__visible_employees=emp) ).distinct() return queryset.order_by('id') def _aggregate_designer_workflow_tasks( *, queryset, month_start_dt: datetime, today_start_dt: datetime, next_day_dt: datetime, designer_names: tuple[str, ...] | None, designer_param_key: str, quantity_param_key: str, time_param_key: str, ) -> dict[tuple[str, str, str], dict[str, Decimal]]: summary: dict[tuple[str, str, str], dict[str, Decimal]] = {} designer_name_filter = set(designer_names) if designer_names is not None else None for state_log in queryset: parameters = _merge_state_log_parameters(state_log) designer_name = str(parameters.get(designer_param_key) or '').strip() if not designer_name: continue if designer_name_filter is not None and designer_name not in designer_name_filter: continue effective_time = _resolve_task_effective_time( parameters.get(time_param_key), fallback=state_log.completed_at, ) if effective_time is None or effective_time < month_start_dt or effective_time >= next_day_dt: continue plate_order = state_log.business_object.plate_order task_quantity = _parse_task_quantity(parameters.get(quantity_param_key)) key = ( designer_name, state_log.state.name, f'{plate_order.plate_type}-{plate_order.production_method}', ) if key not in summary: summary[key] = {'today': Decimal('0'), 'current_month': Decimal('0')} summary[key]['current_month'] += task_quantity if today_start_dt <= effective_time < next_day_dt: summary[key]['today'] += task_quantity return summary def _merge_state_log_parameters(state_log: StateFlowRecord) -> dict: records = getattr(state_log, '_prefetched_parameter_records', None) if records is None: records = state_log.parameter_records.all().order_by('created_at', 'id') merged = {} for record in records: if isinstance(record.parameters, dict): merged.update(record.parameters) return merged def _parse_task_quantity(value) -> Decimal: try: quantity = Decimal(str(value).strip()) except (InvalidOperation, ValueError, TypeError, AttributeError): return DEFAULT_EMPTY_TASK_QUANTITY if quantity > 0: return quantity return DEFAULT_EMPTY_TASK_QUANTITY def _resolve_task_effective_time(value, *, fallback): parsed = None if isinstance(value, datetime): parsed = value elif isinstance(value, str) and value.strip(): raw_value = value.strip() parsed = parse_datetime(raw_value) if parsed is None: for fmt in ('%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M', '%Y-%m-%d'): try: parsed = datetime.strptime(raw_value, fmt) break except ValueError: continue effective = parsed or fallback if effective is None: return None if timezone.is_naive(effective): effective = timezone.make_aware(effective, timezone.get_current_timezone()) return effective.astimezone(timezone.get_current_timezone()) def _format_decimal_quantity(value: Decimal): if value == value.to_integral_value(): return int(value) return str(value.normalize()) def _format_designer_workflow_task_summary( summary: dict[tuple[str, str, str], dict[str, Decimal]], ) -> list[dict]: result: dict[str, dict] = {} for (designer_name, state_name, type_name), counts in sorted(summary.items()): if designer_name not in result: result[designer_name] = { 'designer_name': designer_name, 'task_count': [], 'today': Decimal('0'), 'current_month': Decimal('0'), } result[designer_name]['task_count'].append({ 'state_name': state_name, 'type': type_name, 'today': _format_decimal_quantity(counts['today']), 'current_month': _format_decimal_quantity(counts['current_month']), }) result[designer_name]['today'] += counts['today'] result[designer_name]['current_month'] += counts['current_month'] formatted = [] for item in result.values(): item['today'] = _format_decimal_quantity(item['today']) item['current_month'] = _format_decimal_quantity(item['current_month']) formatted.append(item) return formatted def _get_designer_plate_order_queryset( *, merchant_id: int, user=None, state_names: tuple[str, ...], designer_param_key: str, month_start_dt: datetime, next_day_dt: datetime, ): """获取设计师统计的 stateflow 参数记录基础查询集。""" queryset = StateLogParameterRecord.objects.filter( parameters__has_key=designer_param_key, state_log__is_cancelled=False, state_log__state__name__in=state_names, state_log__business_object__plate_order__merchant_id=merchant_id, state_log__business_object__plate_order__plate_type__isnull=False, state_log__business_object__plate_order__production_method__isnull=False, state_log__business_object__plate_order__plate_date__gte=month_start_dt, state_log__business_object__plate_order__plate_date__lt=next_day_dt, ).annotate( designer_name=KeyTextTransform(designer_param_key, 'parameters'), type=Concat( 'state_log__business_object__plate_order__plate_type', Value('-'), 'state_log__business_object__plate_order__production_method', output_field=CharField(), ), ).exclude( designer_name__isnull=True, ).exclude( designer_name='', ) if user and not user.is_superuser: emp = getattr(user, 'employee', None) if emp: queryset = queryset.filter( Q(state_log__business_object__plate_order__customer__created_by=emp) | Q(state_log__business_object__plate_order__customer__visible_employees=emp) ).distinct() return queryset def _aggregate_plate_orders_by_designer_and_type( *, queryset, today_start_dt: datetime, next_day_dt: datetime, ): """按设计师和类型聚合开版订单数量。""" plate_order_path = 'state_log__business_object__plate_order' return queryset.values( 'designer_name', 'type', ).annotate( today=Count( plate_order_path, filter=Q( state_log__business_object__plate_order__plate_date__gte=today_start_dt, state_log__business_object__plate_order__plate_date__lt=next_day_dt, ), distinct=True, ), current_month=Count( plate_order_path, distinct=True, ), ).order_by('designer_name', 'type') def _format_plate_order_designer_summary(aggregated_data): """格式化设计师聚合结果为 API 返回格式。""" result = {} for item in aggregated_data: designer_name = item['designer_name'] if designer_name not in result: result[designer_name] = { 'designer_name': designer_name, 'plate_order_count': [], } result[designer_name]['plate_order_count'].append({ 'type': item['type'], 'today': item['today'], 'current_month': item['current_month'], }) for designer_name in result: result[designer_name]['plate_order_count'] = _filter_zero_data( result[designer_name]['plate_order_count'] ) return [v for v in result.values() if v['plate_order_count']] def _validate_and_normalize_summary_args( merchant_id: int, settlement_date: date, ) -> date: """校验并标准化统计参数。""" if not isinstance(merchant_id, int) or merchant_id <= 0: raise ValueError('merchant_id 必须为正整数') if isinstance(settlement_date, datetime): settlement_date = settlement_date.date() if not isinstance(settlement_date, date): raise ValueError('settlement_date 必须为 date 或 datetime 类型') return settlement_date def _get_plate_order_queryset(merchant_id: int, user=None): """ 获取开版订单的基础查询集 Args: merchant_id: 商户ID user: 当前用户(用于客户可见性过滤) Returns: QuerySet[PlateOrder]: 过滤后的查询集 """ queryset = PlateOrder.objects.filter( merchant_id=merchant_id, plate_type__isnull=False, production_method__isnull=False ).select_related('customer') if user and not user.is_superuser: emp = getattr(user, 'employee', None) if emp: visible_customer_filter = ( Q(customer__created_by=emp) | Q(customer__visible_employees=emp) ) no_customer_filter = Q(customer__isnull=True) queryset = queryset.filter( visible_customer_filter | no_customer_filter ).distinct() return queryset def _get_month_date_range(settlement_date: date) -> tuple[date, date]: """ 获取从月初到指定日期的日期范围 Args: settlement_date: 统计日期 Returns: tuple[date, date]: (月初日期, 统计日期) """ month_start = settlement_date.replace(day=1) return month_start, settlement_date def _aggregate_plate_orders_by_customer_and_type(queryset, settlement_date: date): """ 按客户和类型分组聚合订单数据 Args: queryset: 基础查询集 settlement_date: 统计日期 Returns: QuerySet[PlateOrder]: 添加了聚合标注的查询集 """ month_start, _ = _get_month_date_range(settlement_date) queryset = queryset.annotate( type=Concat( 'plate_type', Value('-'), 'production_method', output_field=CharField() ) ).values( 'customer__id', 'customer__name', 'type' ).annotate( today=Sum( Case( When( plate_date__date=settlement_date, then=1 ), default=0, output_field=IntegerField() ) ), current_month=Sum( Case( When( plate_date__date__gte=month_start, plate_date__date__lte=settlement_date, then=1 ), default=0, output_field=IntegerField() ) ) ).order_by('customer__name', 'customer__id', 'type') return queryset def _format_plate_order_summary(aggregated_data): """ 格式化聚合结果为 API 返回格式 Args: aggregated_data: 聚合后的查询集 Returns: list[dict]: 格式化后的数据 """ result = {} for item in aggregated_data: client_id = item['customer__id'] client_name = item['customer__name'] client_key = (client_id, client_name) if client_key not in result: result[client_key] = { 'client_id': client_id, 'client_name': client_name, 'plate_order_count': [] } plate_order_count = { 'type': item['type'], 'today': item['today'], 'current_month': item['current_month'] } result[client_key]['plate_order_count'].append(plate_order_count) for client_key in result: result[client_key]['plate_order_count'] = _filter_zero_data( result[client_key]['plate_order_count'] ) return [v for k, v in result.items() if v['plate_order_count']] def _filter_zero_data(plate_order_counts): """ 过滤全0数据 Args: plate_order_counts: 订单统计列表 Returns: list[dict]: 过滤后的列表 """ return [ item for item in plate_order_counts if item['today'] > 0 or item['current_month'] > 0 ] def calculate_plate_order_daily_summary( merchant_id: int, settlement_date: datetime.date ) -> dict: """ 计算开版订单日结汇总(空壳实现) Args: merchant_id: 商户ID settlement_date: 结算日期 Returns: dict: 统计结果(空壳) """ logger.info( f'[settlement.services] 计算开版订单日结汇总: ' f'merchant_id={merchant_id}, date={settlement_date}' ) return { 'merchant_id': merchant_id, 'settlement_date': str(settlement_date), 'total_orders': 0, 'completed_orders': 0, 'in_progress_orders': 0, } def calculate_printing_order_daily_summary( merchant_id: int, settlement_date: datetime.date ) -> dict: """ 计算生产订单日结汇总(空壳实现) Args: merchant_id: 商户ID settlement_date: 结算日期 Returns: dict: 统计结果(空壳) """ logger.info( f'[settlement.services] 计算生产订单日结汇总: ' f'merchant_id={merchant_id}, date={settlement_date}' ) return { 'merchant_id': merchant_id, 'settlement_date': str(settlement_date), 'total_orders': 0, 'completed_orders': 0, 'in_progress_orders': 0, }