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.
- wbcrm/__init__.py +1 -0
- wbcrm/admin/__init__.py +5 -0
- wbcrm/admin/accounts.py +60 -0
- wbcrm/admin/activities.py +104 -0
- wbcrm/admin/events.py +43 -0
- wbcrm/admin/groups.py +8 -0
- wbcrm/admin/products.py +9 -0
- wbcrm/apps.py +5 -0
- wbcrm/configurations/__init__.py +1 -0
- wbcrm/configurations/base.py +16 -0
- wbcrm/dynamic_preferences_registry.py +38 -0
- wbcrm/factories/__init__.py +14 -0
- wbcrm/factories/accounts.py +57 -0
- wbcrm/factories/activities.py +124 -0
- wbcrm/factories/groups.py +24 -0
- wbcrm/factories/products.py +11 -0
- wbcrm/filters/__init__.py +10 -0
- wbcrm/filters/accounts.py +80 -0
- wbcrm/filters/activities.py +204 -0
- wbcrm/filters/groups.py +21 -0
- wbcrm/filters/products.py +38 -0
- wbcrm/filters/signals.py +95 -0
- wbcrm/fixtures/wbcrm.json +1215 -0
- wbcrm/kpi_handlers/activities.py +171 -0
- wbcrm/locale/de/LC_MESSAGES/django.mo +0 -0
- wbcrm/locale/de/LC_MESSAGES/django.po +1557 -0
- wbcrm/locale/de/LC_MESSAGES/django.po.translated +1630 -0
- wbcrm/locale/en/LC_MESSAGES/django.mo +0 -0
- wbcrm/locale/en/LC_MESSAGES/django.po +1466 -0
- wbcrm/locale/fr/LC_MESSAGES/django.mo +0 -0
- wbcrm/locale/fr/LC_MESSAGES/django.po +1467 -0
- wbcrm/migrations/0001_initial_squashed_squashed_0032_productcompanyrelationship_alter_product_prospects_and_more.py +3948 -0
- wbcrm/migrations/0002_alter_activity_repeat_choice.py +32 -0
- wbcrm/migrations/0003_remove_activity_external_id_and_more.py +63 -0
- wbcrm/migrations/0004_alter_activity_status.py +28 -0
- wbcrm/migrations/0005_account_accountrole_accountroletype_and_more.py +182 -0
- wbcrm/migrations/0006_alter_activity_location.py +17 -0
- wbcrm/migrations/0007_alter_account_status.py +23 -0
- wbcrm/migrations/0008_alter_activity_options.py +16 -0
- wbcrm/migrations/0009_alter_account_is_public.py +19 -0
- wbcrm/migrations/0010_alter_account_reference_id.py +17 -0
- wbcrm/migrations/0011_activity_summary.py +22 -0
- wbcrm/migrations/0012_alter_activity_summary.py +17 -0
- wbcrm/migrations/0013_account_action_plan_account_relationship_status_and_more.py +34 -0
- wbcrm/migrations/0014_alter_account_relationship_status.py +24 -0
- wbcrm/migrations/0015_alter_activity_type.py +23 -0
- wbcrm/migrations/0016_auto_20241205_1015.py +106 -0
- wbcrm/migrations/0017_event.py +40 -0
- wbcrm/migrations/0018_activity_search_vector.py +24 -0
- wbcrm/migrations/__init__.py +0 -0
- wbcrm/models/__init__.py +5 -0
- wbcrm/models/accounts.py +648 -0
- wbcrm/models/activities.py +1419 -0
- wbcrm/models/events.py +15 -0
- wbcrm/models/groups.py +119 -0
- wbcrm/models/llm/activity_summaries.py +41 -0
- wbcrm/models/llm/analyze_relationship.py +50 -0
- wbcrm/models/products.py +86 -0
- wbcrm/models/recurrence.py +280 -0
- wbcrm/preferences.py +13 -0
- wbcrm/report/activity_report.py +110 -0
- wbcrm/serializers/__init__.py +23 -0
- wbcrm/serializers/accounts.py +141 -0
- wbcrm/serializers/activities.py +525 -0
- wbcrm/serializers/groups.py +30 -0
- wbcrm/serializers/products.py +58 -0
- wbcrm/serializers/recurrence.py +91 -0
- wbcrm/serializers/signals.py +71 -0
- wbcrm/static/wbcrm/markdown/documentation/activity.md +86 -0
- wbcrm/static/wbcrm/markdown/documentation/activitytype.md +20 -0
- wbcrm/static/wbcrm/markdown/documentation/group.md +2 -0
- wbcrm/static/wbcrm/markdown/documentation/product.md +11 -0
- wbcrm/synchronization/__init__.py +0 -0
- wbcrm/synchronization/activity/__init__.py +0 -0
- wbcrm/synchronization/activity/admin.py +73 -0
- wbcrm/synchronization/activity/backend.py +214 -0
- wbcrm/synchronization/activity/backends/__init__.py +0 -0
- wbcrm/synchronization/activity/backends/google/__init__.py +2 -0
- wbcrm/synchronization/activity/backends/google/google_calendar_backend.py +406 -0
- wbcrm/synchronization/activity/backends/google/request_utils/__init__.py +16 -0
- wbcrm/synchronization/activity/backends/google/request_utils/external_to_internal/create.py +75 -0
- wbcrm/synchronization/activity/backends/google/request_utils/external_to_internal/delete.py +78 -0
- wbcrm/synchronization/activity/backends/google/request_utils/external_to_internal/update.py +155 -0
- wbcrm/synchronization/activity/backends/google/request_utils/internal_to_external/update.py +181 -0
- wbcrm/synchronization/activity/backends/google/tasks.py +21 -0
- wbcrm/synchronization/activity/backends/google/tests/__init__.py +0 -0
- wbcrm/synchronization/activity/backends/google/tests/conftest.py +1 -0
- wbcrm/synchronization/activity/backends/google/tests/test_data.py +81 -0
- wbcrm/synchronization/activity/backends/google/tests/test_google_backend.py +319 -0
- wbcrm/synchronization/activity/backends/google/tests/test_utils.py +274 -0
- wbcrm/synchronization/activity/backends/google/typing_informations.py +139 -0
- wbcrm/synchronization/activity/backends/google/utils.py +217 -0
- wbcrm/synchronization/activity/backends/outlook/__init__.py +0 -0
- wbcrm/synchronization/activity/backends/outlook/backend.py +593 -0
- wbcrm/synchronization/activity/backends/outlook/msgraph.py +436 -0
- wbcrm/synchronization/activity/backends/outlook/parser.py +432 -0
- wbcrm/synchronization/activity/backends/outlook/tests/__init__.py +0 -0
- wbcrm/synchronization/activity/backends/outlook/tests/conftest.py +1 -0
- wbcrm/synchronization/activity/backends/outlook/tests/fixtures.py +606 -0
- wbcrm/synchronization/activity/backends/outlook/tests/test_admin.py +118 -0
- wbcrm/synchronization/activity/backends/outlook/tests/test_backend.py +274 -0
- wbcrm/synchronization/activity/backends/outlook/tests/test_controller.py +249 -0
- wbcrm/synchronization/activity/backends/outlook/tests/test_parser.py +174 -0
- wbcrm/synchronization/activity/controller.py +627 -0
- wbcrm/synchronization/activity/dynamic_preferences_registry.py +119 -0
- wbcrm/synchronization/activity/preferences.py +27 -0
- wbcrm/synchronization/activity/shortcuts.py +16 -0
- wbcrm/synchronization/activity/tasks.py +21 -0
- wbcrm/synchronization/activity/urls.py +7 -0
- wbcrm/synchronization/activity/utils.py +46 -0
- wbcrm/synchronization/activity/views.py +41 -0
- wbcrm/synchronization/admin.py +1 -0
- wbcrm/synchronization/apps.py +14 -0
- wbcrm/synchronization/dynamic_preferences_registry.py +1 -0
- wbcrm/synchronization/management.py +36 -0
- wbcrm/synchronization/tasks.py +1 -0
- wbcrm/synchronization/urls.py +5 -0
- wbcrm/tasks.py +264 -0
- wbcrm/templates/email/activity.html +98 -0
- wbcrm/templates/email/activity_report.html +6 -0
- wbcrm/templates/email/daily_summary.html +72 -0
- wbcrm/templates/email/global_daily_summary.html +85 -0
- wbcrm/tests/__init__.py +0 -0
- wbcrm/tests/accounts/__init__.py +0 -0
- wbcrm/tests/accounts/test_models.py +393 -0
- wbcrm/tests/accounts/test_viewsets.py +88 -0
- wbcrm/tests/conftest.py +76 -0
- wbcrm/tests/disable_signals.py +62 -0
- wbcrm/tests/e2e/__init__.py +1 -0
- wbcrm/tests/e2e/e2e_wbcrm_utility.py +83 -0
- wbcrm/tests/e2e/test_e2e.py +370 -0
- wbcrm/tests/test_assignee_methods.py +40 -0
- wbcrm/tests/test_chartviewsets.py +112 -0
- wbcrm/tests/test_dto.py +64 -0
- wbcrm/tests/test_filters.py +52 -0
- wbcrm/tests/test_models.py +217 -0
- wbcrm/tests/test_recurrence.py +292 -0
- wbcrm/tests/test_report.py +21 -0
- wbcrm/tests/test_serializers.py +171 -0
- wbcrm/tests/test_tasks.py +95 -0
- wbcrm/tests/test_viewsets.py +967 -0
- wbcrm/tests/tests.py +121 -0
- wbcrm/typings.py +109 -0
- wbcrm/urls.py +67 -0
- wbcrm/viewsets/__init__.py +22 -0
- wbcrm/viewsets/accounts.py +122 -0
- wbcrm/viewsets/activities.py +341 -0
- wbcrm/viewsets/buttons/__init__.py +7 -0
- wbcrm/viewsets/buttons/accounts.py +27 -0
- wbcrm/viewsets/buttons/activities.py +89 -0
- wbcrm/viewsets/buttons/signals.py +17 -0
- wbcrm/viewsets/display/__init__.py +12 -0
- wbcrm/viewsets/display/accounts.py +110 -0
- wbcrm/viewsets/display/activities.py +444 -0
- wbcrm/viewsets/display/groups.py +22 -0
- wbcrm/viewsets/display/products.py +105 -0
- wbcrm/viewsets/endpoints/__init__.py +8 -0
- wbcrm/viewsets/endpoints/accounts.py +25 -0
- wbcrm/viewsets/endpoints/activities.py +30 -0
- wbcrm/viewsets/endpoints/groups.py +7 -0
- wbcrm/viewsets/endpoints/products.py +9 -0
- wbcrm/viewsets/groups.py +38 -0
- wbcrm/viewsets/menu/__init__.py +8 -0
- wbcrm/viewsets/menu/accounts.py +18 -0
- wbcrm/viewsets/menu/activities.py +49 -0
- wbcrm/viewsets/menu/groups.py +16 -0
- wbcrm/viewsets/menu/products.py +20 -0
- wbcrm/viewsets/mixins.py +35 -0
- wbcrm/viewsets/previews/__init__.py +1 -0
- wbcrm/viewsets/previews/activities.py +10 -0
- wbcrm/viewsets/products.py +57 -0
- wbcrm/viewsets/recurrence.py +27 -0
- wbcrm/viewsets/titles/__init__.py +13 -0
- wbcrm/viewsets/titles/accounts.py +23 -0
- wbcrm/viewsets/titles/activities.py +61 -0
- wbcrm/viewsets/titles/products.py +13 -0
- wbcrm/viewsets/titles/utils.py +46 -0
- wbcrm/workflows/__init__.py +1 -0
- wbcrm/workflows/assignee_methods.py +25 -0
- wbcrm-1.56.8.dist-info/METADATA +11 -0
- wbcrm-1.56.8.dist-info/RECORD +182 -0
- 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,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
|
+
)
|