Files
Django/jituan/services/finance_detail_stats.py

542 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""财务子模块统计(订单/会员/其他充值),供 /jituan/houtai/*;旧 /houtai/* 不变。"""
from datetime import date, datetime
from decimal import Decimal
from django.db.models import Case, Count, DecimalField, Q, Sum, Value, When
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.czjilu_types import CZJILU_LEIXING_SHANGJIA, exclude_penalty_from_cz_qs
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 _penalty_trunc_func(granularity):
if granularity == 'day':
return TruncDate('UpdateTime')
if granularity == 'month':
return TruncMonth('UpdateTime')
return TruncYear('UpdateTime')
def _penalty_fenhong_aggregates():
"""罚款分红只统计已入账BonusCredited与 process_fadan_fenhong 一致。"""
zero = Value(Decimal('0.00'), output_field=DecimalField(max_digits=12, decimal_places=2))
return {
'fenhong_amount': Sum(
Case(
When(BonusCredited=True, then='ApplicantBonusAmount'),
default=zero,
output_field=DecimalField(max_digits=12, decimal_places=2),
)
),
'fenhong_count': Count(
'id',
filter=Q(BonusCredited=True, ApplicantBonusAmount__gt=0),
),
}
def _czjilu_finance_qs(request, start_dt, end_dt, cz_types):
"""充值财务 czjilu已支付type4 排除罚款误标。"""
qs = filter_queryset_by_club(
Czjilu.query.filter(
CreateTime__gte=start_dt,
CreateTime__lt=end_dt,
leixing__in=cz_types,
zhuangtai=3,
),
request,
)
if CZJILU_LEIXING_SHANGJIA in cz_types:
qs = exclude_penalty_from_cz_qs(qs)
return qs
def _czjilu_summary_qs(request, lx, granularity, summary_q):
filter_kwargs = {'leixing': lx, 'zhuangtai': 3}
filter_kwargs.update(_summary_date_filter(granularity, summary_q))
qs = filter_queryset_by_club(Czjilu.query.filter(**filter_kwargs), request)
if lx == CZJILU_LEIXING_SHANGJIA:
qs = exclude_penalty_from_cz_qs(qs)
return qs
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 = _czjilu_finance_qs(request, start_dt, end_dt, cz_types).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_trunc = _penalty_trunc_func(granularity)
penalty_qs = filter_penalty_qs(
Penalty.query.filter(
UpdateTime__gte=start_dt,
UpdateTime__lt=end_dt,
Status=2,
),
request,
).annotate(period=penalty_trunc).values('period').annotate(
penalty_count=Count('id'),
penalty_amount=Sum('FineAmount'),
**_penalty_fenhong_aggregates(),
).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:
agg = _czjilu_summary_qs(request, lx, granularity, summary_q).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, field_prefix='UpdateTime'))
agg = filter_penalty_qs(Penalty.query.filter(**penalty_filter), request).aggregate(
count=Count('id'),
amount=Sum('FineAmount'),
**_penalty_fenhong_aggregates(),
)
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