diff --git a/apps/fyle/constants.py b/apps/fyle/constants.py new file mode 100644 index 0000000..2171b9a --- /dev/null +++ b/apps/fyle/constants.py @@ -0,0 +1,22 @@ +DEFAULT_FYLE_CONDITIONS = [ + { + 'field_name': 'employee_email', + 'type': 'SELECT', + 'is_custom': False + }, + { + 'field_name': 'claim_number', + 'type': 'TEXT', + 'is_custom': False + }, + { + 'field_name': 'report_title', + 'type': 'TEXT', + 'is_custom': False + }, + { + 'field_name': 'spent_at', + 'type': 'DATE', + 'is_custom': False + } +] diff --git a/apps/fyle/helpers.py b/apps/fyle/helpers.py index 6cfb45d..a65641d 100644 --- a/apps/fyle/helpers.py +++ b/apps/fyle/helpers.py @@ -2,6 +2,11 @@ import requests from django.conf import settings +from fyle_integrations_platform_connector import PlatformConnector + +from apps.workspaces.models import FyleCredential +from apps.fyle.constants import DEFAULT_FYLE_CONDITIONS + def post_request(url, body, refresh_token=None): """ @@ -50,3 +55,27 @@ def get_cluster_domain(refresh_token: str) -> str: cluster_api_url = '{0}/oauth/cluster/'.format(settings.FYLE_BASE_URL) return post_request(cluster_api_url, {}, refresh_token)['cluster_domain'] + + +def get_expense_fields(workspace_id: int): + """ + Get expense custom fields from fyle + :param workspace_id: (int) + :return: list of custom expense fields + """ + + fyle_credentails = FyleCredential.objects.get(workspace_id=workspace_id) + platform = PlatformConnector(fyle_credentails) + custom_fields = platform.expense_custom_fields.list_all() + + response = [] + response.extend(DEFAULT_FYLE_CONDITIONS) + for custom_field in custom_fields: + if custom_field['type'] in ('SELECT', 'NUMBER', 'TEXT', 'BOOLEAN'): + response.append({ + 'field_name': custom_field['field_name'], + 'type': custom_field['type'], + 'is_custom': custom_field['is_custom'] + }) + + return response diff --git a/apps/fyle/serializers.py b/apps/fyle/serializers.py index 30463e8..5b39a2a 100644 --- a/apps/fyle/serializers.py +++ b/apps/fyle/serializers.py @@ -3,14 +3,18 @@ """ import logging from datetime import datetime, timezone +from django.db.models import Q from rest_framework import serializers from rest_framework.response import Response from rest_framework.exceptions import APIException from rest_framework.views import status from fyle_integrations_platform_connector import PlatformConnector +from fyle_accounting_mappings.models import ExpenseAttribute + from apps.fyle.models import ExpenseFilter from apps.workspaces.models import Workspace, FyleCredential +from apps.fyle.helpers import get_expense_fields logger = logging.getLogger(__name__) logger.level = logging.INFO @@ -66,3 +70,56 @@ def create(self, validated_data): except Exception as exception: logger.error('Something unexpected happened workspace_id: %s %s', workspace_id, exception) raise APIException("Internal Server Error", code='server_error') + + +class FyleFieldsSerializer(serializers.Serializer): + """ + Fyle Fields Serializer + """ + + attribute_type = serializers.CharField() + display_name = serializers.CharField() + is_dependant = serializers.BooleanField() + + def format_fyle_fields(self, workspace_id): + """ + Get Fyle Fields + """ + + attribute_types = ['EMPLOYEE', 'CATEGORY', 'PROJECT', 'COST_CENTER', 'TAX_GROUP', 'CORPORATE_CARD', 'MERCHANT'] + + attributes = ExpenseAttribute.objects.filter( + ~Q(attribute_type__in=attribute_types), + workspace_id=workspace_id + ).values('attribute_type', 'display_name', 'detail__is_dependent').distinct() + + attributes_list = [ + {'attribute_type': 'COST_CENTER', 'display_name': 'Cost Center', 'is_dependant': False}, + {'attribute_type': 'PROJECT', 'display_name': 'Project', 'is_dependant': False} + ] + + for attr in attributes: + attributes_list.append({ + 'attribute_type': attr['attribute_type'], + 'display_name': attr['display_name'], + 'is_dependant': attr['detail__is_dependent'] + }) + + return attributes_list + + +class ExpenseFieldSerializer(serializers.Serializer): + """ + Workspace Admin Serializer + """ + expense_fields = serializers.SerializerMethodField() + + def get_expense_fields(self, validated_data): + """ + Get Expense Fields + """ + + workspace_id = self.context['request'].parser_context.get('kwargs').get('workspace_id') + expense_fields = get_expense_fields(workspace_id=workspace_id) + + return expense_fields diff --git a/apps/fyle/urls.py b/apps/fyle/urls.py index 88410cb..f913444 100644 --- a/apps/fyle/urls.py +++ b/apps/fyle/urls.py @@ -15,11 +15,13 @@ """ from django.urls import path -from apps.fyle.views import ExpenseFilterView, ExpenseFilterDeleteView, ImportFyleAttributesView +from apps.fyle.views import ExpenseFilterView, ExpenseFilterDeleteView, ImportFyleAttributesView, FyleFieldsView, CustomFieldView urlpatterns = [ path('expense_filters//', ExpenseFilterDeleteView.as_view(), name='expense-filters'), path('expense_filters/', ExpenseFilterView.as_view(), name='expense-filters'), path('import_attributes/', ImportFyleAttributesView.as_view(), name='import-fyle-attributes'), + path('fields/', FyleFieldsView.as_view(), name='fyle-fields'), + path('expense_fields/', CustomFieldView.as_view(), name='fyle-expense-fields'), ] diff --git a/apps/fyle/views.py b/apps/fyle/views.py index 1b9c86d..71d2e84 100644 --- a/apps/fyle/views.py +++ b/apps/fyle/views.py @@ -1,7 +1,8 @@ import logging from rest_framework import generics from ms_business_central_api.utils import LookupFieldMixin -from apps.fyle.serializers import ExpenseFilterSerializer, ImportFyleAttributesSerializer +from apps.workspaces.models import Workspace +from apps.fyle.serializers import ExpenseFilterSerializer, ImportFyleAttributesSerializer, FyleFieldsSerializer, ExpenseFieldSerializer from apps.fyle.models import ExpenseFilter logger = logging.getLogger(__name__) @@ -31,3 +32,23 @@ class ImportFyleAttributesView(generics.CreateAPIView): Import Fyle Attributes View """ serializer_class = ImportFyleAttributesSerializer + + +class FyleFieldsView(generics.ListAPIView): + """ + Fyle Fields view + """ + + serializer_class = FyleFieldsSerializer + + def get_queryset(self): + return FyleFieldsSerializer().format_fyle_fields(self.kwargs["workspace_id"]) + + +class CustomFieldView(generics.ListAPIView): + """ + Custom Field view + """ + + serializer_class = ExpenseFieldSerializer + queryset = Workspace.objects.all() diff --git a/tests/test_fyle/fixtures.py b/tests/test_fyle/fixtures.py index c3fdcb8..06c33d9 100644 --- a/tests/test_fyle/fixtures.py +++ b/tests/test_fyle/fixtures.py @@ -153,4 +153,52 @@ }, ], }, + "fyle_fields_response": [ + { + 'attribute_type': 'COST_CENTER', + 'display_name': 'Cost Center', + 'is_dependant': False + }, + { + 'attribute_type': 'PROJECT', + 'display_name': 'Project', + 'is_dependant': False + } + ], + "fyle_expense_custom_fields": [ + {"field_name": "employee_email", "type": "SELECT", "is_custom": False}, + {"field_name": "claim_number", "type": "TEXT", "is_custom": False}, + {"field_name": "report_title", "type": "TEXT", "is_custom": False}, + {"field_name": "spent_at", "type": "DATE", "is_custom": False}, + {"field_name": "Class", "type": "SELECT", "is_custom": True}, + {"field_name": "Fyle Categories", "type": "SELECT", "is_custom": True}, + {"field_name": "Operating System", "type": "SELECT", "is_custom": True}, + {"field_name": "User Dimension", "type": "SELECT", "is_custom": True}, + {"field_name": "Asdasdas", "type": "SELECT", "is_custom": True}, + {"field_name": "Nilesh Custom Field", "type": "SELECT", "is_custom": True}, + ], + "get_all_custom_fields": [ + { + "data": [ + { + "category_ids": [142151], + "code": None, + "column_name": "text_column6", + "created_at": "2021-10-22T07:50:04.613487+00:00", + "default_value": None, + "field_name": "Class", + "id": 197380, + "is_custom": True, + "is_enabled": True, + "is_mandatory": False, + "options": ["Servers", "Home", "Office"], + "org_id": "orGcBCVPijjO", + "placeholder": "Select Class", + "seq": 1, + "type": "SELECT", + "updated_at": "2023-01-01T05:35:26.345303+00:00", + }, + ] + } + ], } diff --git a/tests/test_fyle/test_views.py b/tests/test_fyle/test_views.py index 0e80be7..62ff5f4 100644 --- a/tests/test_fyle/test_views.py +++ b/tests/test_fyle/test_views.py @@ -63,3 +63,40 @@ def test_import_fyle_attributes(mocker, api_client, test_connection, create_temp mock_call.side_effect = Exception() response = api_client.post(url, payload) assert response.status_code == 500 + + +def test_fyle_fields(api_client, test_connection, create_temp_workspace, add_fyle_credentials): + + access_token = test_connection.access_token + + url = reverse('fyle-fields', kwargs={'workspace_id': 1}) + + api_client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(access_token)) + + response = api_client.get(url) + assert response.status_code == 200 + + response = json.loads(response.content) + assert response['results'] == data['fyle_fields_response'] + + +def test_fyle_expense_fields(api_client, test_connection, create_temp_workspace, add_fyle_credentials, mocker): + workspace_id = 1 + + access_token = test_connection.access_token + url = reverse('fyle-expense-fields', kwargs={'workspace_id': workspace_id}) + + mocker.patch( + 'fyle.platform.apis.v1beta.admin.ExpenseFields.list_all', + return_value = data['get_all_custom_fields'], + ) + + api_client.credentials(HTTP_AUTHORIZATION="Bearer {}".format(access_token)) + + response = api_client.get(url) + assert response.status_code == 200 + response = json.loads(response.content) + + assert ( + dict_compare_keys(response['results'], data['fyle_expense_custom_fields']) == [] + ), 'expense group api return diffs in keys'