494 lines
19 KiB
Python
494 lines
19 KiB
Python
"""财务子模块统计(订单/会员/其他充值),供 /jituan/houtai/*;旧 /houtai/* 不变。"""
|
|
from datetime import date, datetime
|
|
|
|
from django.db.models import Count, Q, Sum
|
|
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
|
|
|
from jituan.constants import DATA_SCOPE_ALL
|
|
from jituan.models import ClubHuiyuanPrice
|
|
from jituan.services.club_context import (
|
|
filter_queryset_by_club,
|
|
resolve_club_id_from_request,
|
|
resolve_club_scope,
|
|
)
|
|
from jituan.services.club_penalty import filter_gsfenhong_qs, filter_penalty_qs
|
|
from orders.models import Order, Penalty
|
|
from products.models import Czjilu, Gsfenhong, Huiyuan, ShangpinLeixing
|
|
from rank.models import Bankuai
|
|
|
|
|
|
def _meta(request):
|
|
return {
|
|
'club_id': resolve_club_id_from_request(request),
|
|
'scope': resolve_club_scope(request),
|
|
}
|
|
|
|
|
|
def _parse_time_range(granularity, year, month, summary_date):
|
|
now = date.today()
|
|
if granularity == 'day':
|
|
if not year or not month:
|
|
return None, '按日统计需要年份和月份'
|
|
start_date = date(int(year), int(month), 1)
|
|
end_date = date(int(year), int(month) + 1, 1) if int(month) < 12 else date(int(year) + 1, 1, 1)
|
|
trunc_func = TruncDate('CreateTime')
|
|
if not summary_date:
|
|
summary_date = now.strftime('%Y-%m-%d')
|
|
summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date()
|
|
elif granularity == 'month':
|
|
if not year:
|
|
return None, '按月统计需要年份'
|
|
y = int(year)
|
|
start_date = date(y, 1, 1)
|
|
end_date = date(y + 1, 1, 1)
|
|
trunc_func = TruncMonth('CreateTime')
|
|
if not summary_date:
|
|
summary_date = now.strftime('%Y-%m')
|
|
summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date()
|
|
elif granularity == 'year':
|
|
y = int(year or now.year)
|
|
start_date = date(y, 1, 1)
|
|
end_date = date(y + 1, 1, 1)
|
|
trunc_func = TruncYear('CreateTime')
|
|
if not summary_date:
|
|
summary_date = str(y)
|
|
summary_q = date(y, 1, 1)
|
|
else:
|
|
return None, '无效的颗粒度'
|
|
|
|
start_dt = datetime.combine(start_date, datetime.min.time())
|
|
end_dt = datetime.combine(end_date, datetime.min.time())
|
|
return {
|
|
'start_dt': start_dt,
|
|
'end_dt': end_dt,
|
|
'trunc_func': trunc_func,
|
|
'summary_q': summary_q,
|
|
'granularity': granularity,
|
|
'year': int(year) if year is not None else None,
|
|
'month': int(month) if month is not None else None,
|
|
}, None
|
|
|
|
|
|
def _summary_date_filter(granularity, summary_q, field_prefix='CreateTime'):
|
|
if granularity == 'day':
|
|
return {f'{field_prefix}__date': summary_q}
|
|
if granularity == 'month':
|
|
return {f'{field_prefix}__year': summary_q.year, f'{field_prefix}__month': summary_q.month}
|
|
return {f'{field_prefix}__year': summary_q.year}
|
|
|
|
|
|
def build_order_types_payload():
|
|
types = ShangpinLeixing.query.filter(shenhezhuangtai=1).values('id', 'jieshao')
|
|
return list(types)
|
|
|
|
|
|
def build_order_finance_payload(
|
|
request,
|
|
granularity,
|
|
year,
|
|
month,
|
|
type_id=0,
|
|
order_source=0,
|
|
summary_date=None,
|
|
):
|
|
parsed, err = _parse_time_range(granularity, year, month, summary_date)
|
|
if err:
|
|
return None, err
|
|
|
|
start_dt = parsed['start_dt']
|
|
end_dt = parsed['end_dt']
|
|
trunc_func = parsed['trunc_func']
|
|
summary_q = parsed['summary_q']
|
|
granularity = parsed['granularity']
|
|
|
|
filters = {
|
|
'CreateTime__gte': start_dt,
|
|
'CreateTime__lt': end_dt,
|
|
'Status__in': [1, 2, 3, 4, 5, 6, 7, 8],
|
|
}
|
|
if int(type_id or 0) != 0:
|
|
filters['ProductTypeID'] = type_id
|
|
if int(order_source or 0) in [1, 2]:
|
|
filters['Platform'] = int(order_source)
|
|
|
|
order_base = filter_queryset_by_club(
|
|
Order.query.filter(**filters), request, club_field='ClubID',
|
|
)
|
|
qs = order_base.annotate(period=trunc_func).values('period').annotate(
|
|
order_count=Count('id'),
|
|
order_amount=Sum('Amount'),
|
|
success_count=Count('id', filter=Q(Status=3)),
|
|
success_amount=Sum('Amount', filter=Q(Status=3)),
|
|
refund_count=Count('id', filter=Q(Status=5)),
|
|
PlayerCommission_sum=Sum('PlayerCommission', filter=Q(Status=3)),
|
|
dianpu_fenhong_sum=Sum('pingtai_kuozhan__ShopIncome', filter=Q(Status=3, Platform=1)),
|
|
platform_success_amount=Sum('Amount', filter=Q(Status=3, Platform=1)),
|
|
merchant_success_amount=Sum('Amount', filter=Q(Status=3, Platform=2)),
|
|
platform_dashou=Sum('PlayerCommission', filter=Q(Status=3, Platform=1)),
|
|
merchant_dashou=Sum('PlayerCommission', filter=Q(Status=3, Platform=2)),
|
|
).order_by('period')
|
|
|
|
time_series = []
|
|
for item in qs:
|
|
period_str = (
|
|
item['period'].strftime('%Y-%m-%d') if granularity == 'day'
|
|
else item['period'].strftime('%Y-%m') if granularity == 'month'
|
|
else item['period'].strftime('%Y')
|
|
)
|
|
platform_profit = (item['platform_success_amount'] or 0) - (item['platform_dashou'] or 0) - (item['dianpu_fenhong_sum'] or 0)
|
|
merchant_profit = (item['merchant_success_amount'] or 0) - (item['merchant_dashou'] or 0)
|
|
time_series.append({
|
|
'date': period_str,
|
|
'order_count': item['order_count'],
|
|
'order_amount': float(item['order_amount'] or 0),
|
|
'success_count': item['success_count'],
|
|
'success_amount': float(item['success_amount'] or 0),
|
|
'refund_count': item['refund_count'],
|
|
'dashou_fencheng': float(item['PlayerCommission_sum'] or 0),
|
|
'dianpu_fenhong': float(item['dianpu_fenhong_sum'] or 0),
|
|
'profit': round(float(platform_profit + merchant_profit), 2),
|
|
})
|
|
|
|
summary_qs = filter_queryset_by_club(Order.query.filter(**filters), request, club_field='ClubID')
|
|
total_order_count = summary_qs.count()
|
|
total_order_amount = summary_qs.aggregate(s=Sum('Amount'))['s'] or 0
|
|
total_success_count = summary_qs.filter(Status=3).count()
|
|
total_success_amount = summary_qs.filter(Status=3).aggregate(s=Sum('Amount'))['s'] or 0
|
|
total_refund_count = summary_qs.filter(Status=5).count()
|
|
total_dashou = summary_qs.filter(Status=3).aggregate(s=Sum('PlayerCommission'))['s'] or 0
|
|
total_dianpu_fenhong = summary_qs.filter(Status=3, Platform=1).aggregate(
|
|
s=Sum('pingtai_kuozhan__ShopIncome'),
|
|
)['s'] or 0
|
|
platform_amount = summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('Amount'))['s'] or 0
|
|
merchant_amount = summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('Amount'))['s'] or 0
|
|
platform_dashou_sum = summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('PlayerCommission'))['s'] or 0
|
|
merchant_dashou_sum = summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('PlayerCommission'))['s'] or 0
|
|
profit_platform = platform_amount - platform_dashou_sum - total_dianpu_fenhong
|
|
profit_merchant = merchant_amount - merchant_dashou_sum
|
|
total_profit = profit_platform + profit_merchant
|
|
|
|
data = {
|
|
**_meta(request),
|
|
'time_series': time_series,
|
|
'summary': {
|
|
'total_order_count': total_order_count,
|
|
'total_order_amount': round(float(total_order_amount), 2),
|
|
'total_success_count': total_success_count,
|
|
'total_success_amount': round(float(total_success_amount), 2),
|
|
'total_refund_count': total_refund_count,
|
|
'total_PlayerCommission': round(float(total_dashou), 2),
|
|
'total_dianpu_fenhong': round(float(total_dianpu_fenhong), 2),
|
|
'total_profit': round(float(total_profit), 2),
|
|
},
|
|
}
|
|
return data, None
|
|
|
|
|
|
def build_chongzhi_finance_payload(request, granularity, year, month, types=None, summary_date=None):
|
|
if types is None:
|
|
types = [2, 3, 4, 5, 6]
|
|
if isinstance(types, list) and len(types) == 0:
|
|
types = [2, 3, 4, 5, 6]
|
|
|
|
parsed, err = _parse_time_range(granularity, year, month, summary_date)
|
|
if err:
|
|
return None, err
|
|
|
|
start_dt = parsed['start_dt']
|
|
end_dt = parsed['end_dt']
|
|
trunc_func = parsed['trunc_func']
|
|
summary_q = parsed['summary_q']
|
|
granularity = parsed['granularity']
|
|
|
|
time_series_map = {}
|
|
|
|
def ensure_date(ds):
|
|
if ds not in time_series_map:
|
|
time_series_map[ds] = {}
|
|
return time_series_map[ds]
|
|
|
|
cz_types = [t for t in types if t in [2, 3, 4, 5]]
|
|
if cz_types:
|
|
cz_qs = filter_queryset_by_club(
|
|
Czjilu.query.filter(
|
|
CreateTime__gte=start_dt,
|
|
CreateTime__lt=end_dt,
|
|
leixing__in=cz_types,
|
|
zhuangtai=3,
|
|
),
|
|
request,
|
|
).annotate(period=trunc_func).values('period', 'leixing').annotate(
|
|
total_count=Count('id'),
|
|
total_amount=Sum('jine'),
|
|
).order_by('period', 'leixing')
|
|
|
|
for item in cz_qs:
|
|
period_str = (
|
|
item['period'].strftime('%Y-%m-%d') if granularity == 'day'
|
|
else item['period'].strftime('%Y-%m') if granularity == 'month'
|
|
else item['period'].strftime('%Y')
|
|
)
|
|
lx = item['leixing']
|
|
amount = float(item['total_amount'] or 0)
|
|
count = item['total_count']
|
|
profit = amount if lx == 3 else 0.0
|
|
day_data = ensure_date(period_str)
|
|
day_data[str(lx)] = {
|
|
'count': count,
|
|
'amount': amount,
|
|
'profit': profit,
|
|
'fenhong_count': 0,
|
|
'fenhong_amount': 0.0,
|
|
}
|
|
|
|
if 6 in types:
|
|
penalty_qs = filter_penalty_qs(
|
|
Penalty.query.filter(CreateTime__gte=start_dt, CreateTime__lt=end_dt, Status=2),
|
|
request,
|
|
).annotate(period=trunc_func).values('period').annotate(
|
|
penalty_count=Count('id'),
|
|
penalty_amount=Sum('FineAmount'),
|
|
fenhong_amount=Sum('ApplicantBonusAmount'),
|
|
fenhong_count=Count('id', filter=Q(ApplicantBonusAmount__gt=0)),
|
|
).order_by('period')
|
|
|
|
for item in penalty_qs:
|
|
period_str = (
|
|
item['period'].strftime('%Y-%m-%d') if granularity == 'day'
|
|
else item['period'].strftime('%Y-%m') if granularity == 'month'
|
|
else item['period'].strftime('%Y')
|
|
)
|
|
day_data = ensure_date(period_str)
|
|
penalty_amt = float(item['penalty_amount'] or 0)
|
|
fenhong_amt = float(item['fenhong_amount'] or 0)
|
|
day_data['6'] = {
|
|
'count': item['penalty_count'],
|
|
'amount': penalty_amt,
|
|
'profit': round(penalty_amt - fenhong_amt, 2),
|
|
'fenhong_count': item['fenhong_count'],
|
|
'fenhong_amount': fenhong_amt,
|
|
}
|
|
|
|
time_series = []
|
|
for period_str in sorted(time_series_map.keys()):
|
|
entry = {'date': period_str, 'types': {}}
|
|
for type_code in sorted(time_series_map[period_str].keys()):
|
|
entry['types'][type_code] = time_series_map[period_str][type_code]
|
|
time_series.append(entry)
|
|
|
|
summary_result = {'by_type': {}, 'total_count': 0, 'total_amount': 0.0, 'total_profit': 0.0}
|
|
|
|
for lx in cz_types:
|
|
filter_kwargs = {'leixing': lx, 'zhuangtai': 3}
|
|
filter_kwargs.update(_summary_date_filter(granularity, summary_q))
|
|
agg = filter_queryset_by_club(Czjilu.query.filter(**filter_kwargs), request).aggregate(
|
|
count=Count('id'), amount=Sum('jine'),
|
|
)
|
|
count = agg['count'] or 0
|
|
amount = float(agg['amount'] or 0)
|
|
profit = amount if lx == 3 else 0.0
|
|
summary_result['by_type'][str(lx)] = {
|
|
'count': count,
|
|
'amount': round(amount, 2),
|
|
'profit': round(profit, 2),
|
|
'fenhong_count': 0,
|
|
'fenhong_amount': 0.0,
|
|
}
|
|
summary_result['total_count'] += count
|
|
summary_result['total_amount'] += amount
|
|
summary_result['total_profit'] += profit
|
|
|
|
if 6 in types:
|
|
penalty_filter = {'Status': 2}
|
|
penalty_filter.update(_summary_date_filter(granularity, summary_q))
|
|
agg = filter_penalty_qs(Penalty.query.filter(**penalty_filter), request).aggregate(
|
|
count=Count('id'),
|
|
amount=Sum('FineAmount'),
|
|
fenhong_amount=Sum('ApplicantBonusAmount'),
|
|
fenhong_count=Count('id', filter=Q(ApplicantBonusAmount__gt=0)),
|
|
)
|
|
penalty_count = agg['count'] or 0
|
|
penalty_amount = float(agg['amount'] or 0)
|
|
fh_amount = float(agg['fenhong_amount'] or 0)
|
|
fh_count = agg['fenhong_count'] or 0
|
|
profit = round(penalty_amount - fh_amount, 2)
|
|
summary_result['by_type']['6'] = {
|
|
'count': penalty_count,
|
|
'amount': round(penalty_amount, 2),
|
|
'profit': profit,
|
|
'fenhong_count': fh_count,
|
|
'fenhong_amount': round(fh_amount, 2),
|
|
}
|
|
summary_result['total_count'] += penalty_count
|
|
summary_result['total_amount'] += penalty_amount
|
|
summary_result['total_profit'] += profit
|
|
|
|
summary_result['total_amount'] = round(summary_result['total_amount'], 2)
|
|
summary_result['total_profit'] = round(summary_result['total_profit'], 2)
|
|
|
|
return {
|
|
**_meta(request),
|
|
'time_series': time_series,
|
|
'summary': summary_result,
|
|
}, None
|
|
|
|
|
|
def build_huiyuan_bankuai_payload(request):
|
|
club_id = resolve_club_id_from_request(request)
|
|
price_map = {}
|
|
if resolve_club_scope(request) != DATA_SCOPE_ALL:
|
|
for row in ClubHuiyuanPrice.query.filter(club_id=club_id, is_enabled=True):
|
|
price_map[row.huiyuan_id] = {
|
|
'jiage': float(row.jiage or 0),
|
|
'guanshifc': float(row.guanshifc or 0),
|
|
'zuzhangfc': float(row.zuzhangfc or 0),
|
|
}
|
|
|
|
bankuai_list = []
|
|
for bk in Bankuai.query.all():
|
|
members = []
|
|
for m in Huiyuan.query.filter(bankuai=bk).values('huiyuan_id', 'jieshao', 'bankuai_id'):
|
|
item = dict(m)
|
|
if m['huiyuan_id'] in price_map:
|
|
item['club_price'] = price_map[m['huiyuan_id']]
|
|
members.append(item)
|
|
bankuai_list.append({
|
|
'bankuai_id': bk.bankuai_id,
|
|
'mingcheng': bk.mingcheng,
|
|
'members': members,
|
|
})
|
|
|
|
return {**_meta(request), 'bankuai_list': bankuai_list}
|
|
|
|
|
|
def build_huiyuan_stats_payload(
|
|
request,
|
|
huiyuan_id,
|
|
granularity,
|
|
year,
|
|
month,
|
|
include_guanshi=False,
|
|
include_zuzhang=False,
|
|
summary_date=None,
|
|
):
|
|
if not huiyuan_id:
|
|
return None, '缺少会员ID'
|
|
|
|
parsed, err = _parse_time_range(granularity, year, month, summary_date)
|
|
if err:
|
|
return None, err
|
|
|
|
start_dt = parsed['start_dt']
|
|
end_dt = parsed['end_dt']
|
|
trunc_func = parsed['trunc_func']
|
|
summary_q = parsed['summary_q']
|
|
granularity = parsed['granularity']
|
|
|
|
chongzhi_filter = {
|
|
'huiyuan_id': huiyuan_id,
|
|
'leixing': 1,
|
|
'zhuangtai': 3,
|
|
'CreateTime__gte': start_dt,
|
|
'CreateTime__lt': end_dt,
|
|
}
|
|
fenhong_filter = {
|
|
'huiyuan_id': huiyuan_id,
|
|
'CreateTime__gte': start_dt,
|
|
'CreateTime__lt': end_dt,
|
|
}
|
|
|
|
chongzhi_qs = filter_queryset_by_club(
|
|
Czjilu.query.filter(**chongzhi_filter), request,
|
|
).annotate(period=trunc_func).values('period').annotate(
|
|
chongzhi_count=Count('id'),
|
|
chongzhi_amount=Sum('jine'),
|
|
).order_by('period')
|
|
|
|
fenhong_qs = None
|
|
if include_guanshi or include_zuzhang:
|
|
fenhong_agg = {}
|
|
if include_guanshi:
|
|
fenhong_agg['guanshi_amount'] = Sum('fenhong')
|
|
if include_zuzhang:
|
|
fenhong_agg['zuzhang_amount'] = Sum('zuzhang_fenhong')
|
|
fenhong_qs = filter_gsfenhong_qs(
|
|
Gsfenhong.query.filter(**fenhong_filter), request,
|
|
).annotate(period=trunc_func).values('period').annotate(**fenhong_agg).order_by('period')
|
|
|
|
data_map = {}
|
|
for item in chongzhi_qs:
|
|
period_str = (
|
|
item['period'].strftime('%Y-%m-%d') if granularity == 'day'
|
|
else item['period'].strftime('%Y-%m') if granularity == 'month'
|
|
else item['period'].strftime('%Y')
|
|
)
|
|
data_map[period_str] = {
|
|
'chongzhi_count': item['chongzhi_count'],
|
|
'chongzhi_amount': float(item['chongzhi_amount'] or 0),
|
|
}
|
|
|
|
if fenhong_qs:
|
|
for item in fenhong_qs:
|
|
period_str = (
|
|
item['period'].strftime('%Y-%m-%d') if granularity == 'day'
|
|
else item['period'].strftime('%Y-%m') if granularity == 'month'
|
|
else item['period'].strftime('%Y')
|
|
)
|
|
if period_str not in data_map:
|
|
data_map[period_str] = {'chongzhi_count': 0, 'chongzhi_amount': 0.0}
|
|
if include_guanshi:
|
|
data_map[period_str]['guanshi_amount'] = float(item.get('guanshi_amount') or 0)
|
|
if include_zuzhang:
|
|
data_map[period_str]['zuzhang_amount'] = float(item.get('zuzhang_amount') or 0)
|
|
|
|
time_series = []
|
|
for period_str, vals in data_map.items():
|
|
guanshi = vals.get('guanshi_amount', 0) if include_guanshi else 0
|
|
zuzhang = vals.get('zuzhang_amount', 0) if include_zuzhang else 0
|
|
vals['shouyi'] = round(vals['chongzhi_amount'] - guanshi - zuzhang, 2)
|
|
vals['date'] = period_str
|
|
time_series.append(vals)
|
|
time_series.sort(key=lambda x: x['date'])
|
|
|
|
summary_chongzhi_filter = dict(chongzhi_filter)
|
|
summary_chongzhi_filter.pop('CreateTime__gte', None)
|
|
summary_chongzhi_filter.pop('CreateTime__lt', None)
|
|
summary_chongzhi_filter.update(_summary_date_filter(granularity, summary_q))
|
|
summary_chongzhi = filter_queryset_by_club(
|
|
Czjilu.query.filter(**summary_chongzhi_filter), request,
|
|
).aggregate(total_count=Count('id'), total_amount=Sum('jine'))
|
|
total_chongzhi_count = summary_chongzhi['total_count'] or 0
|
|
total_chongzhi_amount = float(summary_chongzhi['total_amount'] or 0)
|
|
|
|
total_guanshi = 0.0
|
|
total_zuzhang = 0.0
|
|
if include_guanshi or include_zuzhang:
|
|
summary_fenhong_filter = dict(fenhong_filter)
|
|
summary_fenhong_filter.pop('CreateTime__gte', None)
|
|
summary_fenhong_filter.pop('CreateTime__lt', None)
|
|
summary_fenhong_filter.update(_summary_date_filter(granularity, summary_q))
|
|
fenhong_agg = {}
|
|
if include_guanshi:
|
|
fenhong_agg['guanshi_sum'] = Sum('fenhong')
|
|
if include_zuzhang:
|
|
fenhong_agg['zuzhang_sum'] = Sum('zuzhang_fenhong')
|
|
summary_fenhong = filter_gsfenhong_qs(
|
|
Gsfenhong.query.filter(**summary_fenhong_filter), request,
|
|
).aggregate(**fenhong_agg)
|
|
total_guanshi = float(summary_fenhong.get('guanshi_sum') or 0)
|
|
total_zuzhang = float(summary_fenhong.get('zuzhang_sum') or 0)
|
|
|
|
total_shouyi = round(total_chongzhi_amount - total_guanshi - total_zuzhang, 2)
|
|
|
|
return {
|
|
**_meta(request),
|
|
'time_series': time_series,
|
|
'summary': {
|
|
'total_chongzhi_count': total_chongzhi_count,
|
|
'total_chongzhi_amount': total_chongzhi_amount,
|
|
'total_guanshi_fenhong': total_guanshi,
|
|
'total_zuzhang_fenhong': total_zuzhang,
|
|
'total_shouyi': total_shouyi,
|
|
},
|
|
}, None
|