wbcrm 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 wbcrm might be problematic. Click here for more details.

Files changed (155) hide show
  1. wbcrm/__init__.py +1 -0
  2. wbcrm/admin/__init__.py +4 -0
  3. wbcrm/admin/accounts.py +59 -0
  4. wbcrm/admin/activities.py +101 -0
  5. wbcrm/admin/groups.py +7 -0
  6. wbcrm/admin/products.py +8 -0
  7. wbcrm/apps.py +5 -0
  8. wbcrm/configurations/__init__.py +1 -0
  9. wbcrm/configurations/base.py +16 -0
  10. wbcrm/dynamic_preferences_registry.py +38 -0
  11. wbcrm/factories/__init__.py +14 -0
  12. wbcrm/factories/accounts.py +56 -0
  13. wbcrm/factories/activities.py +125 -0
  14. wbcrm/factories/groups.py +23 -0
  15. wbcrm/factories/products.py +10 -0
  16. wbcrm/filters/__init__.py +10 -0
  17. wbcrm/filters/accounts.py +67 -0
  18. wbcrm/filters/activities.py +181 -0
  19. wbcrm/filters/groups.py +20 -0
  20. wbcrm/filters/products.py +37 -0
  21. wbcrm/filters/signals.py +94 -0
  22. wbcrm/migrations/0001_initial_squashed_squashed_0032_productcompanyrelationship_alter_product_prospects_and_more.py +3948 -0
  23. wbcrm/migrations/0002_alter_activity_repeat_choice.py +32 -0
  24. wbcrm/migrations/0003_remove_activity_external_id_and_more.py +63 -0
  25. wbcrm/migrations/0004_alter_activity_status.py +28 -0
  26. wbcrm/migrations/0005_account_accountrole_accountroletype_and_more.py +182 -0
  27. wbcrm/migrations/0006_alter_activity_location.py +17 -0
  28. wbcrm/migrations/0007_alter_account_status.py +23 -0
  29. wbcrm/migrations/0008_alter_activity_options.py +16 -0
  30. wbcrm/migrations/0009_alter_account_is_public.py +19 -0
  31. wbcrm/migrations/0010_alter_account_reference_id.py +17 -0
  32. wbcrm/migrations/0011_activity_summary.py +22 -0
  33. wbcrm/migrations/0012_alter_activity_summary.py +17 -0
  34. wbcrm/migrations/0013_account_action_plan_account_relationship_status_and_more.py +34 -0
  35. wbcrm/migrations/0014_alter_account_relationship_status.py +24 -0
  36. wbcrm/migrations/0015_alter_activity_type.py +23 -0
  37. wbcrm/migrations/0016_auto_20241205_1015.py +106 -0
  38. wbcrm/migrations/__init__.py +0 -0
  39. wbcrm/models/__init__.py +4 -0
  40. wbcrm/models/accounts.py +637 -0
  41. wbcrm/models/activities.py +1335 -0
  42. wbcrm/models/groups.py +118 -0
  43. wbcrm/models/products.py +83 -0
  44. wbcrm/models/recurrence.py +279 -0
  45. wbcrm/preferences.py +14 -0
  46. wbcrm/serializers/__init__.py +23 -0
  47. wbcrm/serializers/accounts.py +126 -0
  48. wbcrm/serializers/activities.py +526 -0
  49. wbcrm/serializers/groups.py +30 -0
  50. wbcrm/serializers/products.py +57 -0
  51. wbcrm/serializers/recurrence.py +90 -0
  52. wbcrm/serializers/signals.py +70 -0
  53. wbcrm/synchronization/__init__.py +0 -0
  54. wbcrm/synchronization/activity/__init__.py +0 -0
  55. wbcrm/synchronization/activity/admin.py +72 -0
  56. wbcrm/synchronization/activity/backend.py +207 -0
  57. wbcrm/synchronization/activity/backends/__init__.py +0 -0
  58. wbcrm/synchronization/activity/backends/google/__init__.py +2 -0
  59. wbcrm/synchronization/activity/backends/google/google_calendar_backend.py +399 -0
  60. wbcrm/synchronization/activity/backends/google/request_utils/__init__.py +16 -0
  61. wbcrm/synchronization/activity/backends/google/tasks.py +21 -0
  62. wbcrm/synchronization/activity/backends/google/tests/__init__.py +0 -0
  63. wbcrm/synchronization/activity/backends/google/tests/conftest.py +1 -0
  64. wbcrm/synchronization/activity/backends/google/tests/test_data.py +81 -0
  65. wbcrm/synchronization/activity/backends/google/tests/test_google_backend.py +319 -0
  66. wbcrm/synchronization/activity/backends/google/tests/test_utils.py +274 -0
  67. wbcrm/synchronization/activity/backends/google/typing_informations.py +139 -0
  68. wbcrm/synchronization/activity/backends/google/utils.py +216 -0
  69. wbcrm/synchronization/activity/backends/outlook/__init__.py +0 -0
  70. wbcrm/synchronization/activity/backends/outlook/backend.py +576 -0
  71. wbcrm/synchronization/activity/backends/outlook/msgraph.py +438 -0
  72. wbcrm/synchronization/activity/backends/outlook/parser.py +423 -0
  73. wbcrm/synchronization/activity/backends/outlook/tests/__init__.py +0 -0
  74. wbcrm/synchronization/activity/backends/outlook/tests/conftest.py +1 -0
  75. wbcrm/synchronization/activity/backends/outlook/tests/fixtures.py +606 -0
  76. wbcrm/synchronization/activity/backends/outlook/tests/test_admin.py +117 -0
  77. wbcrm/synchronization/activity/backends/outlook/tests/test_backend.py +269 -0
  78. wbcrm/synchronization/activity/backends/outlook/tests/test_controller.py +237 -0
  79. wbcrm/synchronization/activity/backends/outlook/tests/test_parser.py +173 -0
  80. wbcrm/synchronization/activity/controller.py +545 -0
  81. wbcrm/synchronization/activity/dynamic_preferences_registry.py +107 -0
  82. wbcrm/synchronization/activity/preferences.py +21 -0
  83. wbcrm/synchronization/activity/shortcuts.py +9 -0
  84. wbcrm/synchronization/activity/signals.py +28 -0
  85. wbcrm/synchronization/activity/tasks.py +21 -0
  86. wbcrm/synchronization/activity/urls.py +6 -0
  87. wbcrm/synchronization/activity/utils.py +46 -0
  88. wbcrm/synchronization/activity/views.py +37 -0
  89. wbcrm/synchronization/admin.py +1 -0
  90. wbcrm/synchronization/apps.py +15 -0
  91. wbcrm/synchronization/dynamic_preferences_registry.py +1 -0
  92. wbcrm/synchronization/management.py +36 -0
  93. wbcrm/synchronization/tasks.py +1 -0
  94. wbcrm/synchronization/urls.py +5 -0
  95. wbcrm/tasks.py +312 -0
  96. wbcrm/tests/__init__.py +0 -0
  97. wbcrm/tests/accounts/__init__.py +0 -0
  98. wbcrm/tests/accounts/test_models.py +380 -0
  99. wbcrm/tests/accounts/test_viewsets.py +87 -0
  100. wbcrm/tests/conftest.py +76 -0
  101. wbcrm/tests/disable_signals.py +52 -0
  102. wbcrm/tests/e2e/__init__.py +1 -0
  103. wbcrm/tests/e2e/e2e_wbcrm_utility.py +82 -0
  104. wbcrm/tests/e2e/test_e2e.py +369 -0
  105. wbcrm/tests/test_assignee_methods.py +39 -0
  106. wbcrm/tests/test_chartviewsets.py +111 -0
  107. wbcrm/tests/test_dto.py +63 -0
  108. wbcrm/tests/test_filters.py +51 -0
  109. wbcrm/tests/test_models.py +216 -0
  110. wbcrm/tests/test_recurrence.py +291 -0
  111. wbcrm/tests/test_report.py +20 -0
  112. wbcrm/tests/test_serializers.py +170 -0
  113. wbcrm/tests/test_tasks.py +94 -0
  114. wbcrm/tests/test_viewsets.py +967 -0
  115. wbcrm/tests/tests.py +120 -0
  116. wbcrm/typings.py +107 -0
  117. wbcrm/urls.py +67 -0
  118. wbcrm/viewsets/__init__.py +22 -0
  119. wbcrm/viewsets/accounts.py +121 -0
  120. wbcrm/viewsets/activities.py +315 -0
  121. wbcrm/viewsets/buttons/__init__.py +7 -0
  122. wbcrm/viewsets/buttons/accounts.py +27 -0
  123. wbcrm/viewsets/buttons/activities.py +68 -0
  124. wbcrm/viewsets/buttons/signals.py +17 -0
  125. wbcrm/viewsets/display/__init__.py +12 -0
  126. wbcrm/viewsets/display/accounts.py +110 -0
  127. wbcrm/viewsets/display/activities.py +443 -0
  128. wbcrm/viewsets/display/groups.py +22 -0
  129. wbcrm/viewsets/display/products.py +105 -0
  130. wbcrm/viewsets/endpoints/__init__.py +8 -0
  131. wbcrm/viewsets/endpoints/accounts.py +32 -0
  132. wbcrm/viewsets/endpoints/activities.py +30 -0
  133. wbcrm/viewsets/endpoints/groups.py +7 -0
  134. wbcrm/viewsets/endpoints/products.py +9 -0
  135. wbcrm/viewsets/groups.py +37 -0
  136. wbcrm/viewsets/menu/__init__.py +8 -0
  137. wbcrm/viewsets/menu/accounts.py +18 -0
  138. wbcrm/viewsets/menu/activities.py +61 -0
  139. wbcrm/viewsets/menu/groups.py +16 -0
  140. wbcrm/viewsets/menu/products.py +20 -0
  141. wbcrm/viewsets/mixins.py +34 -0
  142. wbcrm/viewsets/previews/__init__.py +1 -0
  143. wbcrm/viewsets/previews/activities.py +10 -0
  144. wbcrm/viewsets/products.py +56 -0
  145. wbcrm/viewsets/recurrence.py +26 -0
  146. wbcrm/viewsets/titles/__init__.py +13 -0
  147. wbcrm/viewsets/titles/accounts.py +22 -0
  148. wbcrm/viewsets/titles/activities.py +61 -0
  149. wbcrm/viewsets/titles/products.py +13 -0
  150. wbcrm/viewsets/titles/utils.py +46 -0
  151. wbcrm/workflows/__init__.py +1 -0
  152. wbcrm/workflows/assignee_methods.py +25 -0
  153. wbcrm-2.2.1.dist-info/METADATA +11 -0
  154. wbcrm-2.2.1.dist-info/RECORD +155 -0
  155. wbcrm-2.2.1.dist-info/WHEEL +5 -0
