From 353608ddfd12943bcd5b8d85b4b3e2aab106d8ef Mon Sep 17 00:00:00 2001 From: Zeryab Khan Date: Tue, 10 Sep 2024 15:32:40 +0500 Subject: [PATCH 1/3] [ISSUE-3680] Sync ConnectWise configuration records --- djconnectwise/__init__.py | 2 +- djconnectwise/admin.py | 14 ++++ djconnectwise/api.py | 33 +++++++++ djconnectwise/management/commands/cwsync.py | 4 ++ ...rationstatus_configurationtype_and_more.py | 68 ++++++++++++++++++ djconnectwise/models.py | 50 +++++++++++++ djconnectwise/sync.py | 70 +++++++++++++++++++ djconnectwise/tests/fixture_utils.py | 14 ++++ djconnectwise/tests/fixtures.py | 28 ++++++++ djconnectwise/tests/mocks.py | 12 ++++ djconnectwise/tests/test_commands.py | 28 ++++++++ djconnectwise/tests/test_sync.py | 50 +++++++++++++ 12 files changed, 372 insertions(+), 1 deletion(-) create mode 100644 djconnectwise/migrations/0186_configurationstatus_configurationtype_and_more.py diff --git a/djconnectwise/__init__.py b/djconnectwise/__init__.py index ac9eee5..161646a 100644 --- a/djconnectwise/__init__.py +++ b/djconnectwise/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -VERSION = (1, 6, 3, 'final') +VERSION = (1, 6, 4, 'final') # pragma: no cover if VERSION[-1] != "final": diff --git a/djconnectwise/admin.py b/djconnectwise/admin.py index 4beeb1d..ab5a938 100644 --- a/djconnectwise/admin.py +++ b/djconnectwise/admin.py @@ -473,3 +473,17 @@ class CompanyTeamAdmin(admin.ModelAdmin): @admin.register(models.CompanySite) class CompanySiteAdmin(admin.ModelAdmin): list_display = ('id', 'name', 'company', 'inactive') + + +@admin.register(models.ConfigurationStatus) +class ConfigurationStatusAdmin(admin.ModelAdmin): + list_display = ('id', 'description', 'closed_flag', 'default_flag') + list_filter = ('closed_flag', 'default_flag') + search_fields = ['description'] + + +@admin.register(models.ConfigurationType) +class ConfigurationTypeAdmin(admin.ModelAdmin): + list_display = ('id', 'name', 'inactive_flag', 'system_flag') + list_filter = ('inactive_flag', 'system_flag') + search_fields = ['name'] diff --git a/djconnectwise/api.py b/djconnectwise/api.py index c44b2da..f85e54d 100644 --- a/djconnectwise/api.py +++ b/djconnectwise/api.py @@ -1422,6 +1422,39 @@ def get_project_notes_count(self, project_id): return len(res) +class ConfigurationAPIClient(ConnectWiseAPIClient): + API = 'company' + ENDPOINT_CONFIGURATIONS = 'configurations/' + ENDPOINT_STATUS = '{}statuses/'.format(ENDPOINT_CONFIGURATIONS) + ENDPOINT_TYPES = '{}types/'.format(ENDPOINT_CONFIGURATIONS) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def get_configurations(self, company_id, *args, **kwargs): + """ + Retrieves configurations for a given company ID. + """ + params = {'companyId': company_id} + return self.fetch_resource(self.ENDPOINT_CONFIGURATIONS, + params=params, + *args, **kwargs) + + def get_configuration_statuses(self, *args, **kwargs): + """ + Retrieves configuration statuses from the API. + """ + return self.fetch_resource(self.ENDPOINT_STATUS, should_page=True, + *args, **kwargs) + + def get_configuration_types(self, *args, **kwargs): + """ + Retrieves configuration types from the API. + """ + return self.fetch_resource(self.ENDPOINT_STATUS, should_page=True, + *args, **kwargs) + + class FinanceAPIClient(ConnectWiseAPIClient): API = 'finance' ENDPOINT_AGREEMENTS = 'agreements' diff --git a/djconnectwise/management/commands/cwsync.py b/djconnectwise/management/commands/cwsync.py index 73be7e9..cdf6618 100644 --- a/djconnectwise/management/commands/cwsync.py +++ b/djconnectwise/management/commands/cwsync.py @@ -117,6 +117,10 @@ def __init__(self, *args, **kwargs): ('activity_udf', sync.ActivityUDFSynchronizer, _('Activity UDF')), ('opportunity_udf', sync.OpportunityUDFSynchronizer, _('Opportunity UDF')), + ('configuration_status', sync.ConfigurationStatusSynchronizer, + _('Configuration Status')), + ('configuration_type', sync.ConfigurationTypeSynchronizer, + _('Configuration Type')), ) settings = DjconnectwiseSettings().get_settings() diff --git a/djconnectwise/migrations/0186_configurationstatus_configurationtype_and_more.py b/djconnectwise/migrations/0186_configurationstatus_configurationtype_and_more.py new file mode 100644 index 0000000..2250ac8 --- /dev/null +++ b/djconnectwise/migrations/0186_configurationstatus_configurationtype_and_more.py @@ -0,0 +1,68 @@ +# Generated by Django 4.2.11 on 2024-09-09 16:15 + +from django.db import migrations, models +import django_extensions.db.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('djconnectwise', '0185_contact_type'), + ] + + operations = [ + migrations.CreateModel( + name='ConfigurationStatus', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')), + ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')), + ('description', models.CharField(max_length=255)), + ('closed_flag', models.BooleanField(default=False)), + ('default_flag', models.BooleanField(default=False)), + ], + options={ + 'verbose_name_plural': 'Configuration Statuses', + 'ordering': ['description'], + }, + ), + migrations.CreateModel( + name='ConfigurationType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')), + ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')), + ('name', models.TextField(max_length=250)), + ('inactive_flag', models.BooleanField(default=False)), + ('system_flag', models.BooleanField(default=False)), + ], + options={ + 'verbose_name_plural': 'Configuration Types', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='ConfigurationStatusTracker', + fields=[ + ], + options={ + 'db_table': 'djconnectwise_configurationstatus', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('djconnectwise.configurationstatus',), + ), + migrations.CreateModel( + name='ConfigurationTypeTracker', + fields=[ + ], + options={ + 'db_table': 'djconnectwise_configurationtype', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('djconnectwise.configurationtype',), + ), + ] diff --git a/djconnectwise/models.py b/djconnectwise/models.py index e5fe612..5342794 100644 --- a/djconnectwise/models.py +++ b/djconnectwise/models.py @@ -1492,6 +1492,40 @@ def get_connectwise_url(self): ) +class ConfigurationStatus(TimeStampedModel): + description = models.CharField(max_length=255) + closed_flag = models.BooleanField(default=False) + default_flag = models.BooleanField(default=False) + + class Meta: + ordering = ['description'] + verbose_name_plural = 'Configuration Statuses' + + def __str__(self): + return self.description + + @property + def api_class(self): + return api.ConfigurationAPIClient + + +class ConfigurationType(TimeStampedModel): + name = models.TextField(max_length=250) + inactive_flag = models.BooleanField(default=False) + system_flag = models.BooleanField(default=False) + + class Meta: + ordering = ['name'] + verbose_name_plural = 'Configuration Types' + + def __str__(self): + return self.name + + @property + def api_class(self): + return api.ConfigurationAPIClient + + class Ticket(UpdateConnectWiseMixin, TimeStampedModel): SCHEDULE_ENTRY_TYPE = "S" @@ -2629,6 +2663,22 @@ class Meta: db_table = 'djconnectwise_opportunity' +class ConfigurationStatusTracker(ConfigurationStatus): + tracker = FieldTracker() + + class Meta: + proxy = True + db_table = 'djconnectwise_configurationstatus' + + +class ConfigurationTypeTracker(ConfigurationType): + tracker = FieldTracker() + + class Meta: + proxy = True + db_table = 'djconnectwise_configurationtype' + + class ServiceNoteTracker(ServiceNote): tracker = FieldTracker() diff --git a/djconnectwise/sync.py b/djconnectwise/sync.py index f82dc6f..aab6893 100644 --- a/djconnectwise/sync.py +++ b/djconnectwise/sync.py @@ -943,6 +943,76 @@ def client_call(self, opportunity_id, *args, **kwargs): return self.client.get_notes(opportunity_id, *args, **kwargs) +class ConfigurationSynchronizer: + client_class = api.ConfigurationAPIClient + + def __init__(self, *args, **kwargs): + self.api_conditions = [] + self.client = self.client_class() + request_settings = DjconnectwiseSettings().get_settings() + self.batch_size = request_settings['batch_size'] + + def fetch_configurations(self, company_id): + """ + Fetch configurations from a specific company. + """ + return self.client.get_configurations(company_id) + + +class ConfigurationStatusSynchronizer(Synchronizer): + client_class = api.ConfigurationAPIClient + model_class = models.ConfigurationStatusTracker + + related_meta = { + 'company': (models.Company, 'company'), + } + + def _assign_field_data(self, instance, json_data): + """ + Assigns the data from the API instance to the model instance. + """ + instance.id = json_data.get('id') + instance.description = json_data.get('description', '') + instance.closed_flag = json_data.get('closedFlag', False) + instance.default_flag = json_data.get('defaultFlag', False) + + self.set_relations(instance, json_data) + return instance + + def get_page(self, *args, **kwargs): + """ + Retrieves a page of configuration statuses from the API. + """ + return self.client.get_configuration_statuses(*args, **kwargs) + + +class ConfigurationTypeSynchronizer(Synchronizer): + client_class = api.ConfigurationAPIClient + model_class = models.ConfigurationTypeTracker + + related_meta = { + 'company': (models.Company, 'company'), + } + + def _assign_field_data(self, instance, json_data): + """ + Assigns the data from the API instance to the model instance. + """ + instance.id = json_data.get('id') + instance.name = json_data.get('name') + instance.inactive_flag = json_data.get('inactiveFlag', False) + instance.system_flag = json_data.get('systemFlag', False) + + self.set_relations(instance, json_data) + return instance + + def get_page(self, *args, **kwargs): + """ + Retrieves a page of configuration types from the API. + """ + return self.client.get_configuration_types(*args, **kwargs) + + class BoardSynchronizer(Synchronizer): client_class = api.ServiceAPIClient model_class = models.ConnectWiseBoardTracker diff --git a/djconnectwise/tests/fixture_utils.py b/djconnectwise/tests/fixture_utils.py index 97cd913..be157b6 100644 --- a/djconnectwise/tests/fixture_utils.py +++ b/djconnectwise/tests/fixture_utils.py @@ -128,6 +128,20 @@ def init_project_types(): return synchronizer.sync() +def init_configuration_statuses(): + mocks.configuration_api_get_configuration_statuses( + fixtures.API_CONFIGURATION_STATUS) + synchronizer = sync.ConfigurationStatusSynchronizer() + return synchronizer.sync() + + +def init_configuration_types(): + mocks.configuration_api_get_configuration_types( + fixtures.API_CONFIGURATION_TYPES) + synchronizer = sync.ConfigurationTypeSynchronizer() + return synchronizer.sync() + + def init_project_phases(): mocks.projects_api_get_project_phases_call( fixtures.API_PROJECT_PHASE_LIST) diff --git a/djconnectwise/tests/fixtures.py b/djconnectwise/tests/fixtures.py index db36f7c..a7f6a5d 100644 --- a/djconnectwise/tests/fixtures.py +++ b/djconnectwise/tests/fixtures.py @@ -607,6 +607,34 @@ ] +API_CONFIGURATION_TYPES = [ + { + 'id': 1, + 'name': 'Network', + 'inactiveFlag': False, + 'systemFlag': False, + '_info': { + 'lastUpdated': '2001-01-08T18:05:13Z', + 'updatedBy': None + } + } +] + + +API_CONFIGURATION_STATUS = [ + { + 'id': 1, + 'description': 'Testing Network', + 'closedFlag': False, + 'defaultFlag': False, + '_info': { + 'lastUpdated': '2001-01-08T18:05:13Z', + 'updatedBy': None + } + } +] + + API_PROJECT = { 'id': 5, '_info': { diff --git a/djconnectwise/tests/mocks.py b/djconnectwise/tests/mocks.py index 40d1e1d..42b9af4 100644 --- a/djconnectwise/tests/mocks.py +++ b/djconnectwise/tests/mocks.py @@ -93,6 +93,18 @@ def projects_api_get_project_types_call(return_value, raised=None): return create_mock_call(method_name, return_value, side_effect=raised) +def configuration_api_get_configuration_statuses(return_value, raised=None): + method_name = \ + 'djconnectwise.api.ConfigurationAPIClient.get_configuration_statuses' + return create_mock_call(method_name, return_value, side_effect=raised) + + +def configuration_api_get_configuration_types(return_value, raised=None): + method_name = \ + 'djconnectwise.api.ConfigurationAPIClient.get_configuration_types' + return create_mock_call(method_name, return_value, side_effect=raised) + + def projects_api_get_project_phases_call(return_value, raised=None): method_name = 'djconnectwise.api.ProjectAPIClient.get_project_phases' return create_mock_call(method_name, return_value, side_effect=raised) diff --git a/djconnectwise/tests/test_commands.py b/djconnectwise/tests/test_commands.py index 5129744..d8a2904 100644 --- a/djconnectwise/tests/test_commands.py +++ b/djconnectwise/tests/test_commands.py @@ -737,6 +737,30 @@ def setUp(self): ) +class TestSyncConfigurationStatusesCommand(AbstractBaseSyncTest, TestCase): + def setUp(self): + super().setUp() + fixture_utils.init_configuration_statuses() + + args = ( + mocks.configuration_api_get_configuration_statuses, + fixtures.API_CONFIGURATION_STATUS, + 'configuration_status' + ) + + +class TestSyncConfigurationTypesCommand(AbstractBaseSyncTest, TestCase): + def setUp(self): + super().setUp() + fixture_utils.init_configuration_types() + + args = ( + mocks.configuration_api_get_configuration_types, + fixtures.API_CONFIGURATION_TYPES, + 'configuration_type' + ) + + class TestSyncWorkRoleCommand(AbstractBaseSyncTest, TestCase): def setUp(self): super().setUp() @@ -870,6 +894,8 @@ def setUp(self): TestSyncProjectTeamMemberCommand, TestSyncSourceCommand, TestSyncCompanyNoteTypeCommand, + TestSyncConfigurationStatusesCommand, + TestSyncConfigurationTypesCommand, ] self.test_args = [] @@ -986,6 +1012,8 @@ def test_full_sync(self): 'project_team_member': models.ProjectTeamMember, 'source': models.Source, 'company_note_type': models.CompanyNoteType, + 'configuration_status': models.ConfigurationStatus, + 'configuration_type': models.ConfigurationType, } # Run partial sync first diff --git a/djconnectwise/tests/test_sync.py b/djconnectwise/tests/test_sync.py index 20f6739..4fac895 100644 --- a/djconnectwise/tests/test_sync.py +++ b/djconnectwise/tests/test_sync.py @@ -492,6 +492,56 @@ def _assert_fields(self, instance, json_data): self.assertEqual(instance.inactive_flag, json_data['inactiveFlag']) +class TestConfigurationStatusSynchronizer(TestCase, SynchronizerTestMixin): + synchronizer_class = sync.ConfigurationStatusSynchronizer + model_class = models.ConfigurationStatusTracker + fixture = fixtures.API_CONFIGURATION_STATUS + + def call_api(self, return_data): + return mocks.configuration_api_get_configuration_statuses(return_data) + + def _assert_fields(self, instance, json_data): + self.assertEqual(instance.id, json_data['id']) + self.assertEqual(instance.description, json_data['description']) + self.assertEqual(instance.closed_flag, json_data['closedFlag']) + self.assertEqual(instance.default_flag, json_data['defaultFlag']) + + def test_sync_update(self): + self._sync(self.fixture) + + json_data = self.fixture[0] + + instance_id = json_data['id'] + original = self.model_class.objects.get(id=instance_id) + + description = 'Some New Description' + new_json = deepcopy(self.fixture[0]) + new_json['description'] = description + new_json_list = [new_json] + + self._sync(new_json_list) + + changed = self.model_class.objects.get(id=instance_id) + + self.assertNotEqual(original.description, description) + self._assert_fields(changed, new_json) + + +class TestConfigurationTypeSynchronizer(TestCase, SynchronizerTestMixin): + synchronizer_class = sync.ConfigurationTypeSynchronizer + model_class = models.ConfigurationTypeTracker + fixture = fixtures.API_CONFIGURATION_TYPES + + def call_api(self, return_data): + return mocks.configuration_api_get_configuration_types(return_data) + + def _assert_fields(self, instance, json_data): + self.assertEqual(instance.id, json_data['id']) + self.assertEqual(instance.name, json_data['name']) + self.assertEqual(instance.inactive_flag, json_data['inactiveFlag']) + self.assertEqual(instance.system_flag, json_data['systemFlag']) + + class TestProjectPhaseSynchronizer(TestCase, SynchronizerTestMixin): synchronizer_class = sync.ProjectPhaseSynchronizer model_class = models.ProjectPhaseTracker From 99c4e1412ec0d2e4f2985cede77aa11e965f456a Mon Sep 17 00:00:00 2001 From: Zeryab Khan Date: Thu, 19 Sep 2024 18:31:18 +0500 Subject: [PATCH 2/3] [ISSUE-3680] Sync ConnectWise configuration records --- djconnectwise/api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/djconnectwise/api.py b/djconnectwise/api.py index f85e54d..338b92b 100644 --- a/djconnectwise/api.py +++ b/djconnectwise/api.py @@ -1435,7 +1435,8 @@ def get_configurations(self, company_id, *args, **kwargs): """ Retrieves configurations for a given company ID. """ - params = {'companyId': company_id} + api_conditions = f'company/id={company_id}' + params = {'conditions': api_conditions} return self.fetch_resource(self.ENDPOINT_CONFIGURATIONS, params=params, *args, **kwargs) From d64a5fb44826d7260e2c705db096e855370857d9 Mon Sep 17 00:00:00 2001 From: Zeryab Khan Date: Wed, 2 Oct 2024 19:13:25 +0500 Subject: [PATCH 3/3] [ISSUE-3680] Sync ConnectWise configuration records --- djconnectwise/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djconnectwise/api.py b/djconnectwise/api.py index 338b92b..ef8335e 100644 --- a/djconnectwise/api.py +++ b/djconnectwise/api.py @@ -1438,7 +1438,7 @@ def get_configurations(self, company_id, *args, **kwargs): api_conditions = f'company/id={company_id}' params = {'conditions': api_conditions} return self.fetch_resource(self.ENDPOINT_CONFIGURATIONS, - params=params, + params=params, should_page=True, *args, **kwargs) def get_configuration_statuses(self, *args, **kwargs):