wbcrm 1.56.8__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.
Files changed (182) hide show
  1. wbcrm/__init__.py +1 -0
  2. wbcrm/admin/__init__.py +5 -0
  3. wbcrm/admin/accounts.py +60 -0
  4. wbcrm/admin/activities.py +104 -0
  5. wbcrm/admin/events.py +43 -0
  6. wbcrm/admin/groups.py +8 -0
  7. wbcrm/admin/products.py +9 -0
  8. wbcrm/apps.py +5 -0
  9. wbcrm/configurations/__init__.py +1 -0
  10. wbcrm/configurations/base.py +16 -0
  11. wbcrm/dynamic_preferences_registry.py +38 -0
  12. wbcrm/factories/__init__.py +14 -0
  13. wbcrm/factories/accounts.py +57 -0
  14. wbcrm/factories/activities.py +124 -0
  15. wbcrm/factories/groups.py +24 -0
  16. wbcrm/factories/products.py +11 -0
  17. wbcrm/filters/__init__.py +10 -0
  18. wbcrm/filters/accounts.py +80 -0
  19. wbcrm/filters/activities.py +204 -0
  20. wbcrm/filters/groups.py +21 -0
  21. wbcrm/filters/products.py +38 -0
  22. wbcrm/filters/signals.py +95 -0
  23. wbcrm/fixtures/wbcrm.json +1215 -0
  24. wbcrm/kpi_handlers/activities.py +171 -0
  25. wbcrm/locale/de/LC_MESSAGES/django.mo +0 -0
  26. wbcrm/locale/de/LC_MESSAGES/django.po +1557 -0
  27. wbcrm/locale/de/LC_MESSAGES/django.po.translated +1630 -0
  28. wbcrm/locale/en/LC_MESSAGES/django.mo +0 -0
  29. wbcrm/locale/en/LC_MESSAGES/django.po +1466 -0
  30. wbcrm/locale/fr/LC_MESSAGES/django.mo +0 -0
  31. wbcrm/locale/fr/LC_MESSAGES/django.po +1467 -0
  32. wbcrm/migrations/0001_initial_squashed_squashed_0032_productcompanyrelationship_alter_product_prospects_and_more.py +3948 -0
  33. wbcrm/migrations/0002_alter_activity_repeat_choice.py +32 -0
  34. wbcrm/migrations/0003_remove_activity_external_id_and_more.py +63 -0
  35. wbcrm/migrations/0004_alter_activity_status.py +28 -0
  36. wbcrm/migrations/0005_account_accountrole_accountroletype_and_more.py +182 -0
  37. wbcrm/migrations/0006_alter_activity_location.py +17 -0
  38. wbcrm/migrations/0007_alter_account_status.py +23 -0
  39. wbcrm/migrations/0008_alter_activity_options.py +16 -0
  40. wbcrm/migrations/0009_alter_account_is_public.py +19 -0
  41. wbcrm/migrations/0010_alter_account_reference_id.py +17 -0
  42. wbcrm/migrations/0011_activity_summary.py +22 -0
  43. wbcrm/migrations/0012_alter_activity_summary.py +17 -0
  44. wbcrm/migrations/0013_account_action_plan_account_relationship_status_and_more.py +34 -0
  45. wbcrm/migrations/0014_alter_account_relationship_status.py +24 -0
  46. wbcrm/migrations/0015_alter_activity_type.py +23 -0
  47. wbcrm/migrations/0016_auto_20241205_1015.py +106 -0
  48. wbcrm/migrations/0017_event.py +40 -0
  49. wbcrm/migrations/0018_activity_search_vector.py +24 -0
  50. wbcrm/migrations/__init__.py +0 -0
  51. wbcrm/models/__init__.py +5 -0
  52. wbcrm/models/accounts.py +648 -0
  53. wbcrm/models/activities.py +1419 -0
  54. wbcrm/models/events.py +15 -0
  55. wbcrm/models/groups.py +119 -0
  56. wbcrm/models/llm/activity_summaries.py +41 -0
  57. wbcrm/models/llm/analyze_relationship.py +50 -0
  58. wbcrm/models/products.py +86 -0
  59. wbcrm/models/recurrence.py +280 -0
  60. wbcrm/preferences.py +13 -0
  61. wbcrm/report/activity_report.py +110 -0
  62. wbcrm/serializers/__init__.py +23 -0
  63. wbcrm/serializers/accounts.py +141 -0
  64. wbcrm/serializers/activities.py +525 -0
  65. wbcrm/serializers/groups.py +30 -0
  66. wbcrm/serializers/products.py +58 -0
  67. wbcrm/serializers/recurrence.py +91 -0
  68. wbcrm/serializers/signals.py +71 -0
  69. wbcrm/static/wbcrm/markdown/documentation/activity.md +86 -0
  70. wbcrm/static/wbcrm/markdown/documentation/activitytype.md +20 -0
  71. wbcrm/static/wbcrm/markdown/documentation/group.md +2 -0
  72. wbcrm/static/wbcrm/markdown/documentation/product.md +11 -0
  73. wbcrm/synchronization/__init__.py +0 -0
  74. wbcrm/synchronization/activity/__init__.py +0 -0
  75. wbcrm/synchronization/activity/admin.py +73 -0
  76. wbcrm/synchronization/activity/backend.py +214 -0
  77. wbcrm/synchronization/activity/backends/__init__.py +0 -0
  78. wbcrm/synchronization/activity/backends/google/__init__.py +2 -0
  79. wbcrm/synchronization/activity/backends/google/google_calendar_backend.py +406 -0
  80. wbcrm/synchronization/activity/backends/google/request_utils/__init__.py +16 -0
  81. wbcrm/synchronization/activity/backends/google/request_utils/external_to_internal/create.py +75 -0
  82. wbcrm/synchronization/activity/backends/google/request_utils/external_to_internal/delete.py +78 -0
  83. wbcrm/synchronization/activity/backends/google/request_utils/external_to_internal/update.py +155 -0
  84. wbcrm/synchronization/activity/backends/google/request_utils/internal_to_external/update.py +181 -0
  85. wbcrm/synchronization/activity/backends/google/tasks.py +21 -0
  86. wbcrm/synchronization/activity/backends/google/tests/__init__.py +0 -0
  87. wbcrm/synchronization/activity/backends/google/tests/conftest.py +1 -0
  88. wbcrm/synchronization/activity/backends/google/tests/test_data.py +81 -0
  89. wbcrm/synchronization/activity/backends/google/tests/test_google_backend.py +319 -0
  90. wbcrm/synchronization/activity/backends/google/tests/test_utils.py +274 -0
  91. wbcrm/synchronization/activity/backends/google/typing_informations.py +139 -0
  92. wbcrm/synchronization/activity/backends/google/utils.py +217 -0
  93. wbcrm/synchronization/activity/backends/outlook/__init__.py +0 -0
  94. wbcrm/synchronization/activity/backends/outlook/backend.py +593 -0
  95. wbcrm/synchronization/activity/backends/outlook/msgraph.py +436 -0
  96. wbcrm/synchronization/activity/backends/outlook/parser.py +432 -0
  97. wbcrm/synchronization/activity/backends/outlook/tests/__init__.py +0 -0
  98. wbcrm/synchronization/activity/backends/outlook/tests/conftest.py +1 -0
  99. wbcrm/synchronization/activity/backends/outlook/tests/fixtures.py +606 -0
  100. wbcrm/synchronization/activity/backends/outlook/tests/test_admin.py +118 -0
  101. wbcrm/synchronization/activity/backends/outlook/tests/test_backend.py +274 -0
  102. wbcrm/synchronization/activity/backends/outlook/tests/test_controller.py +249 -0
  103. wbcrm/synchronization/activity/backends/outlook/tests/test_parser.py +174 -0
  104. wbcrm/synchronization/activity/controller.py +627 -0
  105. wbcrm/synchronization/activity/dynamic_preferences_registry.py +119 -0
  106. wbcrm/synchronization/activity/preferences.py +27 -0
  107. wbcrm/synchronization/activity/shortcuts.py +16 -0
  108. wbcrm/synchronization/activity/tasks.py +21 -0
  109. wbcrm/synchronization/activity/urls.py +7 -0
  110. wbcrm/synchronization/activity/utils.py +46 -0
  111. wbcrm/synchronization/activity/views.py +41 -0
  112. wbcrm/synchronization/admin.py +1 -0
  113. wbcrm/synchronization/apps.py +14 -0
  114. wbcrm/synchronization/dynamic_preferences_registry.py +1 -0
  115. wbcrm/synchronization/management.py +36 -0
  116. wbcrm/synchronization/tasks.py +1 -0
  117. wbcrm/synchronization/urls.py +5 -0
  118. wbcrm/tasks.py +264 -0
  119. wbcrm/templates/email/activity.html +98 -0
  120. wbcrm/templates/email/activity_report.html +6 -0
  121. wbcrm/templates/email/daily_summary.html +72 -0
  122. wbcrm/templates/email/global_daily_summary.html +85 -0
  123. wbcrm/tests/__init__.py +0 -0
  124. wbcrm/tests/accounts/__init__.py +0 -0
  125. wbcrm/tests/accounts/test_models.py +393 -0
  126. wbcrm/tests/accounts/test_viewsets.py +88 -0
  127. wbcrm/tests/conftest.py +76 -0
  128. wbcrm/tests/disable_signals.py +62 -0
  129. wbcrm/tests/e2e/__init__.py +1 -0
  130. wbcrm/tests/e2e/e2e_wbcrm_utility.py +83 -0
  131. wbcrm/tests/e2e/test_e2e.py +370 -0
  132. wbcrm/tests/test_assignee_methods.py +40 -0
  133. wbcrm/tests/test_chartviewsets.py +112 -0
  134. wbcrm/tests/test_dto.py +64 -0
  135. wbcrm/tests/test_filters.py +52 -0
  136. wbcrm/tests/test_models.py +217 -0
  137. wbcrm/tests/test_recurrence.py +292 -0
  138. wbcrm/tests/test_report.py +21 -0
  139. wbcrm/tests/test_serializers.py +171 -0
  140. wbcrm/tests/test_tasks.py +95 -0
  141. wbcrm/tests/test_viewsets.py +967 -0
  142. wbcrm/tests/tests.py +121 -0
  143. wbcrm/typings.py +109 -0
  144. wbcrm/urls.py +67 -0
  145. wbcrm/viewsets/__init__.py +22 -0
  146. wbcrm/viewsets/accounts.py +122 -0
  147. wbcrm/viewsets/activities.py +341 -0
  148. wbcrm/viewsets/buttons/__init__.py +7 -0
  149. wbcrm/viewsets/buttons/accounts.py +27 -0
  150. wbcrm/viewsets/buttons/activities.py +89 -0
  151. wbcrm/viewsets/buttons/signals.py +17 -0
  152. wbcrm/viewsets/display/__init__.py +12 -0
  153. wbcrm/viewsets/display/accounts.py +110 -0
  154. wbcrm/viewsets/display/activities.py +444 -0
  155. wbcrm/viewsets/display/groups.py +22 -0
  156. wbcrm/viewsets/display/products.py +105 -0
  157. wbcrm/viewsets/endpoints/__init__.py +8 -0
  158. wbcrm/viewsets/endpoints/accounts.py +25 -0
  159. wbcrm/viewsets/endpoints/activities.py +30 -0
  160. wbcrm/viewsets/endpoints/groups.py +7 -0
  161. wbcrm/viewsets/endpoints/products.py +9 -0
  162. wbcrm/viewsets/groups.py +38 -0
  163. wbcrm/viewsets/menu/__init__.py +8 -0
  164. wbcrm/viewsets/menu/accounts.py +18 -0
  165. wbcrm/viewsets/menu/activities.py +49 -0
  166. wbcrm/viewsets/menu/groups.py +16 -0
  167. wbcrm/viewsets/menu/products.py +20 -0
  168. wbcrm/viewsets/mixins.py +35 -0
  169. wbcrm/viewsets/previews/__init__.py +1 -0
  170. wbcrm/viewsets/previews/activities.py +10 -0
  171. wbcrm/viewsets/products.py +57 -0
  172. wbcrm/viewsets/recurrence.py +27 -0
  173. wbcrm/viewsets/titles/__init__.py +13 -0
  174. wbcrm/viewsets/titles/accounts.py +23 -0
  175. wbcrm/viewsets/titles/activities.py +61 -0
  176. wbcrm/viewsets/titles/products.py +13 -0
  177. wbcrm/viewsets/titles/utils.py +46 -0
  178. wbcrm/workflows/__init__.py +1 -0
  179. wbcrm/workflows/assignee_methods.py +25 -0
  180. wbcrm-1.56.8.dist-info/METADATA +11 -0
  181. wbcrm-1.56.8.dist-info/RECORD +182 -0
  182. wbcrm-1.56.8.dist-info/WHEEL +5 -0