@@ -0,0 +1,181 @@
1
+ from datetime import date, timedelta
2
+
3
+ import django_filters
4
+ from django.contrib.auth import get_user_model
5
+ from django.db.models import Exists, OuterRef, Q
6
+ from django.db.models.query import QuerySet
7
+ from django.utils.translation import gettext_lazy
8
+ from django.utils.translation import gettext_lazy as _
9
+ from dynamic_preferences.registries import global_preferences_registry
10
+ from psycopg.types.range import TimestamptzRange
11
+ from wbcore import filters as wb_filters
12
+ from wbcore.contrib.agenda.filters import CalendarItemPeriodBaseFilterSet
13
+ from wbcore.contrib.agenda.models import CalendarItem
14
+ from wbcore.contrib.directory.models.entries import Company, Person
15
+ from wbcore.filters.lookups import ALL_TEXT_LOOKUPS
16
+ from wbcrm.models import Activity, ActivityParticipant, ActivityType, Group
17
+
18
+
19
+ def get_employee_filter_params(request, view):
20
+ if employer_id := global_preferences_registry.manager()["directory__main_company"]:
21
+ return {"employers": employer_id}
22
+ return {}
23
+
24
+
25
+ class ActivityBaseFilterSet(CalendarItemPeriodBaseFilterSet):
26
+ type = wb_filters.ModelMultipleChoiceFilter(
27
+ label=gettext_lazy("Types"),
28
+ queryset=ActivityType.objects.all(),
29
+ endpoint=ActivityType.get_representation_endpoint(),
30
+ value_key=ActivityType.get_representation_value_key(),
31
+ label_key=ActivityType.get_representation_label_key(),
32
+ )
33
+
34
+ visibility = wb_filters.MultipleChoiceFilter(
35
+ choices=CalendarItem.Visibility.choices,
36
+ label=gettext_lazy("Visibility"),
37
+ )
38
+
39
+ participants = wb_filters.ModelMultipleChoiceFilter(
40
+ label=gettext_lazy("Participants"),
41
+ queryset=Person.objects.all(),
42
+ endpoint=Person.get_representation_endpoint(),
43
+ value_key=Person.get_representation_value_key(),
44
+ label_key=Person.get_representation_label_key(),
45
+ )
46
+
47
+ companies = wb_filters.ModelMultipleChoiceFilter(
48
+ label=gettext_lazy("Companies"),
49
+ queryset=Company.objects.all(),
50
+ endpoint=Company.get_representation_endpoint(),
51
+ value_key=Company.get_representation_value_key(),
52
+ label_key=Company.get_representation_label_key(),
53
+ )
54
+
55
+ groups = wb_filters.ModelMultipleChoiceFilter(
56
+ label=gettext_lazy("Groups"),
57
+ queryset=Group.objects.all(),
58
+ endpoint=Group.get_representation_endpoint(),
59
+ value_key=Group.get_representation_value_key(),
60
+ label_key=Group.get_representation_label_key(),
61
+ )
62
+
63
+
64
+ class ActivityFilter(ActivityBaseFilterSet):
65
+ clients_of = wb_filters.ModelChoiceFilter(
66
+ label=gettext_lazy("Clients of"),
67
+ queryset=Person.objects.all(),
68
+ endpoint=Person.get_representation_endpoint(),
69
+ value_key=Person.get_representation_value_key(),
70
+ label_key=Person.get_representation_label_key(),
71
+ method="filter_clients_of",
72
+ filter_params=get_employee_filter_params,
73
+ )
74
+
75
+ latest_reviewer = wb_filters.ModelMultipleChoiceFilter(
76
+ label=gettext_lazy("Latest Reviewers"),
77
+ queryset=Person.objects.all(),
78
+ endpoint=Person.get_representation_endpoint(),
79
+ value_key=Person.get_representation_value_key(),
80
+ label_key=Person.get_representation_label_key(),
81
+ )
82
+
83
+ importance = wb_filters.MultipleChoiceFilter(
84
+ label=gettext_lazy("Importance"), choices=Activity.Importance.choices, widget=django_filters.widgets.CSVWidget
85
+ )
86
+
87
+ created = wb_filters.DateTimeRangeFilter(
88
+ method=wb_filters.DateRangeFilter.base_date_range_filter_method, label=gettext_lazy("Created")
89
+ )
90
+ edited = wb_filters.DateTimeRangeFilter(
91
+ method=wb_filters.DateRangeFilter.base_date_range_filter_method, label=gettext_lazy("Edited")
92
+ )
93
+
94
+ only_recent = wb_filters.BooleanFilter(label=gettext_lazy("Only Recent"), method="boolean_only_recent")
95
+
96
+ def boolean_only_recent(self, queryset: QuerySet[Activity], name, value: bool | None) -> QuerySet[Activity]:
97
+ if value:
98
+ today = date.today()
99
+ next_week = today + timedelta(days=7)
100
+ last_month = today - timedelta(days=30)
101
+ return queryset.filter(period__overlap=TimestamptzRange(last_month, next_week))
102
+ return queryset
103
+
104
+ def filter_clients_of(self, queryset, name, value):
105
+ if value:
106
+ user = self.request.user
107
+ return Activity.objects.filter(
108
+ (Q(participants__relationship_managers=value) | Q(companies__relationship_managers=value))
109
+ & (
110
+ Q(visibility=CalendarItem.Visibility.PUBLIC)
111
+ | (
112
+ Q(visibility=CalendarItem.Visibility.CONFIDENTIAL) # TODO move that to a queryset method
113
+ & Exists(
114
+ get_user_model().objects.filter(
115
+ Q(id=user.id)
116
+ & (
117
+ Q(groups__permissions__codename="administrate_confidential_items")
118
+ | Q(user_permissions__codename="administrate_confidential_items")
119
+ )
120
+ )
121
+ )
122
+ )
123
+ | Q(
124
+ Exists(
125
+ ActivityParticipant.objects.filter(activity_id=OuterRef("pk"), participant=user.profile)
126
+ )
127
+ )
128
+ | Q(assigned_to=user.profile)
129
+ )
130
+ )
131
+ return queryset
132
+
133
+ class Meta:
134
+ model = Activity
135
+ fields = {
136
+ "status": ["exact"],
137
+ "repeat_choice": ["exact"],
138
+ "title": ALL_TEXT_LOOKUPS,
139
+ "result": ALL_TEXT_LOOKUPS,
140
+ "description": ALL_TEXT_LOOKUPS,
141
+ }
142
+
143
+
144
+ class ActivityChartFilter(ActivityBaseFilterSet):
145
+ status = wb_filters.MultipleChoiceFilter(
146
+ label=gettext_lazy("Status"), choices=Activity.Status.choices, widget=django_filters.widgets.CSVWidget
147
+ )
148
+
149
+ class Meta:
150
+ model = Activity
151
+ fields = {}
152
+
153
+
154
+ class ActivityTypeFilter(wb_filters.FilterSet):
155
+ score = wb_filters.MultipleChoiceFilter(
156
+ label=_("Multipliers"), choices=ActivityType.Score.choices[:3], widget=django_filters.widgets.CSVWidget
157
+ )
158
+
159
+ class Meta:
160
+ model = ActivityType
161
+ fields = {
162
+ "title": ["exact", "icontains"],
163
+ "color": ["exact", "icontains"],
164
+ "default": ["exact"],
165
+ }
166
+
167
+
168
+ class ActivityParticipantFilter(wb_filters.FilterSet):
169
+ is_occupied_filter = wb_filters.BooleanFilter(
170
+ label=gettext_lazy("Is occupied by different activity"), method="boolean_is_occupied"
171
+ )
172
+
173
+ def boolean_is_occupied(self, queryset, name, value):
174
+ if value is None:
175
+ return queryset
176
+ else:
177
+ return queryset.filter(is_occupied=value)
178
+
179
+ class Meta:
180
+ model = ActivityParticipant
181
+ fields = {}
@@ -0,0 +1,20 @@
1
+ from django.utils.translation import gettext_lazy as _
2
+ from wbcore import filters as wb_filters
3
+ from wbcore.contrib.directory.models import Entry
4
+ from wbcrm.models import Group
5
+
6
+
7
+ class GroupFilter(wb_filters.FilterSet):
8
+ members = wb_filters.ModelMultipleChoiceFilter(
9
+ label=_("Members"),
10
+ queryset=Entry.objects.all(),
11
+ endpoint=Entry.get_representation_endpoint(),
12
+ value_key=Entry.get_representation_value_key(),
13
+ label_key=Entry.get_representation_label_key(),
14
+ )
15
+
16
+ class Meta:
17
+ model = Group
18
+ fields = {
19
+ "title": ["exact", "icontains"],
20
+ }
@@ -0,0 +1,37 @@
1
+ from django.utils.translation import gettext_lazy as _
2
+ from wbcore import filters as wb_filters
3
+ from wbcore.contrib.directory.models import Company
4
+ from wbcrm.models import Product, ProductCompanyRelationship
5
+
6
+
7
+ class ProductFilterSet(wb_filters.FilterSet):
8
+ prospects = wb_filters.ModelMultipleChoiceFilter(
9
+ label=_("Prospects"),
10
+ queryset=Company.objects.all(),
11
+ endpoint=Company.get_representation_endpoint(),
12
+ value_key=Company.get_representation_value_key(),
13
+ label_key=Company.get_representation_label_key(),
14
+ )
15
+
16
+ class Meta:
17
+ model = Product
18
+ fields = {
19
+ "title": ["exact", "icontains"],
20
+ "is_competitor": ["exact"],
21
+ }
22
+
23
+
24
+ class ProductCompanyFilterSet(wb_filters.FilterSet):
25
+ competitor_product = wb_filters.BooleanFilter(label=_("Is Competitor"), method="filter_competitor_product")
26
+
27
+ def filter_competitor_product(self, queryset, label, value):
28
+ if value is None:
29
+ return queryset
30
+ else:
31
+ return queryset.filter(product__is_competitor=value)
32
+
33
+ class Meta:
34
+ model = ProductCompanyRelationship
35
+ fields = {
36
+ "product": ["exact"],
37
+ }
@@ -0,0 +1,94 @@
1
+ from datetime import timedelta
2
+
3
+ from django.db.models.query import QuerySet
4
+ from django.dispatch import receiver
5
+ from django.utils import timezone
6
+ from django.utils.translation import gettext_lazy as _
7
+ from psycopg.types.range import TimestamptzRange
8
+ from wbcore import filters as wb_filters
9
+ from wbcore.contrib.directory.filters.entries import (
10
+ CompanyFilter,
11
+ EntryFilter,
12
+ PersonFilter,
13
+ UserIsManagerFilter,
14
+ )
15
+ from wbcore.contrib.directory.models import Entry
16
+ from wbcore.signals.filters import add_filters
17
+ from wbcrm.models import Account, AccountRole, Product
18
+
19
+
20
+ def choice_noactivity(queryset: QuerySet[Entry], name, value: str | None) -> QuerySet[Entry]:
21
+ """Returns only those entries who didn't participate in any activities during the selected timeframe"""
22
+
23
+ if value is None:
24
+ return queryset
25
+ else:
26
+ value = int(value)
27
+ if value:
28
+ end = timezone.now()
29
+ start = end - timedelta(days=value)
30
+ no_activity_timeframe = TimestamptzRange(start, end)
31
+ return queryset.exclude(
32
+ calendar_entities__period__overlap=no_activity_timeframe,
33
+ calendar_entities__item_type="wbcrm.Activity",
34
+ )
35
+ # A value of 0 corresponds to "All Time" in the filter so we need exclude any activities the entries participated in
36
+ return queryset.exclude(calendar_entities__item_type="wbcrm.Activity")
37
+
38
+
39
+ @receiver(add_filters, sender=EntryFilter)
40
+ @receiver(add_filters, sender=PersonFilter)
41
+ @receiver(add_filters, sender=CompanyFilter)
42
+ @receiver(add_filters, sender=UserIsManagerFilter)
43
+ def add_account_filter(sender, request=None, *args, **kwargs):
44
+ def _filter_with_account(queryset: QuerySet[Entry], name, value: str | None) -> QuerySet[Entry]:
45
+ accounts = Account.objects.filter_for_user(request.user)
46
+ if value:
47
+ return queryset.filter(accounts__in=accounts).distinct()
48
+ return queryset
49
+
50
+ def _filter_with_account_role(queryset: QuerySet[Entry], name, value: str | None) -> QuerySet[Entry]:
51
+ roles = AccountRole.objects.filter_for_user(request.user)
52
+ if value:
53
+ return queryset.filter(account_roles__in=roles).distinct()
54
+ return queryset
55
+
56
+ def filter_interested_products(queryset, name, value):
57
+ if value:
58
+ return queryset.filter(interested_products__in=value)
59
+ return queryset
60
+
61
+ interested_products = wb_filters.ModelMultipleChoiceFilter(
62
+ label="Interested Products",
63
+ help_text="Filter by products (ours and comptetitors) that the customer is interested in",
64
+ queryset=Product.objects.all(),
65
+ endpoint=Product.get_representation_endpoint(),
66
+ value_key=Product.get_representation_value_key(),
67
+ label_key=Product.get_representation_label_key(),
68
+ method=filter_interested_products,
69
+ )
70
+
71
+ return {
72
+ "without_activity": wb_filters.ChoiceFilter(
73
+ label=_("No Activity"),
74
+ help_text="Filter for entries without any activities",
75
+ field_name="without_activity",
76
+ choices=[
77
+ (31, _("Last Month")),
78
+ (92, _("Last 3 Months")),
79
+ (182, _("Last 6 Months")),
80
+ (365, _("Last 12 Months")),
81
+ (0, _("All Time")),
82
+ ],
83
+ method=choice_noactivity,
84
+ ),
85
+ "with_account": wb_filters.BooleanFilter(
86
+ label="With Account", help_text="Filter for entries without an account", method=_filter_with_account
87
+ ),
88
+ "with_account_role": wb_filters.BooleanFilter(
89
+ label="With Account Role",
90
+ help_text="Filter for entries without an account role",
91
+ method=_filter_with_account_role,
92
+ ),
93
+ "interested_products": interested_products,
94
+ }