wbcommission 1.59.8__tar.gz → 1.61.0__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.
- {wbcommission-1.59.8 → wbcommission-1.61.0}/PKG-INFO +1 -1
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/admin/commission.py +1 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/filters/__init__.py +1 -0
- wbcommission-1.61.0/wbcommission/filters/commissions.py +110 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/generators/rebate_generator.py +7 -5
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/locale/de/LC_MESSAGES/django.po +7 -3
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/locale/en/LC_MESSAGES/django.po +7 -2
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/locale/fr/LC_MESSAGES/django.po +7 -3
- wbcommission-1.61.0/wbcommission/migrations/0010_alter_rebate_unique_together_rebate_unique_rebate.py +24 -0
- wbcommission-1.61.0/wbcommission/migrations/0011_commissionrole_unique_commission_role.py +18 -0
- wbcommission-1.61.0/wbcommission/migrations/0012_commission_root_account.py +40 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/models/account_service.py +7 -5
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/models/commission.py +43 -16
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/models/rebate.py +31 -1
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/serializers/__init__.py +1 -1
- wbcommission-1.61.0/wbcommission/serializers/commissions.py +216 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/tests/models/test_account_service.py +7 -2
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/tests/models/test_commission.py +19 -11
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/tests/models/test_rebate.py +2 -2
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/urls.py +23 -4
- wbcommission-1.61.0/wbcommission/viewsets/__init__.py +13 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/viewsets/buttons/__init__.py +1 -0
- wbcommission-1.61.0/wbcommission/viewsets/buttons/commissions.py +5 -0
- wbcommission-1.61.0/wbcommission/viewsets/commissions.py +150 -0
- wbcommission-1.61.0/wbcommission/viewsets/display/__init__.py +5 -0
- wbcommission-1.61.0/wbcommission/viewsets/display/commissions.py +181 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/viewsets/endpoints/__init__.py +1 -0
- wbcommission-1.61.0/wbcommission/viewsets/endpoints/commissions.py +16 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/viewsets/menu/__init__.py +1 -0
- wbcommission-1.61.0/wbcommission/viewsets/menu/commissions.py +7 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/viewsets/mixins.py +11 -3
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/viewsets/titles/__init__.py +1 -0
- wbcommission-1.61.0/wbcommission/viewsets/titles/commissions.py +9 -0
- wbcommission-1.59.8/wbcommission/serializers/commissions.py +0 -27
- wbcommission-1.59.8/wbcommission/viewsets/__init__.py +0 -7
- wbcommission-1.59.8/wbcommission/viewsets/commissions.py +0 -22
- wbcommission-1.59.8/wbcommission/viewsets/display/__init__.py +0 -5
- wbcommission-1.59.8/wbcommission/viewsets/display/commissions.py +0 -21
- wbcommission-1.59.8/wbcommission/viewsets/endpoints/commissions.py +0 -0
- wbcommission-1.59.8/wbcommission/viewsets/menu/commissions.py +0 -0
- wbcommission-1.59.8/wbcommission/viewsets/titles/commissions.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/.gitignore +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/pyproject.toml +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/__init__.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/admin/__init__.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/admin/accounts.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/admin/rebate.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/analytics/__init__.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/analytics/marginality.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/apps.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/dynamic_preferences_registry.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/factories/__init__.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/factories/commission.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/factories/rebate.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/filters/rebate.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/filters/signals.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/generators/__init__.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/locale/de/LC_MESSAGES/django.mo +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/locale/en/LC_MESSAGES/django.mo +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/locale/fr/LC_MESSAGES/django.mo +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/migrations/0001_initial.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/migrations/0002_commissionrule_remove_accountcustomer_account_and_more.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/migrations/0003_alter_commission_account.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/migrations/0004_rebate_audit_log.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/migrations/0005_alter_rebate_audit_log.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/migrations/0006_commissionrule_consider_zero_percent_for_exclusion.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/migrations/0007_remove_commission_unique_crm_recipient_account_and_more.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/migrations/0008_alter_commission_options_alter_commission_order.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/migrations/0009_rename_rebate_commission_type_date_recipient_product_account_wbcommissio_commiss_432402_idx.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/migrations/__init__.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/models/__init__.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/models/signals.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/permissions.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/reports/__init__.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/reports/audit_report.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/reports/customer_report.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/reports/utils.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/serializers/rebate.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/serializers/signals.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/tests/__init__.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/tests/analytics/__init__.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/tests/analytics/test_marginality.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/tests/conftest.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/tests/models/__init__.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/tests/models/mixins.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/tests/signals.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/tests/test_permissions.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/tests/viewsets/__init__.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/tests/viewsets/test_rebate.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/viewsets/buttons/rebate.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/viewsets/buttons/signals.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/viewsets/display/rebate.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/viewsets/endpoints/rebate.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/viewsets/menu/rebate.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/viewsets/rebate.py +0 -0
- {wbcommission-1.59.8 → wbcommission-1.61.0}/wbcommission/viewsets/titles/rebate.py +0 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
|
|
3
|
+
from django.db.models import Exists, OuterRef, Q
|
|
4
|
+
from wbcore import filters as wb_filters
|
|
5
|
+
from wbportfolio.models import Product
|
|
6
|
+
|
|
7
|
+
from wbcommission.models import Commission, CommissionRule
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def filter_aum(queryset, value):
|
|
11
|
+
return queryset.filter(
|
|
12
|
+
Q(assets_under_management_range__startswith__lte=value)
|
|
13
|
+
& (
|
|
14
|
+
Q(assets_under_management_range__endswith__gt=value)
|
|
15
|
+
| Q(assets_under_management_range__endswith__isnull=True)
|
|
16
|
+
)
|
|
17
|
+
& (Q(percent__gt=0) | Q(consider_zero_percent_for_exclusion=True))
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def filter_timestamp(queryset, value):
|
|
22
|
+
return queryset.filter(
|
|
23
|
+
Q(timespan__startswith__lte=value)
|
|
24
|
+
& Q(timespan__endswith__gt=value)
|
|
25
|
+
& (Q(percent__gt=0) | Q(consider_zero_percent_for_exclusion=True))
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CommissionRuleCommissionFilterSet(wb_filters.FilterSet):
|
|
30
|
+
only_valid_at_date = wb_filters.DateFilter(
|
|
31
|
+
required=False,
|
|
32
|
+
initial=lambda f, r, v: date.today(),
|
|
33
|
+
method="filter_only_valid_at_date",
|
|
34
|
+
label="Only Valid at date",
|
|
35
|
+
help_text="Set to show only rules that are valid for the given date",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def filter_only_valid_at_date(self, queryset, name, value):
|
|
39
|
+
if value:
|
|
40
|
+
return filter_timestamp(queryset, value)
|
|
41
|
+
return queryset
|
|
42
|
+
|
|
43
|
+
only_valid_for_aum = wb_filters.NumberFilter(
|
|
44
|
+
required=False,
|
|
45
|
+
method="filter_only_valid_for_aum",
|
|
46
|
+
label="Only Valid for AUM",
|
|
47
|
+
help_text="Set to show only rules that are valid for the given aum",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def filter_only_valid_for_aum(self, queryset, name, value):
|
|
51
|
+
if value:
|
|
52
|
+
return filter_aum(queryset, value)
|
|
53
|
+
return queryset
|
|
54
|
+
|
|
55
|
+
class Meta:
|
|
56
|
+
model = CommissionRule
|
|
57
|
+
fields = {
|
|
58
|
+
"commission": ["exact"],
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class CommissionAccountFilterSet(wb_filters.FilterSet):
|
|
63
|
+
only_valid_at_date = wb_filters.DateFilter(
|
|
64
|
+
required=False,
|
|
65
|
+
initial=lambda f, r, v: date.today(),
|
|
66
|
+
method="filter_only_valid_at_date",
|
|
67
|
+
label="Only Valid at date",
|
|
68
|
+
help_text="Set to show only rules that are valid for the given date",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def filter_only_valid_at_date(self, queryset, name, value):
|
|
72
|
+
if value:
|
|
73
|
+
return queryset.annotate(
|
|
74
|
+
has_valid_rule=Exists(
|
|
75
|
+
filter_timestamp(CommissionRule.objects.filter(commission=OuterRef("pk")), value)
|
|
76
|
+
)
|
|
77
|
+
).filter(has_valid_rule=True)
|
|
78
|
+
|
|
79
|
+
return queryset
|
|
80
|
+
|
|
81
|
+
only_valid_for_aum = wb_filters.NumberFilter(
|
|
82
|
+
required=False,
|
|
83
|
+
method="filter_only_valid_for_aum",
|
|
84
|
+
label="Only Valid for AUM",
|
|
85
|
+
help_text="Set to show only rules that are valid for the given aum",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def filter_only_valid_for_aum(self, queryset, name, value):
|
|
89
|
+
if value:
|
|
90
|
+
return queryset.annotate(
|
|
91
|
+
has_valid_rule=Exists(filter_aum(CommissionRule.objects.filter(commission=OuterRef("pk")), value))
|
|
92
|
+
).filter(has_valid_rule=True)
|
|
93
|
+
|
|
94
|
+
return queryset
|
|
95
|
+
|
|
96
|
+
simulate_for_product = wb_filters.ModelChoiceFilter(
|
|
97
|
+
label="Simulate for Product",
|
|
98
|
+
queryset=Product.objects.all(),
|
|
99
|
+
endpoint=Product.get_representation_endpoint(),
|
|
100
|
+
value_key=Product.get_representation_value_key(),
|
|
101
|
+
label_key=Product.get_representation_label_key(),
|
|
102
|
+
method="fake_filter",
|
|
103
|
+
depends_on=[{"field": "only_valid_at_date", "options": {}}],
|
|
104
|
+
filter_params=lambda request, view: {"with_claim_for_account": view.account.id},
|
|
105
|
+
help_text='Choose a product to compute the actual percent paid for the bellow recipient (need the "Valide at date" filter)',
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
class Meta:
|
|
109
|
+
model = Commission
|
|
110
|
+
fields = {}
|
|
@@ -17,9 +17,11 @@ class RebateGenerator(AbstractBookingEntryGenerator):
|
|
|
17
17
|
TITLE = "Rebate Generation"
|
|
18
18
|
|
|
19
19
|
@staticmethod
|
|
20
|
-
def generate_booking_entries(
|
|
20
|
+
def generate_booking_entries(
|
|
21
|
+
from_date: date, to_date: date, counterparty: Entry, booking_date: date
|
|
22
|
+
) -> Iterable[BookingEntry]:
|
|
21
23
|
rebates = Rebate.objects.filter(recipient=counterparty, date__gte=from_date, date__lte=to_date)
|
|
22
|
-
|
|
24
|
+
vat = counterparty.entry_accounting_information.vat.get_rate(booking_date)
|
|
23
25
|
for product_id in rebates.distinct("product_id").values_list("product_id", flat=True):
|
|
24
26
|
product = Product.objects.get(id=product_id)
|
|
25
27
|
|
|
@@ -33,10 +35,10 @@ class RebateGenerator(AbstractBookingEntryGenerator):
|
|
|
33
35
|
if rebate_sum > 0:
|
|
34
36
|
yield BookingEntry(
|
|
35
37
|
title=f"{product.name} {product.isin} {rebate_title} Fees",
|
|
36
|
-
booking_date=
|
|
38
|
+
booking_date=booking_date,
|
|
37
39
|
reference_date=to_date,
|
|
38
40
|
gross_value=Decimal(-1 * rebate_sum),
|
|
39
|
-
vat=
|
|
41
|
+
vat=vat,
|
|
40
42
|
currency=product.currency,
|
|
41
43
|
counterparty=counterparty,
|
|
42
44
|
parameters={
|
|
@@ -48,7 +50,7 @@ class RebateGenerator(AbstractBookingEntryGenerator):
|
|
|
48
50
|
"title": "Rebates",
|
|
49
51
|
"reverse": "wbcommission:rebatetable-list",
|
|
50
52
|
"parameters": {
|
|
51
|
-
"date": f
|
|
53
|
+
"date": f"{from_date.strftime('%Y-%m-%d')},{to_date.strftime('%Y-%m-%d')}",
|
|
52
54
|
"product": product_id,
|
|
53
55
|
"recipient": counterparty.id,
|
|
54
56
|
"group_by": "ACCOUNT",
|
|
@@ -3,12 +3,11 @@
|
|
|
3
3
|
# This file is distributed under the same license as the PACKAGE package.
|
|
4
4
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
5
5
|
#
|
|
6
|
-
#, fuzzy
|
|
7
6
|
msgid ""
|
|
8
7
|
msgstr ""
|
|
9
8
|
"Project-Id-Version: PACKAGE VERSION\n"
|
|
10
9
|
"Report-Msgid-Bugs-To: \n"
|
|
11
|
-
"POT-Creation-Date:
|
|
10
|
+
"POT-Creation-Date: 2026-01-16 14:04+0100\n"
|
|
12
11
|
"PO-Revision-Date: 2025-05-30 09:40+0000\n"
|
|
13
12
|
"Language-Team: German (https://app.transifex.com/stainly/teams/171242/de/)\n"
|
|
14
13
|
"MIME-Version: 1.0\n"
|
|
@@ -17,11 +16,16 @@ msgstr ""
|
|
|
17
16
|
"Language: de\n"
|
|
18
17
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
|
19
18
|
|
|
19
|
+
#: serializers/commissions.py:104 serializers/commissions.py:105
|
|
20
|
+
#: serializers/commissions.py:106
|
|
21
|
+
msgid "Exactly one recipient field must be set (others left empty)."
|
|
22
|
+
msgstr ""
|
|
23
|
+
|
|
20
24
|
#: viewsets/buttons/rebate.py:19
|
|
21
25
|
msgid "Start"
|
|
22
26
|
msgstr ""
|
|
23
27
|
|
|
24
|
-
#: viewsets/rebate.py:
|
|
28
|
+
#: viewsets/rebate.py:438
|
|
25
29
|
msgid ""
|
|
26
30
|
"The selected date range includes a Saturday or Sunday. Please note that fees"
|
|
27
31
|
" and rebates are normalized over the weekend, as fees continue to accumulate"
|
|
@@ -7,7 +7,7 @@ msgid ""
|
|
|
7
7
|
msgstr ""
|
|
8
8
|
"Project-Id-Version: PACKAGE VERSION\n"
|
|
9
9
|
"Report-Msgid-Bugs-To: \n"
|
|
10
|
-
"POT-Creation-Date:
|
|
10
|
+
"POT-Creation-Date: 2026-01-16 14:04+0100\n"
|
|
11
11
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
12
12
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
13
13
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
@@ -16,11 +16,16 @@ msgstr ""
|
|
|
16
16
|
"Content-Type: text/plain; charset=UTF-8\n"
|
|
17
17
|
"Content-Transfer-Encoding: 8bit\n"
|
|
18
18
|
|
|
19
|
+
#: serializers/commissions.py:104 serializers/commissions.py:105
|
|
20
|
+
#: serializers/commissions.py:106
|
|
21
|
+
msgid "Exactly one recipient field must be set (others left empty)."
|
|
22
|
+
msgstr ""
|
|
23
|
+
|
|
19
24
|
#: viewsets/buttons/rebate.py:19
|
|
20
25
|
msgid "Start"
|
|
21
26
|
msgstr ""
|
|
22
27
|
|
|
23
|
-
#: viewsets/rebate.py:
|
|
28
|
+
#: viewsets/rebate.py:438
|
|
24
29
|
msgid ""
|
|
25
30
|
"The selected date range includes a Saturday or Sunday. Please note that fees "
|
|
26
31
|
"and rebates are normalized over the weekend, as fees continue to accumulate "
|
|
@@ -3,12 +3,11 @@
|
|
|
3
3
|
# This file is distributed under the same license as the PACKAGE package.
|
|
4
4
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
5
5
|
#
|
|
6
|
-
#, fuzzy
|
|
7
6
|
msgid ""
|
|
8
7
|
msgstr ""
|
|
9
8
|
"Project-Id-Version: PACKAGE VERSION\n"
|
|
10
9
|
"Report-Msgid-Bugs-To: \n"
|
|
11
|
-
"POT-Creation-Date:
|
|
10
|
+
"POT-Creation-Date: 2026-01-16 14:04+0100\n"
|
|
12
11
|
"PO-Revision-Date: 2025-05-30 09:40+0000\n"
|
|
13
12
|
"Language-Team: French (https://app.transifex.com/stainly/teams/171242/fr/)\n"
|
|
14
13
|
"MIME-Version: 1.0\n"
|
|
@@ -17,11 +16,16 @@ msgstr ""
|
|
|
17
16
|
"Language: fr\n"
|
|
18
17
|
"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
|
|
19
18
|
|
|
19
|
+
#: serializers/commissions.py:104 serializers/commissions.py:105
|
|
20
|
+
#: serializers/commissions.py:106
|
|
21
|
+
msgid "Exactly one recipient field must be set (others left empty)."
|
|
22
|
+
msgstr ""
|
|
23
|
+
|
|
20
24
|
#: viewsets/buttons/rebate.py:19
|
|
21
25
|
msgid "Start"
|
|
22
26
|
msgstr ""
|
|
23
27
|
|
|
24
|
-
#: viewsets/rebate.py:
|
|
28
|
+
#: viewsets/rebate.py:438
|
|
25
29
|
msgid ""
|
|
26
30
|
"The selected date range includes a Saturday or Sunday. Please note that fees"
|
|
27
31
|
" and rebates are normalized over the weekend, as fees continue to accumulate"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Generated by Django 5.2.9 on 2025-12-16 15:26
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('directory', '0014_alter_entry_relationship_managers_and_more'),
|
|
10
|
+
('wbcommission', '0009_rename_rebate_commission_type_date_recipient_product_account_wbcommissio_commiss_432402_idx'),
|
|
11
|
+
('wbcrm', '0019_alter_activity_companies_alter_activity_participants_and_more'),
|
|
12
|
+
('wbportfolio', '0095_portfolio_keep_only_essential_positions'),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
operations = [
|
|
16
|
+
migrations.AlterUniqueTogether(
|
|
17
|
+
name='rebate',
|
|
18
|
+
unique_together=set(),
|
|
19
|
+
),
|
|
20
|
+
migrations.AddConstraint(
|
|
21
|
+
model_name='rebate',
|
|
22
|
+
constraint=models.UniqueConstraint(fields=('date', 'recipient', 'account', 'product', 'commission_type'), name='unique_rebate'),
|
|
23
|
+
),
|
|
24
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Generated by Django 5.2.9 on 2026-01-14 09:21
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('directory', '0015_alter_emailcontact_address_and_more'),
|
|
10
|
+
('wbcommission', '0010_alter_rebate_unique_together_rebate_unique_rebate'),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.AddConstraint(
|
|
15
|
+
model_name='commissionrole',
|
|
16
|
+
constraint=models.UniqueConstraint(fields=('person', 'commission'), name='unique_commission_role'),
|
|
17
|
+
),
|
|
18
|
+
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Generated by Django 5.2.9 on 2026-01-15 13:43
|
|
2
|
+
|
|
3
|
+
import django.db.models.deletion
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
|
|
6
|
+
def migrate_root_account(apps, schema_editor):
|
|
7
|
+
Commission = apps.get_model('wbcommission', 'Commission')
|
|
8
|
+
Account = apps.get_model('wbcrm', 'Account')
|
|
9
|
+
objs = []
|
|
10
|
+
for c in Commission.objects.all():
|
|
11
|
+
c.root_account = Account.objects.filter(tree_id=c.account.tree_id, level=0).first()
|
|
12
|
+
objs.append(c)
|
|
13
|
+
Commission.objects.bulk_update(objs, ["root_account"])
|
|
14
|
+
|
|
15
|
+
class Migration(migrations.Migration):
|
|
16
|
+
|
|
17
|
+
dependencies = [
|
|
18
|
+
('wbcommission', '0011_commissionrole_unique_commission_role'),
|
|
19
|
+
('wbcrm', '0020_alter_product_unique_together_product_unique_product'),
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
operations = [
|
|
23
|
+
migrations.AddField(
|
|
24
|
+
model_name='commission',
|
|
25
|
+
name='root_account',
|
|
26
|
+
field=models.ForeignKey(blank=True, null=True,on_delete=django.db.models.deletion.CASCADE, related_name='root_account_commissions', to='wbcrm.account', verbose_name='Root Account'),
|
|
27
|
+
preserve_default=False,
|
|
28
|
+
),
|
|
29
|
+
migrations.RunSQL(sql="SET CONSTRAINTS ALL IMMEDIATE;"),
|
|
30
|
+
migrations.RunPython(migrate_root_account),
|
|
31
|
+
migrations.RunSQL(sql="SET CONSTRAINTS ALL DEFERRED;"),
|
|
32
|
+
migrations.AlterField(
|
|
33
|
+
model_name='commission',
|
|
34
|
+
name='root_account',
|
|
35
|
+
field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE,
|
|
36
|
+
related_name='root_account_commissions', to='wbcrm.account',
|
|
37
|
+
verbose_name='Root Account'),
|
|
38
|
+
preserve_default=False,
|
|
39
|
+
),
|
|
40
|
+
]
|
|
@@ -195,7 +195,7 @@ class AccountRebateManager:
|
|
|
195
195
|
)
|
|
196
196
|
return Decimal(0)
|
|
197
197
|
|
|
198
|
-
def
|
|
198
|
+
def get_terminal_accounts_total_holding(self, compute_date: date) -> dict[int, Decimal]:
|
|
199
199
|
"""
|
|
200
200
|
Calculate the total assets under management (AUM) for the root account.
|
|
201
201
|
|
|
@@ -206,14 +206,16 @@ class AccountRebateManager:
|
|
|
206
206
|
compute_date (date): The date for which the total AUM is calculated.
|
|
207
207
|
|
|
208
208
|
Returns:
|
|
209
|
-
|
|
209
|
+
dict: The total assets under management for each account on the given date.
|
|
210
210
|
|
|
211
211
|
Raises:
|
|
212
212
|
KeyError: If no AUM data is available for any terminal account on the given date,
|
|
213
213
|
a KeyError will be caught, and the function will return Decimal(0).
|
|
214
214
|
"""
|
|
215
|
-
|
|
215
|
+
res = {}
|
|
216
216
|
for terminal_account in self.terminal_accounts:
|
|
217
217
|
with suppress(KeyError):
|
|
218
|
-
|
|
219
|
-
|
|
218
|
+
res[terminal_account.id] = Decimal(
|
|
219
|
+
self.df_aum.loc[(terminal_account.id, slice(None), compute_date)].sum()
|
|
220
|
+
)
|
|
221
|
+
return res
|
|
@@ -86,7 +86,7 @@ class CommissionType(WBModel):
|
|
|
86
86
|
terminal_account: "Account",
|
|
87
87
|
compute_date: date,
|
|
88
88
|
content_object: models.Model,
|
|
89
|
-
|
|
89
|
+
terminal_accounts_total_holding: dict[int, Decimal],
|
|
90
90
|
) -> Generator[tuple["Commission", Decimal], None, None]:
|
|
91
91
|
"""
|
|
92
92
|
Retrieve valid commissions for the given terminal account and parameters.
|
|
@@ -99,20 +99,25 @@ class CommissionType(WBModel):
|
|
|
99
99
|
terminal_account (Account): The terminal account for which to retrieve valid commissions.
|
|
100
100
|
compute_date (date): The date for which to compute the rebates.
|
|
101
101
|
content_object (models.Model): The content object for which to compute rebates.
|
|
102
|
-
|
|
102
|
+
terminal_accounts_total_holding (dict): The total holding for each terminal account of the root account.
|
|
103
103
|
|
|
104
104
|
Yields:
|
|
105
105
|
tuple: A tuple containing a valid Commission instance and its corresponding actual percentage.
|
|
106
106
|
"""
|
|
107
107
|
|
|
108
|
-
applicable_commissions = Commission.objects.filter(
|
|
109
|
-
account__in=terminal_account.get_ancestors(include_self=True).filter(is_active=True), commission_type=self
|
|
110
|
-
).order_by("order")
|
|
111
108
|
available_percent = Decimal(1)
|
|
112
|
-
for
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
109
|
+
# for hiearchical account structure, we prioritize the immidiate parent account (i.e. self)
|
|
110
|
+
for commission in Commission.objects.filter(
|
|
111
|
+
root_account=terminal_account.get_root(), commission_type=self, account__is_active=True
|
|
112
|
+
).order_by("order"):
|
|
113
|
+
account_holding = sum(
|
|
114
|
+
[
|
|
115
|
+
terminal_accounts_total_holding.get(account.id, Decimal("0"))
|
|
116
|
+
for account in commission.account.get_descendants(include_self=True)
|
|
117
|
+
]
|
|
118
|
+
)
|
|
119
|
+
commission.commission_account_holding = account_holding
|
|
120
|
+
if commission.is_valid(compute_date, content_object, account_holding):
|
|
116
121
|
actual_percent = (
|
|
117
122
|
available_percent * commission.validated_percent
|
|
118
123
|
if commission.validated_net_commission
|
|
@@ -125,7 +130,7 @@ class CommissionType(WBModel):
|
|
|
125
130
|
|
|
126
131
|
def compute_rebates(
|
|
127
132
|
self, root_account: "Account", verbose: bool = False, **iterator_kwargs
|
|
128
|
-
) -> Generator[tuple["Account", date, "Commission", models.Model, "Entry", Decimal], None, None]:
|
|
133
|
+
) -> Generator[tuple["Account", date, "Commission", models.Model, "Entry", Decimal, dict], None, None]:
|
|
129
134
|
"""
|
|
130
135
|
Generate rebate information for terminal accounts based on given parameters.
|
|
131
136
|
|
|
@@ -162,7 +167,7 @@ class CommissionType(WBModel):
|
|
|
162
167
|
iterator = tqdm(iterator, total=len(iterator))
|
|
163
168
|
|
|
164
169
|
for terminal_account, content_object, compute_date in iterator:
|
|
165
|
-
|
|
170
|
+
terminal_accounts_total_holding = rebate_manager.get_terminal_accounts_total_holding(compute_date)
|
|
166
171
|
terminal_account_holding_ratio = rebate_manager.get_terminal_account_holding_ratio(
|
|
167
172
|
terminal_account,
|
|
168
173
|
content_object,
|
|
@@ -172,7 +177,7 @@ class CommissionType(WBModel):
|
|
|
172
177
|
# Calculate rebates for the current terminal account
|
|
173
178
|
if terminal_account_holding_ratio:
|
|
174
179
|
for commission, actual_percent in self.get_valid_commissions(
|
|
175
|
-
terminal_account, compute_date, content_object,
|
|
180
|
+
terminal_account, compute_date, content_object, terminal_accounts_total_holding
|
|
176
181
|
):
|
|
177
182
|
# total commission pool for the given object that day
|
|
178
183
|
commission_pool = rebate_manager.get_commission_pool(content_object, compute_date)
|
|
@@ -210,7 +215,7 @@ class CommissionType(WBModel):
|
|
|
210
215
|
rebate_gain,
|
|
211
216
|
{
|
|
212
217
|
"terminal_account_holding_ratio": terminal_account_holding_ratio,
|
|
213
|
-
"
|
|
218
|
+
"commission_account_holding": getattr(commission, "commission_account_holding", None),
|
|
214
219
|
"commission_percent": actual_percent,
|
|
215
220
|
"commission_pool": commission_pool,
|
|
216
221
|
"recipient_percent": recipient_percent,
|
|
@@ -247,6 +252,13 @@ class Commission(OrderedModel, WBModel):
|
|
|
247
252
|
on_delete=models.CASCADE,
|
|
248
253
|
verbose_name="Account",
|
|
249
254
|
)
|
|
255
|
+
root_account = models.ForeignKey[Account](
|
|
256
|
+
to="wbcrm.Account",
|
|
257
|
+
related_name="root_account_commissions",
|
|
258
|
+
on_delete=models.CASCADE,
|
|
259
|
+
verbose_name="Root Account",
|
|
260
|
+
) # we store the root account in order to allow reordering on root account as well as type
|
|
261
|
+
|
|
250
262
|
crm_recipient = models.ForeignKey[Entry](
|
|
251
263
|
to="directory.Entry",
|
|
252
264
|
related_name="recipient_commissions",
|
|
@@ -287,6 +299,7 @@ class Commission(OrderedModel, WBModel):
|
|
|
287
299
|
verbose_name="Account Role to decide with exclusion rule applies",
|
|
288
300
|
)
|
|
289
301
|
objects = CommissionManager()
|
|
302
|
+
order_with_respect_to = ("commission_type", "root_account")
|
|
290
303
|
|
|
291
304
|
if TYPE_CHECKING:
|
|
292
305
|
rules = RelatedManager["CommissionRule"]()
|
|
@@ -333,8 +346,7 @@ class Commission(OrderedModel, WBModel):
|
|
|
333
346
|
),
|
|
334
347
|
]
|
|
335
348
|
|
|
336
|
-
|
|
337
|
-
def recipient_repr(self) -> str:
|
|
349
|
+
def get_recipient_repr(self) -> str:
|
|
338
350
|
if self.crm_recipient:
|
|
339
351
|
return f"Profile {self.crm_recipient.computed_str}"
|
|
340
352
|
elif self.portfolio_role_recipient:
|
|
@@ -342,11 +354,15 @@ class Commission(OrderedModel, WBModel):
|
|
|
342
354
|
else:
|
|
343
355
|
return f"Account Role Type: {self.account_role_type_recipient.title}"
|
|
344
356
|
|
|
357
|
+
def save(self, *args, **kwargs):
|
|
358
|
+
self.root_account = self.account.get_root()
|
|
359
|
+
super().save(*args, **kwargs)
|
|
360
|
+
|
|
345
361
|
def __str__(self) -> str:
|
|
346
362
|
return repr(self)
|
|
347
363
|
|
|
348
364
|
def __repr__(self) -> str:
|
|
349
|
-
return f"Net: {self.net_commission} - {self.account.title} - {self.order} - {self.commission_type} - {self.
|
|
365
|
+
return f"Net: {self.net_commission} - {self.account.title} - {self.order} - {self.commission_type} - {self.get_recipient_repr()} (id: {self.id})"
|
|
350
366
|
|
|
351
367
|
@property
|
|
352
368
|
def validated_percent(self) -> Decimal:
|
|
@@ -606,6 +622,7 @@ class CommissionRole(ComplexToStringMixin, WBModel):
|
|
|
606
622
|
class Meta:
|
|
607
623
|
verbose_name = "Commission Role"
|
|
608
624
|
verbose_name_plural = "Commission Roles"
|
|
625
|
+
constraints = [models.UniqueConstraint(fields=["person", "commission"], name="unique_commission_role")]
|
|
609
626
|
|
|
610
627
|
def compute_str(self) -> str:
|
|
611
628
|
return f"{self.person} -> {self.commission}"
|
|
@@ -631,6 +648,16 @@ class CommissionRole(ComplexToStringMixin, WBModel):
|
|
|
631
648
|
return "id"
|
|
632
649
|
|
|
633
650
|
|
|
651
|
+
@receiver(pre_merge, sender="directory.Person")
|
|
652
|
+
def pre_merge_person_commisionrole(sender: models.Model, merged_object, main_object, **kwargs):
|
|
653
|
+
for role in CommissionRole.objects.filter(person=merged_object):
|
|
654
|
+
if CommissionRole.objects.filter(person=main_object, commission=role.commission).exists():
|
|
655
|
+
role.delete()
|
|
656
|
+
else:
|
|
657
|
+
role.person = main_object
|
|
658
|
+
role.save()
|
|
659
|
+
|
|
660
|
+
|
|
634
661
|
@receiver(post_save, sender="wbcommission.Commission")
|
|
635
662
|
def post_commission_creation(sender, instance, created, **kwargs):
|
|
636
663
|
# if a new commission line is created, we create a general rule
|
|
@@ -13,6 +13,7 @@ from wbcore.contrib.notifications.dispatch import send_notification
|
|
|
13
13
|
from wbcore.contrib.notifications.utils import create_notification_type
|
|
14
14
|
from wbcore.signals import pre_merge
|
|
15
15
|
from wbcore.utils.enum import ChoiceEnum
|
|
16
|
+
from wbcore.utils.models import MergeError
|
|
16
17
|
from wbcore.workers import Queue
|
|
17
18
|
from wbcrm.models.accounts import Account
|
|
18
19
|
|
|
@@ -155,7 +156,11 @@ class Rebate(BookingEntryCalculatedValueMixin, models.Model):
|
|
|
155
156
|
class Meta:
|
|
156
157
|
verbose_name = "Rebate"
|
|
157
158
|
verbose_name_plural = "Rebates"
|
|
158
|
-
|
|
159
|
+
constraints = (
|
|
160
|
+
models.UniqueConstraint(
|
|
161
|
+
name="unique_rebate", fields=("date", "recipient", "account", "product", "commission_type")
|
|
162
|
+
),
|
|
163
|
+
)
|
|
159
164
|
indexes = [models.Index(fields=["commission_type", "date", "recipient", "product", "account"])]
|
|
160
165
|
notification_types = [
|
|
161
166
|
create_notification_type(
|
|
@@ -315,3 +320,28 @@ def handle_pre_merge_account_for_rebate(sender: models.Model, merged_object: Acc
|
|
|
315
320
|
except Rebate.DoesNotExist:
|
|
316
321
|
rebate.account = main_object
|
|
317
322
|
rebate.save()
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@receiver(pre_merge, sender="directory.Entry")
|
|
326
|
+
def pre_merge_entry_rebate(sender: models.Model, merged_object, main_object, **kwargs):
|
|
327
|
+
for commission in Commission.objects.filter(crm_recipient=merged_object):
|
|
328
|
+
if Commission.objects.filter(crm_recipient=main_object, account=commission.account).exists():
|
|
329
|
+
raise MergeError(f"Commission already exists for {main_object} and the account {commission.account}")
|
|
330
|
+
else:
|
|
331
|
+
commission.crm_recipient = main_object
|
|
332
|
+
commission.save()
|
|
333
|
+
|
|
334
|
+
for rebate in Rebate.objects.filter(recipient=merged_object):
|
|
335
|
+
if Rebate.objects.filter(
|
|
336
|
+
date=rebate.date,
|
|
337
|
+
recipient=main_object,
|
|
338
|
+
account=rebate.account,
|
|
339
|
+
product=rebate.product,
|
|
340
|
+
commission_type=rebate.commission_type,
|
|
341
|
+
).exists():
|
|
342
|
+
raise MergeError(
|
|
343
|
+
f"Rebate already exists for {main_object} and the account {rebate.account}, we cannot safely handle this automatically."
|
|
344
|
+
)
|
|
345
|
+
else:
|
|
346
|
+
rebate.recipient = main_object
|
|
347
|
+
rebate.save()
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
from .rebate import RebateModelSerializer, RebateProductMarginalitySerializer
|
|
2
|
-
from .commissions import CommissionTypeRepresentationSerializer, CommissionTypeModelSerializer
|
|
2
|
+
from .commissions import CommissionTypeRepresentationSerializer, CommissionTypeModelSerializer, CommissionModelSerializer, CommissionRepresentationSerializer
|
|
3
3
|
from .signals import *
|