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.

Files changed (76) hide show
  1. wbcommission/__init__.py +1 -0
  2. wbcommission/admin/__init__.py +4 -0
  3. wbcommission/admin/accounts.py +22 -0
  4. wbcommission/admin/commission.py +85 -0
  5. wbcommission/admin/rebate.py +7 -0
  6. wbcommission/analytics/__init__.py +0 -0
  7. wbcommission/analytics/marginality.py +181 -0
  8. wbcommission/apps.py +5 -0
  9. wbcommission/dynamic_preferences_registry.py +0 -0
  10. wbcommission/factories/__init__.py +9 -0
  11. wbcommission/factories/commission.py +100 -0
  12. wbcommission/factories/rebate.py +16 -0
  13. wbcommission/filters/__init__.py +7 -0
  14. wbcommission/filters/rebate.py +187 -0
  15. wbcommission/filters/signals.py +44 -0
  16. wbcommission/generators/__init__.py +2 -0
  17. wbcommission/generators/rebate_generator.py +93 -0
  18. wbcommission/migrations/0001_initial.py +299 -0
  19. wbcommission/migrations/0002_commissionrule_remove_accountcustomer_account_and_more.py +395 -0
  20. wbcommission/migrations/0003_alter_commission_account.py +24 -0
  21. wbcommission/migrations/0004_rebate_audit_log.py +19 -0
  22. wbcommission/migrations/0005_alter_rebate_audit_log.py +20 -0
  23. wbcommission/migrations/0006_commissionrule_consider_zero_percent_for_exclusion.py +21 -0
  24. wbcommission/migrations/0007_remove_commission_unique_crm_recipient_account_and_more.py +50 -0
  25. wbcommission/migrations/0008_alter_commission_options_alter_commission_order.py +26 -0
  26. wbcommission/migrations/__init__.py +0 -0
  27. wbcommission/models/__init__.py +9 -0
  28. wbcommission/models/account_service.py +217 -0
  29. wbcommission/models/commission.py +679 -0
  30. wbcommission/models/rebate.py +319 -0
  31. wbcommission/models/signals.py +45 -0
  32. wbcommission/permissions.py +6 -0
  33. wbcommission/reports/__init__.py +0 -0
  34. wbcommission/reports/audit_report.py +51 -0
  35. wbcommission/reports/customer_report.py +299 -0
  36. wbcommission/reports/utils.py +30 -0
  37. wbcommission/serializers/__init__.py +3 -0
  38. wbcommission/serializers/commissions.py +26 -0
  39. wbcommission/serializers/rebate.py +87 -0
  40. wbcommission/serializers/signals.py +27 -0
  41. wbcommission/tests/__init__.py +0 -0
  42. wbcommission/tests/analytics/__init__.py +0 -0
  43. wbcommission/tests/analytics/test_marginality.py +253 -0
  44. wbcommission/tests/conftest.py +89 -0
  45. wbcommission/tests/models/__init__.py +0 -0
  46. wbcommission/tests/models/mixins.py +22 -0
  47. wbcommission/tests/models/test_account_service.py +293 -0
  48. wbcommission/tests/models/test_commission.py +587 -0
  49. wbcommission/tests/models/test_rebate.py +136 -0
  50. wbcommission/tests/signals.py +0 -0
  51. wbcommission/tests/test_permissions.py +66 -0
  52. wbcommission/tests/viewsets/__init__.py +0 -0
  53. wbcommission/tests/viewsets/test_rebate.py +76 -0
  54. wbcommission/urls.py +42 -0
  55. wbcommission/viewsets/__init__.py +7 -0
  56. wbcommission/viewsets/buttons/__init__.py +2 -0
  57. wbcommission/viewsets/buttons/rebate.py +46 -0
  58. wbcommission/viewsets/buttons/signals.py +53 -0
  59. wbcommission/viewsets/commissions.py +21 -0
  60. wbcommission/viewsets/display/__init__.py +5 -0
  61. wbcommission/viewsets/display/commissions.py +21 -0
  62. wbcommission/viewsets/display/rebate.py +117 -0
  63. wbcommission/viewsets/endpoints/__init__.py +4 -0
  64. wbcommission/viewsets/endpoints/commissions.py +0 -0
  65. wbcommission/viewsets/endpoints/rebate.py +21 -0
  66. wbcommission/viewsets/menu/__init__.py +1 -0
  67. wbcommission/viewsets/menu/commissions.py +0 -0
  68. wbcommission/viewsets/menu/rebate.py +13 -0
  69. wbcommission/viewsets/mixins.py +39 -0
  70. wbcommission/viewsets/rebate.py +481 -0
  71. wbcommission/viewsets/titles/__init__.py +1 -0
  72. wbcommission/viewsets/titles/commissions.py +0 -0
  73. wbcommission/viewsets/titles/rebate.py +11 -0
  74. wbcommission-2.2.1.dist-info/METADATA +11 -0
  75. wbcommission-2.2.1.dist-info/RECORD +76 -0
  76. 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,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.26.3
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any