wbcore 1.54.10__py2.py3-none-any.whl → 1.58.2__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.
- wbcore/cache/decorators.py +3 -3
- wbcore/cache/registry.py +3 -2
- wbcore/configs/decorators.py +1 -1
- wbcore/configurations/configurations/apps.py +2 -2
- wbcore/configurations/configurations/authentication.py +1 -1
- wbcore/configurations/configurations/base.py +1 -1
- wbcore/configurations/configurations/cache.py +1 -1
- wbcore/configurations/configurations/maintenance.py +1 -1
- wbcore/configurations/configurations/media.py +1 -1
- wbcore/configurations/configurations/middleware.py +1 -1
- wbcore/configurations/configurations/rest_framework.py +1 -1
- wbcore/configurations/configurations/static.py +1 -1
- wbcore/configurations/configurations/wbcore.py +1 -1
- wbcore/content_type/serializers.py +1 -1
- wbcore/content_type/utils.py +3 -3
- wbcore/contrib/agenda/viewsets/calendar_items.py +7 -7
- wbcore/contrib/ai/llm/config.py +1 -1
- wbcore/contrib/authentication/admin.py +2 -2
- wbcore/contrib/authentication/filters.py +0 -1
- wbcore/contrib/authentication/models/users.py +3 -3
- wbcore/contrib/authentication/models/users_activities.py +1 -1
- wbcore/contrib/authentication/serializers/users.py +2 -2
- wbcore/contrib/authentication/tests/test_tokens.py +3 -3
- wbcore/contrib/authentication/tests/test_users.py +0 -1
- wbcore/contrib/authentication/viewsets/user_activities.py +2 -1
- wbcore/contrib/authentication/viewsets/users.py +6 -4
- wbcore/contrib/color/models.py +2 -1
- wbcore/contrib/currency/factories.py +1 -1
- wbcore/contrib/currency/import_export/backends/fixerio/currency_fx_rates.py +3 -1
- wbcore/contrib/currency/models.py +28 -8
- wbcore/contrib/currency/serializers.py +5 -1
- wbcore/contrib/currency/tests/test_serializers.py +7 -3
- wbcore/contrib/currency/tests/test_viewsets.py +1 -1
- wbcore/contrib/currency/viewsets/currency.py +2 -2
- wbcore/contrib/dataloader/utils.py +2 -2
- wbcore/contrib/directory/factories/__init__.py +1 -1
- wbcore/contrib/directory/factories/entries.py +1 -1
- wbcore/contrib/directory/models/contacts.py +2 -2
- wbcore/contrib/directory/models/entries.py +18 -4
- wbcore/contrib/directory/models/relationships.py +25 -30
- wbcore/contrib/directory/permissions.py +6 -0
- wbcore/contrib/directory/serializers/companies.py +15 -8
- wbcore/contrib/directory/serializers/contacts.py +8 -8
- wbcore/contrib/directory/serializers/entries.py +24 -15
- wbcore/contrib/directory/serializers/entry_representations.py +4 -2
- wbcore/contrib/directory/serializers/persons.py +8 -9
- wbcore/contrib/directory/serializers/relationships.py +2 -2
- wbcore/contrib/directory/tests/conftest.py +2 -0
- wbcore/contrib/directory/tests/disable_signals.py +11 -1
- wbcore/contrib/directory/tests/signals.py +2 -2
- wbcore/contrib/directory/tests/test_models.py +88 -66
- wbcore/contrib/directory/tests/test_serializers.py +1 -1
- wbcore/contrib/directory/tests/test_viewsets.py +8 -8
- wbcore/contrib/directory/viewsets/buttons/__init__.py +1 -1
- wbcore/contrib/directory/viewsets/buttons/relationships.py +32 -0
- wbcore/contrib/directory/viewsets/contacts.py +6 -6
- wbcore/contrib/directory/viewsets/display/__init__.py +1 -1
- wbcore/contrib/directory/viewsets/display/entries.py +51 -36
- wbcore/contrib/directory/viewsets/display/relationships.py +22 -22
- wbcore/contrib/directory/viewsets/entries.py +4 -5
- wbcore/contrib/directory/viewsets/previews/entries.py +3 -3
- wbcore/contrib/directory/viewsets/relationships.py +16 -2
- wbcore/contrib/directory/viewsets/titles/relationships.py +2 -3
- wbcore/contrib/documents/filters.py +0 -2
- wbcore/contrib/example_app/models.py +4 -4
- wbcore/contrib/example_app/serializers/person_team.py +4 -4
- wbcore/contrib/example_app/tests/e2e/test_teams.py +1 -1
- wbcore/contrib/geography/tests/test_viewsets.py +1 -1
- wbcore/contrib/guardian/tests/test_model_mixins.py +3 -3
- wbcore/contrib/guardian/tests/test_tasks.py +9 -9
- wbcore/contrib/guardian/tests/test_viewsets.py +2 -2
- wbcore/contrib/icons/backends/default.py +1 -0
- wbcore/contrib/icons/backends/material.py +1 -0
- wbcore/contrib/icons/icons.py +5 -8
- wbcore/contrib/io/exceptions.py +8 -0
- wbcore/contrib/io/import_export/backends/stream.py +2 -2
- wbcore/contrib/io/imports.py +10 -5
- wbcore/contrib/io/models.py +17 -14
- wbcore/contrib/io/serializers.py +2 -2
- wbcore/contrib/io/tests/test_backends.py +1 -1
- wbcore/contrib/io/tests/test_imports.py +1 -1
- wbcore/contrib/io/viewset_mixins.py +4 -4
- wbcore/contrib/notifications/dispatch.py +18 -7
- wbcore/contrib/pandas/filterset.py +8 -7
- wbcore/contrib/pandas/views.py +7 -5
- wbcore/contrib/tags/models/tags.py +4 -1
- wbcore/contrib/workflow/factories/display.py +2 -2
- wbcore/contrib/workflow/models/data.py +7 -4
- wbcore/contrib/workflow/models/process.py +2 -2
- wbcore/contrib/workflow/serializers/data.py +8 -8
- wbcore/contrib/workflow/tests/test_models/test_condition.py +1 -1
- wbcore/contrib/workflow/workflows/assignees.py +4 -4
- wbcore/dynamic_preferences_registry.py +23 -9
- wbcore/enums.py +2 -1
- wbcore/filters/fields/content_type.py +5 -4
- wbcore/filters/fields/datetime.py +34 -9
- wbcore/filters/fields/models.py +2 -2
- wbcore/filters/filterset.py +22 -6
- wbcore/filters/mixins.py +6 -2
- wbcore/forms.py +6 -6
- wbcore/fsm/markdown_extensions.py +1 -1
- wbcore/fsm/mixins.py +7 -4
- wbcore/markdown/models.py +8 -5
- wbcore/metadata/configs/buttons/bases.py +6 -6
- wbcore/metadata/configs/buttons/buttons.py +2 -1
- wbcore/metadata/configs/buttons/view_config.py +5 -3
- wbcore/metadata/configs/display/display.py +2 -2
- wbcore/metadata/configs/display/formatting.py +6 -7
- wbcore/metadata/configs/display/list_display.py +6 -7
- wbcore/metadata/configs/display/models.py +6 -0
- wbcore/metadata/configs/fields.py +6 -1
- wbcore/metadata/configs/filter_fields.py +12 -11
- wbcore/models/fields.py +2 -2
- wbcore/permissions/permissions.py +2 -2
- wbcore/permissions/utils.py +2 -2
- wbcore/reversion/viewsets/titles.py +4 -3
- wbcore/serializers/__init__.py +1 -0
- wbcore/serializers/fields/__init__.py +1 -0
- wbcore/serializers/fields/datetime.py +35 -6
- wbcore/serializers/fields/fields.py +1 -1
- wbcore/serializers/fields/fsm.py +1 -1
- wbcore/serializers/fields/list.py +1 -1
- wbcore/serializers/fields/mixins.py +13 -5
- wbcore/serializers/fields/related.py +4 -6
- wbcore/serializers/fields/text.py +1 -1
- wbcore/serializers/fields/types.py +1 -0
- wbcore/serializers/serializers.py +6 -2
- wbcore/tasks.py +2 -2
- wbcore/templates/wbcore/email_base_template.html +3 -3
- wbcore/test/e2e_helpers_methods/e2e_checks.py +10 -4
- wbcore/test/e2e_helpers_methods/e2e_helper_methods.py +4 -2
- wbcore/test/mixins.py +1 -1
- wbcore/test/tests.py +6 -9
- wbcore/test/utils.py +3 -4
- wbcore/tests/e2e/test_e2e.py +2 -2
- wbcore/tests/test_cache/test_decorators.py +3 -3
- wbcore/tests/test_configs.py +1 -1
- wbcore/tests/test_fields/test_number_fields.py +1 -1
- wbcore/tests/test_filters/test_mixins.py +3 -3
- wbcore/tests/test_models/test_mixins.py +1 -1
- wbcore/tests/test_utils/test_date.py +1 -1
- wbcore/tests/test_utils/test_date_builder.py +25 -1
- wbcore/utils/date.py +18 -2
- wbcore/utils/figures.py +2 -2
- wbcore/utils/models.py +3 -2
- wbcore/utils/reportlab.py +7 -0
- wbcore/utils/rrules.py +1 -1
- wbcore/utils/string_loader.py +1 -1
- wbcore/utils/strings.py +2 -2
- wbcore/viewsets/mixins.py +6 -4
- {wbcore-1.54.10.dist-info → wbcore-1.58.2.dist-info}/METADATA +2 -1
- {wbcore-1.54.10.dist-info → wbcore-1.58.2.dist-info}/RECORD +153 -151
- {wbcore-1.54.10.dist-info → wbcore-1.58.2.dist-info}/WHEEL +0 -0
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from unittest.mock import MagicMock
|
|
2
|
-
|
|
3
1
|
import pytest
|
|
4
2
|
from django.db import models
|
|
5
3
|
from django_fsm import TransitionNotAllowed
|
|
@@ -7,16 +5,16 @@ from dynamic_preferences.registries import global_preferences_registry
|
|
|
7
5
|
from pytest_mock import MockerFixture
|
|
8
6
|
|
|
9
7
|
from wbcore.contrib.authentication.factories import InternalUserFactory
|
|
10
|
-
from wbcore.contrib.directory.factories import ClientManagerRelationshipFactory as CMRF
|
|
11
8
|
from wbcore.contrib.directory.factories import (
|
|
9
|
+
ClientManagerRelationshipFactory,
|
|
12
10
|
CompanyFactory,
|
|
13
11
|
EmailContactFactory,
|
|
14
12
|
EntryFactory,
|
|
15
13
|
PersonFactory,
|
|
16
14
|
TelephoneContactFactory,
|
|
17
15
|
)
|
|
18
|
-
from wbcore.contrib.directory.models import ClientManagerRelationship as CMR
|
|
19
16
|
from wbcore.contrib.directory.models import (
|
|
17
|
+
ClientManagerRelationship,
|
|
20
18
|
Company,
|
|
21
19
|
EmailContact,
|
|
22
20
|
Entry,
|
|
@@ -206,37 +204,46 @@ class TestUserDeactivation:
|
|
|
206
204
|
|
|
207
205
|
def test_no_substitute_person(self, test_internal_profile):
|
|
208
206
|
# Arrange
|
|
209
|
-
relationship =
|
|
207
|
+
relationship = ClientManagerRelationshipFactory(relationship_manager=test_internal_profile)
|
|
210
208
|
# Act
|
|
211
209
|
handle_user_deactivation(sender=None, instance=test_internal_profile, substitute_profile=None)
|
|
212
210
|
# Assert
|
|
213
|
-
assert
|
|
211
|
+
assert (
|
|
212
|
+
ClientManagerRelationship.objects.get(id=relationship.id).status
|
|
213
|
+
== ClientManagerRelationship.Status.REMOVED
|
|
214
|
+
)
|
|
214
215
|
|
|
215
216
|
@pytest.mark.parametrize("exists", [True, False])
|
|
216
217
|
def test_not_approved_substitute_relationships(self, test_internal_profile, test_person, exists):
|
|
217
218
|
# Arrange
|
|
218
|
-
old_relationship =
|
|
219
|
+
old_relationship = ClientManagerRelationshipFactory(
|
|
220
|
+
relationship_manager=test_internal_profile, status=ClientManagerRelationship.Status.PENDINGADD
|
|
221
|
+
)
|
|
219
222
|
substitute_relationship = (
|
|
220
|
-
|
|
223
|
+
ClientManagerRelationshipFactory(client=old_relationship.client, relationship_manager=test_person)
|
|
224
|
+
if exists
|
|
225
|
+
else None
|
|
221
226
|
)
|
|
222
227
|
# Act
|
|
223
228
|
message = handle_user_deactivation(
|
|
224
229
|
sender=None, instance=test_internal_profile, substitute_profile=test_person
|
|
225
230
|
)[1]
|
|
226
231
|
relationship_id = substitute_relationship.id if exists else old_relationship.id
|
|
227
|
-
relationship_exists =
|
|
232
|
+
relationship_exists = ClientManagerRelationship.objects.filter(id=old_relationship.id).exists()
|
|
228
233
|
# Assert
|
|
229
234
|
assert message == f"Assigned 1 manager role(s) to {test_person.computed_str}"
|
|
230
235
|
assert (not relationship_exists) if exists else relationship_exists
|
|
231
|
-
assert
|
|
232
|
-
assert
|
|
236
|
+
assert ClientManagerRelationship.objects.get(id=relationship_id).client.id == old_relationship.client.id
|
|
237
|
+
assert ClientManagerRelationship.objects.get(id=relationship_id).relationship_manager.id == test_person.id
|
|
233
238
|
|
|
234
239
|
def test_approved_with_substitute_relationships(self, test_internal_profile, test_person):
|
|
235
240
|
# Arrange
|
|
236
|
-
old_relationship =
|
|
237
|
-
|
|
238
|
-
substitute_relationship =
|
|
239
|
-
client=old_relationship.client,
|
|
241
|
+
old_relationship = ClientManagerRelationshipFactory(relationship_manager=test_internal_profile, primary=True)
|
|
242
|
+
ClientManagerRelationshipFactory(relationship_manager=test_internal_profile)
|
|
243
|
+
substitute_relationship = ClientManagerRelationshipFactory(
|
|
244
|
+
client=old_relationship.client,
|
|
245
|
+
relationship_manager=test_person,
|
|
246
|
+
status=ClientManagerRelationship.Status.PENDINGADD,
|
|
240
247
|
)
|
|
241
248
|
# Act
|
|
242
249
|
message = handle_user_deactivation(
|
|
@@ -247,65 +254,67 @@ class TestUserDeactivation:
|
|
|
247
254
|
assert message == f"Assigned 2 manager role(s) to {test_person.computed_str}"
|
|
248
255
|
assert substitute_relationship.client.id == old_relationship.client.id
|
|
249
256
|
assert substitute_relationship.relationship_manager.id == test_person.id
|
|
250
|
-
assert substitute_relationship.status ==
|
|
257
|
+
assert substitute_relationship.status == ClientManagerRelationship.Status.APPROVED
|
|
251
258
|
assert substitute_relationship.primary is True
|
|
252
259
|
|
|
253
260
|
def test_approved_without_substitute_relationships_needs_primary(self, test_internal_profile, test_person):
|
|
254
261
|
# Arrange
|
|
255
|
-
old_relationship =
|
|
262
|
+
old_relationship = ClientManagerRelationshipFactory(relationship_manager=test_internal_profile, primary=True)
|
|
256
263
|
# Act
|
|
257
264
|
message = handle_user_deactivation(
|
|
258
265
|
sender=None, instance=test_internal_profile, substitute_profile=test_person
|
|
259
266
|
)[1]
|
|
260
|
-
relationship_exists =
|
|
261
|
-
relationship_manager=test_person,
|
|
267
|
+
relationship_exists = ClientManagerRelationship.objects.filter(
|
|
268
|
+
relationship_manager=test_person,
|
|
269
|
+
client=old_relationship.client,
|
|
270
|
+
primary=True,
|
|
271
|
+
status=ClientManagerRelationship.Status.APPROVED,
|
|
262
272
|
).exists()
|
|
263
273
|
old_relationship.refresh_from_db()
|
|
264
274
|
# Assert
|
|
265
275
|
assert message == f"Assigned 1 manager role(s) to {test_person.computed_str}"
|
|
266
276
|
assert relationship_exists
|
|
267
|
-
assert old_relationship.status ==
|
|
277
|
+
assert old_relationship.status == ClientManagerRelationship.Status.REMOVED
|
|
268
278
|
|
|
269
279
|
def test_approved_without_substitute_relationships_doesnt_need_primary(self, test_internal_profile, test_person):
|
|
270
280
|
# Arrange
|
|
271
|
-
old_relationship =
|
|
272
|
-
|
|
281
|
+
old_relationship = ClientManagerRelationshipFactory(relationship_manager=test_internal_profile)
|
|
282
|
+
ClientManagerRelationshipFactory(client=old_relationship.client, primary=True)
|
|
273
283
|
# Act
|
|
274
284
|
message = handle_user_deactivation(
|
|
275
285
|
sender=None, instance=test_internal_profile, substitute_profile=test_person
|
|
276
286
|
)[1]
|
|
277
|
-
relationship_exists =
|
|
278
|
-
relationship_manager=test_person,
|
|
287
|
+
relationship_exists = ClientManagerRelationship.objects.filter(
|
|
288
|
+
relationship_manager=test_person,
|
|
289
|
+
client=old_relationship.client,
|
|
290
|
+
primary=False,
|
|
291
|
+
status=ClientManagerRelationship.Status.APPROVED,
|
|
279
292
|
).exists()
|
|
280
293
|
old_relationship.refresh_from_db()
|
|
281
294
|
# Assert
|
|
282
295
|
assert message == f"Assigned 1 manager role(s) to {test_person.computed_str}"
|
|
283
296
|
assert relationship_exists
|
|
284
|
-
assert old_relationship.status ==
|
|
297
|
+
assert old_relationship.status == ClientManagerRelationship.Status.REMOVED
|
|
285
298
|
|
|
286
299
|
|
|
287
300
|
@pytest.mark.directory_model_tests
|
|
288
301
|
class TestSpecificModelsClientManagerRelationship:
|
|
289
|
-
def _mock_relationship_query(self, is_primary: bool, mocker: MockerFixture, mock_path: str) -> MagicMock:
|
|
290
|
-
mock_filter = mocker.patch(f"{mock_path}.objects.filter")
|
|
291
|
-
mock_exists = mock_filter.return_value.exists
|
|
292
|
-
mock_exists.return_value = is_primary
|
|
293
|
-
mock_update = mock_filter.return_value.update
|
|
294
|
-
mock_update.return_value = None
|
|
295
|
-
return mock_update
|
|
296
|
-
|
|
297
302
|
@pytest.fixture
|
|
298
303
|
def test_cmr(self):
|
|
299
|
-
return
|
|
304
|
+
return ClientManagerRelationship()
|
|
300
305
|
|
|
301
306
|
@pytest.mark.parametrize(
|
|
302
307
|
"method_name, initial_status, expected_status",
|
|
303
308
|
[
|
|
304
|
-
("submit",
|
|
305
|
-
("deny",
|
|
306
|
-
("denyremoval",
|
|
307
|
-
(
|
|
308
|
-
|
|
309
|
+
("submit", ClientManagerRelationship.Status.DRAFT, ClientManagerRelationship.Status.PENDINGADD),
|
|
310
|
+
("deny", ClientManagerRelationship.Status.PENDINGADD, ClientManagerRelationship.Status.DRAFT),
|
|
311
|
+
("denyremoval", ClientManagerRelationship.Status.PENDINGREMOVE, ClientManagerRelationship.Status.APPROVED),
|
|
312
|
+
(
|
|
313
|
+
"approveremoval",
|
|
314
|
+
ClientManagerRelationship.Status.PENDINGREMOVE,
|
|
315
|
+
ClientManagerRelationship.Status.REMOVED,
|
|
316
|
+
),
|
|
317
|
+
("reinstate", ClientManagerRelationship.Status.REMOVED, ClientManagerRelationship.Status.PENDINGADD),
|
|
309
318
|
],
|
|
310
319
|
)
|
|
311
320
|
def test_status_transitions(self, test_cmr, mocker: MockerFixture, method_name, initial_status, expected_status):
|
|
@@ -318,42 +327,34 @@ class TestSpecificModelsClientManagerRelationship:
|
|
|
318
327
|
# Assert
|
|
319
328
|
assert test_cmr.status == expected_status
|
|
320
329
|
|
|
321
|
-
@pytest.mark.parametrize("method_name", ["approve", "mngapprove"])
|
|
322
|
-
@pytest.mark.parametrize("is_primary", [True, False])
|
|
323
|
-
def test_approval_methods(self,
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
method()
|
|
335
|
-
# Assert
|
|
336
|
-
assert test_cmr.status == CMR.Status.APPROVED
|
|
337
|
-
assert test_cmr.primary
|
|
338
|
-
if is_primary:
|
|
339
|
-
mock_update.assert_called_once_with(primary=False)
|
|
340
|
-
else:
|
|
341
|
-
mock_update.assert_not_called()
|
|
330
|
+
# @pytest.mark.parametrize("method_name", ["approve", "mngapprove"])
|
|
331
|
+
# @pytest.mark.parametrize("is_primary", [True, False])
|
|
332
|
+
# def test_approval_methods(self, client_manager_relationship_factory, method_name, is_primary):
|
|
333
|
+
# # Arrange
|
|
334
|
+
# status = ClientManagerRelationship.Status.PENDINGADD if method_name == "approve" else ClientManagerRelationship.Status.DRAFT
|
|
335
|
+
# test_cmr = client_manager_relationship_factory.create(status=status, primary=False)
|
|
336
|
+
# # Act
|
|
337
|
+
# method = getattr(test_cmr, method_name)
|
|
338
|
+
# method()
|
|
339
|
+
# test_cmr.save() # we need to call save because the logic of handling primary happens in the parent save method (primarymixin)
|
|
340
|
+
# # Assert
|
|
341
|
+
# assert test_cmr.status == ClientManagerRelationship.Status.APPROVED
|
|
342
|
+
# assert test_cmr.primary
|
|
342
343
|
|
|
343
344
|
@pytest.mark.parametrize("is_primary", [True, False])
|
|
344
345
|
def test_make_primary(self, is_primary, test_cmr, mocker: MockerFixture):
|
|
345
346
|
# Arrange
|
|
346
347
|
mocker.patch.object(test_cmr, "primary", is_primary)
|
|
347
|
-
mocker.patch.object(test_cmr, "status",
|
|
348
|
+
mocker.patch.object(test_cmr, "status", ClientManagerRelationship.Status.APPROVED)
|
|
348
349
|
mocker.patch("django_fsm.transition", return_value=None)
|
|
349
350
|
# Act & Assert
|
|
350
351
|
if is_primary:
|
|
351
352
|
with pytest.raises(TransitionNotAllowed):
|
|
352
353
|
test_cmr.makeprimary()
|
|
353
|
-
assert test_cmr.status ==
|
|
354
|
+
assert test_cmr.status == ClientManagerRelationship.Status.APPROVED
|
|
354
355
|
else:
|
|
355
356
|
test_cmr.makeprimary()
|
|
356
|
-
assert test_cmr.status ==
|
|
357
|
+
assert test_cmr.status == ClientManagerRelationship.Status.PENDINGADD
|
|
357
358
|
assert test_cmr.primary
|
|
358
359
|
|
|
359
360
|
@pytest.mark.parametrize("is_primary", [True, False])
|
|
@@ -367,16 +368,16 @@ class TestSpecificModelsClientManagerRelationship:
|
|
|
367
368
|
mock_exclude.return_value = mock_queryset
|
|
368
369
|
mocker.patch(f"{mock_path}.client", new_callable=mocker.PropertyMock) # Mock client property
|
|
369
370
|
mocker.patch.object(test_cmr, "primary", is_primary)
|
|
370
|
-
mocker.patch.object(test_cmr, "status",
|
|
371
|
+
mocker.patch.object(test_cmr, "status", ClientManagerRelationship.Status.APPROVED)
|
|
371
372
|
mocker.patch("django_fsm.transition", return_value=None)
|
|
372
373
|
# Act & Assert
|
|
373
374
|
if not is_primary and exists:
|
|
374
375
|
test_cmr.remove()
|
|
375
|
-
assert test_cmr.status ==
|
|
376
|
+
assert test_cmr.status == ClientManagerRelationship.Status.PENDINGREMOVE
|
|
376
377
|
else:
|
|
377
378
|
with pytest.raises(TransitionNotAllowed):
|
|
378
379
|
test_cmr.remove()
|
|
379
|
-
assert test_cmr.status ==
|
|
380
|
+
assert test_cmr.status == ClientManagerRelationship.Status.APPROVED
|
|
380
381
|
|
|
381
382
|
|
|
382
383
|
@pytest.mark.directory_model_tests
|
|
@@ -404,3 +405,24 @@ class TestSpecificModelsRelationships:
|
|
|
404
405
|
to_entry=to_entry,
|
|
405
406
|
)
|
|
406
407
|
assert rel.__str__() == "John Doe is Type of Jane Doe"
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
@pytest.mark.django_db
|
|
411
|
+
class TestEntry:
|
|
412
|
+
def test_get_banking_contact(self, entry, banking_contact_factory, currency_factory):
|
|
413
|
+
eur = currency_factory.create()
|
|
414
|
+
usd = currency_factory.create()
|
|
415
|
+
|
|
416
|
+
euro_banking_contact = banking_contact_factory.create(entry=entry, currency=eur)
|
|
417
|
+
assert entry.get_banking_contact(eur) == euro_banking_contact
|
|
418
|
+
assert (
|
|
419
|
+
entry.get_banking_contact(usd) == euro_banking_contact
|
|
420
|
+
) # even if usd does not exist, we need to return at least a banking contact
|
|
421
|
+
|
|
422
|
+
usd_banking_contact = banking_contact_factory.create(entry=entry, currency=usd, primary=True)
|
|
423
|
+
|
|
424
|
+
assert entry.get_banking_contact(eur) == euro_banking_contact
|
|
425
|
+
assert entry.get_banking_contact(usd) == usd_banking_contact
|
|
426
|
+
|
|
427
|
+
new_primary_usd_banking_contact = banking_contact_factory.create(entry=entry, currency=usd, primary=True)
|
|
428
|
+
assert entry.get_banking_contact(usd) == new_primary_usd_banking_contact
|
|
@@ -110,7 +110,7 @@ class TestContactSerializersValidation:
|
|
|
110
110
|
return IBAN("AD1400080001001234567890")
|
|
111
111
|
|
|
112
112
|
@pytest.fixture
|
|
113
|
-
def contact_data(
|
|
113
|
+
def contact_data(self, mocker: MockerFixture):
|
|
114
114
|
return {
|
|
115
115
|
"AddressContact": {
|
|
116
116
|
"serializer": AddressContactSerializer(),
|
|
@@ -7,7 +7,7 @@ from rest_framework.test import APIRequestFactory
|
|
|
7
7
|
|
|
8
8
|
from wbcore.contrib.authentication.factories import SuperUserFactory, UserFactory
|
|
9
9
|
from wbcore.contrib.authentication.models import User
|
|
10
|
-
from wbcore.contrib.directory.models import ClientManagerRelationship
|
|
10
|
+
from wbcore.contrib.directory.models import ClientManagerRelationship
|
|
11
11
|
from wbcore.test.utils import (
|
|
12
12
|
get_data_from_factory,
|
|
13
13
|
get_kwargs,
|
|
@@ -580,15 +580,15 @@ class TestRelationshipViewSets:
|
|
|
580
580
|
def test_relationship_partial_update(self, api_request_factory, super_user, relationship_factory, person_factory):
|
|
581
581
|
# Arrange
|
|
582
582
|
relationship = relationship_factory()
|
|
583
|
-
|
|
584
|
-
request = api_request_factory.patch("", data={"to_entry":
|
|
583
|
+
new_person = person_factory()
|
|
584
|
+
request = api_request_factory.patch("", data={"to_entry": new_person.id})
|
|
585
585
|
request.user = super_user
|
|
586
586
|
view = RelationshipModelViewSet.as_view({"patch": "partial_update"})
|
|
587
587
|
# Act
|
|
588
588
|
response = view(request, pk=relationship.id).render()
|
|
589
589
|
# Assert
|
|
590
590
|
assert response.status_code == status.HTTP_200_OK
|
|
591
|
-
assert response.data["instance"]["to_entry"] ==
|
|
591
|
+
assert response.data["instance"]["to_entry"] == new_person.id
|
|
592
592
|
|
|
593
593
|
|
|
594
594
|
# =====================================================================================================================
|
|
@@ -601,7 +601,7 @@ class TestRelationshipViewSets:
|
|
|
601
601
|
@pytest.mark.django_db
|
|
602
602
|
class TestClientManagerViewSet:
|
|
603
603
|
@pytest.mark.parametrize("mvs", [ClientManagerViewSet])
|
|
604
|
-
def
|
|
604
|
+
def test_none_qs(self, api_request_factory, normal_user, mvs):
|
|
605
605
|
request = api_request_factory.get("")
|
|
606
606
|
request.user = normal_user
|
|
607
607
|
obj = ClientManagerRelationshipFactory()
|
|
@@ -615,7 +615,7 @@ class TestClientManagerViewSet:
|
|
|
615
615
|
request = api_request_factory.delete("")
|
|
616
616
|
request.user = super_user
|
|
617
617
|
obj1 = ClientManagerRelationshipFactory()
|
|
618
|
-
obj2 = ClientManagerRelationshipFactory(client=obj1.client, status=
|
|
618
|
+
obj2 = ClientManagerRelationshipFactory(client=obj1.client, status=ClientManagerRelationship.Status.DRAFT)
|
|
619
619
|
view = mvs.as_view({"delete": "destroy"})
|
|
620
620
|
response = view(request, pk=obj2.id).render()
|
|
621
621
|
assert response.status_code == status.HTTP_204_NO_CONTENT
|
|
@@ -645,7 +645,7 @@ class TestClientManagerViewSet:
|
|
|
645
645
|
|
|
646
646
|
@pytest.mark.parametrize("mvs", [ClientManagerViewSet])
|
|
647
647
|
def test_put(self, api_request_factory, super_user, mvs):
|
|
648
|
-
obj_old = ClientManagerRelationshipFactory(status=
|
|
648
|
+
obj_old = ClientManagerRelationshipFactory(status=ClientManagerRelationship.Status.DRAFT)
|
|
649
649
|
obj_new = ClientManagerRelationshipFactory()
|
|
650
650
|
user = super_user
|
|
651
651
|
data = get_data_from_factory(obj_new, mvs, superuser=user, delete=True)
|
|
@@ -822,7 +822,7 @@ class TestContactViewsets:
|
|
|
822
822
|
SocialMediaContactEntryViewSet,
|
|
823
823
|
],
|
|
824
824
|
)
|
|
825
|
-
def
|
|
825
|
+
def test_primary_deleteendpointmixin(self, api_request_factory, super_user, mvs):
|
|
826
826
|
request = api_request_factory.delete("")
|
|
827
827
|
request.user = super_user
|
|
828
828
|
factory = get_model_factory(mvs.queryset.model)
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
from django.utils.translation import gettext as _
|
|
2
2
|
from rest_framework.reverse import reverse
|
|
3
3
|
|
|
4
|
+
from wbcore.contrib.directory.models import ClientManagerRelationship
|
|
5
|
+
from wbcore.contrib.directory.permissions import IsClientManagerRelationshipAdmin
|
|
4
6
|
from wbcore.contrib.icons import WBIcon
|
|
7
|
+
from wbcore.enums import RequestType
|
|
5
8
|
from wbcore.metadata.configs import buttons as bt
|
|
6
9
|
from wbcore.metadata.configs.buttons.view_config import ButtonViewConfig
|
|
7
10
|
|
|
@@ -30,3 +33,32 @@ class EmployerEmployeeRelationshipButtonConfig(ButtonViewConfig):
|
|
|
30
33
|
)
|
|
31
34
|
}
|
|
32
35
|
return {}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ClientManagerRelationshipButtonConfig(ButtonViewConfig):
|
|
39
|
+
def get_custom_buttons(self):
|
|
40
|
+
buttons = set()
|
|
41
|
+
if (
|
|
42
|
+
IsClientManagerRelationshipAdmin().has_permission(self.request, self.view)
|
|
43
|
+
and ClientManagerRelationship.objects.filter(
|
|
44
|
+
status__in=[
|
|
45
|
+
ClientManagerRelationship.Status.PENDINGADD,
|
|
46
|
+
ClientManagerRelationship.Status.PENDINGREMOVE,
|
|
47
|
+
]
|
|
48
|
+
).exists()
|
|
49
|
+
):
|
|
50
|
+
buttons.add(
|
|
51
|
+
bt.ActionButton(
|
|
52
|
+
method=RequestType.PATCH,
|
|
53
|
+
identifiers=("directory:clientmanagerrelationship",),
|
|
54
|
+
endpoint=reverse("wbcore:directory:clientmanagerrelationship-approveallpendingrequests"),
|
|
55
|
+
label=_("Approve All Pending Requests"),
|
|
56
|
+
icon=WBIcon.APPROVE.icon,
|
|
57
|
+
description_fields=_(
|
|
58
|
+
"<p> Are you sure you want to approve all pending (add and remove) requests ? </p>"
|
|
59
|
+
),
|
|
60
|
+
title=_("Approve All Pending Requests"),
|
|
61
|
+
action_label=_("Approved"),
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
return buttons
|
|
@@ -11,13 +11,13 @@ from wbcore.contrib.directory.models import (
|
|
|
11
11
|
BankingContact,
|
|
12
12
|
Company,
|
|
13
13
|
EmailContact,
|
|
14
|
+
EmployerEmployeeRelationship,
|
|
14
15
|
Entry,
|
|
15
16
|
Person,
|
|
16
17
|
SocialMediaContact,
|
|
17
18
|
TelephoneContact,
|
|
18
19
|
WebsiteContact,
|
|
19
20
|
)
|
|
20
|
-
from wbcore.contrib.directory.models import EmployerEmployeeRelationship as EER
|
|
21
21
|
|
|
22
22
|
from ..filters import (
|
|
23
23
|
AddressContactCompanyFilter,
|
|
@@ -129,16 +129,16 @@ class ContactModelMixin(_Base):
|
|
|
129
129
|
if not entry.is_company:
|
|
130
130
|
person = entry.get_casted_entry()
|
|
131
131
|
if (
|
|
132
|
-
|
|
133
|
-
and
|
|
132
|
+
EmployerEmployeeRelationship.objects.filter(employee=person, primary=True).exists()
|
|
133
|
+
and EmployerEmployeeRelationship.objects.filter(employee=person).count() > 1
|
|
134
134
|
):
|
|
135
135
|
return (
|
|
136
136
|
super()
|
|
137
137
|
.get_queryset()
|
|
138
138
|
.exclude(
|
|
139
|
-
entry__in=
|
|
140
|
-
|
|
141
|
-
)
|
|
139
|
+
entry__in=EmployerEmployeeRelationship.objects.filter(
|
|
140
|
+
employee=person, primary=False
|
|
141
|
+
).values_list("employer", flat=True)
|
|
142
142
|
)
|
|
143
143
|
)
|
|
144
144
|
return super().get_queryset()
|
|
@@ -285,13 +285,23 @@ class PersonModelDisplay(EntryModelDisplay):
|
|
|
285
285
|
return dp.ListDisplay(
|
|
286
286
|
fields=[
|
|
287
287
|
dp.Field(key="name", label=_("Name")),
|
|
288
|
-
dp.Field(key="primary_employer_repr", label=_("Primary Employer")),
|
|
289
288
|
dp.Field(key="customer_status", label=_("Status")),
|
|
290
289
|
dp.Field(key="position_in_company", label=_("Position")),
|
|
291
290
|
dp.Field(key="cities", label=_("City")),
|
|
292
|
-
dp.Field(key="primary_telephone", label=_("Primary Phone Number")),
|
|
293
291
|
dp.Field(key="tier", label=_("Tier")),
|
|
294
|
-
dp.Field(
|
|
292
|
+
dp.Field(
|
|
293
|
+
key=None,
|
|
294
|
+
label=_("Primary Contacts"),
|
|
295
|
+
children=[
|
|
296
|
+
dp.Field(key="primary_employer_repr", label=_("Primary Employer")),
|
|
297
|
+
dp.Field(key="primary_manager_repr", label=_("Relationship Manager")),
|
|
298
|
+
dp.Field(key="primary_telephone", label=_("Telephone")),
|
|
299
|
+
dp.Field(key="primary_email", label=_("Email")),
|
|
300
|
+
dp.Field(key="primary_address", label=_("Address"), show="open"),
|
|
301
|
+
dp.Field(key="primary_website", label=_("Website"), show="open"),
|
|
302
|
+
dp.Field(key="primary_social", label=_("Social"), show="open"),
|
|
303
|
+
],
|
|
304
|
+
),
|
|
295
305
|
dp.Field(
|
|
296
306
|
key=None,
|
|
297
307
|
label=_("Last Event"),
|
|
@@ -347,19 +357,36 @@ class CompanyModelDisplay(EntryModelDisplay):
|
|
|
347
357
|
),
|
|
348
358
|
)
|
|
349
359
|
|
|
350
|
-
|
|
360
|
+
grid_template_areas = [
|
|
361
|
+
["profile_image", "name", "customer_status", "activity_table"],
|
|
362
|
+
["profile_image", "primary_telephone", "primary_telephone", "activity_table"],
|
|
363
|
+
["profile_image", "type", "tier", "activity_table"],
|
|
364
|
+
["profile_image", "activity_heat", "activity_heat", "activity_table"],
|
|
365
|
+
["employees_section", "employees_section", "employees_section", "employees_section"],
|
|
366
|
+
]
|
|
367
|
+
sections = [employees_section]
|
|
368
|
+
if portfolio_fields:
|
|
369
|
+
grid_template_areas.insert(
|
|
370
|
+
4,
|
|
371
|
+
[
|
|
372
|
+
portfolio_fields.key,
|
|
373
|
+
portfolio_fields.key,
|
|
374
|
+
portfolio_fields.key,
|
|
375
|
+
"activity_table",
|
|
376
|
+
],
|
|
377
|
+
)
|
|
378
|
+
sections.append(portfolio_fields)
|
|
379
|
+
if aum_table:
|
|
380
|
+
grid_template_areas[-1][-1] = aum_table.key
|
|
381
|
+
sections.append(aum_table)
|
|
382
|
+
|
|
383
|
+
return Display(
|
|
351
384
|
pages=[
|
|
352
385
|
Page(
|
|
353
386
|
title=_("Main Information"),
|
|
354
387
|
layouts={
|
|
355
388
|
default(): Layout(
|
|
356
|
-
grid_template_areas=
|
|
357
|
-
["profile_image", "name", "customer_status", "activity_table"],
|
|
358
|
-
["profile_image", "primary_telephone", "primary_telephone", "activity_table"],
|
|
359
|
-
["profile_image", "type", "tier", "activity_table"],
|
|
360
|
-
["profile_image", "activity_heat", "activity_heat", "activity_table"],
|
|
361
|
-
["employees_section", "employees_section", "employees_section", "employees_section"],
|
|
362
|
-
],
|
|
389
|
+
grid_template_areas=grid_template_areas,
|
|
363
390
|
grid_template_columns=[
|
|
364
391
|
Style.MIN_CONTENT,
|
|
365
392
|
"minmax(min-content, 1fr)",
|
|
@@ -368,7 +395,7 @@ class CompanyModelDisplay(EntryModelDisplay):
|
|
|
368
395
|
],
|
|
369
396
|
grid_template_rows=[Style.rem(6), Style.rem(6), Style.rem(6)],
|
|
370
397
|
grid_auto_rows=Style.MIN_CONTENT,
|
|
371
|
-
sections=
|
|
398
|
+
sections=sections,
|
|
372
399
|
inlines=[
|
|
373
400
|
Inline(
|
|
374
401
|
key="activity_table",
|
|
@@ -393,29 +420,6 @@ class CompanyModelDisplay(EntryModelDisplay):
|
|
|
393
420
|
]
|
|
394
421
|
)
|
|
395
422
|
|
|
396
|
-
# Need to insert the portfolio fields into the display
|
|
397
|
-
for page in display.pages:
|
|
398
|
-
if page.title == "Main Information":
|
|
399
|
-
for layout_key in page.layouts.keys():
|
|
400
|
-
if portfolio_fields:
|
|
401
|
-
# Insert the section with the AUM fields below the profile picture at the left
|
|
402
|
-
page.layouts[layout_key].grid_template_areas.insert(
|
|
403
|
-
4,
|
|
404
|
-
[
|
|
405
|
-
portfolio_fields.key,
|
|
406
|
-
portfolio_fields.key,
|
|
407
|
-
portfolio_fields.key,
|
|
408
|
-
"activity_table",
|
|
409
|
-
],
|
|
410
|
-
)
|
|
411
|
-
page.layouts[layout_key].sections.append(portfolio_fields)
|
|
412
|
-
if aum_table:
|
|
413
|
-
# Insert the section with the AUM table at the bottom right
|
|
414
|
-
page.layouts[layout_key].grid_template_areas[-1][-1] = aum_table.key
|
|
415
|
-
page.layouts[layout_key].sections.append(aum_table)
|
|
416
|
-
break
|
|
417
|
-
return display
|
|
418
|
-
|
|
419
423
|
@classmethod
|
|
420
424
|
def _get_new_company_instance_display(cls) -> Display:
|
|
421
425
|
"""Returns the display for creating a new company
|
|
@@ -468,7 +472,18 @@ class CompanyModelDisplay(EntryModelDisplay):
|
|
|
468
472
|
dp.Field(key="type", label=_("Type")),
|
|
469
473
|
dp.Field(key="tier", label=_("Tier")),
|
|
470
474
|
dp.Field(key="customer_status", label=_("Status")),
|
|
471
|
-
dp.Field(
|
|
475
|
+
dp.Field(
|
|
476
|
+
key=None,
|
|
477
|
+
label=_("Primary Contacts"),
|
|
478
|
+
children=[
|
|
479
|
+
dp.Field(key="primary_manager_repr", label=_("Relationship Manager")),
|
|
480
|
+
dp.Field(key="primary_telephone", label=_("Telephone")),
|
|
481
|
+
dp.Field(key="primary_email", label=_("Email")),
|
|
482
|
+
dp.Field(key="primary_address", label=_("Address"), show="open"),
|
|
483
|
+
dp.Field(key="primary_website", label=_("Website"), show="open"),
|
|
484
|
+
dp.Field(key="primary_social", label=_("Social"), show="open"),
|
|
485
|
+
],
|
|
486
|
+
),
|
|
472
487
|
dp.Field(
|
|
473
488
|
key=None,
|
|
474
489
|
label=_("Last Event"),
|