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,44 @@
1
+ from django.db.models import Q
2
+ from django.dispatch import receiver
3
+ from wbcore import filters as wb_filters
4
+ from wbcore.contrib.directory.filters import EntryFilter
5
+ from wbcore.signals.filters import add_filters
6
+ from wbcrm.models.accounts import Account
7
+ from wbportfolio.contrib.company_portfolio.filters import PersonFilter
8
+
9
+
10
+ @receiver(add_filters, sender=EntryFilter)
11
+ def add_account_filter(sender, request=None, *args, **kwargs):
12
+ def filter_account(queryset, name, value):
13
+ if value:
14
+ return queryset.filter(account_customers__account=value)
15
+ return queryset
16
+
17
+ return {
18
+ "account": wb_filters.ModelChoiceFilter(
19
+ label="Account",
20
+ field_name="account",
21
+ queryset=Account.objects.all(),
22
+ endpoint=Account.get_representation_endpoint(),
23
+ value_key=Account.get_representation_value_key(),
24
+ label_key=Account.get_representation_label_key(),
25
+ method=filter_account,
26
+ )
27
+ }
28
+
29
+
30
+ @receiver(add_filters, sender=PersonFilter)
31
+ def add_has_user_account_filter(sender, request=None, *args, **kwargs):
32
+ def filter_has_user_account(queryset, name, value):
33
+ if value is True:
34
+ return queryset.filter(Q(user_account__isnull=False))
35
+ elif value is False:
36
+ return queryset.filter(Q(user_account__isnull=True))
37
+ else:
38
+ return queryset
39
+
40
+ return {
41
+ "has_user_account": wb_filters.BooleanFilter(
42
+ field_name="has_user_account", label="Has User Account", method=filter_has_user_account
43
+ )
44
+ }
@@ -0,0 +1,2 @@
1
+ from .rebate_generator import RebateGenerator
2
+
@@ -0,0 +1,93 @@
1
+ from collections import defaultdict
2
+ from datetime import date, datetime
3
+ from decimal import Decimal
4
+ from functools import reduce
5
+ from typing import Iterable
6
+
7
+ from django.db.models import QuerySet, Sum
8
+ from wbaccounting.generators.base import AbstractBookingEntryGenerator
9
+ from wbaccounting.models import BookingEntry
10
+ from wbcommission.models import Rebate
11
+ from wbcore.contrib.directory.models import Entry
12
+ from wbportfolio.models import Product
13
+
14
+
15
+ class RebateGenerator(AbstractBookingEntryGenerator):
16
+ TITLE = "Rebate Generation"
17
+
18
+ @staticmethod
19
+ def generate_booking_entries(from_date: date, to_date: date, counterparty: Entry) -> Iterable[BookingEntry]:
20
+ rebates = Rebate.objects.filter(recipient=counterparty, date__gte=from_date, date__lte=to_date)
21
+
22
+ for product_id in rebates.distinct("product_id").values_list("product_id", flat=True):
23
+ product = Product.objects.get(id=product_id)
24
+
25
+ for rebate_key, rebate_title in (("management", "Management"), ("performance", "Performance")):
26
+ rebate_sum = (
27
+ rebates.filter(product_id=product_id, commission_type__key=rebate_key)
28
+ .aggregate(sum=Sum("value"))
29
+ .get("sum")
30
+ or 0.0
31
+ )
32
+ if rebate_sum > 0:
33
+ yield BookingEntry(
34
+ title=f"{product.name} {product.isin} {rebate_title} Fees",
35
+ booking_date=date.today(),
36
+ reference_date=to_date,
37
+ gross_value=Decimal(-1 * rebate_sum),
38
+ vat=Decimal(counterparty.entry_accounting_information.vat) or Decimal(0.0),
39
+ currency=product.currency,
40
+ counterparty=counterparty,
41
+ parameters={
42
+ "from_date": from_date.strftime("%d.%m.%Y"),
43
+ "to_date": to_date.strftime("%d.%m.%Y"),
44
+ },
45
+ backlinks={
46
+ "comm-rebates": {
47
+ "title": "Rebates",
48
+ "reverse": "wbcommission:rebatetable-list",
49
+ "parameters": {
50
+ "date": f'{from_date.strftime("%Y-%m-%d")},{to_date.strftime("%Y-%m-%d")}',
51
+ "product": product_id,
52
+ "recipient": counterparty.id,
53
+ "group_by": "ACCOUNT",
54
+ },
55
+ },
56
+ },
57
+ )
58
+
59
+ @staticmethod
60
+ def _compare(d1: str, d2: str) -> str:
61
+ d1_lb, d1_ub = d1.split(",")
62
+ d2_lb, d2_ub = d2.split(",")
63
+
64
+ lb = min(datetime.strptime(d1_lb, "%Y-%m-%d"), datetime.strptime(d2_lb, "%Y-%m-%d"))
65
+ ub = max(datetime.strptime(d1_ub, "%Y-%m-%d"), datetime.strptime(d2_ub, "%Y-%m-%d"))
66
+
67
+ return f"{lb.strftime('%Y-%m-%d')},{ub.strftime('%Y-%m-%d')}"
68
+
69
+ @staticmethod
70
+ def _merge_key_into_dict(d: dict, key: str, value: dict) -> dict:
71
+ d[key]["title"] = value["title"]
72
+ d[key]["reverse"] = value["reverse"]
73
+ if "recipient" in value["parameters"]:
74
+ d[key]["parameters"]["recipient"] = value["parameters"]["recipient"]
75
+ d[key]["parameters"]["date"] = RebateGenerator._compare(
76
+ d[key]["parameters"].get("date", "9999-12-31,1111-01-01"), value["parameters"]["date"]
77
+ )
78
+ return d
79
+
80
+ @staticmethod
81
+ def _iter_backlinks(booking_entries: QuerySet) -> Iterable:
82
+ for backlink in booking_entries:
83
+ yield from backlink.items()
84
+
85
+ @staticmethod
86
+ def merge_backlinks(booking_entries: QuerySet[BookingEntry]) -> dict:
87
+ return reduce(
88
+ lambda d, v: RebateGenerator._merge_key_into_dict(d, *v),
89
+ RebateGenerator._iter_backlinks(
90
+ booking_entries.filter(backlinks__isnull=False).values_list("backlinks", flat=True)
91
+ ),
92
+ defaultdict(lambda: dict(parameters=dict())),
93
+ )
@@ -0,0 +1,299 @@
1
+ # Generated by Django 4.1.9 on 2023-06-28 12:21
2
+
3
+ from decimal import Decimal
4
+
5
+ import django.db.models.deletion
6
+ import wbcommission.models.rebate
7
+ from django.contrib.postgres.operations import BtreeGistExtension
8
+ from django.db import migrations, models
9
+
10
+
11
+ class Migration(migrations.Migration):
12
+ initial = True
13
+
14
+ dependencies = [
15
+ ("wbcrm", "0005_account_accountrole_accountroletype_and_more"),
16
+ ("wbportfolio", "0046_add_product_default_account"),
17
+ ("directory", "0004_entry_is_draft_entry"),
18
+ ]
19
+
20
+ operations = [
21
+ BtreeGistExtension(),
22
+ migrations.CreateModel(
23
+ name="AccountCustomer",
24
+ fields=[
25
+ (
26
+ "id",
27
+ models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"),
28
+ ),
29
+ (
30
+ "customer_type",
31
+ models.CharField(
32
+ choices=[("DIRECT", "Direct"), ("INDIRECT", "Indirect")],
33
+ default="DIRECT",
34
+ max_length=16,
35
+ verbose_name="Customer Type",
36
+ ),
37
+ ),
38
+ (
39
+ "account",
40
+ models.ForeignKey(
41
+ on_delete=django.db.models.deletion.CASCADE,
42
+ related_name="customers",
43
+ to="wbcrm.account",
44
+ verbose_name="Account",
45
+ ),
46
+ ),
47
+ (
48
+ "entry",
49
+ models.ForeignKey(
50
+ on_delete=django.db.models.deletion.CASCADE,
51
+ related_name="account_customers",
52
+ to="directory.entry",
53
+ verbose_name="Customer",
54
+ ),
55
+ ),
56
+ ],
57
+ options={
58
+ "verbose_name": "Customer",
59
+ "verbose_name_plural": "Customers",
60
+ "notification_types": [
61
+ (
62
+ "wbcommission.accountcustomer.notify",
63
+ "New Account Customer Notification",
64
+ "Notifies you when a new account customer has been created",
65
+ True,
66
+ True,
67
+ False,
68
+ )
69
+ ],
70
+ },
71
+ ),
72
+ migrations.CreateModel(
73
+ name="CommissionAccountRole",
74
+ fields=[
75
+ (
76
+ "id",
77
+ models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"),
78
+ ),
79
+ (
80
+ "role_type",
81
+ models.CharField(
82
+ choices=[("SALES", "Sales"), ("CUSTOMER_MANAGER", "Customer Manager")],
83
+ default="SALES",
84
+ max_length=16,
85
+ verbose_name="Role Type",
86
+ ),
87
+ ),
88
+ ("start", models.DateField(blank=True, null=True, verbose_name="Start")),
89
+ ("end", models.DateField(blank=True, null=True, verbose_name="End")),
90
+ ("weighting", models.FloatField(default=1, verbose_name="Weight")),
91
+ (
92
+ "account",
93
+ models.ForeignKey(
94
+ on_delete=django.db.models.deletion.CASCADE,
95
+ related_name="rebates_roles",
96
+ to="wbcrm.account",
97
+ verbose_name="Account Rebate roles",
98
+ ),
99
+ ),
100
+ (
101
+ "customer",
102
+ models.ForeignKey(
103
+ on_delete=django.db.models.deletion.CASCADE,
104
+ related_name="rebate_account_customer",
105
+ to="directory.entry",
106
+ verbose_name="Customer",
107
+ ),
108
+ ),
109
+ (
110
+ "person",
111
+ models.ForeignKey(
112
+ on_delete=django.db.models.deletion.PROTECT,
113
+ related_name="rebates_account_roles",
114
+ to="directory.person",
115
+ verbose_name="Person",
116
+ ),
117
+ ),
118
+ ],
119
+ options={
120
+ "verbose_name": "Commission Account Role",
121
+ "verbose_name_plural": "Commission Account Roles",
122
+ },
123
+ ),
124
+ migrations.CreateModel(
125
+ name="AccountCustomerCommissionConstraint",
126
+ fields=[
127
+ (
128
+ "id",
129
+ models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"),
130
+ ),
131
+ ("start_year", models.PositiveIntegerField(blank=True, null=True)),
132
+ ("end_year", models.PositiveIntegerField(blank=True, null=True)),
133
+ ("payable_year_end", models.BooleanField(default=True)),
134
+ ("payable_continous", models.BooleanField(default=True)),
135
+ (
136
+ "customer",
137
+ models.ForeignKey(
138
+ on_delete=django.db.models.deletion.CASCADE,
139
+ related_name="constraints",
140
+ to="wbcommission.accountcustomer",
141
+ ),
142
+ ),
143
+ (
144
+ "products",
145
+ models.ManyToManyField(blank=True, related_name="customer_constraints", to="wbportfolio.product"),
146
+ ),
147
+ ],
148
+ options={
149
+ "verbose_name": "Account Customer Commission Constraint",
150
+ "verbose_name_plural": "Account Customer Commission Constrains",
151
+ },
152
+ ),
153
+ migrations.CreateModel(
154
+ name="Rebate",
155
+ fields=[
156
+ (
157
+ "id",
158
+ models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"),
159
+ ),
160
+ ("date", models.DateField(verbose_name="Date")),
161
+ (
162
+ "management_fees",
163
+ models.DecimalField(
164
+ decimal_places=4, default=Decimal(0.0), max_digits=16, verbose_name="Management Fees"
165
+ ),
166
+ ),
167
+ (
168
+ "pre_performance_fees",
169
+ models.DecimalField(
170
+ decimal_places=4,
171
+ default=Decimal(0.0),
172
+ max_digits=16,
173
+ verbose_name="Pre Performance Fees",
174
+ ),
175
+ ),
176
+ (
177
+ "performance_fees",
178
+ models.DecimalField(
179
+ decimal_places=4, default=Decimal(0.0), max_digits=16, verbose_name="Performance Fees"
180
+ ),
181
+ ),
182
+ (
183
+ "account",
184
+ models.ForeignKey(
185
+ on_delete=django.db.models.deletion.CASCADE,
186
+ related_name="rebates",
187
+ to="wbcrm.account",
188
+ verbose_name="Account",
189
+ ),
190
+ ),
191
+ (
192
+ "product",
193
+ models.ForeignKey(
194
+ on_delete=django.db.models.deletion.CASCADE,
195
+ related_name="rebates",
196
+ to="wbportfolio.product",
197
+ verbose_name="Product",
198
+ ),
199
+ ),
200
+ (
201
+ "recipient",
202
+ models.ForeignKey(
203
+ on_delete=django.db.models.deletion.PROTECT,
204
+ related_name="recipient_rebates",
205
+ to="directory.entry",
206
+ verbose_name="Recipient",
207
+ ),
208
+ ),
209
+ ],
210
+ options={
211
+ "verbose_name": "Rebate",
212
+ "verbose_name_plural": "Rebates",
213
+ "unique_together": {("date", "recipient", "account", "product")},
214
+ "index_together": {("date", "recipient", "product")},
215
+ },
216
+ bases=(wbcommission.models.rebate.BookingEntryCalculatedValueMixin, models.Model),
217
+ ),
218
+ migrations.CreateModel(
219
+ name="Commission",
220
+ fields=[
221
+ (
222
+ "id",
223
+ models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID"),
224
+ ),
225
+ (
226
+ "role_recipient",
227
+ models.CharField(
228
+ blank=True,
229
+ choices=[
230
+ ("portfolio_managers", "Portfolio Managers"),
231
+ ("analysts", "Analysts"),
232
+ ("sales", "Sales"),
233
+ ("customer_managers", "Customer Managers"),
234
+ ],
235
+ max_length=32,
236
+ null=True,
237
+ verbose_name="Recipient Role",
238
+ ),
239
+ ),
240
+ ("order", models.IntegerField()),
241
+ (
242
+ "commission_type",
243
+ models.CharField(
244
+ choices=[
245
+ ("net_management_fees", "Net Management Fees"),
246
+ ("gross_management_fees", "Gross Management Fees"),
247
+ ("net_performance_fees", "Net Performance Fees"),
248
+ ("gross_performance_fees", "Gross Performance Fees"),
249
+ ],
250
+ default="net_management_fees",
251
+ max_length=32,
252
+ verbose_name="Type",
253
+ ),
254
+ ),
255
+ ("start_date", models.DateField(verbose_name="Start Date")),
256
+ (
257
+ "minimum_assets_under_management",
258
+ models.IntegerField(default=0, verbose_name="Minimum AUM"),
259
+ ),
260
+ ("percent", models.FloatField(verbose_name="Percent")),
261
+ (
262
+ "account",
263
+ models.ForeignKey(
264
+ limit_choices_to=models.Q(("is_terminal_account", True)),
265
+ on_delete=django.db.models.deletion.CASCADE,
266
+ related_name="account_commissions",
267
+ to="wbcrm.account",
268
+ verbose_name="Account",
269
+ ),
270
+ ),
271
+ (
272
+ "crm_recipient",
273
+ models.ForeignKey(
274
+ blank=True,
275
+ null=True,
276
+ on_delete=django.db.models.deletion.PROTECT,
277
+ related_name="recipient_commissions",
278
+ to="directory.entry",
279
+ verbose_name="Recipient",
280
+ ),
281
+ ),
282
+ ],
283
+ options={
284
+ "verbose_name": "Commission",
285
+ "verbose_name_plural": "Commissions",
286
+ "unique_together": {
287
+ (
288
+ "account",
289
+ "order",
290
+ "crm_recipient",
291
+ "role_recipient",
292
+ "start_date",
293
+ "commission_type",
294
+ "minimum_assets_under_management",
295
+ )
296
+ },
297
+ },
298
+ ),
299
+ ]