wbcommission 2.2.1__tar.gz

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-2.2.1/.gitignore +181 -0
  2. wbcommission-2.2.1/PKG-INFO +11 -0
  3. wbcommission-2.2.1/pyproject.toml +38 -0
  4. wbcommission-2.2.1/wbcommission/__init__.py +1 -0
  5. wbcommission-2.2.1/wbcommission/admin/__init__.py +4 -0
  6. wbcommission-2.2.1/wbcommission/admin/accounts.py +22 -0
  7. wbcommission-2.2.1/wbcommission/admin/commission.py +85 -0
  8. wbcommission-2.2.1/wbcommission/admin/rebate.py +7 -0
  9. wbcommission-2.2.1/wbcommission/analytics/__init__.py +0 -0
  10. wbcommission-2.2.1/wbcommission/analytics/marginality.py +181 -0
  11. wbcommission-2.2.1/wbcommission/apps.py +5 -0
  12. wbcommission-2.2.1/wbcommission/dynamic_preferences_registry.py +0 -0
  13. wbcommission-2.2.1/wbcommission/factories/__init__.py +9 -0
  14. wbcommission-2.2.1/wbcommission/factories/commission.py +100 -0
  15. wbcommission-2.2.1/wbcommission/factories/rebate.py +16 -0
  16. wbcommission-2.2.1/wbcommission/filters/__init__.py +7 -0
  17. wbcommission-2.2.1/wbcommission/filters/rebate.py +187 -0
  18. wbcommission-2.2.1/wbcommission/filters/signals.py +44 -0
  19. wbcommission-2.2.1/wbcommission/generators/__init__.py +2 -0
  20. wbcommission-2.2.1/wbcommission/generators/rebate_generator.py +93 -0
  21. wbcommission-2.2.1/wbcommission/migrations/0001_initial.py +299 -0
  22. wbcommission-2.2.1/wbcommission/migrations/0002_commissionrule_remove_accountcustomer_account_and_more.py +395 -0
  23. wbcommission-2.2.1/wbcommission/migrations/0003_alter_commission_account.py +24 -0
  24. wbcommission-2.2.1/wbcommission/migrations/0004_rebate_audit_log.py +19 -0
  25. wbcommission-2.2.1/wbcommission/migrations/0005_alter_rebate_audit_log.py +20 -0
  26. wbcommission-2.2.1/wbcommission/migrations/0006_commissionrule_consider_zero_percent_for_exclusion.py +21 -0
  27. wbcommission-2.2.1/wbcommission/migrations/0007_remove_commission_unique_crm_recipient_account_and_more.py +50 -0
  28. wbcommission-2.2.1/wbcommission/migrations/0008_alter_commission_options_alter_commission_order.py +26 -0
  29. wbcommission-2.2.1/wbcommission/migrations/__init__.py +0 -0
  30. wbcommission-2.2.1/wbcommission/models/__init__.py +9 -0
  31. wbcommission-2.2.1/wbcommission/models/account_service.py +217 -0
  32. wbcommission-2.2.1/wbcommission/models/commission.py +679 -0
  33. wbcommission-2.2.1/wbcommission/models/rebate.py +319 -0
  34. wbcommission-2.2.1/wbcommission/models/signals.py +45 -0
  35. wbcommission-2.2.1/wbcommission/permissions.py +6 -0
  36. wbcommission-2.2.1/wbcommission/reports/__init__.py +0 -0
  37. wbcommission-2.2.1/wbcommission/reports/audit_report.py +51 -0
  38. wbcommission-2.2.1/wbcommission/reports/customer_report.py +299 -0
  39. wbcommission-2.2.1/wbcommission/reports/utils.py +30 -0
  40. wbcommission-2.2.1/wbcommission/serializers/__init__.py +3 -0
  41. wbcommission-2.2.1/wbcommission/serializers/commissions.py +26 -0
  42. wbcommission-2.2.1/wbcommission/serializers/rebate.py +87 -0
  43. wbcommission-2.2.1/wbcommission/serializers/signals.py +27 -0
  44. wbcommission-2.2.1/wbcommission/tests/__init__.py +0 -0
  45. wbcommission-2.2.1/wbcommission/tests/analytics/__init__.py +0 -0
  46. wbcommission-2.2.1/wbcommission/tests/analytics/test_marginality.py +253 -0
  47. wbcommission-2.2.1/wbcommission/tests/conftest.py +89 -0
  48. wbcommission-2.2.1/wbcommission/tests/models/__init__.py +0 -0
  49. wbcommission-2.2.1/wbcommission/tests/models/mixins.py +22 -0
  50. wbcommission-2.2.1/wbcommission/tests/models/test_account_service.py +293 -0
  51. wbcommission-2.2.1/wbcommission/tests/models/test_commission.py +587 -0
  52. wbcommission-2.2.1/wbcommission/tests/models/test_rebate.py +136 -0
  53. wbcommission-2.2.1/wbcommission/tests/signals.py +0 -0
  54. wbcommission-2.2.1/wbcommission/tests/test_permissions.py +66 -0
  55. wbcommission-2.2.1/wbcommission/tests/viewsets/__init__.py +0 -0
  56. wbcommission-2.2.1/wbcommission/tests/viewsets/test_rebate.py +76 -0
  57. wbcommission-2.2.1/wbcommission/urls.py +42 -0
  58. wbcommission-2.2.1/wbcommission/viewsets/__init__.py +7 -0
  59. wbcommission-2.2.1/wbcommission/viewsets/buttons/__init__.py +2 -0
  60. wbcommission-2.2.1/wbcommission/viewsets/buttons/rebate.py +46 -0
  61. wbcommission-2.2.1/wbcommission/viewsets/buttons/signals.py +53 -0
  62. wbcommission-2.2.1/wbcommission/viewsets/commissions.py +21 -0
  63. wbcommission-2.2.1/wbcommission/viewsets/display/__init__.py +5 -0
  64. wbcommission-2.2.1/wbcommission/viewsets/display/commissions.py +21 -0
  65. wbcommission-2.2.1/wbcommission/viewsets/display/rebate.py +117 -0
  66. wbcommission-2.2.1/wbcommission/viewsets/endpoints/__init__.py +4 -0
  67. wbcommission-2.2.1/wbcommission/viewsets/endpoints/commissions.py +0 -0
  68. wbcommission-2.2.1/wbcommission/viewsets/endpoints/rebate.py +21 -0
  69. wbcommission-2.2.1/wbcommission/viewsets/menu/__init__.py +1 -0
  70. wbcommission-2.2.1/wbcommission/viewsets/menu/commissions.py +0 -0
  71. wbcommission-2.2.1/wbcommission/viewsets/menu/rebate.py +13 -0
  72. wbcommission-2.2.1/wbcommission/viewsets/mixins.py +39 -0
  73. wbcommission-2.2.1/wbcommission/viewsets/rebate.py +481 -0
  74. wbcommission-2.2.1/wbcommission/viewsets/titles/__init__.py +1 -0
  75. wbcommission-2.2.1/wbcommission/viewsets/titles/commissions.py +0 -0
  76. wbcommission-2.2.1/wbcommission/viewsets/titles/rebate.py +11 -0
