247 строки
8.1 KiB
Python
247 строки
8.1 KiB
Python
from decimal import Decimal
|
|
|
|
from django.db.models import Count, Q, Sum
|
|
|
|
import elasticutils.contrib.django as elasticutils
|
|
|
|
import amo
|
|
from amo.utils import create_es_index_if_missing
|
|
from mkt import MKT_CUT
|
|
from mkt.inapp_pay.models import InappPayment
|
|
from mkt.webapps.models import Installed
|
|
from stats.models import Contribution
|
|
|
|
|
|
def get_finance_total(qs, addon, field=None, **kwargs):
|
|
"""
|
|
sales/revenue/refunds per app overall
|
|
field -- breakdown field name contained by kwargs
|
|
"""
|
|
q = Q()
|
|
if field:
|
|
kwargs_copy = {field: kwargs[field]}
|
|
q = handle_kwargs(q, field, kwargs)
|
|
|
|
revenue = (qs.values('addon').filter(q, refund=None, **kwargs).
|
|
annotate(revenue=Sum('price_tier__price')))
|
|
sales = (qs.values('addon').filter(q, refund=None, **kwargs).
|
|
annotate(sales=Count('id')))
|
|
refunds = (qs.filter(q, refund__isnull=False, **kwargs).
|
|
values('addon').annotate(refunds=Count('id')))
|
|
document = {
|
|
'addon': addon,
|
|
'count': sales[0]['sales'] if sales.count() else 0,
|
|
'revenue': cut(revenue[0]['revenue'] if revenue.count() else 0),
|
|
'refunds': refunds[0]['refunds'] if refunds.count() else 0,
|
|
}
|
|
if field:
|
|
# Edge case, handle None values.
|
|
if kwargs_copy[field] == None:
|
|
kwargs_copy[field] = ''
|
|
document[field] = kwargs_copy[field]
|
|
|
|
# Non-USD-normalized revenue, calculated from currency's amount rather
|
|
# than price tier.
|
|
if field == 'currency':
|
|
document['revenue_non_normalized'] = cut(qs.values('addon')
|
|
.filter(q, refund=None, **kwargs)
|
|
.annotate(revenue=Sum('amount'))
|
|
[0]['revenue'] if revenue.count() else 0)
|
|
|
|
return document
|
|
|
|
|
|
def get_finance_total_inapp(qs, addon, inapp_name='', field=None, **kwargs):
|
|
"""
|
|
sales/revenue/refunds per in-app overall
|
|
field -- breakdown field name contained by kwargs
|
|
"""
|
|
q = Q()
|
|
if field:
|
|
kwargs_copy = {field: kwargs[field]}
|
|
q = handle_kwargs(q, field, kwargs, join_field='contribution__')
|
|
|
|
revenue = (qs.filter(q, contribution__refund=None, **kwargs).
|
|
values('config__addon').annotate(
|
|
revenue=Sum('contribution__price_tier__price')))
|
|
sales = (qs.filter(q, contribution__refund=None, **kwargs).
|
|
values('config__addon').
|
|
annotate(sales=Count('id')))
|
|
refunds = (qs.filter(q, contribution__refund__isnull=False, **kwargs).
|
|
values('config__addon').annotate(refunds=Count('id')))
|
|
document = {
|
|
'addon': addon,
|
|
'inapp': inapp_name,
|
|
'count': sales[0]['sales'] if sales.count() else 0,
|
|
'revenue': cut(revenue[0]['revenue'] if revenue.count() else 0),
|
|
'refunds': refunds[0]['refunds'] if refunds.count() else 0,
|
|
}
|
|
if field:
|
|
# Edge case, handle None values.
|
|
if kwargs_copy[field] == None:
|
|
kwargs_copy[field] = ''
|
|
document[field] = kwargs_copy[field]
|
|
|
|
# Non-USD-normalized revenue, calculated from currency's amount rather
|
|
# than price tier.
|
|
if field == 'currency':
|
|
document['revenue_non_normalized'] = cut(qs.values('config__addon')
|
|
.filter(q, contribution__refund=None, **kwargs)
|
|
.annotate(revenue=Sum('contribution__amount'))
|
|
[0]['revenue'] if revenue.count() else 0)
|
|
|
|
return document
|
|
|
|
|
|
def get_finance_daily(contribution):
|
|
"""
|
|
sales per day
|
|
revenue per day
|
|
refunds per day
|
|
"""
|
|
addon_id = contribution['addon']
|
|
date = contribution['created'].date()
|
|
return {
|
|
'date': date,
|
|
'addon': addon_id,
|
|
'count': Contribution.objects.filter(
|
|
addon__id=addon_id,
|
|
refund=None,
|
|
created__year=date.year,
|
|
created__month=date.month,
|
|
created__day=date.day).count() or 0,
|
|
# TODO: non-USD-normalized revenue (daily_by_currency)?
|
|
'revenue': cut(Contribution.objects.filter(
|
|
addon__id=addon_id,
|
|
refund=None,
|
|
type=amo.CONTRIB_PURCHASE,
|
|
created__year=date.year,
|
|
created__month=date.month,
|
|
created__day=date.day)
|
|
.aggregate(revenue=Sum('price_tier__price'))['revenue']
|
|
or 0),
|
|
'refunds': Contribution.objects.filter(
|
|
addon__id=addon_id,
|
|
refund__isnull=False,
|
|
created__year=date.year,
|
|
created__month=date.month,
|
|
created__day=date.day).count() or 0,
|
|
}
|
|
|
|
|
|
def get_finance_daily_inapp(payment):
|
|
"""
|
|
sales per day for inapp
|
|
revenue per day for inapp
|
|
refunds per day for inapp
|
|
"""
|
|
addon_id = payment['config__addon']
|
|
inapp = payment['name']
|
|
date = payment['created'].date()
|
|
return {
|
|
'date': date,
|
|
'addon': addon_id,
|
|
'inapp': inapp,
|
|
'count': InappPayment.objects.filter(
|
|
config__addon__id=addon_id,
|
|
contribution__refund=None,
|
|
created__year=date.year,
|
|
created__month=date.month,
|
|
created__day=date.day).count() or 0,
|
|
# TODO: non-USD-normalized revenue (daily_inapp_by_currency)?
|
|
'revenue': cut(InappPayment.objects.filter(
|
|
config__addon__id=addon_id,
|
|
contribution__refund=None,
|
|
contribution__type=amo.CONTRIB_PURCHASE,
|
|
created__year=date.year,
|
|
created__month=date.month,
|
|
created__day=date.day)
|
|
.aggregate(rev=Sum('contribution__price_tier__price'))['rev']
|
|
or 0),
|
|
'refunds': InappPayment.objects.filter(
|
|
contribution__addon__id=addon_id,
|
|
contribution__refund__isnull=False,
|
|
created__year=date.year,
|
|
created__month=date.month,
|
|
created__day=date.day).count() or 0,
|
|
}
|
|
|
|
|
|
def get_installed_daily(installed):
|
|
"""
|
|
installs per day
|
|
"""
|
|
addon_id = installed['addon']
|
|
date = installed['created'].date()
|
|
return {
|
|
'date': date,
|
|
'addon': addon_id,
|
|
'count': Installed.objects.filter(
|
|
created__year=date.year,
|
|
created__month=date.month,
|
|
created__day=date.day).count()
|
|
}
|
|
|
|
|
|
def setup_mkt_indexes():
|
|
"""
|
|
Define explicit ES mappings for models. If a field is not explicitly
|
|
defined and a field is inserted, ES will dynamically guess the type and
|
|
insert it, in a schemaless manner.
|
|
"""
|
|
es = elasticutils.get_es()
|
|
for model in [Contribution, InappPayment]:
|
|
index = model._get_index()
|
|
create_es_index_if_missing(index)
|
|
|
|
mapping = {
|
|
'properties': {
|
|
'id': {'type': 'long'},
|
|
'date': {'format': 'dateOptionalTime',
|
|
'type': 'date'},
|
|
'count': {'type': 'long'},
|
|
'revenue': {'type': 'double'},
|
|
|
|
# Try to tell ES not to 'analyze' the field to querying with
|
|
# hyphens and lowercase letters.
|
|
'currency': {'type': 'string',
|
|
'index': 'not_analyzed'},
|
|
'source': {'type': 'string',
|
|
'index': 'not_analyzed'},
|
|
'inapp': {'type': 'string',
|
|
'index': 'not_analyzed'}
|
|
}
|
|
}
|
|
|
|
es.put_mapping(model._meta.db_table, mapping,
|
|
model._get_index())
|
|
|
|
|
|
def cut(revenue):
|
|
"""
|
|
Takes away Marketplace's cut from developers' revenue.
|
|
"""
|
|
return Decimal(str(round(Decimal(str(revenue)) *
|
|
Decimal(str(MKT_CUT)), 2)))
|
|
|
|
|
|
def handle_kwargs(q, field, kwargs, join_field=None):
|
|
"""
|
|
Processes kwargs to combine '' and None values and make it ready for
|
|
filters. Returns Q object to use in filter.
|
|
"""
|
|
if join_field:
|
|
join_field += field
|
|
kwargs[join_field] = kwargs[field]
|
|
|
|
# Have '' and None have the same meaning.
|
|
if not kwargs[field]:
|
|
q = Q(**{field + '__in': ['', None]})
|
|
del(kwargs[field])
|
|
|
|
# We are using the join field to filter so get rid of the plain one.
|
|
if join_field:
|
|
del(kwargs[field])
|
|
|
|
return q
|