"""每日收支详细统计(供 /jituan/houtai/szxx,旧 /houtai/szxx 不变)。""" from datetime import date from django.db.models import Sum from django.db.models.functions import ExtractDay, ExtractMonth, ExtractYear from config.models import DailyIncomeStat, DailyPayoutStat from jituan.services.club_context import ( filter_club_char_field, resolve_club_id_from_request, resolve_club_scope, ) from jituan.services.daily_finance_display import ( sum_income_stat_for_date, sum_payout_stat_for_date, today_income_for_dashboard, ) def _month_bounds(y, m): start = date(int(y), int(m), 1) if int(m) == 12: end = date(int(y) + 1, 1, 1) else: end = date(int(y), int(m) + 1, 1) return start, end def _year_bounds(y): y = int(y) return date(y, 1, 1), date(y + 1, 1, 1) def build_szxx_payload(request, granularity, year, month, summary_date): """ 与 SzxxView 返回结构一致。 统一用 date 字段查询(与 /houtai/caiwu 四个板子一致),不用 year/month/day 冗余列。 """ now = date.today() income_base = filter_club_char_field(DailyIncomeStat.objects.all(), request, field='club_id') payout_base = filter_club_char_field(DailyPayoutStat.objects.all(), request, field='club_id') if granularity == 'day': if not year or not month: return None, '按日统计需要年份和月份' year, month = int(year), int(month) chart_start, chart_end = _month_bounds(year, month) income_rows = ( income_base.filter(date__gte=chart_start, date__lt=chart_end) .annotate(day_key=ExtractDay('date')) .values('day_key') .annotate(total_income=Sum('total_amount'), total_count=Sum('total_count')) .order_by('day_key') ) payout_rows = ( payout_base.filter(date__gte=chart_start, date__lt=chart_end) .annotate(day_key=ExtractDay('date')) .values('day_key') .annotate(total_payout=Sum('total_amount'), total_count=Sum('total_count')) .order_by('day_key') ) if not summary_date: summary_date = now.strftime('%Y-%m-%d') summary_d = date.fromisoformat(str(summary_date)[:10]) if summary_d == now: inc_amount, inc_count = today_income_for_dashboard(summary_d, request) else: inc_amount, inc_count = sum_income_stat_for_date(summary_d, request) pay_amount, pay_count = sum_payout_stat_for_date(summary_d, request) income_summary = {'total_income': inc_amount, 'total_count': inc_count} payout_summary = {'total_payout': pay_amount, 'total_count': pay_count} def _fmt_day(key): return f'{year}-{month:02d}-{int(key):02d}' elif granularity == 'month': if not year: return None, '按月统计需要年份' year = int(year) chart_start, chart_end = _year_bounds(year) income_rows = ( income_base.filter(date__gte=chart_start, date__lt=chart_end) .annotate(month_key=ExtractMonth('date')) .values('month_key') .annotate(total_income=Sum('total_amount'), total_count=Sum('total_count')) .order_by('month_key') ) payout_rows = ( payout_base.filter(date__gte=chart_start, date__lt=chart_end) .annotate(month_key=ExtractMonth('date')) .values('month_key') .annotate(total_payout=Sum('total_amount'), total_count=Sum('total_count')) .order_by('month_key') ) if not summary_date: summary_date = now.strftime('%Y-%m') parts = str(summary_date).split('-') s_start, s_end = _month_bounds(parts[0], parts[1]) income_summary = income_base.filter(date__gte=s_start, date__lt=s_end).aggregate( total_income=Sum('total_amount'), total_count=Sum('total_count'), ) payout_summary = payout_base.filter(date__gte=s_start, date__lt=s_end).aggregate( total_payout=Sum('total_amount'), total_count=Sum('total_count'), ) def _fmt_day(key): return f'{year}-{int(key):02d}' elif granularity == 'year': income_rows = ( income_base.annotate(year_key=ExtractYear('date')) .values('year_key') .annotate(total_income=Sum('total_amount'), total_count=Sum('total_count')) .order_by('year_key') ) payout_rows = ( payout_base.annotate(year_key=ExtractYear('date')) .values('year_key') .annotate(total_payout=Sum('total_amount'), total_count=Sum('total_count')) .order_by('year_key') ) if not summary_date: summary_date = str(now.year) s_start, s_end = _year_bounds(int(summary_date)) income_summary = income_base.filter(date__gte=s_start, date__lt=s_end).aggregate( total_income=Sum('total_amount'), total_count=Sum('total_count'), ) payout_summary = payout_base.filter(date__gte=s_start, date__lt=s_end).aggregate( total_payout=Sum('total_amount'), total_count=Sum('total_count'), ) def _fmt_day(key): return str(int(key)) else: return None, '无效的颗粒度' data_map = {} if granularity == 'day': key_field = 'day_key' elif granularity == 'month': key_field = 'month_key' else: key_field = 'year_key' for item in income_rows: key = item[key_field] data_map[key] = { 'income': float(item['total_income'] or 0), 'income_count': item['total_count'] or 0, 'payout': 0.0, 'payout_count': 0, } for item in payout_rows: key = item[key_field] if key not in data_map: data_map[key] = {'income': 0.0, 'income_count': 0, 'payout': 0.0, 'payout_count': 0} data_map[key]['payout'] = float(item['total_payout'] or 0) data_map[key]['payout_count'] = item['total_count'] or 0 time_series = [] for key, vals in data_map.items(): profit = round(vals['income'] - vals['payout'], 2) time_series.append({ 'date': _fmt_day(key), 'income': vals['income'], 'payout': vals['payout'], 'profit': profit, }) time_series.sort(key=lambda x: x['date']) total_income = float(income_summary['total_income'] or 0) total_payout = float(payout_summary['total_payout'] or 0) return { 'club_id': resolve_club_id_from_request(request), 'scope': resolve_club_scope(request), 'time_series': time_series, 'summary': { 'total_income': total_income, 'total_income_count': income_summary['total_count'] or 0, 'total_payout': total_payout, 'total_payout_count': payout_summary['total_count'] or 0, 'total_profit': round(total_income - total_payout, 2), }, }, None