wbcommission 2.2.1__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wbcommission might be problematic. Click here for more details.
- wbcommission/__init__.py +1 -0
- wbcommission/admin/__init__.py +4 -0
- wbcommission/admin/accounts.py +22 -0
- wbcommission/admin/commission.py +85 -0
- wbcommission/admin/rebate.py +7 -0
- wbcommission/analytics/__init__.py +0 -0
- wbcommission/analytics/marginality.py +181 -0
- wbcommission/apps.py +5 -0
- wbcommission/dynamic_preferences_registry.py +0 -0
- wbcommission/factories/__init__.py +9 -0
- wbcommission/factories/commission.py +100 -0
- wbcommission/factories/rebate.py +16 -0
- wbcommission/filters/__init__.py +7 -0
- wbcommission/filters/rebate.py +187 -0
- wbcommission/filters/signals.py +44 -0
- wbcommission/generators/__init__.py +2 -0
- wbcommission/generators/rebate_generator.py +93 -0
- wbcommission/migrations/0001_initial.py +299 -0
- wbcommission/migrations/0002_commissionrule_remove_accountcustomer_account_and_more.py +395 -0
- wbcommission/migrations/0003_alter_commission_account.py +24 -0
- wbcommission/migrations/0004_rebate_audit_log.py +19 -0
- wbcommission/migrations/0005_alter_rebate_audit_log.py +20 -0
- wbcommission/migrations/0006_commissionrule_consider_zero_percent_for_exclusion.py +21 -0
- wbcommission/migrations/0007_remove_commission_unique_crm_recipient_account_and_more.py +50 -0
- wbcommission/migrations/0008_alter_commission_options_alter_commission_order.py +26 -0
- wbcommission/migrations/__init__.py +0 -0
- wbcommission/models/__init__.py +9 -0
- wbcommission/models/account_service.py +217 -0
- wbcommission/models/commission.py +679 -0
- wbcommission/models/rebate.py +319 -0
- wbcommission/models/signals.py +45 -0
- wbcommission/permissions.py +6 -0
- wbcommission/reports/__init__.py +0 -0
- wbcommission/reports/audit_report.py +51 -0
- wbcommission/reports/customer_report.py +299 -0
- wbcommission/reports/utils.py +30 -0
- wbcommission/serializers/__init__.py +3 -0
- wbcommission/serializers/commissions.py +26 -0
- wbcommission/serializers/rebate.py +87 -0
- wbcommission/serializers/signals.py +27 -0
- wbcommission/tests/__init__.py +0 -0
- wbcommission/tests/analytics/__init__.py +0 -0
- wbcommission/tests/analytics/test_marginality.py +253 -0
- wbcommission/tests/conftest.py +89 -0
- wbcommission/tests/models/__init__.py +0 -0
- wbcommission/tests/models/mixins.py +22 -0
- wbcommission/tests/models/test_account_service.py +293 -0
- wbcommission/tests/models/test_commission.py +587 -0
- wbcommission/tests/models/test_rebate.py +136 -0
- wbcommission/tests/signals.py +0 -0
- wbcommission/tests/test_permissions.py +66 -0
- wbcommission/tests/viewsets/__init__.py +0 -0
- wbcommission/tests/viewsets/test_rebate.py +76 -0
- wbcommission/urls.py +42 -0
- wbcommission/viewsets/__init__.py +7 -0
- wbcommission/viewsets/buttons/__init__.py +2 -0
- wbcommission/viewsets/buttons/rebate.py +46 -0
- wbcommission/viewsets/buttons/signals.py +53 -0
- wbcommission/viewsets/commissions.py +21 -0
- wbcommission/viewsets/display/__init__.py +5 -0
- wbcommission/viewsets/display/commissions.py +21 -0
- wbcommission/viewsets/display/rebate.py +117 -0
- wbcommission/viewsets/endpoints/__init__.py +4 -0
- wbcommission/viewsets/endpoints/commissions.py +0 -0
- wbcommission/viewsets/endpoints/rebate.py +21 -0
- wbcommission/viewsets/menu/__init__.py +1 -0
- wbcommission/viewsets/menu/commissions.py +0 -0
- wbcommission/viewsets/menu/rebate.py +13 -0
- wbcommission/viewsets/mixins.py +39 -0
- wbcommission/viewsets/rebate.py +481 -0
- wbcommission/viewsets/titles/__init__.py +1 -0
- wbcommission/viewsets/titles/commissions.py +0 -0
- wbcommission/viewsets/titles/rebate.py +11 -0
- wbcommission-2.2.1.dist-info/METADATA +11 -0
- wbcommission-2.2.1.dist-info/RECORD +76 -0
- wbcommission-2.2.1.dist-info/WHEEL +5 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from datetime import date, datetime
|
|
2
|
+
|
|
3
|
+
from django.http import HttpRequest
|
|
4
|
+
from django.utils.functional import cached_property
|
|
5
|
+
from wbcommission.models import Commission, Rebate
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CommissionPermissionMixin:
|
|
9
|
+
queryset = Commission.objects.all()
|
|
10
|
+
request: HttpRequest
|
|
11
|
+
|
|
12
|
+
@cached_property
|
|
13
|
+
def validity_date(self) -> date:
|
|
14
|
+
if validity_date_repr := self.request.GET.get("validity_date"):
|
|
15
|
+
return datetime.strptime(validity_date_repr, "%Y-%m-%d")
|
|
16
|
+
return date.today()
|
|
17
|
+
|
|
18
|
+
def get_queryset(self):
|
|
19
|
+
# allow the user to see claim only on accounts it can see
|
|
20
|
+
return (
|
|
21
|
+
super().get_queryset().filter_for_user(self.request.user, validity_date=self.validity_date) # type: ignore
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RebatePermissionMixin:
|
|
26
|
+
queryset = Rebate.objects.all()
|
|
27
|
+
request: HttpRequest
|
|
28
|
+
|
|
29
|
+
@cached_property
|
|
30
|
+
def validity_date(self) -> date:
|
|
31
|
+
if validity_date_repr := self.request.GET.get("validity_date"):
|
|
32
|
+
return datetime.strptime(validity_date_repr, "%Y-%m-%d")
|
|
33
|
+
return date.today()
|
|
34
|
+
|
|
35
|
+
def get_queryset(self):
|
|
36
|
+
# allow the user to see claim only on accounts it can see
|
|
37
|
+
return (
|
|
38
|
+
super().get_queryset().filter_for_user(self.request.user, validity_date=self.validity_date) # type: ignore
|
|
39
|
+
)
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from datetime import date
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from django.db.models import Exists, ExpressionWrapper, F, FloatField, OuterRef, Sum
|
|
8
|
+
from django.db.models.functions import Coalesce
|
|
9
|
+
from django.shortcuts import get_object_or_404
|
|
10
|
+
from django.utils.dateparse import parse_date
|
|
11
|
+
from django.utils.functional import cached_property
|
|
12
|
+
from rest_framework import status
|
|
13
|
+
from rest_framework.decorators import action
|
|
14
|
+
from rest_framework.response import Response
|
|
15
|
+
from wbcommission.filters import (
|
|
16
|
+
CustomerRebateGroupByFilter,
|
|
17
|
+
RebateDateFilter,
|
|
18
|
+
RebateGroupByFilter,
|
|
19
|
+
RebateMarginalityFilter,
|
|
20
|
+
)
|
|
21
|
+
from wbcommission.models import CommissionType, Rebate
|
|
22
|
+
from wbcommission.models.rebate import RebateGroupbyChoice, manage_rebate_as_task
|
|
23
|
+
from wbcommission.reports.audit_report import create_audit_report_and_send_as_task
|
|
24
|
+
from wbcommission.reports.customer_report import create_customer_report_and_send_as_task
|
|
25
|
+
from wbcommission.serializers import RebateModelSerializer
|
|
26
|
+
from wbcommission.viewsets.buttons.rebate import RebateTableButtonConfig
|
|
27
|
+
from wbcommission.viewsets.display.rebate import (
|
|
28
|
+
RebatePandasViewDisplayConfig,
|
|
29
|
+
RebateProductMarginalityDisplayConfig,
|
|
30
|
+
)
|
|
31
|
+
from wbcommission.viewsets.endpoints.rebate import (
|
|
32
|
+
RebatePandasViewEndpointConfig,
|
|
33
|
+
RebateProductMarginalityEndpointConfig,
|
|
34
|
+
)
|
|
35
|
+
from wbcommission.viewsets.titles.rebate import (
|
|
36
|
+
RebatePandasViewTitleConfig,
|
|
37
|
+
RebateProductMarginalityTitleConfig,
|
|
38
|
+
)
|
|
39
|
+
from wbcore import serializers as wb_serializers
|
|
40
|
+
from wbcore import viewsets
|
|
41
|
+
from wbcore.contrib.currency.models import CurrencyFXRates
|
|
42
|
+
from wbcore.contrib.directory.models import Entry
|
|
43
|
+
from wbcore.contrib.io.viewsets import ExportPandasAPIViewSet
|
|
44
|
+
from wbcore.pandas import fields as pf
|
|
45
|
+
from wbcore.permissions.permissions import IsInternalUser
|
|
46
|
+
from wbcore.serializers import decorator
|
|
47
|
+
from wbcore.utils.date import get_date_interval_from_request
|
|
48
|
+
from wbcore.utils.strings import format_number
|
|
49
|
+
from wbcrm.models.accounts import Account
|
|
50
|
+
from wbfdm.models import Classification, ClassificationGroup, InstrumentPrice
|
|
51
|
+
from wbfdm.preferences import get_default_classification_group
|
|
52
|
+
from wbportfolio.models import Product
|
|
53
|
+
|
|
54
|
+
from ..analytics.marginality import MarginalityCalculator
|
|
55
|
+
from ..permissions import IsCommissionAdmin
|
|
56
|
+
from .mixins import RebatePermissionMixin
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class RebateModelViewSet(RebatePermissionMixin, viewsets.ModelViewSet):
|
|
60
|
+
serializer_class = RebateModelSerializer
|
|
61
|
+
queryset = Rebate.objects.all()
|
|
62
|
+
filterset_class = RebateDateFilter
|
|
63
|
+
|
|
64
|
+
endpoint_config_class = RebatePandasViewEndpointConfig
|
|
65
|
+
|
|
66
|
+
def get_aggregates(self, queryset, paginated_queryset):
|
|
67
|
+
return {"value_usd": {"Σ": format_number(queryset.aggregate(s=Sum(F("value_usd")))["s"] or 0)}}
|
|
68
|
+
|
|
69
|
+
def get_queryset(self):
|
|
70
|
+
return (
|
|
71
|
+
super()
|
|
72
|
+
.get_queryset()
|
|
73
|
+
.annotate(
|
|
74
|
+
fx_rate=Coalesce(CurrencyFXRates.get_fx_rates_subquery("date"), Decimal(1)),
|
|
75
|
+
value_usd=ExpressionWrapper(F("value") * F("fx_rate"), output_field=FloatField()),
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
@action(detail=False, methods=["PATCH"], permission_classes=[IsCommissionAdmin])
|
|
80
|
+
def recompute(self, request, pk=None):
|
|
81
|
+
if "start_date" in request.POST:
|
|
82
|
+
prune_existing = request.POST.get("prune_existing", "false") == "true"
|
|
83
|
+
start_date = parse_date(request.POST["start_date"])
|
|
84
|
+
if only_account_ids_repr := request.POST.get("only_accounts", None):
|
|
85
|
+
only_account_ids = only_account_ids_repr.split(",")
|
|
86
|
+
accounts = Account.objects.filter(id__in=only_account_ids, level=0)
|
|
87
|
+
else:
|
|
88
|
+
accounts = Account.objects.filter(level=0)
|
|
89
|
+
for account in accounts:
|
|
90
|
+
manage_rebate_as_task.delay(
|
|
91
|
+
account.id, start_date=start_date, prune_existing=prune_existing, user=request.user
|
|
92
|
+
)
|
|
93
|
+
return Response({"status": f"{accounts.count()} are being recomputed"}, status=status.HTTP_200_OK)
|
|
94
|
+
return Response({}, status=status.HTTP_400_BAD_REQUEST)
|
|
95
|
+
|
|
96
|
+
@action(detail=False, methods=["GET", "PATCH"], permission_classes=[IsInternalUser])
|
|
97
|
+
def customerreport(self, request, pk=None):
|
|
98
|
+
start, end = get_date_interval_from_request(request, request_type="POST")
|
|
99
|
+
user = request.user
|
|
100
|
+
recipient = get_object_or_404(Entry, pk=request.GET.get("recipient_id", None))
|
|
101
|
+
create_customer_report_and_send_as_task.delay(user.id, recipient.id, start, end)
|
|
102
|
+
return Response({"__notification": {"title": "Report send."}}, status=status.HTTP_200_OK)
|
|
103
|
+
|
|
104
|
+
@action(detail=False, methods=["GET", "PATCH"], permission_classes=[IsCommissionAdmin])
|
|
105
|
+
def auditreport(self, request, pk=None):
|
|
106
|
+
start, end = get_date_interval_from_request(request, request_type="POST")
|
|
107
|
+
user = request.user
|
|
108
|
+
recipient = get_object_or_404(Entry, pk=request.GET.get("recipient_id", None))
|
|
109
|
+
create_audit_report_and_send_as_task.delay(user.id, recipient.id, start, end)
|
|
110
|
+
return Response({"__notification": {"title": "Report send."}}, status=status.HTTP_200_OK)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class RebatePandasView(RebatePermissionMixin, ExportPandasAPIViewSet):
|
|
114
|
+
queryset = Rebate.objects.all()
|
|
115
|
+
|
|
116
|
+
filterset_class = RebateGroupByFilter
|
|
117
|
+
|
|
118
|
+
search_fields = ["title"]
|
|
119
|
+
|
|
120
|
+
display_config_class = RebatePandasViewDisplayConfig
|
|
121
|
+
title_config_class = RebatePandasViewTitleConfig
|
|
122
|
+
endpoint_config_class = RebatePandasViewEndpointConfig
|
|
123
|
+
button_config_class = RebateTableButtonConfig
|
|
124
|
+
|
|
125
|
+
def get_ordering_fields(self):
|
|
126
|
+
return ["rebate_total", *map(lambda x: f"rebate_{x}", self.rebate_types.keys())]
|
|
127
|
+
|
|
128
|
+
def get_pandas_fields(self, request):
|
|
129
|
+
fields = [pf.PKField(key="id", label="ID"), pf.CharField(key="title", label="Title")]
|
|
130
|
+
for key, label in self.rebate_types.items():
|
|
131
|
+
fields.append(
|
|
132
|
+
pf.FloatField(
|
|
133
|
+
key="rebate_" + key,
|
|
134
|
+
label=label,
|
|
135
|
+
precision=2,
|
|
136
|
+
decorators=[decorator(decorator_type="text", position="left", value="$")],
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
fields.append(
|
|
140
|
+
pf.FloatField(
|
|
141
|
+
key="rebate_total",
|
|
142
|
+
label="Total Rebate",
|
|
143
|
+
precision=2,
|
|
144
|
+
decorators=[decorator(decorator_type="text", position="left", value="$")],
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
return pf.PandasFields(fields=fields)
|
|
148
|
+
|
|
149
|
+
@cached_property
|
|
150
|
+
def groupby_classification_group(self) -> ClassificationGroup:
|
|
151
|
+
try:
|
|
152
|
+
return ClassificationGroup.objects.get(id=self.request.GET["groupby_classification_group"])
|
|
153
|
+
except (ValueError, KeyError):
|
|
154
|
+
return get_default_classification_group()
|
|
155
|
+
|
|
156
|
+
@cached_property
|
|
157
|
+
def rebate_types(self):
|
|
158
|
+
return dict(CommissionType.objects.values_list("key", "name"))
|
|
159
|
+
|
|
160
|
+
def get_queryset(self):
|
|
161
|
+
qs = (
|
|
162
|
+
super()
|
|
163
|
+
.get_queryset()
|
|
164
|
+
.annotate(
|
|
165
|
+
fx_rate=CurrencyFXRates.get_fx_rates_subquery("date", lookup_expr="exact"),
|
|
166
|
+
value_usd=F("value") * F("fx_rate"),
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
qs = Account.annotate_root_account_info(qs)
|
|
170
|
+
qs = self.groupby_classification_group.annotate_queryset(qs, 0, "product")
|
|
171
|
+
return (
|
|
172
|
+
qs.select_related("product")
|
|
173
|
+
.prefetch_related("product__currency")
|
|
174
|
+
.prefetch_related("product__parent")
|
|
175
|
+
.select_related("recipient")
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def get_dataframe(self, request, queryset, **kwargs):
|
|
179
|
+
groupby = self.request.GET.get("group_by", "PRODUCT")
|
|
180
|
+
groupby_map = RebateGroupbyChoice.map[groupby]
|
|
181
|
+
pivot = groupby_map["pk"]
|
|
182
|
+
pivot_label = groupby_map["title_key"]
|
|
183
|
+
if pivot == "classification_id":
|
|
184
|
+
df = pd.DataFrame(
|
|
185
|
+
queryset.values_list(
|
|
186
|
+
"value_usd",
|
|
187
|
+
"commission_type__key",
|
|
188
|
+
"classifications",
|
|
189
|
+
),
|
|
190
|
+
columns=["value_usd", "commission_type__key", "classifications"],
|
|
191
|
+
)
|
|
192
|
+
df = (
|
|
193
|
+
df.explode("classifications")
|
|
194
|
+
.rename(columns={"classifications": "classification_id"})
|
|
195
|
+
.replace([np.inf, -np.inf, np.nan], None)
|
|
196
|
+
)
|
|
197
|
+
df["classification_title"] = df.classification_id.map(
|
|
198
|
+
dict(Classification.objects.filter(id__in=df.classification_id.unique()).values_list("id", "name")),
|
|
199
|
+
na_action="ignore",
|
|
200
|
+
)
|
|
201
|
+
else:
|
|
202
|
+
df = pd.DataFrame(
|
|
203
|
+
queryset.values_list(
|
|
204
|
+
"value_usd",
|
|
205
|
+
"commission_type__key",
|
|
206
|
+
pivot,
|
|
207
|
+
pivot_label,
|
|
208
|
+
),
|
|
209
|
+
columns=["value_usd", "commission_type__key", pivot, pivot_label],
|
|
210
|
+
)
|
|
211
|
+
df = df.rename(columns={pivot: "id", pivot_label: "title"})
|
|
212
|
+
df = pd.pivot_table(
|
|
213
|
+
df,
|
|
214
|
+
index=["id", "title"],
|
|
215
|
+
columns="commission_type__key",
|
|
216
|
+
values="value_usd",
|
|
217
|
+
aggfunc="sum",
|
|
218
|
+
fill_value=0,
|
|
219
|
+
)
|
|
220
|
+
df["total"] = df.sum(axis=1)
|
|
221
|
+
# Ensure all type are always present as a columns
|
|
222
|
+
for commission_type in self.rebate_types.keys():
|
|
223
|
+
if commission_type not in df.columns:
|
|
224
|
+
df[commission_type] = 0
|
|
225
|
+
df = df.add_prefix("rebate_").reset_index()
|
|
226
|
+
df.id = df.id.fillna(0)
|
|
227
|
+
df.title = df.title.fillna("None")
|
|
228
|
+
df[df.columns.difference(["id", "title"])] = df[df.columns.difference(["id", "title"])].astype("float")
|
|
229
|
+
return df
|
|
230
|
+
|
|
231
|
+
def get_aggregates(self, request, df):
|
|
232
|
+
if df.empty:
|
|
233
|
+
return {}
|
|
234
|
+
return {
|
|
235
|
+
**{
|
|
236
|
+
"rebate_"
|
|
237
|
+
+ commission_type.key: {
|
|
238
|
+
"Σ": format_number(df["rebate_" + commission_type.key].sum()),
|
|
239
|
+
}
|
|
240
|
+
for commission_type in CommissionType.objects.all()
|
|
241
|
+
},
|
|
242
|
+
"rebate_total": {
|
|
243
|
+
"Σ": format_number(df["rebate_total"].sum()),
|
|
244
|
+
},
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
def get_filterset_class(self, request):
|
|
248
|
+
profile = request.user.profile
|
|
249
|
+
if profile.is_internal or request.user.is_superuser:
|
|
250
|
+
return RebateGroupByFilter
|
|
251
|
+
return CustomerRebateGroupByFilter
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class RebateProductMarginalityViewSet(ExportPandasAPIViewSet):
|
|
255
|
+
IDENTIFIER = "wbcommission:product-marginality"
|
|
256
|
+
|
|
257
|
+
filterset_class = RebateMarginalityFilter
|
|
258
|
+
|
|
259
|
+
pandas_fields = pf.PandasFields(
|
|
260
|
+
fields=[
|
|
261
|
+
pf.PKField(key="id", label="ID"),
|
|
262
|
+
pf.CharField(key="title", label="title"),
|
|
263
|
+
pf.CharField(key="currency_symbol", label="Currency"),
|
|
264
|
+
pf.FloatField(key="base_management_fees_percent", label="Base Management Fees", percent=True),
|
|
265
|
+
pf.FloatField(
|
|
266
|
+
key="management_fees",
|
|
267
|
+
label="management_fees",
|
|
268
|
+
decorators=[
|
|
269
|
+
wb_serializers.decorator(decorator_type="text", position="left", value="{{currency_symbol}}")
|
|
270
|
+
],
|
|
271
|
+
),
|
|
272
|
+
pf.FloatField(
|
|
273
|
+
key="management_rebates",
|
|
274
|
+
label="management_rebates",
|
|
275
|
+
decorators=[
|
|
276
|
+
wb_serializers.decorator(decorator_type="text", position="left", value="{{currency_symbol}}")
|
|
277
|
+
],
|
|
278
|
+
),
|
|
279
|
+
pf.FloatField(
|
|
280
|
+
key="management_marginality",
|
|
281
|
+
label="management_marginality",
|
|
282
|
+
decorators=[
|
|
283
|
+
wb_serializers.decorator(decorator_type="text", position="left", value="{{currency_symbol}}")
|
|
284
|
+
],
|
|
285
|
+
),
|
|
286
|
+
pf.FloatField(key="base_performance_fees_percent", label="Base Performance Fees", percent=True),
|
|
287
|
+
pf.FloatField(
|
|
288
|
+
key="performance_fees",
|
|
289
|
+
label="performance_fees",
|
|
290
|
+
decorators=[
|
|
291
|
+
wb_serializers.decorator(decorator_type="text", position="left", value="{{currency_symbol}}")
|
|
292
|
+
],
|
|
293
|
+
),
|
|
294
|
+
pf.FloatField(
|
|
295
|
+
key="performance_rebates",
|
|
296
|
+
label="performance_rebates",
|
|
297
|
+
decorators=[
|
|
298
|
+
wb_serializers.decorator(decorator_type="text", position="left", value="{{currency_symbol}}")
|
|
299
|
+
],
|
|
300
|
+
),
|
|
301
|
+
pf.FloatField(
|
|
302
|
+
key="performance_marginality",
|
|
303
|
+
label="performance_marginality",
|
|
304
|
+
decorators=[
|
|
305
|
+
wb_serializers.decorator(decorator_type="text", position="left", value="{{currency_symbol}}")
|
|
306
|
+
],
|
|
307
|
+
),
|
|
308
|
+
pf.FloatField(
|
|
309
|
+
key="total_fees",
|
|
310
|
+
label="total_fees",
|
|
311
|
+
decorators=[
|
|
312
|
+
wb_serializers.decorator(decorator_type="text", position="left", value="{{currency_symbol}}")
|
|
313
|
+
],
|
|
314
|
+
),
|
|
315
|
+
pf.FloatField(
|
|
316
|
+
key="total_rebates",
|
|
317
|
+
label="total_rebates",
|
|
318
|
+
decorators=[
|
|
319
|
+
wb_serializers.decorator(decorator_type="text", position="left", value="{{currency_symbol}}"),
|
|
320
|
+
],
|
|
321
|
+
),
|
|
322
|
+
pf.FloatField(key="total_marginality_percent", label="total_marginality_percent", percent=True),
|
|
323
|
+
pf.FloatField(
|
|
324
|
+
key="total_fees_usd",
|
|
325
|
+
label="total_fees_usd",
|
|
326
|
+
decorators=[wb_serializers.decorator(decorator_type="text", position="left", value="$")],
|
|
327
|
+
),
|
|
328
|
+
pf.FloatField(
|
|
329
|
+
key="total_rebates_usd",
|
|
330
|
+
label="total_rebates_usd",
|
|
331
|
+
decorators=[wb_serializers.decorator(decorator_type="text", position="left", value="$")],
|
|
332
|
+
),
|
|
333
|
+
pf.FloatField(
|
|
334
|
+
key="total_marginality_usd",
|
|
335
|
+
label="total_marginality_usd",
|
|
336
|
+
decorators=[wb_serializers.decorator(decorator_type="text", position="left", value="$")],
|
|
337
|
+
),
|
|
338
|
+
pf.FloatField(key="net_management_marginality", label="net_management_marginality", percent=True),
|
|
339
|
+
pf.FloatField(key="net_performance_marginality", label="net_performance_marginality", percent=True),
|
|
340
|
+
]
|
|
341
|
+
)
|
|
342
|
+
queryset = Product.objects.all()
|
|
343
|
+
ordering_fields = [
|
|
344
|
+
"title",
|
|
345
|
+
"base_management_fees_percent",
|
|
346
|
+
"base_performance_fees_percent",
|
|
347
|
+
"management_fees",
|
|
348
|
+
"management_rebates",
|
|
349
|
+
"management_marginality",
|
|
350
|
+
"performance_fees",
|
|
351
|
+
"performance_rebates",
|
|
352
|
+
"performance_marginality",
|
|
353
|
+
"total_fees",
|
|
354
|
+
"total_rebates",
|
|
355
|
+
"total_marginality_percent",
|
|
356
|
+
"total_fees_usd",
|
|
357
|
+
"total_rebates_usd",
|
|
358
|
+
"total_marginality_usd",
|
|
359
|
+
"net_management_marginality",
|
|
360
|
+
]
|
|
361
|
+
search_fields = ["title", "bank_title"]
|
|
362
|
+
ordering = ["title"]
|
|
363
|
+
|
|
364
|
+
display_config_class = RebateProductMarginalityDisplayConfig
|
|
365
|
+
title_config_class = RebateProductMarginalityTitleConfig
|
|
366
|
+
endpoint_config_class = RebateProductMarginalityEndpointConfig
|
|
367
|
+
|
|
368
|
+
def get_aggregates(self, request, df):
|
|
369
|
+
aggregates = {}
|
|
370
|
+
if not df.empty:
|
|
371
|
+
total_fees = df.total_fees.sum()
|
|
372
|
+
total_rebates = df.total_rebates.sum()
|
|
373
|
+
total_marginality_percent = 1 - total_rebates / total_fees if total_fees != 0 else 0
|
|
374
|
+
|
|
375
|
+
aggregates = {
|
|
376
|
+
"total_marginality_percent": {"Σ": format_number(total_marginality_percent)},
|
|
377
|
+
"total_fees_usd": {"Σ": format_number(df.total_fees_usd.sum())},
|
|
378
|
+
"total_rebates_usd": {"Σ": format_number(df.total_rebates_usd.sum())},
|
|
379
|
+
"total_marginality_usd": {"Σ": format_number(df.total_marginality_usd.sum())},
|
|
380
|
+
}
|
|
381
|
+
if aggregated_net_management_marginality := getattr(self, "aggregated_net_management_marginality", None):
|
|
382
|
+
aggregates["net_management_marginality"] = {
|
|
383
|
+
"Σ": format_number(aggregated_net_management_marginality, decimal=4)
|
|
384
|
+
}
|
|
385
|
+
if aggregated_net_performance_marginality := getattr(self, "aggregated_net_performance_marginality", None):
|
|
386
|
+
aggregates["net_performance_marginality"] = {
|
|
387
|
+
"Σ": format_number(aggregated_net_performance_marginality, decimal=4)
|
|
388
|
+
}
|
|
389
|
+
currency_aggregates = defaultdict(dict)
|
|
390
|
+
for currency_symbol in df["currency_symbol"].unique():
|
|
391
|
+
for field in [
|
|
392
|
+
"management_fees",
|
|
393
|
+
"management_rebates",
|
|
394
|
+
"performance_fees",
|
|
395
|
+
"performance_rebates",
|
|
396
|
+
"total_fees",
|
|
397
|
+
"total_rebates",
|
|
398
|
+
"management_marginality",
|
|
399
|
+
"performance_marginality",
|
|
400
|
+
]:
|
|
401
|
+
currency_aggregates[field][currency_symbol] = format_number(
|
|
402
|
+
df.loc[df["currency_symbol"] == currency_symbol, field].sum()
|
|
403
|
+
)
|
|
404
|
+
aggregates.update(currency_aggregates)
|
|
405
|
+
return aggregates
|
|
406
|
+
|
|
407
|
+
@cached_property
|
|
408
|
+
def start(self) -> date | None:
|
|
409
|
+
return get_date_interval_from_request(self.request, exclude_weekend=True)[0]
|
|
410
|
+
|
|
411
|
+
@cached_property
|
|
412
|
+
def end(self) -> date | None:
|
|
413
|
+
return get_date_interval_from_request(self.request, exclude_weekend=True)[1]
|
|
414
|
+
|
|
415
|
+
def get_queryset(self):
|
|
416
|
+
if self.request.user.has_perm("wbcommission.administrate_commission"):
|
|
417
|
+
has_aum_subquery = InstrumentPrice.objects.filter(instrument=OuterRef("pk"), outstanding_shares__gt=0)
|
|
418
|
+
if self.start and self.end:
|
|
419
|
+
has_aum_subquery = has_aum_subquery.filter(
|
|
420
|
+
date__gte=self.start,
|
|
421
|
+
date__lte=self.end,
|
|
422
|
+
)
|
|
423
|
+
return super().get_queryset().annotate(has_aum=Exists(has_aum_subquery)).filter(has_aum=True)
|
|
424
|
+
return Product.objects.none()
|
|
425
|
+
|
|
426
|
+
def get_dataframe(self, request, queryset, **kwargs):
|
|
427
|
+
df = pd.DataFrame()
|
|
428
|
+
if queryset.exists() and self.start and self.end:
|
|
429
|
+
marginality_calculator = MarginalityCalculator(queryset, self.start, self.end)
|
|
430
|
+
df_products = (
|
|
431
|
+
pd.DataFrame(
|
|
432
|
+
queryset.values(
|
|
433
|
+
"id", "computed_str", "current_management_fees", "current_performance_fees", "currency__symbol"
|
|
434
|
+
)
|
|
435
|
+
)
|
|
436
|
+
.set_index("id")
|
|
437
|
+
.rename(
|
|
438
|
+
columns={
|
|
439
|
+
"currency__symbol": "currency_symbol",
|
|
440
|
+
"computed_str": "title",
|
|
441
|
+
"current_management_fees": "base_management_fees_percent",
|
|
442
|
+
"current_performance_fees": "base_performance_fees_percent",
|
|
443
|
+
}
|
|
444
|
+
)
|
|
445
|
+
)
|
|
446
|
+
# Concat dataframe and add extra statistic columns
|
|
447
|
+
df = pd.concat(
|
|
448
|
+
[
|
|
449
|
+
df_products["title"],
|
|
450
|
+
df_products["base_management_fees_percent"],
|
|
451
|
+
df_products["base_performance_fees_percent"],
|
|
452
|
+
df_products["currency_symbol"],
|
|
453
|
+
marginality_calculator.management_fees,
|
|
454
|
+
marginality_calculator.performance_fees,
|
|
455
|
+
marginality_calculator.total_fees,
|
|
456
|
+
marginality_calculator.total_rebates,
|
|
457
|
+
marginality_calculator.total_marginality_percent,
|
|
458
|
+
marginality_calculator.management_rebates,
|
|
459
|
+
marginality_calculator.performance_rebates,
|
|
460
|
+
marginality_calculator.management_marginality,
|
|
461
|
+
marginality_calculator.performance_marginality,
|
|
462
|
+
marginality_calculator.management_marginality_percent,
|
|
463
|
+
marginality_calculator.total_fees_usd,
|
|
464
|
+
marginality_calculator.total_rebates_usd,
|
|
465
|
+
marginality_calculator.total_marginality_usd,
|
|
466
|
+
],
|
|
467
|
+
axis=1,
|
|
468
|
+
)
|
|
469
|
+
df["net_management_marginality"] = marginality_calculator.get_net_marginality("management")
|
|
470
|
+
df["net_performance_marginality"] = marginality_calculator.get_net_marginality("performance")
|
|
471
|
+
self.aggregated_net_management_marginality = marginality_calculator.get_aggregated_net_marginality(
|
|
472
|
+
"management"
|
|
473
|
+
)
|
|
474
|
+
self.aggregated_net_performance_marginality = marginality_calculator.get_aggregated_net_marginality(
|
|
475
|
+
"performance"
|
|
476
|
+
)
|
|
477
|
+
# Sanitize
|
|
478
|
+
df = df.replace([np.inf, -np.inf, np.nan], 0).reset_index()
|
|
479
|
+
df = df[(df["total_fees"] != 0) | (df["total_rebates"] != 0)]
|
|
480
|
+
return df
|
|
481
|
+
return df
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .rebate import RebatePandasViewTitleConfig, RebateProductMarginalityTitleConfig
|
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from wbcore.metadata.configs.titles import TitleViewConfig
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class RebatePandasViewTitleConfig(TitleViewConfig):
|
|
5
|
+
def get_list_title(self):
|
|
6
|
+
return "Rebates"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RebateProductMarginalityTitleConfig(TitleViewConfig):
|
|
10
|
+
def get_list_title(self):
|
|
11
|
+
return "Marginality per Product"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: wbcommission
|
|
3
|
+
Version: 2.2.1
|
|
4
|
+
Summary: A workbench module for managing human resources.
|
|
5
|
+
Author-email: Christopher Wittlinger <c.wittlinger@stainly.com>
|
|
6
|
+
Requires-Dist: reportlab==3.*
|
|
7
|
+
Requires-Dist: wbcompliance
|
|
8
|
+
Requires-Dist: wbcore
|
|
9
|
+
Requires-Dist: wbcrm
|
|
10
|
+
Requires-Dist: wbfdm
|
|
11
|
+
Requires-Dist: wbnews
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
wbcommission/__init__.py,sha256=J-j-u0itpEFT6irdmWmixQqYMadNl1X91TxUmoiLHMI,22
|
|
2
|
+
wbcommission/apps.py,sha256=3q9lAeEBfv83rCzud3P0-VZgbWUfG0S61G5h3zFk1Ds,97
|
|
3
|
+
wbcommission/dynamic_preferences_registry.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
wbcommission/permissions.py,sha256=Qjmterwqsm1st8YZNfj_7-Gl6WnvVvkE6SCgYynMhSU,219
|
|
5
|
+
wbcommission/urls.py,sha256=6IgkFTSk7u7eD2CO3DXZP0oLFQtEQvUOg1EKqFLdeF4,1101
|
|
6
|
+
wbcommission/admin/__init__.py,sha256=xEYZYO9IUSNExPsY7jsxFS-Jjkl92tXnb7eSfwGpkZg,151
|
|
7
|
+
wbcommission/admin/accounts.py,sha256=8QtAEK5iCjbXlpOoxWC9-aO-ekvCRgKczOUb2oPqGRo,815
|
|
8
|
+
wbcommission/admin/commission.py,sha256=lye6PicAUKCGBVMmYxuIiz9g0DRbbTrv9eSryH0Uulg,2077
|
|
9
|
+
wbcommission/admin/rebate.py,sha256=XoDyGl1MoUAiUrm-GL5wlHvCzASgZqmGWvfMVOm2rJA,230
|
|
10
|
+
wbcommission/analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
wbcommission/analytics/marginality.py,sha256=bSIpjglq7_yYK2z1sfJ7ymUXUMS_FR_b_gwthVEVs2U,8116
|
|
12
|
+
wbcommission/factories/__init__.py,sha256=3EdqwSHMqJIa5C_yH15M75u_frkLEno151aFoxrOLN0,249
|
|
13
|
+
wbcommission/factories/commission.py,sha256=Hf99FIEwQaj42YivziqEWcIFu4aB_8uUiug1dh3RNDU,3020
|
|
14
|
+
wbcommission/factories/rebate.py,sha256=GnKE2LOb4ynNUxF1-A8sDpnxAO4HHdcrRWrFSTKDdTA,682
|
|
15
|
+
wbcommission/filters/__init__.py,sha256=9XoFMsmHcdTqy7ujQFlxvAJFvZoc4hjZ1SR6pyY8-9k,156
|
|
16
|
+
wbcommission/filters/rebate.py,sha256=42wDFwSsfPZHstawAnSnENx6fkLeel3St5w3_58IA2o,8300
|
|
17
|
+
wbcommission/filters/signals.py,sha256=J-a9F3IwWhgl7HZE9I2Z6PA6H7fKDnMrhtSQZ4wqhGU,1583
|
|
18
|
+
wbcommission/generators/__init__.py,sha256=Pt8TjtYCmqhROvWNmf_dxuONKVcrj7SH14nJAcv7_1Q,47
|
|
19
|
+
wbcommission/generators/rebate_generator.py,sha256=Cp_JvdI64N0tlTYDiOkk67ZwB8HTzCzTEHn2gdTVxEo,4154
|
|
20
|
+
wbcommission/migrations/0001_initial.py,sha256=mE-vL8OsbdVvpnAqOQm38ZHQj6MVlpsu78b5WvJaxl0,11816
|
|
21
|
+
wbcommission/migrations/0002_commissionrule_remove_accountcustomer_account_and_more.py,sha256=gAqysHE_jwEbNWkMTAbocBS6B5R9IDfV3fAn9wrPWyE,16384
|
|
22
|
+
wbcommission/migrations/0003_alter_commission_account.py,sha256=P0EciB1Rne8LsbJqgtBnzfSD6O6pVSmmfeB2Ue5zeUk,727
|
|
23
|
+
wbcommission/migrations/0004_rebate_audit_log.py,sha256=861gZjI0XwIjKgM60v663I4fj-IzGVYx91p2ydQmOkM,543
|
|
24
|
+
wbcommission/migrations/0005_alter_rebate_audit_log.py,sha256=u_1c9dGGG3zoppPNgnGT2eoSVOdPbksNC3ifUHrgUH0,538
|
|
25
|
+
wbcommission/migrations/0006_commissionrule_consider_zero_percent_for_exclusion.py,sha256=6km7vAPs49c7mAjxgVQDwKYOPs4exbfo2Hd0RFbtAvc,679
|
|
26
|
+
wbcommission/migrations/0007_remove_commission_unique_crm_recipient_account_and_more.py,sha256=Olo_Acmg-aGOq4ZwQA5HjvxcHEZlnIMqbJObBBUQz5k,1947
|
|
27
|
+
wbcommission/migrations/0008_alter_commission_options_alter_commission_order.py,sha256=iNwNF1QcmVoj5Eu2yPJEHfI7bIA3e4WZMILjCfBVEx0,830
|
|
28
|
+
wbcommission/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
29
|
+
wbcommission/models/__init__.py,sha256=BJyah0_zKssfU2ohvu4qcQSHAT1ovcZ_go2-sLEagyw,183
|
|
30
|
+
wbcommission/models/account_service.py,sha256=WUzcuWoZEr5twkZFEd-ejofkg6ZPCL3o-0vYXvIVAkI,10652
|
|
31
|
+
wbcommission/models/commission.py,sha256=tkWb7ej2wjIGc_GIj-m02AGEwSEoElQ12LO6nyE1xUg,29574
|
|
32
|
+
wbcommission/models/rebate.py,sha256=TWrnTEjJkbr_KQb_pWnvdPWbFzRxJZyxoyZT5cfI1c4,12699
|
|
33
|
+
wbcommission/models/signals.py,sha256=d-123hxTlbp2A9AwUOE_E90jrDN1e3_r7d8aGq7SmiY,2270
|
|
34
|
+
wbcommission/reports/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
35
|
+
wbcommission/reports/audit_report.py,sha256=FAcZbXrzRfchYcAgC9B6XyQFY_o4vYnCVbkdoB5jfTg,2177
|
|
36
|
+
wbcommission/reports/customer_report.py,sha256=_vciCoE2D43W7r2tCaTK-n_jm01dfFGM_wYD82-yGhc,14245
|
|
37
|
+
wbcommission/reports/utils.py,sha256=Twc1SOwRIkB-_RJHFO1-tJaQ3ocCjFz0GsUXZPMZ3i8,1078
|
|
38
|
+
wbcommission/serializers/__init__.py,sha256=t38bchJeNgOcogmedHLfLBQBtRXuR-_hj0US_55Fug4,196
|
|
39
|
+
wbcommission/serializers/commissions.py,sha256=mWSxVuKAZdSHFfBuqB0c7Nf_cYBXL8kb7SQCgVNXuRw,677
|
|
40
|
+
wbcommission/serializers/rebate.py,sha256=q69cDY8YO7-m9QkNg5gatgY7QnXZq9AnR42lS7EuBQY,4037
|
|
41
|
+
wbcommission/serializers/signals.py,sha256=lX-oBPhWoIrDTSvP3UbKRh6VA16T9-NtisMFPwXP8-c,1427
|
|
42
|
+
wbcommission/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
43
|
+
wbcommission/tests/conftest.py,sha256=WEuFy8wOiCvUxKHObOBU0xsCZ_AZubXS0jtfIakTrGY,2605
|
|
44
|
+
wbcommission/tests/signals.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
45
|
+
wbcommission/tests/test_permissions.py,sha256=My-dDaBfkBcVD7aakhYkHFtNVS_6ag8C0c7kRP--wIU,2537
|
|
46
|
+
wbcommission/tests/analytics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
47
|
+
wbcommission/tests/analytics/test_marginality.py,sha256=AtAVSD_DRG2YiwQCkeicrssjYtWK1HDegM9HBRqlBZg,11719
|
|
48
|
+
wbcommission/tests/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
49
|
+
wbcommission/tests/models/mixins.py,sha256=s0Z_zMZLee6KCdUOrOTAutdgTz2nZhzJJsi8cO8xDjc,817
|
|
50
|
+
wbcommission/tests/models/test_account_service.py,sha256=ABHIyW82404d3RIlTPqmUt7kGUPdH8bUtSyUtJZ_AHc,13490
|
|
51
|
+
wbcommission/tests/models/test_commission.py,sha256=dBtvh3SorcAVzTFE5jUHd3gsmqa8QaTBJQ-ooxreySo,28064
|
|
52
|
+
wbcommission/tests/models/test_rebate.py,sha256=pbgao6plo5TCGpkiRIQtl6Y0sSP4D0UCtBmqjswFVd0,5745
|
|
53
|
+
wbcommission/tests/viewsets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
54
|
+
wbcommission/tests/viewsets/test_rebate.py,sha256=q-naStrCA-uqQVzOYPE7EmG9c7dbgSLgUJ-_rMbNMgM,3261
|
|
55
|
+
wbcommission/viewsets/__init__.py,sha256=iuWo3Y9t8XxPgRG5DIURBk0J8hDoK2oTmMhv9hiAUcU,232
|
|
56
|
+
wbcommission/viewsets/commissions.py,sha256=pL8GOaGlHbEgX86OXKa1HHZSLnQl8uNtpukuhotB-WM,759
|
|
57
|
+
wbcommission/viewsets/mixins.py,sha256=g5hopDDpDlimlOoH55x-C4HIZxsn_RJnpLztjNY_vDY,1308
|
|
58
|
+
wbcommission/viewsets/rebate.py,sha256=mnkifgroC7qKzbUHJKoy5zaEhpmgqHBRnV6NjLoQgfM,20947
|
|
59
|
+
wbcommission/viewsets/buttons/__init__.py,sha256=hebcLFrOBe36pjHpYLxkY4ucT75WtQiQxIsjaO0rD14,67
|
|
60
|
+
wbcommission/viewsets/buttons/rebate.py,sha256=UXwwtlz24Dyko-6HF4ad3GKvc3YNfFNzVjWOLztI1Vs,2376
|
|
61
|
+
wbcommission/viewsets/buttons/signals.py,sha256=7c-TI_SxGuCi-Y4e8yHFUva6TLIdXI5NHOKGowrARNI,2220
|
|
62
|
+
wbcommission/viewsets/display/__init__.py,sha256=_aqbPI2tZZJNgzIB05MH_eFbWmraTb8tB0R6llAPXFw,160
|
|
63
|
+
wbcommission/viewsets/display/commissions.py,sha256=4ifC3j3-wE1t57Yy9x02RnT1VAYbeXUtIMxq4ljdt-c,671
|
|
64
|
+
wbcommission/viewsets/display/rebate.py,sha256=T45d_FP3W6934BwWVBAiT58tb4Dr7BmIA9nPnAdoOEA,5287
|
|
65
|
+
wbcommission/viewsets/endpoints/__init__.py,sha256=ghffTV9z9LxVzxNS4divoQ54sVosYZiJCEuqeUJ5gmo,135
|
|
66
|
+
wbcommission/viewsets/endpoints/commissions.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
67
|
+
wbcommission/viewsets/endpoints/rebate.py,sha256=4QOR3MHD7JZVr2LhPi51d1xKg_ttbfD4YbJ4mGq9QjU,728
|
|
68
|
+
wbcommission/viewsets/menu/__init__.py,sha256=i2Sn3h8e-KZefQHg72QS4ba5P34pTGFe3s-dobYW5WU,65
|
|
69
|
+
wbcommission/viewsets/menu/commissions.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
70
|
+
wbcommission/viewsets/menu/rebate.py,sha256=Qzhwr2xG3_j7frxs3eWLZuMoUzT2nCBrWmEYbZ8fKfU,484
|
|
71
|
+
wbcommission/viewsets/titles/__init__.py,sha256=Sg64aMWm_OD4-P57YoaN6Mqv6NRVU5m7ii_WvaWuTsY,85
|
|
72
|
+
wbcommission/viewsets/titles/commissions.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
73
|
+
wbcommission/viewsets/titles/rebate.py,sha256=VTSHJW3voqCMVhDZ_zVeVmZid_WY3P4X9k11VdJM_GQ,301
|
|
74
|
+
wbcommission-2.2.1.dist-info/METADATA,sha256=0uHLSJaMN3PDweadP-mBkB3zUztq5wabMuxCGuTotuM,322
|
|
75
|
+
wbcommission-2.2.1.dist-info/WHEEL,sha256=aO3RJuuiFXItVSnAUEmQ0yRBvv9e1sbJh68PtuQkyAE,105
|
|
76
|
+
wbcommission-2.2.1.dist-info/RECORD,,
|