@@ -0,0 +1,181 @@
1
+ ~
2
+
3
+ # Docker volumes
4
+ volumes/
5
+ # Poetry auth file
6
+ auth.toml
7
+
8
+ media/*
9
+ media/
10
+ mediafiles/
11
+ mediafiles/*
12
+ test/*
13
+ staticfiles/*
14
+ staticfiles/
15
+ #
16
+ # Byte-compiled / optimized / DLL files
17
+ __pycache__/
18
+ *.py[cod]
19
+ *$py.class
20
+
21
+ # C extensions
22
+ *.so
23
+
24
+ # Distribution / packaging
25
+ .Python
26
+ build/
27
+ develop-eggs/
28
+ dist/
29
+ info/
30
+ downloads/
31
+ eggs/
32
+ .eggs/
33
+ lib/
34
+ lib64/
35
+ parts/
36
+ sdist/
37
+ var/
38
+ wheels/
39
+ share/python-wheels/
40
+ *.egg-info/
41
+ .installed.cfg
42
+ *.egg
43
+ MANIFEST
44
+
45
+ # PyInstaller
46
+ # Usually these files are written by a python script from a template
47
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
48
+ *.manifest
49
+ *.spec
50
+
51
+ # Installer logs
52
+ pip-log.txt
53
+ pip-delete-this-directory.txt
54
+
55
+ # Unit test / coverage reports
56
+ htmlcov/
57
+ .tox/
58
+ .nox/
59
+ .coverage
60
+ .coverage.*
61
+ .cache
62
+ .dccache
63
+ nosetests.xml
64
+ coverage.xml
65
+ *.cover
66
+ *.py,cover
67
+ .hypothesis/
68
+ .pytest_cache/
69
+ cover/
70
+ report.xml
71
+ */report.xml
72
+
73
+ # Translations
74
+ *.mo
75
+ *.pot
76
+
77
+ # Django stuff:
78
+ *.log
79
+ local_settings.py
80
+ *.sqlite3
81
+ db.sqlite3-journal
82
+
83
+ # Flask stuff:
84
+ instance/
85
+ .webassets-cache
86
+
87
+ # Scrapy stuff:
88
+ .scrapy
89
+
90
+ # Sphinx documentation
91
+ docs/_build/
92
+
93
+ # PyBuilder
94
+ .pybuilder/
95
+ target/
96
+
97
+ # Jupyter Notebook
98
+ .ipynb_checkpoints
99
+ *.ipynb
100
+ # IPython
101
+ profile_default/
102
+ ipython_config.py
103
+
104
+ # pyenv
105
+ # For a library or package, you might want to ignore these files since the code is
106
+ # intended to run in multiple environments; otherwise, check them in:
107
+ # .python-version
108
+
109
+ # pipenv
110
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
111
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
112
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
113
+ # install all needed dependencies.
114
+ #Pipfile.lock
115
+
116
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
117
+ __pypackages__/
118
+
119
+ # Celery stuff
120
+ celerybeat-schedule
121
+ celerybeat.pid
122
+
123
+ # SageMath parsed files
124
+ *.sage.py
125
+
126
+ # Environments
127
+ .env
128
+ .envrc
129
+ .venv
130
+ env/
131
+ venv/
132
+ ENV/
133
+ env.bak/
134
+ venv.bak/
135
+ .vscode/
136
+ .idea/
137
+ .idea.bkp/
138
+
139
+ # Spyder project settings
140
+ .spyderproject
141
+ .spyproject
142
+
143
+ # Rope project settings
144
+ .ropeproject
145
+
146
+ # mkdocs documentation
147
+ /site
148
+
149
+ # mypy
150
+ .mypy_cache/
151
+ .dmypy.json
152
+ dmypy.json
153
+
154
+ # Pyre type checker
155
+ .pyre/
156
+
157
+ # pytype static type analyzer
158
+ .pytype/
159
+ crm/
160
+ # Cython debug symbols
161
+ cython_debug/
162
+
163
+ # Gitlab Runner
164
+ builds
165
+ builds/
166
+
167
+ # Integrator Office 365 : reverse proxy tunnel for outlook365
168
+ ngrok
169
+ */ngrok
170
+ /modules/**/system/
171
+
172
+ /modules/wbmailing/files/*
173
+ /modules/wbmailing/mailing/*
174
+
175
+ /projects/*/requirements.txt
176
+ public
177
+
178
+ # Ignore archive localization generated folder
179
+ backend/modules/**/archive/*
180
+ **/**/requirements.txt
181
+ CHANGELOG-*
@@ -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,38 @@
1
+ [project]
2
+ name = "wbcommission"
3
+ description = "A workbench module for managing human resources."
4
+ authors = [{ name = "Christopher Wittlinger", email = "c.wittlinger@stainly.com"}]
5
+ dynamic = ["version"]
6
+
7
+ dependencies = [
8
+ "wbcore",
9
+ "wbcrm",
10
+ "wbnews",
11
+ "wbfdm",
12
+ "wbcompliance",
13
+ "reportlab == 3.*",
14
+ ]
15
+
16
+ [tool.uv.sources]
17
+ wbcore = { workspace = true }
18
+ wbcrm = { workspace = true }
19
+ wbnews = { workspace = true }
20
+ wbfdm = { workspace = true }
21
+ wbcompliance = { workspace = true }
22
+
23
+ [tool.uv]
24
+ package = true
25
+
26
+ [tool.hatch.version]
27
+ path = "../../pyproject.toml"
28
+
29
+ [tool.hatch.build.targets.sdist]
30
+ include = ["wbcommission/*"]
31
+
32
+ [tool.hatch.build.targets.wheel]
33
+ packages = ["wbcommission"]
34
+ only-packages = true
35
+
36
+ [build-system]
37
+ requires = ["hatchling"]
38
+ build-backend = "hatchling.build"
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
@@ -0,0 +1,4 @@
1
+ from .accounts import AccountModelAdmin
2
+ from .commission import CommissionModelAdmin, CommissionTypeModelAdmin
3
+
4
+ # from .rebate import RebateModelAdmin
@@ -0,0 +1,22 @@
1
+ from django.contrib import admin
2
+ from wbcommission.models.rebate import manage_rebate_as_task
3
+ from wbcrm.admin.accounts import AccountModelAdmin as BaseAccountModelAdmin
4
+ from wbcrm.models.accounts import Account
5
+
6
+ from .commission import CommissionTabularInline
7
+
8
+ admin.site.unregister(Account)
9
+
10
+
11
+ @admin.register(Account)
12
+ class AccountModelAdmin(BaseAccountModelAdmin):
13
+ def make_rebates(self, request, queryset):
14
+ for account in queryset:
15
+ manage_rebate_as_task.delay(account.id)
16
+
17
+ def get_assets_under_management(self, request, queryset):
18
+ for account in queryset:
19
+ account.get_assets_under_management()
20
+
21
+ actions = list(BaseAccountModelAdmin.actions) + [make_rebates, get_assets_under_management]
22
+ inlines = BaseAccountModelAdmin.inlines + [CommissionTabularInline]
@@ -0,0 +1,85 @@
1
+ from django.contrib import admin
2
+ from wbcommission.models import (
3
+ Commission,
4
+ CommissionExclusionRule,
5
+ CommissionRole,
6
+ CommissionRule,
7
+ CommissionType,
8
+ )
9
+
10
+
11
+ @admin.register(CommissionType)
12
+ class CommissionTypeModelAdmin(admin.ModelAdmin):
13
+ pass
14
+
15
+
16
+ class CommissionRuleTabularInline(admin.TabularInline):
17
+ ordering = ("timespan__startswith", "assets_under_management_range__startswith")
18
+ fields = (
19
+ "timespan",
20
+ "assets_under_management_range",
21
+ "percent",
22
+ )
23
+ model = CommissionRule
24
+ extra = 0
25
+
26
+
27
+ class CommissionRoleTabularInline(admin.TabularInline):
28
+ model = CommissionRole
29
+ autocomplete_fields = ["person"]
30
+ fields = (
31
+ "person",
32
+ "commission",
33
+ )
34
+ extra = 0
35
+
36
+
37
+ class CommissionTabularInline(admin.TabularInline):
38
+ extra = 0
39
+ model = Commission
40
+ fields = [
41
+ "order",
42
+ "crm_recipient",
43
+ "account_role_type_recipient",
44
+ "portfolio_role_recipient",
45
+ "commission_type",
46
+ "net_commission",
47
+ "is_hidden",
48
+ "exclusion_rule_account_role_type",
49
+ ]
50
+ readonly_fields = ["order"]
51
+ ordering = ["order"]
52
+ autocomplete_fields = ["account", "crm_recipient"]
53
+ show_change_link = True
54
+
55
+
56
+ @admin.register(Commission)
57
+ class CommissionModelAdmin(admin.ModelAdmin):
58
+ list_display = [
59
+ "account",
60
+ "crm_recipient",
61
+ "portfolio_role_recipient",
62
+ "account_role_type_recipient",
63
+ "order",
64
+ "net_commission",
65
+ "commission_type",
66
+ "is_hidden",
67
+ "exclusion_rule_account_role_type",
68
+ ]
69
+
70
+ autocomplete_fields = ["account", "crm_recipient"]
71
+
72
+ inlines = [CommissionRoleTabularInline, CommissionRuleTabularInline]
73
+
74
+
75
+ @admin.register(CommissionExclusionRule)
76
+ class CommissionExclusionRuleAdmin(admin.ModelAdmin):
77
+ list_display = [
78
+ "product",
79
+ "commission_type",
80
+ "account_role_type",
81
+ "timespan",
82
+ "overriding_percent",
83
+ "overriding_net_or_gross_commission",
84
+ ]
85
+ autocomplete_fields = ["product", "account_role_type"]
@@ -0,0 +1,7 @@
1
+ # @admin.register(Rebate)
2
+ # class RebateModelAdmin(admin.ModelAdmin):
3
+ # def has_add_permission(self, request, obj=None):
4
+ # return False
5
+
6
+ # def has_delete_permission(self, request, obj=None):
7
+ # return False
File without changes
@@ -0,0 +1,181 @@
1
+ from datetime import date
2
+ from decimal import Decimal
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+ from django.db.models import Case, OuterRef, Subquery, Value, When
7
+ from django.db.models.functions import Coalesce
8
+ from wbcommission.models import CommissionType, Rebate
9
+ from wbcore.contrib.currency.models import CurrencyFXRates
10
+ from wbfdm.models import InstrumentPrice
11
+ from wbportfolio.models import Fees
12
+
13
+
14
+ class MarginalityCalculator:
15
+ FEE_MAP = {
16
+ "MANAGEMENT": "management",
17
+ "PERFORMANCE": "performance",
18
+ "PERFORMANCE_CRYSTALIZED": "performance",
19
+ }
20
+
21
+ def __init__(self, products, from_date: date, to_date: date):
22
+ products = products.annotate(
23
+ fx_rate=Coalesce(
24
+ Subquery(
25
+ CurrencyFXRates.objects.filter(
26
+ currency=OuterRef("currency"), date=OuterRef("last_valuation_date")
27
+ ).values("value")[:1]
28
+ ),
29
+ Decimal(1.0),
30
+ )
31
+ )
32
+
33
+ self.fx_rates = (
34
+ pd.DataFrame(products.values_list("id", "fx_rate"), columns=["id", "fx_rate"])
35
+ .set_index("id")["fx_rate"]
36
+ .astype(float)
37
+ )
38
+
39
+ # compute net marginality
40
+ self.df_aum = pd.DataFrame(
41
+ InstrumentPrice.objects.annotate_base_data()
42
+ .filter(instrument__in=products, date__gte=from_date, date__lte=to_date)
43
+ .values_list("calculated", "net_value_usd", "date", "outstanding_shares", "instrument"),
44
+ columns=["calculated", "net_value_usd", "date", "outstanding_shares", "instrument"],
45
+ ).rename(columns={"instrument": "id"})
46
+ self.df_aum["date"] = pd.to_datetime(self.df_aum["date"])
47
+ self.df_aum = (
48
+ self.df_aum.sort_values(by="calculated")
49
+ .groupby(["id", "date"])
50
+ .agg({"net_value_usd": "last", "outstanding_shares": "first"})
51
+ )
52
+ self.df_aum = (self.df_aum.net_value_usd * self.df_aum.outstanding_shares).astype(float)
53
+ self.df_aum = self.df_aum.reindex(
54
+ pd.MultiIndex.from_product(
55
+ [
56
+ self.df_aum.index.levels[0],
57
+ pd.date_range(
58
+ self.df_aum.index.get_level_values("date").min(),
59
+ self.df_aum.index.get_level_values("date").max(),
60
+ ),
61
+ ],
62
+ names=["id", "date"],
63
+ ),
64
+ method="ffill",
65
+ ).dropna()
66
+
67
+ # Build the fees dataframe where product id is the index and colum are the every fees type available and value are the amount.
68
+
69
+ fees = Fees.valid_objects.filter(
70
+ transaction_date__lte=to_date,
71
+ transaction_date__gte=from_date,
72
+ transaction_subtype__in=self.FEE_MAP.keys(),
73
+ linked_product__in=products,
74
+ ).annotate(
75
+ fee_type=Case(
76
+ *[When(transaction_subtype=k, then=Value(v)) for k, v in self.FEE_MAP.items()],
77
+ default=Value("management"),
78
+ )
79
+ )
80
+ self.df_fees = pd.DataFrame(
81
+ fees.values_list("linked_product", "fee_type", "total_value", "transaction_date", "calculated"),
82
+ columns=["linked_product", "fee_type", "total_value", "transaction_date", "calculated"],
83
+ ).rename(columns={"linked_product": "id", "transaction_date": "date"})
84
+ self.df_fees["date"] = pd.to_datetime(self.df_fees["date"])
85
+
86
+ self.df_fees = (
87
+ self.df_fees[["fee_type", "total_value", "id", "date"]]
88
+ .pivot_table(index=["id", "date"], columns="fee_type", values="total_value", aggfunc="sum")
89
+ .astype("float")
90
+ .round(4)
91
+ )
92
+ self.df_fees["total"] = self.df_fees.sum(axis=1)
93
+ self.df_fees = self.df_fees.reindex(self.df_aum.index, fill_value=0)
94
+ self.df_fees = self._rolling_average_monday(self.df_fees)
95
+
96
+ # Build the fees dataframe where product id is the index and colum are the every fees type available and value are the amount.
97
+ self.df_rebates = pd.DataFrame(
98
+ Rebate.objects.filter(date__gte=from_date, date__lte=to_date, product__in=products).values_list(
99
+ "product", "value", "date", "commission_type__key"
100
+ ),
101
+ columns=["product", "value", "date", "commission_type__key"],
102
+ ).rename(columns={"product": "id"})
103
+ self.df_rebates["date"] = pd.to_datetime(self.df_rebates["date"])
104
+
105
+ self.df_rebates = (
106
+ pd.pivot_table(
107
+ self.df_rebates,
108
+ index=["id", "date"],
109
+ columns="commission_type__key",
110
+ values="value",
111
+ aggfunc="sum",
112
+ fill_value=0,
113
+ )
114
+ .astype("float")
115
+ .round(4)
116
+ )
117
+ self.df_rebates = self.df_rebates.reindex(self.df_aum.index, fill_value=0)
118
+ self.df_rebates["total"] = self.df_rebates.sum(axis=1)
119
+ self.df_rebates = self._rolling_average_monday(self.df_rebates)
120
+ # Iniliaze basic column
121
+
122
+ self.empty_column = pd.Series(0.0, dtype="float64", index=self.df_fees.index)
123
+ self._set_basics_statistics()
124
+
125
+ def _set_basics_statistics(self):
126
+ groupby_fees = self.df_fees.groupby(level=0).sum(numeric_only=True)
127
+ groupby_rebates = self.df_rebates.groupby(level=0).sum(numeric_only=True)
128
+ for key in [*CommissionType.objects.values_list("key", flat=True), "total"]:
129
+ fees = groupby_fees.get(key, self.empty_column)
130
+ rebates = groupby_rebates.get(key, self.empty_column)
131
+ fees_usd = fees * self.fx_rates
132
+ rebates_usd = rebates * self.fx_rates
133
+ marginality = fees - rebates
134
+ marginality_usd = fees_usd - rebates_usd
135
+ marginality_percent = (fees - rebates) / fees.replace(0, np.nan)
136
+ marginality_percent_usd = (fees_usd - rebates_usd) / fees_usd.replace(0, np.nan)
137
+
138
+ setattr(self, f"{key}_fees", fees.rename(f"{key}_fees"))
139
+ setattr(self, f"{key}_rebates", rebates.rename(f"{key}_rebates"))
140
+ setattr(self, f"{key}_fees_usd", fees_usd.rename(f"{key}_fees_usd"))
141
+ setattr(self, f"{key}_rebates_usd", rebates_usd.rename(f"{key}_rebates_usd"))
142
+ setattr(self, f"{key}_marginality", marginality.rename(f"{key}_marginality"))
143
+ setattr(self, f"{key}_marginality_usd", marginality_usd.rename(f"{key}_marginality_usd"))
144
+ setattr(self, f"{key}_marginality_percent", marginality_percent.rename(f"{key}_marginality_percent"))
145
+ setattr(
146
+ self,
147
+ f"{key}_marginality_percent_usd",
148
+ marginality_percent_usd.rename(f"{key}_marginality_percent_usd"),
149
+ )
150
+
151
+ def _rolling_average_monday(self, df):
152
+ """
153
+ This utility method take a dataframe and assum the values on Mondays are accumulated over the weekend. So we need to average every Saturday, Sunday and Monday together.
154
+ """
155
+
156
+ monday = df[df.index.get_level_values("date").weekday == 0] / 3
157
+ monday = monday.reindex(df.index, method="bfill")
158
+ df[df.index.get_level_values("date").weekday.isin([5, 6, 0])] = monday[
159
+ df.index.get_level_values("date").weekday.isin([5, 6, 0])
160
+ ]
161
+ return df
162
+
163
+ def get_net_marginality(self, type: str) -> pd.Series:
164
+ return (
165
+ ((self.df_fees.get(type, self.empty_column) - self.df_rebates.get(type, self.empty_column)) / self.df_aum)
166
+ .groupby("id")
167
+ .mean()
168
+ ) * 360
169
+
170
+ def get_aggregated_net_marginality(self, type: str) -> pd.Series:
171
+ # we compute the total net marginality for management for the aggregate function
172
+ total_aum = self.df_aum.groupby(level=1).sum()
173
+ return (
174
+ (
175
+ (
176
+ self.df_fees.get(type, self.empty_column).groupby(level=1).sum()
177
+ - self.df_rebates.get(type, self.empty_column).groupby(level=1).sum()
178
+ )
179
+ / total_aum
180
+ ).mean(axis=0)
181
+ ) * 360
@@ -0,0 +1,5 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class CommissionConfig(AppConfig):
5
+ name = "wbcommission"
@@ -0,0 +1,9 @@
1
+ from .commission import (
2
+ AccountTypeRoleCommissionFactory,
3
+ CommissionFactory,
4
+ CommissionRoleFactory,
5
+ PortfolioRoleCommissionFactory,
6
+ CommissionExclusionRuleFactory,
7
+ CommissionTypeFactory,
8
+ )
9
+ from .rebate import RebateFactory
@@ -0,0 +1,100 @@
1
+ from datetime import date
2
+
3
+ import factory
4
+ from psycopg.types.range import DateRange
5
+ from wbcommission.models.commission import (
6
+ Commission,
7
+ CommissionExclusionRule,
8
+ CommissionRole,
9
+ CommissionType,
10
+ )
11
+ from wbportfolio.models.roles import PortfolioRole
12
+
13
+
14
+ class CommissionTypeFactory(factory.django.DjangoModelFactory):
15
+ name = "MANAGEMENT"
16
+ key = factory.LazyAttribute(lambda x: x.name.lower())
17
+
18
+ class Meta:
19
+ model = CommissionType
20
+ django_get_or_create = ["key"]
21
+
22
+
23
+ class CommissionFactory(factory.django.DjangoModelFactory):
24
+ account = factory.SubFactory("wbcrm.factories.AccountFactory")
25
+ crm_recipient = factory.SubFactory("wbcore.contrib.directory.factories.entries.EntryFactory")
26
+
27
+ portfolio_role_recipient = None
28
+ account_role_type_recipient = None
29
+ order = 0
30
+ commission_type = factory.SubFactory(CommissionTypeFactory)
31
+ net_commission = True
32
+ is_hidden = False
33
+
34
+ class Meta:
35
+ model = Commission
36
+
37
+ @factory.post_generation
38
+ def rule_timespan(self, create, extracted, **kwargs):
39
+ if not create:
40
+ return
41
+ if extracted:
42
+ v = self.rules.first()
43
+ v.timespan = extracted
44
+ v.save()
45
+
46
+ @factory.post_generation
47
+ def rule_aum(self, create, extracted, **kwargs):
48
+ if not create:
49
+ return
50
+ if extracted:
51
+ v = self.rules.first()
52
+ v.assets_under_management_range = extracted
53
+ v.save()
54
+
55
+ @factory.post_generation
56
+ def rule_percent(self, create, extracted, **kwargs):
57
+ if not create:
58
+ return
59
+ if extracted:
60
+ v = self.rules.first()
61
+ v.percent = extracted
62
+ v.save()
63
+
64
+
65
+ class PortfolioRoleCommissionFactory(CommissionFactory):
66
+ crm_recipient = None
67
+ account_role_type_recipient = None
68
+ portfolio_role_recipient = factory.Iterator([role_choice[0] for role_choice in PortfolioRole.RoleType.choices])
69
+
70
+ class Meta:
71
+ model = Commission
72
+
73
+
74
+ class AccountTypeRoleCommissionFactory(CommissionFactory):
75
+ crm_recipient = None
76
+ portfolio_role_recipient = None
77
+ account_role_type_recipient = factory.SubFactory("wbcrm.factories.AccountRoleTypeFactory")
78
+
79
+ class Meta:
80
+ model = Commission
81
+
82
+
83
+ class CommissionRoleFactory(factory.django.DjangoModelFactory):
84
+ commission = factory.SubFactory(CommissionFactory)
85
+ person = factory.SubFactory("wbcore.contrib.directory.factories.entries.PersonFactory")
86
+
87
+ class Meta:
88
+ model = CommissionRole
89
+
90
+
91
+ class CommissionExclusionRuleFactory(factory.django.DjangoModelFactory):
92
+ product = factory.SubFactory("wbportfolio.factories.products.ProductFactory")
93
+ commission_type = factory.SubFactory(CommissionTypeFactory)
94
+ overriding_percent = factory.Faker("pydecimal", min_value=0, max_value=1, right_digits=2)
95
+ overriding_net_or_gross_commission = "DEFAULT"
96
+ account_role_type = None
97
+ timespan = DateRange(date.min, date.max)
98
+
99
+ class Meta:
100
+ model = CommissionExclusionRule
@@ -0,0 +1,16 @@
1
+ import factory
2
+ from wbcommission.models import Rebate
3
+
4
+
5
+ class RebateFactory(factory.django.DjangoModelFactory):
6
+ date = factory.Faker("date_object")
7
+
8
+ commission = factory.SubFactory("wbcommission.factories.CommissionFactory")
9
+ account = factory.LazyAttribute(lambda o: o.commission.account)
10
+ commission_type = factory.LazyAttribute(lambda o: o.commission.commission_type)
11
+ product = factory.SubFactory("wbportfolio.factories.ProductFactory")
12
+ recipient = factory.SubFactory("wbcore.contrib.directory.factories.entries.PersonFactory")
13
+ value = factory.Faker("pydecimal", positive=True, max_value=1000000, right_digits=4)
14
+
15
+ class Meta:
16
+ model = Rebate
@@ -0,0 +1,7 @@
1
+ from .rebate import (
2
+ CustomerRebateGroupByFilter,
3
+ RebateDateFilter,
4
+ RebateGroupByFilter,
5
+ RebateMarginalityFilter,
6
+ )
7
+ from .signals import *