@@ -0,0 +1,341 @@
1
+ import pandas as pd
2
+ import plotly.express as px
3
+ import plotly.graph_objects as go
4
+ from django.contrib.messages import info, warning
5
+ from django.db.models import Exists, F, OuterRef, QuerySet, Subquery
6
+ from django.shortcuts import get_object_or_404
7
+ from django.utils.functional import cached_property
8
+ from django.utils.translation import gettext as _
9
+ from rest_framework.decorators import action
10
+ from rest_framework.response import Response
11
+ from wbcore import viewsets
12
+ from wbcore.contrib.agenda.models import CalendarItem
13
+ from wbcore.contrib.agenda.viewsets import CalendarItemViewSet
14
+ from wbcore.contrib.directory.models import (
15
+ Company,
16
+ EmailContact,
17
+ EmployerEmployeeRelationship,
18
+ Entry,
19
+ Person,
20
+ TelephoneContact,
21
+ )
22
+ from wbcore.filters import DjangoFilterBackend
23
+
24
+ from wbcrm import serializers as crm_serializers
25
+ from wbcrm.filters import (
26
+ ActivityChartFilter,
27
+ ActivityFilter,
28
+ ActivityParticipantFilter,
29
+ ActivityTypeFilter,
30
+ )
31
+ from wbcrm.models.activities import (
32
+ Activity,
33
+ ActivityParticipant,
34
+ ActivityType,
35
+ send_invitation_participant_as_task,
36
+ )
37
+ from wbcrm.viewsets.buttons import ActivityButtonConfig, ActivityParticipantButtonConfig
38
+ from wbcrm.viewsets.display import (
39
+ ActivityDisplay,
40
+ ActivityParticipantDisplayConfig,
41
+ ActivityTypeDisplay,
42
+ )
43
+ from wbcrm.viewsets.endpoints import (
44
+ ActivityEndpointConfig,
45
+ ActivityParticipantModelEndpointConfig,
46
+ )
47
+ from wbcrm.viewsets.previews.activities import ActivityPreviewConfig
48
+ from wbcrm.viewsets.titles import (
49
+ ActivityChartModelTitleConfig,
50
+ ActivityParticipantTitleConfig,
51
+ ActivityTitleConfig,
52
+ ActivityTypeTitleConfig,
53
+ )
54
+
55
+ from ..serializers import (
56
+ ActivityTypeModelSerializer,
57
+ ActivityTypeRepresentationSerializer,
58
+ )
59
+ from .recurrence import RecurrenceModelViewSetMixin
60
+
61
+
62
+ class ActivityTypeModelViewSet(viewsets.ModelViewSet):
63
+ IDENTIFIER = "wbcrm:activitytype"
64
+ LIST_DOCUMENTATION = "wbcrm/markdown/documentation/activitytype.md"
65
+ queryset = ActivityType.objects.all()
66
+ serializer_class = ActivityTypeModelSerializer
67
+ display_config_class = ActivityTypeDisplay
68
+ title_config_class = ActivityTypeTitleConfig
69
+ search_fields = ("title",)
70
+ filterset_class = ActivityTypeFilter
71
+ ordering = ("title",)
72
+ ordering_fields = (
73
+ "title",
74
+ "color",
75
+ "score",
76
+ "icon",
77
+ "default",
78
+ )
79
+
80
+
81
+ class ActivityTypeRepresentationViewSet(viewsets.RepresentationViewSet):
82
+ IDENTIFIER = "wbcrm:activitytyperepresentation"
83
+ serializer_class = ActivityTypeRepresentationSerializer
84
+ queryset = ActivityType.objects.all()
85
+
86
+
87
+ class ActivityRepresentationViewSet(viewsets.RepresentationViewSet):
88
+ queryset = Activity.objects.all()
89
+ serializer_class = crm_serializers.ActivityRepresentationSerializer
90
+
91
+
92
+ class ActivityViewSet(RecurrenceModelViewSetMixin, CalendarItemViewSet):
93
+ IDENTIFIER = "wbcrm:activity"
94
+ DEPENDANT_IDENTIFIER = ["wbcrm:calendaritem"]
95
+ LIST_DOCUMENTATION = "wbcrm/markdown/documentation/activity.md"
96
+
97
+ ordering = ["-edited", "id"]
98
+ search_fields = ("search_vector",)
99
+ serializer_class = crm_serializers.ActivityModelSerializer
100
+ display_config_class = ActivityDisplay
101
+ title_config_class = ActivityTitleConfig
102
+ filterset_class = ActivityFilter
103
+ endpoint_config_class = ActivityEndpointConfig
104
+ preview_config_class = ActivityPreviewConfig
105
+ button_config_class = ActivityButtonConfig
106
+ queryset = Activity.objects.all()
107
+
108
+ ordering_fields = [
109
+ "created",
110
+ "description",
111
+ "edited",
112
+ "period",
113
+ "latest_reviewer__computed_str",
114
+ "type",
115
+ "result",
116
+ "title",
117
+ ]
118
+
119
+ @cached_property
120
+ def is_external_activity(self):
121
+ if "pk" in self.kwargs:
122
+ if activity := self.get_object():
123
+ return (creator := activity.creator) and not creator.is_internal
124
+ return False
125
+
126
+ @cached_property
127
+ def is_private_for_user(self):
128
+ if "pk" in self.kwargs:
129
+ if activity := self.get_object():
130
+ return activity.is_private_for_user(self.request.user)
131
+ return not self.new_mode
132
+
133
+ @cached_property
134
+ def is_confidential_for_user(self):
135
+ if "pk" in self.kwargs:
136
+ if activity := self.get_object():
137
+ return activity.is_confidential_for_user(self.request.user)
138
+ return not self.new_mode
139
+
140
+ @cached_property
141
+ def participants(self) -> QuerySet[Person]:
142
+ try:
143
+ participant_ids = self.request.GET["participants"].split(",")
144
+ except KeyError:
145
+ participant_ids = []
146
+ return Person.objects.filter(id__in=participant_ids)
147
+
148
+ @cached_property
149
+ def companies(self) -> QuerySet[Company]:
150
+ try:
151
+ company_ids = self.request.GET["companies"].split(",")
152
+ except KeyError:
153
+ company_ids = []
154
+ return Company.objects.filter(id__in=company_ids)
155
+
156
+ @cached_property
157
+ def entry(self) -> Entry | None:
158
+ try:
159
+ return Entry.all_objects.get(id=self.kwargs.get("person_id", self.kwargs["company_id"]))
160
+ except (KeyError, Entry.DoesNotExist):
161
+ return self.participants.first() or self.companies.first()
162
+
163
+ def get_serializer_class(self):
164
+ if self.get_action() in ["list", "list-metadata"]:
165
+ return crm_serializers.ActivityModelListSerializer
166
+ if (
167
+ self.is_private_for_user
168
+ or self.is_confidential_for_user
169
+ or (
170
+ "pk" in self.kwargs
171
+ and (obj := self.get_object())
172
+ and obj.status in [Activity.Status.CANCELLED, Activity.Status.REVIEWED]
173
+ )
174
+ ):
175
+ return crm_serializers.ReadOnlyActivityModelSerializer
176
+ return super().get_serializer_class()
177
+
178
+ def get_queryset(self):
179
+ user = self.request.user
180
+ return (
181
+ Activity.get_activities_for_user(user, base_qs=super().get_queryset())
182
+ .select_related("latest_reviewer", "type")
183
+ .prefetch_related("groups", "participants", "companies", "activity_companies")
184
+ )
185
+
186
+ def add_messages(self, request, instance: Activity | None = None, **kwargs):
187
+ super().add_messages(request, instance=instance, **kwargs)
188
+ if instance:
189
+ if instance.status in [Activity.Status.CANCELLED, Activity.Status.REVIEWED]:
190
+ info(
191
+ self.request,
192
+ _(
193
+ "You can only modify the review text for an activity that is either cancelled or already reviewed."
194
+ ),
195
+ )
196
+ if self.is_external_activity:
197
+ warning(
198
+ self.request,
199
+ _(
200
+ "This activity was created by an external user and synchronized. The modification is restricted."
201
+ ),
202
+ )
203
+ if warning_message := instance.participants_company_check_message():
204
+ warning(request, warning_message, extra_tags="auto_close=0")
205
+
206
+ # Throws a warning message when there are more people (probably) participating in an activity on-site
207
+ # than the selected conference room has capacity for.
208
+ if (
209
+ instance.conference_room
210
+ and instance.conference_room.capacity is not None
211
+ and instance.participants.exclude(
212
+ activity_participants__participation_status__in=[
213
+ ActivityParticipant.ParticipationStatus.ATTENDS_DIGITALLY,
214
+ ActivityParticipant.ParticipationStatus.CANCELLED,
215
+ ]
216
+ ).count()
217
+ > instance.conference_room.capacity
218
+ ):
219
+ warning(
220
+ request,
221
+ _(
222
+ "There are more participants currently participating in this activity than the maximum capacity of the selected conference room allows."
223
+ ),
224
+ )
225
+
226
+
227
+ class ActivityChartModelViewSet(viewsets.ChartViewSet):
228
+ filter_backends = (DjangoFilterBackend,)
229
+ queryset = Activity.objects.all()
230
+ filterset_class = ActivityChartFilter
231
+ IDENTIFIER = "wbcrm:activitychart"
232
+ title_config_class = ActivityChartModelTitleConfig
233
+
234
+ def get_queryset(self):
235
+ return (
236
+ Activity.get_activities_for_user(self.request.user)
237
+ .exclude(period__isnull=True)
238
+ .filter(visibility=CalendarItem.Visibility.PUBLIC)
239
+ .annotate(
240
+ activity_type_color=F("type__color"),
241
+ activity_type_title=F("type__title"),
242
+ start_date=F("period__startswith"),
243
+ end_date=F("period__endswith"),
244
+ )
245
+ .select_related("type")
246
+ )
247
+
248
+ def get_plotly(self, queryset):
249
+ fig = go.Figure()
250
+ if queryset.exists():
251
+ df = pd.DataFrame(
252
+ queryset.values("type", "start_date", "end_date", "activity_type_color", "activity_type_title")
253
+ )
254
+ df["start_date"] = df["start_date"].dt.floor("h")
255
+ df["end_date"] = df["end_date"].dt.ceil("h")
256
+ df = (
257
+ pd.concat(
258
+ [
259
+ pd.DataFrame(index=pd.date_range(r.start_date, r.end_date, freq="1h")).assign(
260
+ type=r.type,
261
+ activity_type_color=r.activity_type_color,
262
+ activity_type_title=r.activity_type_title,
263
+ )
264
+ for r in df.itertuples()
265
+ ]
266
+ ).reset_index()
267
+ ).rename(columns={"index": "Period", "activity_type_title": "Type"})
268
+ df["Period"] = pd.to_datetime(df["Period"])
269
+ fig = px.histogram(
270
+ df,
271
+ x="Period",
272
+ color="Type",
273
+ labels="Type",
274
+ nbins=len(pd.date_range(df["Period"].min(), df["Period"].max(), freq="1h")) + 1,
275
+ )
276
+ fig.update_layout(
277
+ paper_bgcolor="rgba(0,0,0,0)",
278
+ plot_bgcolor="rgba(0,0,0,0)",
279
+ title=_("<b>User Activity</b>"),
280
+ bargap=0.2,
281
+ autosize=True,
282
+ )
283
+
284
+ return fig
285
+
286
+
287
+ class ActivityParticipantModelViewSet(viewsets.ModelViewSet):
288
+ IDENTIFIER = "wbcrm:activity-participant"
289
+
290
+ button_config_class = ActivityParticipantButtonConfig
291
+ display_config_class = ActivityParticipantDisplayConfig
292
+ endpoint_config_class = ActivityParticipantModelEndpointConfig
293
+ filterset_class = ActivityParticipantFilter
294
+ serializer_class = crm_serializers.ActivityParticipantModelSerializer
295
+ search_fields = []
296
+ title_config_class = ActivityParticipantTitleConfig
297
+ ordering = ["participation_status", "id"]
298
+ queryset = ActivityParticipant.objects.all()
299
+
300
+ def get_queryset(self):
301
+ activity = get_object_or_404(Activity, id=self.kwargs["activity_id"])
302
+ return (
303
+ super()
304
+ .get_queryset()
305
+ .filter(activity=activity)
306
+ .annotate(
307
+ customer_status=Subquery(
308
+ EmployerEmployeeRelationship.objects.filter(
309
+ primary=True, employee=OuterRef("participant__pk")
310
+ ).values("employer__customer_status__title")[:1]
311
+ ),
312
+ position=Subquery(
313
+ EmployerEmployeeRelationship.objects.filter(
314
+ primary=True, employee=OuterRef("participant__pk")
315
+ ).values("position__title")[:1]
316
+ ),
317
+ primary_telephone=Subquery(
318
+ TelephoneContact.objects.filter(primary=True, entry__id=OuterRef("participant__pk")).values(
319
+ "number"
320
+ )[:1],
321
+ ),
322
+ primary_email=Subquery(
323
+ EmailContact.objects.filter(primary=True, entry__id=OuterRef("participant__pk")).values("address")[
324
+ :1
325
+ ],
326
+ ),
327
+ is_occupied=Exists(
328
+ Activity.objects.filter(
329
+ period__overlap=activity.period,
330
+ participants__id=OuterRef("participant__pk"),
331
+ )
332
+ .exclude(id=activity.id)
333
+ .exclude(status=Activity.Status.CANCELLED)
334
+ ),
335
+ )
336
+ )
337
+
338
+ @action(methods=["PATCH"], detail=False)
339
+ def send_external_invitation(self, request, activity_id: int, pk=None):
340
+ send_invitation_participant_as_task.delay(activity_id)
341
+ return Response({"__notification": {"title": "Invitation sent to external participants"}})
@@ -0,0 +1,7 @@
1
+ from wbcrm.viewsets.buttons.activities import (
2
+ ActivityButtonConfig,
3
+ ActivityParticipantButtonConfig,
4
+ )
5
+
6
+ from .accounts import AccountButtonConfig
7
+ from .signals import *
@@ -0,0 +1,27 @@
1
+ from django.dispatch import receiver
2
+ from wbcore.contrib.directory.viewsets import (
3
+ CompanyModelViewSet,
4
+ EntryModelViewSet,
5
+ PersonModelViewSet,
6
+ )
7
+ from wbcore.contrib.icons import WBIcon
8
+ from wbcore.metadata.configs import buttons as bt
9
+ from wbcore.metadata.configs.buttons.view_config import ButtonViewConfig
10
+ from wbcore.signals.instance_buttons import add_instance_button
11
+
12
+
13
+ class AccountButtonConfig(ButtonViewConfig):
14
+ def get_custom_instance_buttons(self):
15
+ return {bt.WidgetButton(key="claims", label="Show Claims", icon=WBIcon.TRADE.icon)}
16
+
17
+ def get_custom_list_instance_buttons(self):
18
+ return self.get_custom_instance_buttons()
19
+
20
+
21
+ @receiver(add_instance_button, sender=PersonModelViewSet)
22
+ @receiver(add_instance_button, sender=EntryModelViewSet)
23
+ @receiver(add_instance_button, sender=CompanyModelViewSet)
24
+ def crm_adding_instance_buttons(sender, many, *args, **kwargs):
25
+ if many:
26
+ return
27
+ return bt.WidgetButton(key="account", label="Accounts", icon=WBIcon.FOLDERS_MONEY.icon)
@@ -0,0 +1,89 @@
1
+ from django.utils.translation import gettext as _
2
+ from rest_framework.reverse import reverse
3
+ from wbcore.contrib.icons import WBIcon
4
+ from wbcore.enums import RequestType
5
+ from wbcore.metadata.configs import buttons as bt
6
+ from wbcore.metadata.configs.buttons.view_config import ButtonViewConfig
7
+
8
+ from wbcrm.models.activities import Activity, ActivityParticipant
9
+ from wbcrm.synchronization.activity.shortcuts import get_backend
10
+
11
+ DESCRIPTION: str = _(
12
+ "<p> Are you sure you want to delete all the future instances of this activity? <br> \
13
+ Only 'Planned' and 'Canceled' activities will be deleted. <br>\
14
+ Depending on the number of activities to be deleted <br> \
15
+ it may take some time until the deleted activities are no longer displayed in the activity list. </p>",
16
+ )
17
+
18
+
19
+ class ActivityButtonConfig(ButtonViewConfig):
20
+ def get_custom_instance_buttons(self) -> set:
21
+ if self.view.kwargs.get("pk"):
22
+ return {
23
+ bt.WidgetButton(
24
+ label=_("Parent Activity"), icon=WBIcon.CALENDAR.icon, key="get_parent_occurrence", weight=110
25
+ ),
26
+ bt.ActionButton(
27
+ method=RequestType.DELETE,
28
+ identifiers=("wbcrm:activity",),
29
+ key="delete_next_occurrences",
30
+ label=_("Delete Next Occurrences"),
31
+ icon=WBIcon.DELETE.icon,
32
+ description_fields=DESCRIPTION,
33
+ title=_("Delete"),
34
+ action_label=_("Delete"),
35
+ weight=140,
36
+ ),
37
+ bt.WidgetButton(label=_("Next Activity"), icon=WBIcon.NEXT.icon, key="next_occurrence", weight=130),
38
+ bt.WidgetButton(
39
+ label=_("Previous Activity"),
40
+ icon=WBIcon.PREVIOUS.icon,
41
+ key="previous_occurrence",
42
+ weight=120,
43
+ ),
44
+ }
45
+
46
+ return set()
47
+
48
+
49
+ class ActivityParticipantButtonConfig(ButtonViewConfig):
50
+ def get_custom_buttons(self) -> set:
51
+ buttons = set()
52
+ if not self.view.kwargs.get("pk"):
53
+ base_url = reverse("wbcrm:activity-list", args=[], request=self.request)
54
+ activity: Activity = Activity.all_objects.get(id=self.view.kwargs.get("activity_id"))
55
+ if activity.period:
56
+ participants_id_set: set[int] = set(activity.participants.values_list("id", flat=True))
57
+ id_str = ",".join(str(id) for id in participants_id_set)
58
+
59
+ start = activity.period.lower.date()
60
+ end = activity.period.upper.date()
61
+
62
+ endpoint = f"{base_url}?participants={id_str}&period={start:%Y-%m-%d},{end:%Y-%m-%d}"
63
+
64
+ buttons.add(
65
+ bt.WidgetButton(
66
+ endpoint=endpoint,
67
+ label=_("Show Participants' Activities"),
68
+ icon=WBIcon.CALENDAR.icon,
69
+ )
70
+ )
71
+
72
+ # Activity sync button to send invitation to external participants
73
+ if get_backend():
74
+ if activity.activity_participants.filter(
75
+ participation_status=ActivityParticipant.ParticipationStatus.PENDING_INVITATION
76
+ ).exists():
77
+ buttons.add(
78
+ bt.ActionButton(
79
+ method=RequestType.PATCH,
80
+ endpoint=reverse(
81
+ "wbcrm:activity-participant-send-external-invitation",
82
+ args=[activity.id],
83
+ request=self.request,
84
+ ),
85
+ label="Send invitation to External",
86
+ icon=WBIcon.SEND_LATER.icon,
87
+ )
88
+ )
89
+ return buttons
@@ -0,0 +1,17 @@
1
+ from django.dispatch import receiver
2
+ from django.utils.translation import gettext as _
3
+ from wbcore.contrib.directory.viewsets import (
4
+ CompanyModelViewSet,
5
+ EntryModelViewSet,
6
+ PersonModelViewSet,
7
+ )
8
+ from wbcore.contrib.icons import WBIcon
9
+ from wbcore.metadata.configs import buttons as bt
10
+ from wbcore.signals.instance_buttons import add_instance_button
11
+
12
+
13
+ @receiver(add_instance_button, sender=PersonModelViewSet)
14
+ @receiver(add_instance_button, sender=EntryModelViewSet)
15
+ @receiver(add_instance_button, sender=CompanyModelViewSet)
16
+ def add_activity_instance_button_in_directory_viewsets(sender, many, *args, **kwargs):
17
+ return bt.WidgetButton(key="activity", label=_("Activities"), icon=WBIcon.CALENDAR.icon, weight=1)
@@ -0,0 +1,12 @@
1
+ from .accounts import (
2
+ AccountDisplayConfig,
3
+ AccountRoleAccountDisplayConfig,
4
+ InheritedAccountRoleAccountDisplayConfig,
5
+ )
6
+ from .activities import (
7
+ ActivityDisplay,
8
+ ActivityParticipantDisplayConfig,
9
+ ActivityTypeDisplay,
10
+ )
11
+ from .groups import GroupModelDisplay
12
+ from .products import ProductCompanyRelationshipDisplay, ProductDisplay
@@ -0,0 +1,110 @@
1
+ from typing import Optional
2
+
3
+ from rest_framework.reverse import reverse
4
+ from wbcore.metadata.configs import display as dp
5
+ from wbcore.metadata.configs.display.instance_display import Display
6
+ from wbcore.metadata.configs.display.instance_display.shortcuts import (
7
+ create_simple_display,
8
+ create_simple_section,
9
+ )
10
+ from wbcore.metadata.configs.display.instance_display.utils import repeat_field
11
+ from wbcore.metadata.configs.display.view_config import DisplayViewConfig
12
+
13
+
14
+ class AccountDisplayConfig(DisplayViewConfig):
15
+ def get_list_display(self) -> Optional[dp.ListDisplay]:
16
+ return dp.ListDisplay(
17
+ fields=[
18
+ dp.Field(key="title", label="Title", pinned="left"),
19
+ dp.Field(key="status", label="Status"),
20
+ dp.Field(
21
+ key="owner",
22
+ label="Owner",
23
+ ),
24
+ dp.Field(key="reference_id", label="Reference ID"),
25
+ dp.Field(key="is_terminal_account", label="Terminal Account"),
26
+ dp.Field(key="is_public", label="Public"),
27
+ dp.Field(
28
+ key="llm",
29
+ label="LLM Analysis",
30
+ children=[
31
+ dp.Field(key="relationship_status", label="Relationship Status"),
32
+ dp.Field(key="relationship_summary", label="Relationship Summary", show="open"),
33
+ dp.Field(key="action_plan", label="Action Plan", show="open"),
34
+ ],
35
+ ),
36
+ ],
37
+ tree=True,
38
+ tree_group_field="title",
39
+ tree_group_field_sortable=True,
40
+ tree_group_level_options=[
41
+ dp.TreeGroupLevelOption(
42
+ filter_key="parent",
43
+ filter_depth=1,
44
+ # lookup="id_repr",
45
+ filter_blacklist=["parent__isnull"],
46
+ list_endpoint=reverse(
47
+ "wbcrm:account-list",
48
+ args=[],
49
+ request=self.request,
50
+ ),
51
+ )
52
+ ],
53
+ )
54
+
55
+ def get_instance_display(self) -> Display:
56
+ child_account_section = create_simple_section(
57
+ "child_account_section", "Child Accounts", [["childaccounts"]], "childaccounts", collapsed=True
58
+ )
59
+ account_role_section = create_simple_section(
60
+ "account_role_section", "Account Roles", [["accountroles"]], "accountroles", collapsed=True
61
+ )
62
+ inherited_account_role_section = create_simple_section(
63
+ "inherited_account_role_section",
64
+ "Inherited Account Roles",
65
+ [["inheritedaccountroles"]],
66
+ "inheritedaccountroles",
67
+ collapsed=True,
68
+ )
69
+ return create_simple_display(
70
+ [
71
+ [repeat_field(3, "status")],
72
+ ["title", "title", "reference_id"],
73
+ ["is_active", "is_terminal_account", "is_public"],
74
+ ["parent", "owner", "owner"] if "account_id" not in self.view.kwargs else [repeat_field(3, "owner")],
75
+ [repeat_field(3, "child_account_section")],
76
+ [repeat_field(3, "account_role_section")],
77
+ [repeat_field(3, "inherited_account_role_section")],
78
+ ],
79
+ [child_account_section, account_role_section, inherited_account_role_section],
80
+ )
81
+
82
+
83
+ class AccountRoleAccountDisplayConfig(DisplayViewConfig):
84
+ def get_instance_display(self) -> Display:
85
+ return create_simple_display([["role_type", "entry"], ["is_hidden", "authorized_hidden_users"]])
86
+
87
+ def get_list_display(self) -> Optional[dp.ListDisplay]:
88
+ return dp.ListDisplay(
89
+ fields=[
90
+ dp.Field(key="role_type", label="Role"),
91
+ dp.Field(key="entry", label="entry"),
92
+ dp.Field(key="is_currently_valid", label="Valid"),
93
+ dp.Field(key="is_hidden", label="Hidden"),
94
+ dp.Field(key="authorized_hidden_users", label="Authorized Hidden Users"),
95
+ ]
96
+ )
97
+
98
+
99
+ class InheritedAccountRoleAccountDisplayConfig(AccountRoleAccountDisplayConfig):
100
+ def get_list_display(self) -> Optional[dp.ListDisplay]:
101
+ return dp.ListDisplay(
102
+ fields=[
103
+ dp.Field(key="account", label="Account"),
104
+ dp.Field(key="role_type", label="Role"),
105
+ dp.Field(key="entry", label="Entry"),
106
+ dp.Field(key="is_currently_valid", label="Valid"),
107
+ dp.Field(key="is_hidden", label="Hidden"),
108
+ dp.Field(key="authorized_hidden_users", label="Authorized Hidden Users"),
109
+ ]
110
+ )