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