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,525 @@
|
|
|
1
|
+
from collections import OrderedDict
|
|
2
|
+
from datetime import date, timedelta
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from django.contrib.messages import warning
|
|
6
|
+
from django.db.models import Q
|
|
7
|
+
from django.utils import timezone
|
|
8
|
+
from django.utils.functional import cached_property
|
|
9
|
+
from django.utils.translation import gettext as _
|
|
10
|
+
from django.utils.translation import gettext_lazy
|
|
11
|
+
from psycopg.types.range import TimestamptzRange
|
|
12
|
+
from rest_framework.exceptions import ValidationError
|
|
13
|
+
from rest_framework.reverse import reverse
|
|
14
|
+
from slugify import slugify
|
|
15
|
+
from wbcore import serializers as wb_serializers
|
|
16
|
+
from wbcore.contrib.agenda.models import CalendarItem
|
|
17
|
+
from wbcore.contrib.agenda.serializers import ConferenceRoomRepresentationSerializer
|
|
18
|
+
from wbcore.contrib.authentication.models import User
|
|
19
|
+
from wbcore.contrib.directory.models import Company, Entry, Person
|
|
20
|
+
from wbcore.contrib.directory.preferences import get_main_company
|
|
21
|
+
from wbcore.contrib.directory.serializers import PersonRepresentationSerializer
|
|
22
|
+
from wbcore.utils.date import calendar_item_shortcuts
|
|
23
|
+
|
|
24
|
+
from wbcrm.models import Activity, ActivityType
|
|
25
|
+
from wbcrm.models.activities import ActivityCompanyThroughModel, ActivityParticipant
|
|
26
|
+
|
|
27
|
+
from .groups import GroupRepresentationSerializer
|
|
28
|
+
from .recurrence import RecurrenceModelSerializerMixin
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ActivityTypeModelSerializer(wb_serializers.ModelSerializer):
|
|
32
|
+
class Meta:
|
|
33
|
+
model = ActivityType
|
|
34
|
+
fields = (
|
|
35
|
+
"id",
|
|
36
|
+
"title",
|
|
37
|
+
"icon",
|
|
38
|
+
"color",
|
|
39
|
+
"score",
|
|
40
|
+
"default",
|
|
41
|
+
"_additional_resources",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def validate(self, data):
|
|
45
|
+
title = data.get("title", None)
|
|
46
|
+
if title:
|
|
47
|
+
type = ActivityType.objects.filter(slugify_title=slugify(title, separator=" "))
|
|
48
|
+
if self.instance:
|
|
49
|
+
type = type.exclude(id=self.instance.id)
|
|
50
|
+
if type.exists():
|
|
51
|
+
raise ValidationError({"title": _("Cannot add a duplicate activity type.")})
|
|
52
|
+
|
|
53
|
+
return data
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ActivityTypeRepresentationSerializer(wb_serializers.RepresentationSerializer):
|
|
57
|
+
endpoint = "wbcrm:activitytyperepresentation-list"
|
|
58
|
+
_detail = wb_serializers.HyperlinkField(reverse_name="wbcrm:activitytype-detail")
|
|
59
|
+
|
|
60
|
+
class Meta:
|
|
61
|
+
model = ActivityType
|
|
62
|
+
fields = (
|
|
63
|
+
"id",
|
|
64
|
+
"title",
|
|
65
|
+
"icon",
|
|
66
|
+
"_detail",
|
|
67
|
+
"color",
|
|
68
|
+
"score",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ActivityRepresentationSerializer(wb_serializers.RepresentationSerializer):
|
|
73
|
+
_detail = wb_serializers.HyperlinkField(reverse_name="wbcrm:activity-detail")
|
|
74
|
+
_detail_preview = wb_serializers.HyperlinkField(reverse_name="wbcrm:activity-detail")
|
|
75
|
+
end__date = wb_serializers.SerializerMethodField()
|
|
76
|
+
title = wb_serializers.CharField(max_length=255, read_only=True)
|
|
77
|
+
|
|
78
|
+
label_key = "{{end__date}}: {{title}}"
|
|
79
|
+
|
|
80
|
+
def get_end__date(self, obj):
|
|
81
|
+
if obj.end is not None:
|
|
82
|
+
return obj.end.strftime("%d.%m.%Y")
|
|
83
|
+
return ""
|
|
84
|
+
|
|
85
|
+
@wb_serializers.register_resource()
|
|
86
|
+
def activity(self, instance, request, user):
|
|
87
|
+
return {"activity": f'{reverse("wbcrm:activity-list")}?participants={instance.id}'}
|
|
88
|
+
|
|
89
|
+
class Meta:
|
|
90
|
+
model = Activity
|
|
91
|
+
fields = ("id", "_detail", "_detail_preview", "end", "end__date", "start", "title", "_additional_resources")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_default_period(*args, **kwargs):
|
|
95
|
+
return TimestamptzRange(timezone.now(), timezone.now() + timedelta(hours=1)) # type: ignore
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def handle_representation(representation: OrderedDict) -> OrderedDict:
|
|
99
|
+
"""
|
|
100
|
+
This method is used to remove certain representation values if the representation is for private/confidential use.
|
|
101
|
+
By removing these values, the corresponding fields in the Workbench appear empty.
|
|
102
|
+
|
|
103
|
+
:param representation: Dict of primitive datatypes representing the serializer fields.
|
|
104
|
+
:return: Either the unchanged representation or a privatized version of the representation
|
|
105
|
+
"""
|
|
106
|
+
keys_to_preserve = ["id", "period", "status"]
|
|
107
|
+
private_keys_to_preserve = [
|
|
108
|
+
"participants",
|
|
109
|
+
"_participants",
|
|
110
|
+
"companies",
|
|
111
|
+
"_companies",
|
|
112
|
+
"groups",
|
|
113
|
+
"_groups",
|
|
114
|
+
"assigned_to",
|
|
115
|
+
"_assigned_to",
|
|
116
|
+
"creator",
|
|
117
|
+
"_creator",
|
|
118
|
+
]
|
|
119
|
+
is_private: bool
|
|
120
|
+
if not ((is_private := representation.get("is_private", False)) or representation.get("is_confidential")):
|
|
121
|
+
return representation
|
|
122
|
+
hidden_representation = OrderedDict.fromkeys(representation, None)
|
|
123
|
+
hidden_representation |= {key: representation.get(key) for key in keys_to_preserve}
|
|
124
|
+
if is_private:
|
|
125
|
+
hidden_representation |= {key: representation.get(key) for key in private_keys_to_preserve}
|
|
126
|
+
hidden_representation["title"] = _("Private Activity") if is_private else _("Confidential Activity")
|
|
127
|
+
return hidden_representation
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _get_default_recurrence_end():
|
|
131
|
+
"""
|
|
132
|
+
Default to 6 months in the future
|
|
133
|
+
"""
|
|
134
|
+
return (date.today() + pd.tseries.offsets.DateOffset(months=6)).date()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class ActivityCompanyThroughModelRepresentationSerializer(wb_serializers.RepresentationSerializer):
|
|
138
|
+
endpoint = "wbcore:directory:companyrepresentation-list"
|
|
139
|
+
value_key = "id"
|
|
140
|
+
|
|
141
|
+
def to_representation(self, value):
|
|
142
|
+
rep = super().to_representation(value)
|
|
143
|
+
rep["id"] = value.company_id
|
|
144
|
+
return rep
|
|
145
|
+
|
|
146
|
+
class Meta:
|
|
147
|
+
model = ActivityCompanyThroughModel
|
|
148
|
+
fields = (
|
|
149
|
+
"id",
|
|
150
|
+
# "company",
|
|
151
|
+
"computed_str",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class ActivityModelListSerializer(RecurrenceModelSerializerMixin, wb_serializers.ModelSerializer):
|
|
156
|
+
_companies = ActivityCompanyThroughModelRepresentationSerializer(
|
|
157
|
+
source="activity_companies", related_key="companies", many=True
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
_participants = PersonRepresentationSerializer(source="participants", many=True)
|
|
161
|
+
|
|
162
|
+
heat = wb_serializers.EmojiRatingField(label="Sentiment")
|
|
163
|
+
_groups = GroupRepresentationSerializer(source="groups", many=True)
|
|
164
|
+
_type = ActivityTypeRepresentationSerializer(source="type")
|
|
165
|
+
_latest_reviewer = PersonRepresentationSerializer(source="latest_reviewer")
|
|
166
|
+
is_private = wb_serializers.BooleanField(default=False, read_only=True)
|
|
167
|
+
is_confidential = wb_serializers.BooleanField(default=False, read_only=True)
|
|
168
|
+
|
|
169
|
+
def to_representation(self, instance):
|
|
170
|
+
return handle_representation(super().to_representation(instance))
|
|
171
|
+
|
|
172
|
+
def validate(self, data):
|
|
173
|
+
main_company = get_main_company()
|
|
174
|
+
request = self.context["request"]
|
|
175
|
+
companies = data.get("companies", [])
|
|
176
|
+
if main_company and main_company in companies:
|
|
177
|
+
warning(
|
|
178
|
+
request,
|
|
179
|
+
f"The main company {main_company} was removed from the list of companies",
|
|
180
|
+
)
|
|
181
|
+
return data
|
|
182
|
+
|
|
183
|
+
class Meta:
|
|
184
|
+
model = Activity
|
|
185
|
+
fields = (
|
|
186
|
+
"id",
|
|
187
|
+
"_additional_resources",
|
|
188
|
+
"type",
|
|
189
|
+
"_type",
|
|
190
|
+
"title",
|
|
191
|
+
"status",
|
|
192
|
+
"period",
|
|
193
|
+
"companies",
|
|
194
|
+
"_companies",
|
|
195
|
+
"participants",
|
|
196
|
+
"_participants",
|
|
197
|
+
"groups",
|
|
198
|
+
"_groups",
|
|
199
|
+
"edited",
|
|
200
|
+
"created",
|
|
201
|
+
"description",
|
|
202
|
+
"result",
|
|
203
|
+
"latest_reviewer",
|
|
204
|
+
"is_private",
|
|
205
|
+
"is_confidential",
|
|
206
|
+
"summary",
|
|
207
|
+
"heat",
|
|
208
|
+
"_latest_reviewer",
|
|
209
|
+
)
|
|
210
|
+
read_only_fields = list(filter(lambda x: x not in ["result"], fields))
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class ActivityModelSerializer(ActivityModelListSerializer):
|
|
214
|
+
type = wb_serializers.PrimaryKeyRelatedField(
|
|
215
|
+
queryset=ActivityType.objects.all(), default=lambda: ActivityType.get_default_activity_type()
|
|
216
|
+
)
|
|
217
|
+
repeat_choice = wb_serializers.ChoiceField(
|
|
218
|
+
help_text="Repeat activity at the specified frequency",
|
|
219
|
+
label="Recurrence Frequency",
|
|
220
|
+
required=False,
|
|
221
|
+
read_only=lambda view: not view.new_mode,
|
|
222
|
+
choices=Activity.ReoccuranceChoice.choices,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
recurrence_end = wb_serializers.DateField(
|
|
226
|
+
required=False, label=gettext_lazy("Recurrence End"), default=_get_default_recurrence_end
|
|
227
|
+
)
|
|
228
|
+
visibility = wb_serializers.ChoiceField(
|
|
229
|
+
choices=Activity.Visibility.choices,
|
|
230
|
+
help_text=gettext_lazy(
|
|
231
|
+
"Set to private for the activity to hide sensitive information from anyone but the assignee and participants. Set to confidential to hide from anyone but users with manager permissions."
|
|
232
|
+
),
|
|
233
|
+
label=gettext_lazy("Visibility"),
|
|
234
|
+
)
|
|
235
|
+
assigned_to = wb_serializers.PrimaryKeyRelatedField(
|
|
236
|
+
default=wb_serializers.CurrentUserDefault("profile"),
|
|
237
|
+
queryset=Person.objects.all(),
|
|
238
|
+
label=gettext_lazy("Assigned to"),
|
|
239
|
+
)
|
|
240
|
+
_assigned_to = PersonRepresentationSerializer(source="assigned_to")
|
|
241
|
+
creator = wb_serializers.PrimaryKeyRelatedField(
|
|
242
|
+
default=wb_serializers.CurrentUserDefault("profile"),
|
|
243
|
+
many=False,
|
|
244
|
+
read_only=True,
|
|
245
|
+
)
|
|
246
|
+
_creator = PersonRepresentationSerializer(
|
|
247
|
+
source="creator",
|
|
248
|
+
)
|
|
249
|
+
companies = wb_serializers.PrimaryKeyRelatedField(
|
|
250
|
+
default=wb_serializers.DefaultFromGET("companies", many=True),
|
|
251
|
+
many=True,
|
|
252
|
+
queryset=Company.objects.all(),
|
|
253
|
+
label=gettext_lazy("Companies"),
|
|
254
|
+
)
|
|
255
|
+
participants = wb_serializers.PrimaryKeyRelatedField(
|
|
256
|
+
default=wb_serializers.DefaultFromGET("participants", many=True),
|
|
257
|
+
many=True,
|
|
258
|
+
queryset=Person.objects.all(),
|
|
259
|
+
label=gettext_lazy("Participants"),
|
|
260
|
+
)
|
|
261
|
+
period = wb_serializers.DateTimeRangeField(
|
|
262
|
+
label=gettext_lazy("Period"),
|
|
263
|
+
default=get_default_period,
|
|
264
|
+
shortcuts=calendar_item_shortcuts,
|
|
265
|
+
read_only=lambda view: view.is_external_activity,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
is_private = wb_serializers.BooleanField(default=False, read_only=True)
|
|
269
|
+
is_confidential = wb_serializers.BooleanField(default=False, read_only=True)
|
|
270
|
+
_conference_room = ConferenceRoomRepresentationSerializer(source="conference_room")
|
|
271
|
+
|
|
272
|
+
title = wb_serializers.CharField(
|
|
273
|
+
default=wb_serializers.DefaultFromGET("title"),
|
|
274
|
+
label=gettext_lazy("Title"),
|
|
275
|
+
read_only=lambda view: view.is_external_activity,
|
|
276
|
+
)
|
|
277
|
+
all_day = wb_serializers.BooleanField(read_only=lambda view: view.is_external_activity)
|
|
278
|
+
description = wb_serializers.TextField(read_only=lambda view: view.is_external_activity)
|
|
279
|
+
|
|
280
|
+
@cached_property
|
|
281
|
+
def user(self) -> User | None:
|
|
282
|
+
if request := self.context.get("request"):
|
|
283
|
+
return request.user
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
@wb_serializers.register_resource()
|
|
287
|
+
def activity_participants_table(self, instance, request, user):
|
|
288
|
+
return {
|
|
289
|
+
"activity_participants_table": reverse(
|
|
290
|
+
"wbcrm:activity-participant-list", kwargs={"activity_id": instance.id}, request=request
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
def to_representation(self, instance):
|
|
295
|
+
return handle_representation(super().to_representation(instance))
|
|
296
|
+
|
|
297
|
+
def validate(self, data): # noqa: C901
|
|
298
|
+
if (
|
|
299
|
+
(result := data.get("result", None))
|
|
300
|
+
and result not in ["", "<p></p>"]
|
|
301
|
+
and result != getattr(self.instance, "result", "")
|
|
302
|
+
):
|
|
303
|
+
data["latest_reviewer"] = self.user.profile
|
|
304
|
+
data["reviewed_at"] = timezone.now()
|
|
305
|
+
if data.get("creator", None) is None:
|
|
306
|
+
data["creator"] = self.user.profile
|
|
307
|
+
|
|
308
|
+
if not data.get("title", self.instance.title if self.instance else None):
|
|
309
|
+
raise ValidationError({"title": "You need to specify a title for this activity."})
|
|
310
|
+
|
|
311
|
+
period = data.get("period", self.instance.period if self.instance else None)
|
|
312
|
+
if not data.get("type", self.instance.type if self.instance else None):
|
|
313
|
+
raise ValidationError({"type": _("Please add an activity type.")})
|
|
314
|
+
|
|
315
|
+
if room := data.get("conference_room", self.instance.conference_room if self.instance else None):
|
|
316
|
+
qs = Activity.objects.filter(status=Activity.Status.PLANNED, conference_room=room)
|
|
317
|
+
if self.instance:
|
|
318
|
+
qs = qs.exclude(id=self.instance.id)
|
|
319
|
+
conference_room_activity = Activity.get_inrange_activities(
|
|
320
|
+
qs, period.lower + timedelta(seconds=60), period.upper - timedelta(seconds=60)
|
|
321
|
+
).first()
|
|
322
|
+
if conference_room_activity and (user := self.user):
|
|
323
|
+
if (
|
|
324
|
+
conference_room_activity.visibility == Activity.Visibility.PRIVATE
|
|
325
|
+
and user.profile not in conference_room_activity.participants.all()
|
|
326
|
+
and user.profile != conference_room_activity.assigned_to
|
|
327
|
+
):
|
|
328
|
+
raise ValidationError(
|
|
329
|
+
{
|
|
330
|
+
"non_field_errors": _(
|
|
331
|
+
"A private activity already uses this conference room at the same time."
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
)
|
|
335
|
+
elif (
|
|
336
|
+
conference_room_activity.visibility == Activity.Visibility.CONFIDENTIAL
|
|
337
|
+
and not CalendarItem.has_user_administrate_permission(user)
|
|
338
|
+
):
|
|
339
|
+
raise ValidationError(
|
|
340
|
+
{
|
|
341
|
+
"non_field_errors": _(
|
|
342
|
+
"A confidential activity already uses this conference room at the same time."
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
)
|
|
346
|
+
else:
|
|
347
|
+
raise ValidationError(
|
|
348
|
+
{
|
|
349
|
+
"non_field_errors": _(
|
|
350
|
+
'The activity "{title}" already uses this conference room at the same time.'
|
|
351
|
+
).format(title=conference_room_activity.title)
|
|
352
|
+
}
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
# In the following section we validate the groups and the group members. If a user trys to remove a group member from the participants/companies fields, we want to raise an validation error.
|
|
356
|
+
if self.instance and self.instance.groups.exists() and not ("groups" in data and data["groups"] == []):
|
|
357
|
+
# If there are no companies or participants in the current instance, you cannot remove a group member; therefore there is no need to validate the groups. This can happen if you add to an existing and empty activity a group.
|
|
358
|
+
if self.instance.companies.exists() or self.instance.participants.exists():
|
|
359
|
+
companies = data.get("companies", [])
|
|
360
|
+
participants = data.get("participants", [])
|
|
361
|
+
# If there are new companies or participants in data, we need to check if existing group members have been removed.
|
|
362
|
+
if "companies" in data or "participants" in data:
|
|
363
|
+
new_groups = data.get("groups", [])
|
|
364
|
+
new_groups_id_list = [group.id for group in new_groups]
|
|
365
|
+
old_groups = self.instance.groups.exclude(id__in=new_groups_id_list)
|
|
366
|
+
|
|
367
|
+
# If there are no new groups in data, we need to check if members of the old group have been removed.
|
|
368
|
+
# If there are new groups and also old groups, we need to check for the old groups if members have been removed.
|
|
369
|
+
# If only new groups are present, we don't need to validate.
|
|
370
|
+
if not new_groups or old_groups.exists():
|
|
371
|
+
new_companies_id_list = (
|
|
372
|
+
[companies.id for companies in companies]
|
|
373
|
+
if "companies" in data
|
|
374
|
+
else self.instance.companies.values_list("id", flat=True)
|
|
375
|
+
)
|
|
376
|
+
new_participants_id_list = (
|
|
377
|
+
[participant.id for participant in participants]
|
|
378
|
+
if "participants" in data
|
|
379
|
+
else self.instance.participants.values_list("id", flat=True)
|
|
380
|
+
)
|
|
381
|
+
new_entries = Entry.objects.filter(
|
|
382
|
+
Q(id__in=new_companies_id_list) | Q(id__in=new_participants_id_list)
|
|
383
|
+
).distinct()
|
|
384
|
+
missing_members = (
|
|
385
|
+
Entry.objects.filter(groups__in=old_groups).exclude(id__in=new_entries).distinct()
|
|
386
|
+
)
|
|
387
|
+
# If there are group members who are not in the new entries field, we can assume, that a group member has been removed and we need to throw an error.
|
|
388
|
+
if missing_members.exists():
|
|
389
|
+
missing_members_computed_strs = ", ".join(
|
|
390
|
+
list(missing_members.values_list("computed_str", flat=True))
|
|
391
|
+
)
|
|
392
|
+
util_str = _("is a member")
|
|
393
|
+
if missing_members.count() > 1:
|
|
394
|
+
start, x, end = missing_members_computed_strs.rpartition(",")
|
|
395
|
+
missing_members_computed_strs = start + _(" and") + end
|
|
396
|
+
util_str = _("are members")
|
|
397
|
+
|
|
398
|
+
groups = ", ".join(
|
|
399
|
+
list(dict.fromkeys(missing_members.values_list("groups__title", flat=True)))
|
|
400
|
+
)
|
|
401
|
+
error_message = _(
|
|
402
|
+
"{missing_members} {util_str} of the following group(s): {groups}\n. You cannot remove members of selected groups.",
|
|
403
|
+
).format(missing_members=missing_members_computed_strs, util_str=util_str, groups=groups)
|
|
404
|
+
|
|
405
|
+
raise ValidationError({"groups": error_message})
|
|
406
|
+
return super().validate(data)
|
|
407
|
+
|
|
408
|
+
class Meta:
|
|
409
|
+
model = Activity
|
|
410
|
+
required_fields = ("importance", "period", "reminder_choice", "repeat_choice", "start", "title", "type")
|
|
411
|
+
read_only_fields = (
|
|
412
|
+
"creator",
|
|
413
|
+
"edited",
|
|
414
|
+
"latest_reviewer",
|
|
415
|
+
"reviewed_at",
|
|
416
|
+
"is_cancelled",
|
|
417
|
+
)
|
|
418
|
+
fields = (
|
|
419
|
+
"id",
|
|
420
|
+
"_additional_resources",
|
|
421
|
+
"all_day",
|
|
422
|
+
"assigned_to",
|
|
423
|
+
"_assigned_to",
|
|
424
|
+
"companies",
|
|
425
|
+
"_companies",
|
|
426
|
+
"conference_room",
|
|
427
|
+
"_conference_room",
|
|
428
|
+
"created",
|
|
429
|
+
"creator",
|
|
430
|
+
"_creator",
|
|
431
|
+
"description",
|
|
432
|
+
"disable_participant_check",
|
|
433
|
+
"edited",
|
|
434
|
+
"end",
|
|
435
|
+
"groups",
|
|
436
|
+
"_groups",
|
|
437
|
+
"importance",
|
|
438
|
+
"visibility",
|
|
439
|
+
"latest_reviewer",
|
|
440
|
+
"_latest_reviewer",
|
|
441
|
+
"location",
|
|
442
|
+
"location_latitude",
|
|
443
|
+
"location_longitude",
|
|
444
|
+
"online_meeting",
|
|
445
|
+
"participants",
|
|
446
|
+
"_participants",
|
|
447
|
+
"summary",
|
|
448
|
+
"period",
|
|
449
|
+
"propagate_for_all_children",
|
|
450
|
+
"recurrence_count",
|
|
451
|
+
"recurrence_end",
|
|
452
|
+
"reminder_choice",
|
|
453
|
+
"repeat_choice",
|
|
454
|
+
"result",
|
|
455
|
+
"reviewed_at",
|
|
456
|
+
"start",
|
|
457
|
+
"status",
|
|
458
|
+
"title",
|
|
459
|
+
"type",
|
|
460
|
+
"_type",
|
|
461
|
+
"is_cancelled",
|
|
462
|
+
"is_private",
|
|
463
|
+
"is_confidential",
|
|
464
|
+
"_buttons",
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
class ReadOnlyActivityModelSerializer(ActivityModelSerializer):
|
|
469
|
+
class Meta(ActivityModelSerializer.Meta):
|
|
470
|
+
read_only_fields = list(filter(lambda x: x not in ["result"], ActivityModelSerializer.Meta.fields))
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
class ActivityParticipantModelSerializer(wb_serializers.ModelSerializer):
|
|
474
|
+
_activity = ActivityRepresentationSerializer(source="activity")
|
|
475
|
+
activity = wb_serializers.PrimaryKeyRelatedField(
|
|
476
|
+
default=wb_serializers.DefaultFromGET("activity_id"),
|
|
477
|
+
queryset=Activity.objects.all(),
|
|
478
|
+
label=gettext_lazy("Activity"),
|
|
479
|
+
)
|
|
480
|
+
customer_status = wb_serializers.CharField(
|
|
481
|
+
default="", label=gettext_lazy("Status"), allow_null=True, read_only=True
|
|
482
|
+
)
|
|
483
|
+
position = wb_serializers.CharField(default="", label=gettext_lazy("Position"), read_only=True)
|
|
484
|
+
primary_telephone = wb_serializers.TelephoneField(
|
|
485
|
+
default="", label=gettext_lazy("Primary Telephone"), allow_null=True, read_only=True
|
|
486
|
+
)
|
|
487
|
+
primary_email = wb_serializers.CharField(
|
|
488
|
+
allow_null=True, default="", label=gettext_lazy("Primary Email"), read_only=True
|
|
489
|
+
)
|
|
490
|
+
_participant = PersonRepresentationSerializer(source="participant")
|
|
491
|
+
is_occupied = wb_serializers.BooleanField(default=False)
|
|
492
|
+
|
|
493
|
+
def validate(self, data):
|
|
494
|
+
data_activity = data.get("activity", None)
|
|
495
|
+
data_participant = data.get("participant", None)
|
|
496
|
+
|
|
497
|
+
if ActivityParticipant.objects.filter(
|
|
498
|
+
activity=data_activity,
|
|
499
|
+
participant=data_participant,
|
|
500
|
+
).exists():
|
|
501
|
+
raise ValidationError({"participant": _("The person is already a participant in this activity.")})
|
|
502
|
+
|
|
503
|
+
return super().validate(data)
|
|
504
|
+
|
|
505
|
+
def create(self, validated_data):
|
|
506
|
+
validated_data.pop("is_occupied", None)
|
|
507
|
+
return super().create(validated_data)
|
|
508
|
+
|
|
509
|
+
class Meta:
|
|
510
|
+
model = ActivityParticipant
|
|
511
|
+
fields = (
|
|
512
|
+
"_activity",
|
|
513
|
+
"_additional_resources",
|
|
514
|
+
"_participant",
|
|
515
|
+
"activity",
|
|
516
|
+
"customer_status",
|
|
517
|
+
"position",
|
|
518
|
+
"id",
|
|
519
|
+
"is_occupied",
|
|
520
|
+
"participant",
|
|
521
|
+
"participation_status",
|
|
522
|
+
"status_changed",
|
|
523
|
+
"primary_telephone",
|
|
524
|
+
"primary_email",
|
|
525
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from wbcore import serializers as wb_serializers
|
|
2
|
+
from wbcore.contrib.directory.serializers import EntryRepresentationSerializer
|
|
3
|
+
|
|
4
|
+
from ..models import Group
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class GroupRepresentationSerializer(wb_serializers.RepresentationSerializer):
|
|
8
|
+
endpoint = "wbcrm:grouprepresentation-list"
|
|
9
|
+
_detail = wb_serializers.HyperlinkField(reverse_name="wbcrm:group-detail")
|
|
10
|
+
|
|
11
|
+
class Meta:
|
|
12
|
+
model = Group
|
|
13
|
+
fields = (
|
|
14
|
+
"id",
|
|
15
|
+
"_detail",
|
|
16
|
+
"title",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class GroupModelSerializer(wb_serializers.ModelSerializer):
|
|
21
|
+
_members = EntryRepresentationSerializer(source="members", many=True)
|
|
22
|
+
|
|
23
|
+
class Meta:
|
|
24
|
+
model = Group
|
|
25
|
+
fields = (
|
|
26
|
+
"id",
|
|
27
|
+
"title",
|
|
28
|
+
"members",
|
|
29
|
+
"_members",
|
|
30
|
+
)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from django.forms import ValidationError
|
|
2
|
+
from django.utils.translation import gettext_lazy as _
|
|
3
|
+
from slugify import slugify
|
|
4
|
+
from wbcore import serializers
|
|
5
|
+
from wbcore import serializers as wb_serializers
|
|
6
|
+
from wbcore.contrib.directory.serializers import CompanyRepresentationSerializer
|
|
7
|
+
|
|
8
|
+
from wbcrm.models import Product, ProductCompanyRelationship
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ProductRepresentationSerializer(wb_serializers.RepresentationSerializer):
|
|
12
|
+
endpoint = "wbcrm:productrepresentation-list"
|
|
13
|
+
_detail = wb_serializers.HyperlinkField(reverse_name="wbcrm:product-detail")
|
|
14
|
+
|
|
15
|
+
class Meta:
|
|
16
|
+
model = Product
|
|
17
|
+
fields = (
|
|
18
|
+
"id",
|
|
19
|
+
"title",
|
|
20
|
+
"computed_str",
|
|
21
|
+
"_detail",
|
|
22
|
+
"is_competitor",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ProductCompanyRelationshipModelSerializer(serializers.ModelSerializer):
|
|
27
|
+
_product = ProductRepresentationSerializer(source="product")
|
|
28
|
+
competitor_product = wb_serializers.BooleanField(read_only=True)
|
|
29
|
+
|
|
30
|
+
class Meta:
|
|
31
|
+
model = ProductCompanyRelationship
|
|
32
|
+
fields = ("id", "product", "_product", "company", "competitor_product")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ProductModelSerializer(serializers.ModelSerializer):
|
|
36
|
+
_prospects = CompanyRepresentationSerializer(source="prospects", many=True)
|
|
37
|
+
|
|
38
|
+
def validate(self, data):
|
|
39
|
+
title = data.get("title")
|
|
40
|
+
competitor = data.get("is_competitor")
|
|
41
|
+
if title:
|
|
42
|
+
product = Product.objects.filter(is_competitor=competitor, slugify_title=slugify(title, separator=" "))
|
|
43
|
+
if obj := self.instance:
|
|
44
|
+
product = product.exclude(id=obj.id)
|
|
45
|
+
if product.exists():
|
|
46
|
+
product_type = _("competitor ") if competitor else ""
|
|
47
|
+
raise ValidationError({"title": _("Cannot add a duplicate {}product.").format(product_type)})
|
|
48
|
+
return data
|
|
49
|
+
|
|
50
|
+
class Meta:
|
|
51
|
+
model = Product
|
|
52
|
+
fields = (
|
|
53
|
+
"id",
|
|
54
|
+
"title",
|
|
55
|
+
"prospects",
|
|
56
|
+
"_prospects",
|
|
57
|
+
"is_competitor",
|
|
58
|
+
)
|