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.
Files changed (153) hide show
  1. wbcore/cache/decorators.py +3 -3
  2. wbcore/cache/registry.py +3 -2
  3. wbcore/configs/decorators.py +1 -1
  4. wbcore/configurations/configurations/apps.py +2 -2
  5. wbcore/configurations/configurations/authentication.py +1 -1
  6. wbcore/configurations/configurations/base.py +1 -1
  7. wbcore/configurations/configurations/cache.py +1 -1
  8. wbcore/configurations/configurations/maintenance.py +1 -1
  9. wbcore/configurations/configurations/media.py +1 -1
  10. wbcore/configurations/configurations/middleware.py +1 -1
  11. wbcore/configurations/configurations/rest_framework.py +1 -1
  12. wbcore/configurations/configurations/static.py +1 -1
  13. wbcore/configurations/configurations/wbcore.py +1 -1
  14. wbcore/content_type/serializers.py +1 -1
  15. wbcore/content_type/utils.py +3 -3
  16. wbcore/contrib/agenda/viewsets/calendar_items.py +7 -7
  17. wbcore/contrib/ai/llm/config.py +1 -1
  18. wbcore/contrib/authentication/admin.py +2 -2
  19. wbcore/contrib/authentication/filters.py +0 -1
  20. wbcore/contrib/authentication/models/users.py +3 -3
  21. wbcore/contrib/authentication/models/users_activities.py +1 -1
  22. wbcore/contrib/authentication/serializers/users.py +2 -2
  23. wbcore/contrib/authentication/tests/test_tokens.py +3 -3
  24. wbcore/contrib/authentication/tests/test_users.py +0 -1
  25. wbcore/contrib/authentication/viewsets/user_activities.py +2 -1
  26. wbcore/contrib/authentication/viewsets/users.py +6 -4
  27. wbcore/contrib/color/models.py +2 -1
  28. wbcore/contrib/currency/factories.py +1 -1
  29. wbcore/contrib/currency/import_export/backends/fixerio/currency_fx_rates.py +3 -1
  30. wbcore/contrib/currency/models.py +28 -8
  31. wbcore/contrib/currency/serializers.py +5 -1
  32. wbcore/contrib/currency/tests/test_serializers.py +7 -3
  33. wbcore/contrib/currency/tests/test_viewsets.py +1 -1
  34. wbcore/contrib/currency/viewsets/currency.py +2 -2
  35. wbcore/contrib/dataloader/utils.py +2 -2
  36. wbcore/contrib/directory/factories/__init__.py +1 -1
  37. wbcore/contrib/directory/factories/entries.py +1 -1
  38. wbcore/contrib/directory/models/contacts.py +2 -2
  39. wbcore/contrib/directory/models/entries.py +18 -4
  40. wbcore/contrib/directory/models/relationships.py +25 -30
  41. wbcore/contrib/directory/permissions.py +6 -0
  42. wbcore/contrib/directory/serializers/companies.py +15 -8
  43. wbcore/contrib/directory/serializers/contacts.py +8 -8
  44. wbcore/contrib/directory/serializers/entries.py +24 -15
  45. wbcore/contrib/directory/serializers/entry_representations.py +4 -2
  46. wbcore/contrib/directory/serializers/persons.py +8 -9
  47. wbcore/contrib/directory/serializers/relationships.py +2 -2
  48. wbcore/contrib/directory/tests/conftest.py +2 -0
  49. wbcore/contrib/directory/tests/disable_signals.py +11 -1
  50. wbcore/contrib/directory/tests/signals.py +2 -2
  51. wbcore/contrib/directory/tests/test_models.py +88 -66
  52. wbcore/contrib/directory/tests/test_serializers.py +1 -1
  53. wbcore/contrib/directory/tests/test_viewsets.py +8 -8
  54. wbcore/contrib/directory/viewsets/buttons/__init__.py +1 -1
  55. wbcore/contrib/directory/viewsets/buttons/relationships.py +32 -0
  56. wbcore/contrib/directory/viewsets/contacts.py +6 -6
  57. wbcore/contrib/directory/viewsets/display/__init__.py +1 -1
  58. wbcore/contrib/directory/viewsets/display/entries.py +51 -36
  59. wbcore/contrib/directory/viewsets/display/relationships.py +22 -22
  60. wbcore/contrib/directory/viewsets/entries.py +4 -5
  61. wbcore/contrib/directory/viewsets/previews/entries.py +3 -3
  62. wbcore/contrib/directory/viewsets/relationships.py +16 -2
  63. wbcore/contrib/directory/viewsets/titles/relationships.py +2 -3
  64. wbcore/contrib/documents/filters.py +0 -2
  65. wbcore/contrib/example_app/models.py +4 -4
  66. wbcore/contrib/example_app/serializers/person_team.py +4 -4
  67. wbcore/contrib/example_app/tests/e2e/test_teams.py +1 -1
  68. wbcore/contrib/geography/tests/test_viewsets.py +1 -1
  69. wbcore/contrib/guardian/tests/test_model_mixins.py +3 -3
  70. wbcore/contrib/guardian/tests/test_tasks.py +9 -9
  71. wbcore/contrib/guardian/tests/test_viewsets.py +2 -2
  72. wbcore/contrib/icons/backends/default.py +1 -0
  73. wbcore/contrib/icons/backends/material.py +1 -0
  74. wbcore/contrib/icons/icons.py +5 -8
  75. wbcore/contrib/io/exceptions.py +8 -0
  76. wbcore/contrib/io/import_export/backends/stream.py +2 -2
  77. wbcore/contrib/io/imports.py +10 -5
  78. wbcore/contrib/io/models.py +17 -14
  79. wbcore/contrib/io/serializers.py +2 -2
  80. wbcore/contrib/io/tests/test_backends.py +1 -1
  81. wbcore/contrib/io/tests/test_imports.py +1 -1
  82. wbcore/contrib/io/viewset_mixins.py +4 -4
  83. wbcore/contrib/notifications/dispatch.py +18 -7
  84. wbcore/contrib/pandas/filterset.py +8 -7
  85. wbcore/contrib/pandas/views.py +7 -5
  86. wbcore/contrib/tags/models/tags.py +4 -1
  87. wbcore/contrib/workflow/factories/display.py +2 -2
  88. wbcore/contrib/workflow/models/data.py +7 -4
  89. wbcore/contrib/workflow/models/process.py +2 -2
  90. wbcore/contrib/workflow/serializers/data.py +8 -8
  91. wbcore/contrib/workflow/tests/test_models/test_condition.py +1 -1
  92. wbcore/contrib/workflow/workflows/assignees.py +4 -4
  93. wbcore/dynamic_preferences_registry.py +23 -9
  94. wbcore/enums.py +2 -1
  95. wbcore/filters/fields/content_type.py +5 -4
  96. wbcore/filters/fields/datetime.py +34 -9
  97. wbcore/filters/fields/models.py +2 -2
  98. wbcore/filters/filterset.py +22 -6
  99. wbcore/filters/mixins.py +6 -2
  100. wbcore/forms.py +6 -6
  101. wbcore/fsm/markdown_extensions.py +1 -1
  102. wbcore/fsm/mixins.py +7 -4
  103. wbcore/markdown/models.py +8 -5
  104. wbcore/metadata/configs/buttons/bases.py +6 -6
  105. wbcore/metadata/configs/buttons/buttons.py +2 -1
  106. wbcore/metadata/configs/buttons/view_config.py +5 -3
  107. wbcore/metadata/configs/display/display.py +2 -2
  108. wbcore/metadata/configs/display/formatting.py +6 -7
  109. wbcore/metadata/configs/display/list_display.py +6 -7
  110. wbcore/metadata/configs/display/models.py +6 -0
  111. wbcore/metadata/configs/fields.py +6 -1
  112. wbcore/metadata/configs/filter_fields.py +12 -11
  113. wbcore/models/fields.py +2 -2
  114. wbcore/permissions/permissions.py +2 -2
  115. wbcore/permissions/utils.py +2 -2
  116. wbcore/reversion/viewsets/titles.py +4 -3
  117. wbcore/serializers/__init__.py +1 -0
  118. wbcore/serializers/fields/__init__.py +1 -0
  119. wbcore/serializers/fields/datetime.py +35 -6
  120. wbcore/serializers/fields/fields.py +1 -1
  121. wbcore/serializers/fields/fsm.py +1 -1
  122. wbcore/serializers/fields/list.py +1 -1
  123. wbcore/serializers/fields/mixins.py +13 -5
  124. wbcore/serializers/fields/related.py +4 -6
  125. wbcore/serializers/fields/text.py +1 -1
  126. wbcore/serializers/fields/types.py +1 -0
  127. wbcore/serializers/serializers.py +6 -2
  128. wbcore/tasks.py +2 -2
  129. wbcore/templates/wbcore/email_base_template.html +3 -3
  130. wbcore/test/e2e_helpers_methods/e2e_checks.py +10 -4
  131. wbcore/test/e2e_helpers_methods/e2e_helper_methods.py +4 -2
  132. wbcore/test/mixins.py +1 -1
  133. wbcore/test/tests.py +6 -9
  134. wbcore/test/utils.py +3 -4
  135. wbcore/tests/e2e/test_e2e.py +2 -2
  136. wbcore/tests/test_cache/test_decorators.py +3 -3
  137. wbcore/tests/test_configs.py +1 -1
  138. wbcore/tests/test_fields/test_number_fields.py +1 -1
  139. wbcore/tests/test_filters/test_mixins.py +3 -3
  140. wbcore/tests/test_models/test_mixins.py +1 -1
  141. wbcore/tests/test_utils/test_date.py +1 -1
  142. wbcore/tests/test_utils/test_date_builder.py +25 -1
  143. wbcore/utils/date.py +18 -2
  144. wbcore/utils/figures.py +2 -2
  145. wbcore/utils/models.py +3 -2
  146. wbcore/utils/reportlab.py +7 -0
  147. wbcore/utils/rrules.py +1 -1
  148. wbcore/utils/string_loader.py +1 -1
  149. wbcore/utils/strings.py +2 -2
  150. wbcore/viewsets/mixins.py +6 -4
  151. {wbcore-1.54.10.dist-info → wbcore-1.58.2.dist-info}/METADATA +2 -1
  152. {wbcore-1.54.10.dist-info → wbcore-1.58.2.dist-info}/RECORD +153 -151
  153. {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 = CMRF(relationship_manager=test_internal_profile)
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 CMR.objects.get(id=relationship.id).status == CMR.Status.REMOVED
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 = CMRF(relationship_manager=test_internal_profile, status=CMR.Status.PENDINGADD)
219
+ old_relationship = ClientManagerRelationshipFactory(
220
+ relationship_manager=test_internal_profile, status=ClientManagerRelationship.Status.PENDINGADD
221
+ )
219
222
  substitute_relationship = (
220
- CMRF(client=old_relationship.client, relationship_manager=test_person) if exists else None
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 = CMR.objects.filter(id=old_relationship.id).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 CMR.objects.get(id=relationship_id).client.id == old_relationship.client.id
232
- assert CMR.objects.get(id=relationship_id).relationship_manager.id == test_person.id
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 = CMRF(relationship_manager=test_internal_profile, primary=True)
237
- CMRF(relationship_manager=test_internal_profile)
238
- substitute_relationship = CMRF(
239
- client=old_relationship.client, relationship_manager=test_person, status=CMR.Status.PENDINGADD
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 == CMR.Status.APPROVED
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 = CMRF(relationship_manager=test_internal_profile, primary=True)
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 = CMR.objects.filter(
261
- relationship_manager=test_person, client=old_relationship.client, primary=True, status=CMR.Status.APPROVED
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 == CMR.Status.REMOVED
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 = CMRF(relationship_manager=test_internal_profile)
272
- CMRF(client=old_relationship.client, primary=True)
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 = CMR.objects.filter(
278
- relationship_manager=test_person, client=old_relationship.client, primary=False, status=CMR.Status.APPROVED
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 == CMR.Status.REMOVED
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 CMR()
304
+ return ClientManagerRelationship()
300
305
 
301
306
  @pytest.mark.parametrize(
302
307
  "method_name, initial_status, expected_status",
303
308
  [
304
- ("submit", CMR.Status.DRAFT, CMR.Status.PENDINGADD),
305
- ("deny", CMR.Status.PENDINGADD, CMR.Status.DRAFT),
306
- ("denyremoval", CMR.Status.PENDINGREMOVE, CMR.Status.APPROVED),
307
- ("approveremoval", CMR.Status.PENDINGREMOVE, CMR.Status.REMOVED),
308
- ("reinstate", CMR.Status.REMOVED, CMR.Status.PENDINGADD),
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, test_cmr, method_name, is_primary, mocker: MockerFixture):
324
- # Arrange
325
- status = CMR.Status.PENDINGADD if method_name == "approve" else CMR.Status.DRAFT
326
- mock_path = "wbcore.contrib.directory.models.ClientManagerRelationship"
327
- mocker.patch(f"{mock_path}.client", new_callable=mocker.PropertyMock) # Mock client property
328
- mock_update = self._mock_relationship_query(is_primary, mocker, mock_path)
329
- mocker.patch("django_fsm.transition", return_value=None)
330
- mocker.patch.object(test_cmr, "primary", is_primary)
331
- mocker.patch.object(test_cmr, "status", status)
332
- # Act
333
- method = getattr(test_cmr, method_name)
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", CMR.Status.APPROVED)
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 == CMR.Status.APPROVED
354
+ assert test_cmr.status == ClientManagerRelationship.Status.APPROVED
354
355
  else:
355
356
  test_cmr.makeprimary()
356
- assert test_cmr.status == CMR.Status.PENDINGADD
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", CMR.Status.APPROVED)
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 == CMR.Status.PENDINGREMOVE
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 == CMR.Status.APPROVED
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(iban, mocker: MockerFixture):
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 as CMR
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
- new_Person = person_factory()
584
- request = api_request_factory.patch("", data={"to_entry": new_Person.id})
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"] == new_Person.id
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 test_None_qs(self, api_request_factory, normal_user, mvs):
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=CMR.Status.DRAFT)
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=CMR.Status.DRAFT)
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 test_primary_DeleteEndpointMixin(self, api_request_factory, super_user, mvs):
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)
@@ -4,4 +4,4 @@ from .entries import (
4
4
  EntryModelButtonConfig,
5
5
  PersonModelButtonConfig,
6
6
  )
7
- from .relationships import EmployerEmployeeRelationshipButtonConfig
7
+ from .relationships import EmployerEmployeeRelationshipButtonConfig, ClientManagerRelationshipButtonConfig
@@ -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
- EER.objects.filter(employee=person, primary=True).exists()
133
- and EER.objects.filter(employee=person).count() > 1
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=EER.objects.filter(employee=person, primary=False).values_list(
140
- "employer", flat=True
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()
@@ -19,7 +19,7 @@ from .entries import (
19
19
  )
20
20
  from .relationships import (
21
21
  ClientManagerModelDisplay,
22
- CMR_Color,
22
+ ClientManagerRelationshipColor,
23
23
  EmployerEmployeeRelationshipDisplayConfig,
24
24
  EmployeeEmployerDisplayConfig,
25
25
  EmployerEmployeeDisplayConfig,
@@ -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(key="primary_manager_repr", label=_("Primary Relationship Manager")),
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
- display = Display(
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=[employees_section],
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(key="primary_manager_repr", label=_("Primary Relationship Manager")),
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"),