wbcrm 2.2.1__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wbcrm might be problematic. Click here for more details.
- wbcrm/__init__.py +1 -0
- wbcrm/admin/__init__.py +4 -0
- wbcrm/admin/accounts.py +59 -0
- wbcrm/admin/activities.py +101 -0
- wbcrm/admin/groups.py +7 -0
- wbcrm/admin/products.py +8 -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 +56 -0
- wbcrm/factories/activities.py +125 -0
- wbcrm/factories/groups.py +23 -0
- wbcrm/factories/products.py +10 -0
- wbcrm/filters/__init__.py +10 -0
- wbcrm/filters/accounts.py +67 -0
- wbcrm/filters/activities.py +181 -0
- wbcrm/filters/groups.py +20 -0
- wbcrm/filters/products.py +37 -0
- wbcrm/filters/signals.py +94 -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/__init__.py +0 -0
- wbcrm/models/__init__.py +4 -0
- wbcrm/models/accounts.py +637 -0
- wbcrm/models/activities.py +1335 -0
- wbcrm/models/groups.py +118 -0
- wbcrm/models/products.py +83 -0
- wbcrm/models/recurrence.py +279 -0
- wbcrm/preferences.py +14 -0
- wbcrm/serializers/__init__.py +23 -0
- wbcrm/serializers/accounts.py +126 -0
- wbcrm/serializers/activities.py +526 -0
- wbcrm/serializers/groups.py +30 -0
- wbcrm/serializers/products.py +57 -0
- wbcrm/serializers/recurrence.py +90 -0
- wbcrm/serializers/signals.py +70 -0
- wbcrm/synchronization/__init__.py +0 -0
- wbcrm/synchronization/activity/__init__.py +0 -0
- wbcrm/synchronization/activity/admin.py +72 -0
- wbcrm/synchronization/activity/backend.py +207 -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 +399 -0
- wbcrm/synchronization/activity/backends/google/request_utils/__init__.py +16 -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 +216 -0
- wbcrm/synchronization/activity/backends/outlook/__init__.py +0 -0
- wbcrm/synchronization/activity/backends/outlook/backend.py +576 -0
- wbcrm/synchronization/activity/backends/outlook/msgraph.py +438 -0
- wbcrm/synchronization/activity/backends/outlook/parser.py +423 -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 +117 -0
- wbcrm/synchronization/activity/backends/outlook/tests/test_backend.py +269 -0
- wbcrm/synchronization/activity/backends/outlook/tests/test_controller.py +237 -0
- wbcrm/synchronization/activity/backends/outlook/tests/test_parser.py +173 -0
- wbcrm/synchronization/activity/controller.py +545 -0
- wbcrm/synchronization/activity/dynamic_preferences_registry.py +107 -0
- wbcrm/synchronization/activity/preferences.py +21 -0
- wbcrm/synchronization/activity/shortcuts.py +9 -0
- wbcrm/synchronization/activity/signals.py +28 -0
- wbcrm/synchronization/activity/tasks.py +21 -0
- wbcrm/synchronization/activity/urls.py +6 -0
- wbcrm/synchronization/activity/utils.py +46 -0
- wbcrm/synchronization/activity/views.py +37 -0
- wbcrm/synchronization/admin.py +1 -0
- wbcrm/synchronization/apps.py +15 -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 +312 -0
- wbcrm/tests/__init__.py +0 -0
- wbcrm/tests/accounts/__init__.py +0 -0
- wbcrm/tests/accounts/test_models.py +380 -0
- wbcrm/tests/accounts/test_viewsets.py +87 -0
- wbcrm/tests/conftest.py +76 -0
- wbcrm/tests/disable_signals.py +52 -0
- wbcrm/tests/e2e/__init__.py +1 -0
- wbcrm/tests/e2e/e2e_wbcrm_utility.py +82 -0
- wbcrm/tests/e2e/test_e2e.py +369 -0
- wbcrm/tests/test_assignee_methods.py +39 -0
- wbcrm/tests/test_chartviewsets.py +111 -0
- wbcrm/tests/test_dto.py +63 -0
- wbcrm/tests/test_filters.py +51 -0
- wbcrm/tests/test_models.py +216 -0
- wbcrm/tests/test_recurrence.py +291 -0
- wbcrm/tests/test_report.py +20 -0
- wbcrm/tests/test_serializers.py +170 -0
- wbcrm/tests/test_tasks.py +94 -0
- wbcrm/tests/test_viewsets.py +967 -0
- wbcrm/tests/tests.py +120 -0
- wbcrm/typings.py +107 -0
- wbcrm/urls.py +67 -0
- wbcrm/viewsets/__init__.py +22 -0
- wbcrm/viewsets/accounts.py +121 -0
- wbcrm/viewsets/activities.py +315 -0
- wbcrm/viewsets/buttons/__init__.py +7 -0
- wbcrm/viewsets/buttons/accounts.py +27 -0
- wbcrm/viewsets/buttons/activities.py +68 -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 +443 -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 +32 -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 +37 -0
- wbcrm/viewsets/menu/__init__.py +8 -0
- wbcrm/viewsets/menu/accounts.py +18 -0
- wbcrm/viewsets/menu/activities.py +61 -0
- wbcrm/viewsets/menu/groups.py +16 -0
- wbcrm/viewsets/menu/products.py +20 -0
- wbcrm/viewsets/mixins.py +34 -0
- wbcrm/viewsets/previews/__init__.py +1 -0
- wbcrm/viewsets/previews/activities.py +10 -0
- wbcrm/viewsets/products.py +56 -0
- wbcrm/viewsets/recurrence.py +26 -0
- wbcrm/viewsets/titles/__init__.py +13 -0
- wbcrm/viewsets/titles/accounts.py +22 -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-2.2.1.dist-info/METADATA +11 -0
- wbcrm-2.2.1.dist-info/RECORD +155 -0
- wbcrm-2.2.1.dist-info/WHEEL +5 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from django.dispatch import receiver
|
|
2
|
+
from wbcore.contrib.agenda.signals import complete_post_delete, complete_post_save
|
|
3
|
+
from wbcrm.typings import Activity as ActivityDTO
|
|
4
|
+
from wbcrm.typings import ParticipantStatus as ParticipantStatusDTO
|
|
5
|
+
|
|
6
|
+
from .controller import ActivityController
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@receiver(complete_post_save, sender="wbcrm.Activity")
|
|
10
|
+
def pre_save_activity(sender, activity_dto: ActivityDTO, pre_save_activity_dto: ActivityDTO = None, **kwargs):
|
|
11
|
+
ActivityController().handle_outbound(activity_dto, old_activity_dto=pre_save_activity_dto)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@receiver(complete_post_delete, sender="wbcrm.Activity")
|
|
15
|
+
def pre_delete_activity(sender, activity_dto: ActivityDTO, pre_delete_activity_dto: ActivityDTO, **kwargs):
|
|
16
|
+
ActivityController().handle_outbound(activity_dto, old_activity_dto=pre_delete_activity_dto, is_deleted=True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@receiver(complete_post_save, sender="wbcrm.ActivityParticipant")
|
|
20
|
+
def pre_save_activity_participant(
|
|
21
|
+
sender, participant_dto: ParticipantStatusDTO, pre_save_participant_dto: ParticipantStatusDTO = None, **kwargs
|
|
22
|
+
):
|
|
23
|
+
ActivityController().handle_outbound_participant(participant_dto, old_participant_dto=pre_save_participant_dto)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@receiver(complete_post_delete, sender="wbcrm.ActivityParticipant")
|
|
27
|
+
def pre_delete_activity_participant(sender, participant_dto: ParticipantStatusDTO, **kwargs):
|
|
28
|
+
ActivityController().handle_outbound_participant(participant_dto, is_deleted=True)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from celery import shared_task
|
|
2
|
+
|
|
3
|
+
from .shortcuts import get_backend
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@shared_task(queue="synchronization")
|
|
7
|
+
def periodic_notify_admins_of_webhook_inconsistencies_task(emails: list | None = None):
|
|
8
|
+
"""
|
|
9
|
+
Periodic tasks to notify webhook inconsistencies
|
|
10
|
+
"""
|
|
11
|
+
if emails and (backend := get_backend()):
|
|
12
|
+
backend().notify_admins_of_webhook_inconsistencies(emails)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@shared_task(queue="synchronization")
|
|
16
|
+
def periodic_renew_web_hooks_task():
|
|
17
|
+
"""
|
|
18
|
+
Periodic tasks to renew active webhooks
|
|
19
|
+
"""
|
|
20
|
+
if backend := get_backend():
|
|
21
|
+
backend().renew_web_hooks()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def flattened_metadata_lookup(obj: dict, key_string: str = "") -> tuple[str, Any]:
|
|
5
|
+
"""
|
|
6
|
+
allows to flatten in nested dictionary which can be used in a lookup query
|
|
7
|
+
"""
|
|
8
|
+
if isinstance(obj, dict):
|
|
9
|
+
key_string = key_string + "__" if key_string else key_string
|
|
10
|
+
for k in obj:
|
|
11
|
+
yield from flattened_metadata_lookup(obj[k], key_string + str(k))
|
|
12
|
+
else:
|
|
13
|
+
if isinstance(obj, list):
|
|
14
|
+
yield key_string + "__contains", obj
|
|
15
|
+
else:
|
|
16
|
+
yield key_string, obj
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def merge_nested_dict(dct: dict, nested_to_merge: dict):
|
|
20
|
+
"""
|
|
21
|
+
allows to merge 2 nested dictionaries
|
|
22
|
+
"""
|
|
23
|
+
for k, v in nested_to_merge.items():
|
|
24
|
+
if k in dct:
|
|
25
|
+
if isinstance(dct[k], dict) and isinstance(nested_to_merge[k], dict): # noqa
|
|
26
|
+
merge_nested_dict(dct[k], nested_to_merge[k])
|
|
27
|
+
else:
|
|
28
|
+
if not isinstance(dct[k], list):
|
|
29
|
+
dct[k] = [dct[k]]
|
|
30
|
+
dct[k].append(nested_to_merge[k])
|
|
31
|
+
else:
|
|
32
|
+
dct[k] = nested_to_merge[k]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def flattened_dict_into_nested_dict(obj: dict) -> dict:
|
|
36
|
+
"""
|
|
37
|
+
allows to nest a flattened dictionary
|
|
38
|
+
"""
|
|
39
|
+
nested = {}
|
|
40
|
+
for key, value in obj.items():
|
|
41
|
+
keys = key.split(".")
|
|
42
|
+
dct = {keys.pop(): value}
|
|
43
|
+
while keys:
|
|
44
|
+
dct = {keys.pop(): dct}
|
|
45
|
+
merge_nested_dict(nested, dct)
|
|
46
|
+
return nested
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from celery import shared_task
|
|
2
|
+
from django.db.utils import OperationalError
|
|
3
|
+
from django.http import HttpRequest, HttpResponse
|
|
4
|
+
from django.views.decorators.csrf import csrf_exempt
|
|
5
|
+
|
|
6
|
+
from .controller import ActivityController
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@shared_task(
|
|
10
|
+
queue="synchronization",
|
|
11
|
+
default_retry_delay=5,
|
|
12
|
+
autoretry_for=(OperationalError,),
|
|
13
|
+
max_retries=4,
|
|
14
|
+
retry_backoff=True,
|
|
15
|
+
)
|
|
16
|
+
def handle_inbound_as_task(event: dict):
|
|
17
|
+
"""
|
|
18
|
+
the events received from the webhook are handled in a task
|
|
19
|
+
which will allow to create, modify or delete the activity without interrupting the main server
|
|
20
|
+
"""
|
|
21
|
+
ActivityController().handle_inbound(event)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@csrf_exempt
|
|
25
|
+
def event_watch(request: HttpRequest) -> HttpResponse:
|
|
26
|
+
# TODO this is unsecure as it is prone to DDOS attack
|
|
27
|
+
status_code = 200
|
|
28
|
+
try:
|
|
29
|
+
controller = ActivityController()
|
|
30
|
+
if response := controller.handle_inbound_validation_response(request):
|
|
31
|
+
return response
|
|
32
|
+
for event in controller.get_events_from_inbound_request(request):
|
|
33
|
+
handle_inbound_as_task.delay(event)
|
|
34
|
+
except Exception as e:
|
|
35
|
+
print(e) # noqa: T201
|
|
36
|
+
status_code = 500
|
|
37
|
+
return HttpResponse(status=status_code)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import wbcrm.synchronization.activity.admin # noqa
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from django.apps import AppConfig
|
|
2
|
+
from django.db.models.signals import post_migrate
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class WbSyncConfig(AppConfig):
|
|
6
|
+
name = "wbcrm.synchronization"
|
|
7
|
+
|
|
8
|
+
def ready(self) -> None:
|
|
9
|
+
import wbcrm.synchronization.activity.signals # noqa
|
|
10
|
+
from wbcrm.synchronization.management import initialize_task
|
|
11
|
+
|
|
12
|
+
post_migrate.connect(
|
|
13
|
+
initialize_task,
|
|
14
|
+
dispatch_uid="wbcrm.synchronization.initialize_task",
|
|
15
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import wbcrm.synchronization.activity.dynamic_preferences_registry # noqa
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from django.apps import apps as global_apps
|
|
2
|
+
from django.db import DEFAULT_DB_ALIAS
|
|
3
|
+
from django_celery_beat.models import CrontabSchedule, PeriodicTask
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def initialize_task(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, apps=global_apps, **kwargs):
|
|
7
|
+
crontab1, _ = CrontabSchedule.objects.get_or_create(
|
|
8
|
+
minute="0",
|
|
9
|
+
hour="1",
|
|
10
|
+
day_of_week="*",
|
|
11
|
+
day_of_month="*",
|
|
12
|
+
month_of_year="*",
|
|
13
|
+
)
|
|
14
|
+
crontab2, _ = CrontabSchedule.objects.get_or_create(
|
|
15
|
+
minute="0",
|
|
16
|
+
hour="7",
|
|
17
|
+
day_of_week="*",
|
|
18
|
+
day_of_month="*",
|
|
19
|
+
month_of_year="*",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Automatically register the utility periodic tasks
|
|
23
|
+
PeriodicTask.objects.update_or_create(
|
|
24
|
+
task="wbcrm.synchronization.activity.tasks.periodic_renew_web_hooks_task",
|
|
25
|
+
defaults={
|
|
26
|
+
"name": "Wbactivity_sync: Renewal of the Activity Sync webhook",
|
|
27
|
+
"crontab": crontab1,
|
|
28
|
+
},
|
|
29
|
+
)
|
|
30
|
+
PeriodicTask.objects.update_or_create(
|
|
31
|
+
task="wbcrm.synchronization.activity.tasks.periodic_notify_admins_of_webhook_inconsistencies_task",
|
|
32
|
+
defaults={
|
|
33
|
+
"name": "Wbactivity_sync: Notification of webhook inconsistencies",
|
|
34
|
+
"crontab": crontab2,
|
|
35
|
+
},
|
|
36
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .activity.tasks import * # noqa
|
wbcrm/tasks.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
from __future__ import absolute_import, unicode_literals
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import date, datetime, time, timedelta
|
|
5
|
+
|
|
6
|
+
from celery import shared_task
|
|
7
|
+
from django.contrib.auth import get_user_model
|
|
8
|
+
from django.db.models import (
|
|
9
|
+
F,
|
|
10
|
+
FloatField,
|
|
11
|
+
Func,
|
|
12
|
+
Max,
|
|
13
|
+
OuterRef,
|
|
14
|
+
Q,
|
|
15
|
+
QuerySet,
|
|
16
|
+
Subquery,
|
|
17
|
+
Sum,
|
|
18
|
+
Value,
|
|
19
|
+
)
|
|
20
|
+
from django.db.models.functions import Cast, Coalesce, Least
|
|
21
|
+
from django.template.loader import render_to_string
|
|
22
|
+
from django.utils import timezone
|
|
23
|
+
from django.utils.timezone import make_aware
|
|
24
|
+
from django.utils.translation import gettext as _
|
|
25
|
+
from dynamic_preferences.registries import global_preferences_registry
|
|
26
|
+
from psycopg.types.range import TimestamptzRange
|
|
27
|
+
from rest_framework.reverse import reverse
|
|
28
|
+
from wbcore.contrib.directory.models import Company, Person
|
|
29
|
+
from wbcore.contrib.notifications.dispatch import send_notification
|
|
30
|
+
from wbcrm.models import Activity, ActivityType
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger()
|
|
33
|
+
User = get_user_model()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@shared_task
|
|
37
|
+
def notify(time_offset: int = 60, now: datetime | None = None):
|
|
38
|
+
"""
|
|
39
|
+
Cron task that runs every 60s and checks which activities will happen during the notify interval
|
|
40
|
+
|
|
41
|
+
Arguments:
|
|
42
|
+
time_offset (int): The notification period. Defaults to 60.
|
|
43
|
+
now (datetime | None, optional): The time at which activity needs to be checked for a notification. Defaults to None.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
if not now:
|
|
47
|
+
now = timezone.now()
|
|
48
|
+
base_queryset: QuerySet[Activity] = Activity.objects.filter(Q(status=Activity.Status.PLANNED))
|
|
49
|
+
reminder_choices = Activity.ReminderChoice.values
|
|
50
|
+
|
|
51
|
+
# we don't notify activity if Reminder is Never
|
|
52
|
+
reminder_choices.remove(Activity.ReminderChoice.NEVER)
|
|
53
|
+
for reminder in reminder_choices:
|
|
54
|
+
# get the reminder correspondance in minutes
|
|
55
|
+
reminder_minutes = Activity.ReminderChoice.get_minutes_correspondance(reminder)
|
|
56
|
+
reminder_range = TimestamptzRange(
|
|
57
|
+
now + timedelta(minutes=reminder_minutes),
|
|
58
|
+
now + timedelta(minutes=reminder_minutes) + timedelta(seconds=time_offset),
|
|
59
|
+
) # type: ignore #ErrMsg: Expected no arguments to "TimestamptzRange" constructor
|
|
60
|
+
# get all incoming activity with same reminder that happen during the notify interval
|
|
61
|
+
upcoming_occurence = base_queryset.filter(
|
|
62
|
+
reminder_choice=reminder, period__startswith__contained_by=reminder_range
|
|
63
|
+
)
|
|
64
|
+
for activity in upcoming_occurence:
|
|
65
|
+
participants = activity.get_participants()
|
|
66
|
+
|
|
67
|
+
# For each Employee in the activity participants
|
|
68
|
+
for employee in Person.objects.filter_only_internal().filter(id__in=participants.values("id")).all():
|
|
69
|
+
# formant and create Notification
|
|
70
|
+
activity_type_label = activity.type.title
|
|
71
|
+
desc = (
|
|
72
|
+
activity.description
|
|
73
|
+
if activity.description and activity.description not in ["", "<p></p>"]
|
|
74
|
+
else None
|
|
75
|
+
)
|
|
76
|
+
message = render_to_string(
|
|
77
|
+
"email/activity.html",
|
|
78
|
+
{
|
|
79
|
+
"participants": participants,
|
|
80
|
+
"type": activity_type_label,
|
|
81
|
+
"title": activity.title,
|
|
82
|
+
"start": activity.period.lower,
|
|
83
|
+
"end": activity.period.upper,
|
|
84
|
+
"description": desc,
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
send_notification(
|
|
88
|
+
code="wbcrm.activity.reminder",
|
|
89
|
+
title=_("{type} in {reminder} Minutes").format(
|
|
90
|
+
type=activity_type_label, reminder=reminder_minutes
|
|
91
|
+
),
|
|
92
|
+
body=message,
|
|
93
|
+
user=employee.user_account,
|
|
94
|
+
reverse_name="wbcrm:activity-detail",
|
|
95
|
+
reverse_args=[activity.pk],
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@shared_task
|
|
100
|
+
def yesterdays_activity_summary(yesterday: date | None = None, report_receiver_user_ids: list[int] | None = None):
|
|
101
|
+
"""A daily task that sends a summary of all employees' yesterday's activities to the users assigned to this task
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
yesterday (date | None, optional): Date of the previous day. Defaults to None.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
if not yesterday:
|
|
108
|
+
yesterday = date.today() - timedelta(days=1)
|
|
109
|
+
yesterday = datetime.combine(yesterday, time(0, 0, 0)) # we convert the date to datetime
|
|
110
|
+
time_range = TimestamptzRange(make_aware(yesterday), make_aware(yesterday + timedelta(days=1))) # type: ignore #ErrMsg: Expected no arguments to "TimestamptzRange" constructor
|
|
111
|
+
|
|
112
|
+
# Create the list of all employees' activities for yesterday
|
|
113
|
+
employees_list: list[Person] = list(Person.objects.filter_only_internal())
|
|
114
|
+
internal_activities: QuerySet[Activity] = (
|
|
115
|
+
Activity.objects.exclude(status=Activity.Status.CANCELLED)
|
|
116
|
+
.filter(period__overlap=time_range, participants__in=employees_list)
|
|
117
|
+
.order_by("period__startswith")
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if not (internal_activities.exists() or report_receiver_user_ids):
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
activity_lists: list[list[dict]] = [] # contains an activity list for each employee
|
|
124
|
+
employee_names = []
|
|
125
|
+
for employee in employees_list:
|
|
126
|
+
if internal_activities.filter(participants=employee).exists():
|
|
127
|
+
employees_activities = internal_activities.filter(participants=employee)
|
|
128
|
+
# Create activity list with formatted activity dictionaries for employee
|
|
129
|
+
activity_lists.append(
|
|
130
|
+
[
|
|
131
|
+
{
|
|
132
|
+
"type": activity.type.title,
|
|
133
|
+
"title": activity.title,
|
|
134
|
+
"start": activity.period.lower, # type: ignore #ErrMsg: Cannot access member "lower" for type "TimestamptzRange"
|
|
135
|
+
"end": activity.period.upper, # type: ignore #ErrMsg: Cannot access member "upper" for type "TimestamptzRange"
|
|
136
|
+
"endpoint": reverse("wbcrm:activity-detail", args=[activity.pk]),
|
|
137
|
+
}
|
|
138
|
+
for activity in employees_activities
|
|
139
|
+
]
|
|
140
|
+
)
|
|
141
|
+
employee_names.append(employee.full_name)
|
|
142
|
+
|
|
143
|
+
# Create the notification for each person with the right permission
|
|
144
|
+
for user in User.objects.filter(id__in=report_receiver_user_ids).distinct():
|
|
145
|
+
context = {
|
|
146
|
+
"map_activities": zip(employee_names, activity_lists),
|
|
147
|
+
"activities_count": internal_activities.count(),
|
|
148
|
+
"report_date": yesterday.strftime("%d.%m.%Y"),
|
|
149
|
+
}
|
|
150
|
+
message = render_to_string("email/global_daily_summary.html", context)
|
|
151
|
+
send_notification(
|
|
152
|
+
code="wbcrm.activity.daily_summary",
|
|
153
|
+
title=_("Activity Summary {}").format(yesterday.strftime("%d.%m.%Y")),
|
|
154
|
+
body=message,
|
|
155
|
+
user=user,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@shared_task
|
|
160
|
+
def todays_activity_summary(today: date | None = None):
|
|
161
|
+
"""Creates a summary of the daily upcoming activities for all employees
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
today (date | None, optional): Date of today. Defaults to None.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
if not today:
|
|
168
|
+
today = date.today()
|
|
169
|
+
tz_info = timezone.get_current_timezone()
|
|
170
|
+
today_range = TimestamptzRange(
|
|
171
|
+
lower=datetime.combine(today, time(0, 0, 0)).replace(tzinfo=tz_info),
|
|
172
|
+
upper=datetime.combine(today + timedelta(days=1), time(0, 0, 0).replace(tzinfo=tz_info)),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
for employee in Person.objects.filter_only_internal():
|
|
176
|
+
# Get all the employee's activities from that day
|
|
177
|
+
activity_qs: QuerySet[Activity] = (
|
|
178
|
+
Activity.objects.exclude(status=Activity.Status.CANCELLED)
|
|
179
|
+
.filter(period__overlap=today_range, participants=employee)
|
|
180
|
+
.order_by("period__startswith")
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Create the formatted activity dictionaries
|
|
184
|
+
activity_list = []
|
|
185
|
+
for activity in activity_qs:
|
|
186
|
+
activity_list.append(
|
|
187
|
+
{
|
|
188
|
+
"type": activity.type.title,
|
|
189
|
+
"title": activity.title,
|
|
190
|
+
"start": activity.period.lower,
|
|
191
|
+
"end": activity.period.upper,
|
|
192
|
+
"endpoint": reverse("wbcrm:activity-detail", args=[activity.pk]),
|
|
193
|
+
}
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Create the proper notification for each employee
|
|
197
|
+
if activity_list and employee.primary_email_contact():
|
|
198
|
+
context = {"activities": activity_list}
|
|
199
|
+
message = render_to_string("email/daily_summary.html", context)
|
|
200
|
+
send_notification(
|
|
201
|
+
code="wbcrm.activity.daily_summary",
|
|
202
|
+
title=_("Your Schedule for Today"),
|
|
203
|
+
body=message,
|
|
204
|
+
user=employee.user_account,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@shared_task
|
|
209
|
+
def finish(now: datetime | None = None):
|
|
210
|
+
"""Cron task running every X Seconds. Checks all activities that have finished and sends a reminder to review the activity to the assigned person.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
now (datetime | None, optional): Current datetime. Defaults to None.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
if not now:
|
|
217
|
+
now = timezone.now()
|
|
218
|
+
# Get all finished activities during the cron task interval
|
|
219
|
+
finished_activities: QuerySet[Activity] = Activity.objects.filter(
|
|
220
|
+
Q(status=Activity.Status.PLANNED.name)
|
|
221
|
+
& Q(repeat_choice=Activity.ReoccuranceChoice.NEVER)
|
|
222
|
+
& Q(period__endswith__lte=now)
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# For each of these activities, Send the Notification to the person in charge of that activity
|
|
226
|
+
for activity in finished_activities:
|
|
227
|
+
activity.finish()
|
|
228
|
+
activity.save()
|
|
229
|
+
if (assignee := activity.assigned_to) and assignee.is_internal and assignee.user_account:
|
|
230
|
+
send_notification(
|
|
231
|
+
code="wbcrm.activity.finished",
|
|
232
|
+
title=_("Activity Finished"),
|
|
233
|
+
body=_('The activity "{title}" just finished and you are in charge of it. Please review.').format(
|
|
234
|
+
title=activity.title
|
|
235
|
+
),
|
|
236
|
+
user=assignee.user_account,
|
|
237
|
+
reverse_name="wbcrm:activity-detail",
|
|
238
|
+
reverse_args=[activity.pk],
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@shared_task
|
|
243
|
+
def default_activity_heat_calculation(check_datetime: datetime | None = None):
|
|
244
|
+
"""A script that calculates the activity heat of companies and
|
|
245
|
+
persons on a scale from 0.0 to 1.0. The type and time interval of
|
|
246
|
+
completed activities serve as the basis for the heat calculation for companies. A person's rating is based
|
|
247
|
+
on the score of the person's employer.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
check_datetime (datetime | None, optional): The datetime of the activity heat check. Defaults to None.
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
if not check_datetime:
|
|
254
|
+
check_datetime = timezone.now()
|
|
255
|
+
|
|
256
|
+
class JulianDay(Func):
|
|
257
|
+
"""
|
|
258
|
+
The Julian day is the continuous count of days since the beginning of the Julian period.
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
function = ""
|
|
262
|
+
output_field = FloatField() # type: ignore #ErrMsg: Expression of type "FloatField[float]" cannot be assigned to declared type "property"
|
|
263
|
+
|
|
264
|
+
def as_postgresql(self, compiler, connection):
|
|
265
|
+
self.template = "CAST (to_char(%(expressions)s, 'J') AS INTEGER)"
|
|
266
|
+
return self.as_sql(compiler, connection)
|
|
267
|
+
|
|
268
|
+
global_preferences_manager = global_preferences_registry.manager()
|
|
269
|
+
main_company: int = global_preferences_manager["directory__main_company"]
|
|
270
|
+
|
|
271
|
+
external_employees: QuerySet[Person] = Person.objects.exclude(id__in=Person.objects.filter_only_internal())
|
|
272
|
+
external_companies: QuerySet[Company] = Company.objects.exclude(id=main_company)
|
|
273
|
+
# Calculate the activity heat of a person in the last 180 days.
|
|
274
|
+
activity_score = (
|
|
275
|
+
Activity.objects.filter(
|
|
276
|
+
companies__id=OuterRef("id"),
|
|
277
|
+
status__in=["REVIEWED", "FINISHED"],
|
|
278
|
+
period__endswith__gte=check_datetime - timedelta(days=180),
|
|
279
|
+
period__endswith__lte=check_datetime,
|
|
280
|
+
)
|
|
281
|
+
.annotate(
|
|
282
|
+
ratio=JulianDay(Value(check_datetime)) - JulianDay(F("period__endswith")),
|
|
283
|
+
date_score=(Value(365) - F("ratio")) / Value(365.0),
|
|
284
|
+
score=Cast(F("type__score"), FloatField()) * F("date_score"),
|
|
285
|
+
)
|
|
286
|
+
.values("companies__id")
|
|
287
|
+
.annotate(sum_score=Sum(F("score")))
|
|
288
|
+
.values("sum_score")
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
company_score = (
|
|
292
|
+
Company.objects.filter(
|
|
293
|
+
id=OuterRef("id"),
|
|
294
|
+
)
|
|
295
|
+
.annotate(
|
|
296
|
+
norm_score=Coalesce(
|
|
297
|
+
Subquery(activity_score) / float(ActivityType.Score.MAX),
|
|
298
|
+
Value(0.0),
|
|
299
|
+
),
|
|
300
|
+
abs_norm_score=Least(F("norm_score"), 1.0),
|
|
301
|
+
)
|
|
302
|
+
.values("abs_norm_score")
|
|
303
|
+
)
|
|
304
|
+
employer_max_score = (
|
|
305
|
+
external_companies.filter(employees__id=OuterRef("id"))
|
|
306
|
+
.values("employees__id")
|
|
307
|
+
.annotate(max_score=Max("activity_heat"))
|
|
308
|
+
.values("max_score")
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
external_companies.update(activity_heat=Subquery(company_score))
|
|
312
|
+
external_employees.filter(employers__id__in=external_companies).update(activity_heat=Subquery(employer_max_score))
|
wbcrm/tests/__init__.py
ADDED
|
File without changes
|
|
File without changes
|