187 lines
6.7 KiB
Python
187 lines
6.7 KiB
Python
"""每日收支详细统计(供 /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,
|
||
)
|
||
|
||
|
||
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])
|
||
income_summary = income_base.filter(date=summary_d).aggregate(
|
||
total_income=Sum('total_amount'), total_count=Sum('total_count'),
|
||
)
|
||
payout_summary = payout_base.filter(date=summary_d).aggregate(
|
||
total_payout=Sum('total_amount'), total_count=Sum('total_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
|