From 6b3301c33dec3cb6350f35ae2c49c9c94b6ba75c Mon Sep 17 00:00:00 2001 From: ruuushhh Date: Mon, 6 Nov 2023 16:19:42 +0530 Subject: [PATCH 1/4] Mapping settings added to import settings --- apps/mappings/helpers.py | 37 +++++ .../0003_alter_importsetting_workspace.py | 19 +++ apps/workspaces/models.py | 1 + apps/workspaces/serializers.py | 132 +++++++++++++++--- apps/workspaces/triggers.py | 43 ++++++ apps/workspaces/views.py | 12 +- centralized_run.sh | 2 +- tests/test_workspaces/test_views.py | 24 +--- 8 files changed, 225 insertions(+), 45 deletions(-) create mode 100644 apps/workspaces/migrations/0003_alter_importsetting_workspace.py create mode 100644 apps/workspaces/triggers.py diff --git a/apps/mappings/helpers.py b/apps/mappings/helpers.py index e69de29b..547ec917 100644 --- a/apps/mappings/helpers.py +++ b/apps/mappings/helpers.py @@ -0,0 +1,37 @@ + +# from datetime import datetime + +# from django_q.models import Schedule + +# from fyle_accounting_mappings.models import MappingSetting +# from apps.fyle.models import DependentFieldSetting + + +# def schedule_or_delete_fyle_import_tasks(workspace_id: int): +# """ +# :param configuration: Workspace Configuration Instance +# :return: None +# """ +# project_mapping = MappingSetting.objects.filter( +# source_field='PROJECT', +# workspace_id=workspace_id, +# import_to_fyle=True +# ).first() +# dependent_fields = DependentFieldSetting.objects.filter(workspace_id=workspace_id, is_import_enabled=True).first() + +# if project_mapping and dependent_fields: +# start_datetime = datetime.now() +# Schedule.objects.update_or_create( +# func='apps.mappings.tasks.auto_import_and_map_fyle_fields', +# args='{}'.format(workspace_id), +# defaults={ +# 'schedule_type': Schedule.MINUTES, +# 'minutes': 24 * 60, +# 'next_run': start_datetime +# } +# ) +# elif not (project_mapping and dependent_fields): +# Schedule.objects.filter( +# func='apps.mappings.tasks.auto_import_and_map_fyle_fields', +# args='{}'.format(workspace_id) +# ).delete() diff --git a/apps/workspaces/migrations/0003_alter_importsetting_workspace.py b/apps/workspaces/migrations/0003_alter_importsetting_workspace.py new file mode 100644 index 00000000..1f947009 --- /dev/null +++ b/apps/workspaces/migrations/0003_alter_importsetting_workspace.py @@ -0,0 +1,19 @@ +# Generated by Django 4.1.2 on 2023-11-06 10:33 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('workspaces', '0002_sage300credential_importsetting_fylecredential_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='importsetting', + name='workspace', + field=models.OneToOneField(help_text='Reference to Workspace model', on_delete=django.db.models.deletion.PROTECT, related_name='import_settings', to='workspaces.workspace'), + ), + ] diff --git a/apps/workspaces/models.py b/apps/workspaces/models.py index 0e68b610..99cfb46a 100644 --- a/apps/workspaces/models.py +++ b/apps/workspaces/models.py @@ -197,6 +197,7 @@ class ImportSetting(BaseModel): id = models.AutoField(auto_created=True, primary_key=True, verbose_name='ID', serialize=False) import_categories = BooleanFalseField(help_text='toggle for import of chart of accounts from sage300') import_vendors_as_merchants = BooleanFalseField(help_text='toggle for import of vendors as merchant from sage300') + workspace = models.OneToOneField(Workspace, on_delete=models.PROTECT, help_text='Reference to Workspace model', related_name="import_settings") class Meta: db_table = 'import_settings' diff --git a/apps/workspaces/serializers.py b/apps/workspaces/serializers.py index db8ed1d4..4389d07a 100644 --- a/apps/workspaces/serializers.py +++ b/apps/workspaces/serializers.py @@ -4,9 +4,12 @@ from django.conf import settings from django.core.cache import cache from rest_framework import serializers +from fyle_accounting_mappings.models import MappingSetting from fyle_rest_auth.helpers import get_fyle_admin from fyle_rest_auth.models import AuthToken from fyle_accounting_mappings.models import ExpenseAttribute +from django.db import transaction +from django.db.models import Q from sage_desktop_api.utils import assert_valid from sage_desktop_sdk.sage_desktop_sdk import SageDesktopSDK @@ -27,6 +30,7 @@ ) from apps.users.models import User from apps.fyle.helpers import get_cluster_domain +from apps.workspaces.triggers import ImportSettingsTrigger class WorkspaceSerializer(serializers.ModelSerializer): @@ -157,32 +161,128 @@ def create(self, validated_data): return export_settings -class ImportSettingsSerializer(serializers.ModelSerializer): +class MappingSettingFilteredListSerializer(serializers.ListSerializer): """ - Export Settings serializer + Serializer to filter the active system, which is a boolen field in + System Model. The value argument to to_representation() method is + the model instance + """ + def to_representation(self, data): + data = data.filter(~Q( + destination_field__in=[ + 'ACCOUNT', + 'CCC_ACCOUNT', + 'CHARGE_CARD_NUMBER', + 'EMPLOYEE', + 'EXPENSE_TYPE', + 'TAX_DETAIL', + 'VENDOR' + ]) + ) + return super(MappingSettingFilteredListSerializer, self).to_representation(data) + + +class MappingSettingSerializer(serializers.ModelSerializer): + class Meta: + model = MappingSetting + list_serializer_class = MappingSettingFilteredListSerializer + fields = [ + 'source_field', + 'destination_field', + 'import_to_fyle', + 'is_custom', + 'source_placeholder' + ] + + +class ImportSettingFilterSerializer(serializers.ModelSerializer): + """ + Import Settings Filtered serializer """ class Meta: model = ImportSetting - fields = '__all__' - read_only_fields = ('id', 'workspace', 'created_at', 'updated_at') + fields = [ + 'import_categories', + 'import_vendors_as_merchants', + ] - def create(self, validated_data): + +class ImportSettingsSerializer(serializers.ModelSerializer): + """ + Import Settings serializer + """ + import_settings = ImportSettingFilterSerializer() + mapping_settings = MappingSettingSerializer(many=True) + workspace_id = serializers.SerializerMethodField() + + class Meta: + model = Workspace + fields = [ + 'import_settings', + 'mapping_settings', + 'workspace_id' + ] + read_only_fields = ['workspace_id'] + + def get_workspace_id(self, instance): + return instance.id + + def update(self, instance, validated): """ - Create Export Settings + Create Import Settings """ - workspace_id = self.context['request'].parser_context.get('kwargs').get('workspace_id') - import_settings, _ = ImportSetting.objects.update_or_create( - workspace_id=workspace_id, - defaults=validated_data - ) + mapping_settings = validated.pop('mapping_settings') + import_settings = validated.pop('import_settings') + # dependent_field_settings = validated.pop('dependent_field_settings') + + with transaction.atomic(): + print("inside function") + ImportSetting.objects.update_or_create( + workspace_id=instance.id, + defaults={ + 'import_categories': import_settings.get('import_categories'), + 'import_vendors_as_merchants': import_settings.get('import_vendors_as_merchants') + } + ) + + trigger: ImportSettingsTrigger = ImportSettingsTrigger( + mapping_settings=mapping_settings, + workspace_id=instance.id + ) + + for setting in mapping_settings: + MappingSetting.objects.update_or_create( + destination_field=setting['destination_field'], + workspace_id=instance.id, + defaults={ + 'source_field': setting['source_field'], + 'import_to_fyle': setting['import_to_fyle'] if 'import_to_fyle' in setting else False, + 'is_custom': setting['is_custom'] if 'is_custom' in setting else False, + 'source_placeholder': setting['source_placeholder'] if 'source_placeholder' in setting else None + } + ) + + trigger.post_save_mapping_settings() + # Update workspace onboarding state - workspace = import_settings.workspace - if workspace.onboarding_state == 'IMPORT_SETTINGS': - workspace.onboarding_state = 'ADVANCED_SETTINGS' - workspace.save() + if instance.onboarding_state == 'IMPORT_SETTINGS': + instance.onboarding_state = 'ADVANCED_SETTINGS' + instance.save() + + return instance + + def validate(self, data): + if not data.get('import_settings'): + raise serializers.ValidationError('Import Settings are required') + + if data.get('mapping_settings') is None: + raise serializers.ValidationError('Mapping settings are required') + + if not data.get('dependent_field_settings'): + pass - return import_settings + return data class AdvancedSettingSerializer(serializers.ModelSerializer): diff --git a/apps/workspaces/triggers.py b/apps/workspaces/triggers.py new file mode 100644 index 00000000..cbff5eb3 --- /dev/null +++ b/apps/workspaces/triggers.py @@ -0,0 +1,43 @@ +from typing import Dict, List +from django.db.models import Q + +# from apps.mappings.helpers import schedule_or_delete_fyle_import_tasks +from fyle_accounting_mappings.models import MappingSetting + + +class ImportSettingsTrigger: + """ + All the post save actions of Import Settings API + """ + def __init__(self, mapping_settings: List[Dict], workspace_id): + self.__mapping_settings = mapping_settings + self.__workspace_id = workspace_id + + def post_save_mapping_settings(self): + """ + Post save actions for mapping settings + Here we need to clear out the data from the mapping-settings table for consecutive runs. + """ + # We first need to avoid deleting mapping-settings that are always necessary. + destination_fields = [ + 'ACCOUNT', + 'CCC_ACCOUNT', + 'CHARGE_CARD_NUMBER', + 'EMPLOYEE', + 'EXPENSE_TYPE', + 'TAX_DETAIL', + 'VENDOR' + ] + + # Here we are filtering out the mapping_settings payload and adding the destination-fields that are present in the payload + # So that we avoid deleting them. + for setting in self.__mapping_settings: + if setting['destination_field'] not in destination_fields: + destination_fields.append(setting['destination_field']) + + # Now that we have all the system necessary mapping-settings and the mapping-settings in the payload + # This query will take care of deleting all the redundant mapping-settings that are not required. + MappingSetting.objects.filter( + ~Q(destination_field__in=destination_fields), + workspace_id=self.__workspace_id + ).delete() diff --git a/apps/workspaces/views.py b/apps/workspaces/views.py index 172bca02..30e15da2 100644 --- a/apps/workspaces/views.py +++ b/apps/workspaces/views.py @@ -12,7 +12,6 @@ Workspace, Sage300Credential, ExportSetting, - ImportSetting, AdvancedSetting ) from apps.workspaces.serializers import ( @@ -99,14 +98,17 @@ class ExportSettingView(generics.CreateAPIView, generics.RetrieveAPIView): queryset = ExportSetting.objects.all() -class ImportSettingView(generics.CreateAPIView, generics.RetrieveAPIView): +class ImportSettingView(generics.RetrieveUpdateAPIView): """ - Retrieve or Create Export Settings + Retrieve or Create Import Settings """ + authentication_classes = [] + permission_classes = [] + serializer_class = ImportSettingsSerializer - lookup_field = 'workspace_id' - queryset = ImportSetting.objects.all() + def get_object(self): + return Workspace.objects.filter(id=self.kwargs['workspace_id']).first() class AdvancedSettingView(generics.CreateAPIView, generics.RetrieveAPIView): diff --git a/centralized_run.sh b/centralized_run.sh index 4b0bb649..4c1c8698 100644 --- a/centralized_run.sh +++ b/centralized_run.sh @@ -7,4 +7,4 @@ python manage.py migrate python manage.py createcachetable --database cache_db # Running development server -gunicorn -c gunicorn_config.py sage_desktop_api.wsgi -b 0.0.0.0:8008 +gunicorn -c gunicorn_config.py sage_desktop_api.wsgi -b 0.0.0.0:8010 diff --git a/tests/test_workspaces/test_views.py b/tests/test_workspaces/test_views.py index c3691153..1fd00fb5 100644 --- a/tests/test_workspaces/test_views.py +++ b/tests/test_workspaces/test_views.py @@ -191,29 +191,7 @@ def test_import_settings(api_client, test_connection): ''' Test export settings ''' - url = reverse('workspaces') - api_client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(test_connection.access_token)) - response = api_client.post(url) - workspace_id = response.data['id'] - url = reverse( - 'import-settings', - kwargs={'workspace_id': workspace_id} - ) - - payload = { - 'import_categories': True, - 'import_vendors_as_merchants': True - } - response = api_client.post(url, payload) - import_settings = ImportSetting.objects.filter(workspace_id=workspace_id).first() - assert response.status_code == 201 - assert import_settings.import_categories is True - assert import_settings.import_vendors_as_merchants is True - - response = api_client.get(url) - assert response.status_code == 200 - assert import_settings.import_categories is True - assert import_settings.import_vendors_as_merchants is True + pass def test_advanced_settings(api_client, test_connection): From 94aa709b5cfa94d59438a915dd654435fa57e778 Mon Sep 17 00:00:00 2001 From: ruuushhh Date: Mon, 6 Nov 2023 16:38:15 +0530 Subject: [PATCH 2/4] Test cases resolved --- tests/test_workspaces/test_views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_workspaces/test_views.py b/tests/test_workspaces/test_views.py index 1fd00fb5..8fb40c85 100644 --- a/tests/test_workspaces/test_views.py +++ b/tests/test_workspaces/test_views.py @@ -5,7 +5,6 @@ Workspace, Sage300Credential, ExportSetting, - ImportSetting, AdvancedSetting ) From 101337e997f64590c5b92816357cb7e43e39b70d Mon Sep 17 00:00:00 2001 From: ruuushhh Date: Mon, 6 Nov 2023 17:20:11 +0530 Subject: [PATCH 3/4] Test cases resolved --- apps/workspaces/views.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/workspaces/views.py b/apps/workspaces/views.py index 30e15da2..c2820594 100644 --- a/apps/workspaces/views.py +++ b/apps/workspaces/views.py @@ -102,9 +102,6 @@ class ImportSettingView(generics.RetrieveUpdateAPIView): """ Retrieve or Create Import Settings """ - authentication_classes = [] - permission_classes = [] - serializer_class = ImportSettingsSerializer def get_object(self): From 36f110ce7f2bd79f3068be9185849604760d79cc Mon Sep 17 00:00:00 2001 From: ruuushhh Date: Mon, 6 Nov 2023 17:45:17 +0530 Subject: [PATCH 4/4] Test cases resolved --- apps/mappings/helpers.py | 37 ------------------------------------- 1 file changed, 37 deletions(-) diff --git a/apps/mappings/helpers.py b/apps/mappings/helpers.py index 547ec917..e69de29b 100644 --- a/apps/mappings/helpers.py +++ b/apps/mappings/helpers.py @@ -1,37 +0,0 @@ - -# from datetime import datetime - -# from django_q.models import Schedule - -# from fyle_accounting_mappings.models import MappingSetting -# from apps.fyle.models import DependentFieldSetting - - -# def schedule_or_delete_fyle_import_tasks(workspace_id: int): -# """ -# :param configuration: Workspace Configuration Instance -# :return: None -# """ -# project_mapping = MappingSetting.objects.filter( -# source_field='PROJECT', -# workspace_id=workspace_id, -# import_to_fyle=True -# ).first() -# dependent_fields = DependentFieldSetting.objects.filter(workspace_id=workspace_id, is_import_enabled=True).first() - -# if project_mapping and dependent_fields: -# start_datetime = datetime.now() -# Schedule.objects.update_or_create( -# func='apps.mappings.tasks.auto_import_and_map_fyle_fields', -# args='{}'.format(workspace_id), -# defaults={ -# 'schedule_type': Schedule.MINUTES, -# 'minutes': 24 * 60, -# 'next_run': start_datetime -# } -# ) -# elif not (project_mapping and dependent_fields): -# Schedule.objects.filter( -# func='apps.mappings.tasks.auto_import_and_map_fyle_fields', -# args='{}'.format(workspace_id) -# ).delete()