From 76018abbdaefb01955787f4e55cf2f2196e0bbe5 Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Mon, 23 Oct 2023 12:15:43 +0530 Subject: [PATCH 01/18] add fyle expense fields --- apps/fyle/constants.py | 22 ++++++++++++++++++++++ apps/fyle/urls.py | 7 +++++++ apps/fyle/views.py | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 apps/fyle/constants.py diff --git a/apps/fyle/constants.py b/apps/fyle/constants.py new file mode 100644 index 00000000..2171b9aa --- /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/urls.py b/apps/fyle/urls.py index e69de29b..c7dc3448 100644 --- a/apps/fyle/urls.py +++ b/apps/fyle/urls.py @@ -0,0 +1,7 @@ +from django.urls import path + +from apps.fyle.views import CustomFieldView + +urlpatterns = [ + path('expense_fields/', CustomFieldView.as_view(), name='custom-field'), +] diff --git a/apps/fyle/views.py b/apps/fyle/views.py index e69de29b..f8a803e4 100644 --- a/apps/fyle/views.py +++ b/apps/fyle/views.py @@ -0,0 +1,40 @@ +from rest_framework.views import status +from rest_framework import generics +from rest_framework.response import Response + +from fyle_integrations_platform_connector import PlatformConnector + +from apps.workspaces.models import FyleCredential +from apps.fyle.constants import DEFAULT_FYLE_CONDITIONS + + +class CustomFieldView(generics.RetrieveAPIView): + """ + Custom Field view + """ + def get(self, request, *args, **kwargs): + """ + Get Custom Fields + """ + workspace_id = self.kwargs['workspace_id'] + + 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( + data=response, + status=status.HTTP_200_OK + ) From 983c41f9b8c9512633139b61755dffcdb131a2a0 Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Mon, 23 Oct 2023 14:28:50 +0530 Subject: [PATCH 02/18] add support for expense fields with tests --- apps/fyle/models.py | 8 ++++ apps/fyle/urls.py | 2 +- apps/workspaces/urls.py | 3 +- tests/helper.py | 37 +++++++++++++++++ tests/test_fyle/fixtures.py | 78 +++++++++++++++++++++++++---------- tests/test_fyle/test_views.py | 36 ++++++++++++++++ 6 files changed, 141 insertions(+), 23 deletions(-) create mode 100644 tests/helper.py create mode 100644 tests/test_fyle/test_views.py diff --git a/apps/fyle/models.py b/apps/fyle/models.py index e69de29b..a2ce7cec 100644 --- a/apps/fyle/models.py +++ b/apps/fyle/models.py @@ -0,0 +1,8 @@ + + +class Reimbursement: + """ + Creating a dummy class to be able to user + fyle_integrations_platform_connector correctly + """ + pass diff --git a/apps/fyle/urls.py b/apps/fyle/urls.py index c7dc3448..a0789ae0 100644 --- a/apps/fyle/urls.py +++ b/apps/fyle/urls.py @@ -3,5 +3,5 @@ from apps.fyle.views import CustomFieldView urlpatterns = [ - path('expense_fields/', CustomFieldView.as_view(), name='custom-field'), + path("expense_fields/", CustomFieldView.as_view(), name="fyle-expense-fields"), ] diff --git a/apps/workspaces/urls.py b/apps/workspaces/urls.py index 480477ab..f2fa4a66 100644 --- a/apps/workspaces/urls.py +++ b/apps/workspaces/urls.py @@ -34,7 +34,8 @@ ] other_app_paths = [ - path('/sage_300/', include('apps.sage300.urls')), + path('/sage_300/', include('apps.sage300.urls')), + path('/fyle/', include('apps.fyle.urls')) ] urlpatterns = [] diff --git a/tests/helper.py b/tests/helper.py new file mode 100644 index 00000000..cc24a019 --- /dev/null +++ b/tests/helper.py @@ -0,0 +1,37 @@ +import json +from os import path + +def dict_compare_keys(d1, d2, key_path=''): + """ + Compare two dicts recursively and see if dict1 has any keys that dict2 does not + Returns: list of key paths + """ + res = [] + if not d1: + return res + if not isinstance(d1, dict): + return res + for k in d1: + if k not in d2: + missing_key_path = f'{key_path}->{k}' + res.append(missing_key_path) + else: + if isinstance(d1[k], dict): + key_path1 = f'{key_path}->{k}' + res1 = dict_compare_keys(d1[k], d2[k], key_path1) + res = res + res1 + elif isinstance(d1[k], list): + key_path1 = f'{key_path}->{k}[0]' + dv1 = d1[k][0] if len(d1[k]) > 0 else None + dv2 = d2[k][0] if len(d2[k]) > 0 else None + res1 = dict_compare_keys(dv1, dv2, key_path1) + res = res + res1 + return res + + +def get_response_dict(filename): + basepath = path.dirname(__file__) + filepath = path.join(basepath, filename) + mock_json = open(filepath, 'r').read() + mock_dict = json.loads(mock_json) + return mock_dict diff --git a/tests/test_fyle/fixtures.py b/tests/test_fyle/fixtures.py index 95dea99b..a638f882 100644 --- a/tests/test_fyle/fixtures.py +++ b/tests/test_fyle/fixtures.py @@ -1,27 +1,63 @@ fixtures = { - 'get_my_profile': { - 'data': { - 'org': { - 'currency': 'USD', - 'domain': 'fyleforqvd.com', - 'id': 'orNoatdUnm1w', - 'name': 'Fyle For MS Dynamics Demo', + "get_my_profile": { + "data": { + "org": { + "currency": "USD", + "domain": "fyleforqvd.com", + "id": "orNoatdUnm1w", + "name": "Fyle For MS Dynamics Demo", }, - 'org_id': 'orNoatdUnm1w', - 'roles': [ - 'FYLER', - 'VERIFIER', - 'PAYMENT_PROCESSOR', - 'FINANCE', - 'ADMIN', - 'AUDITOR', + "org_id": "orNoatdUnm1w", + "roles": [ + "FYLER", + "VERIFIER", + "PAYMENT_PROCESSOR", + "FINANCE", + "ADMIN", + "AUDITOR", ], - 'user': { - 'email': 'ashwin.t@fyle.in', - 'full_name': 'Joanna', - 'id': 'usqywo0f3nBY' + "user": { + "email": "ashwin.t@fyle.in", + "full_name": "Joanna", + "id": "usqywo0f3nBY", }, - 'user_id': 'usqywo0f3nBY', + "user_id": "usqywo0f3nBY", } - } + }, + "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 new file mode 100644 index 00000000..2df61536 --- /dev/null +++ b/tests/test_fyle/test_views.py @@ -0,0 +1,36 @@ +import pytest +import json +from django.urls import reverse + +from tests.helper import dict_compare_keys +from tests.test_fyle.fixtures import fixtures as fyle_fixtures + + +@pytest.mark.django_db(databases=["default"]) +def test_fyle_expense_fields( + api_client, + test_connection, + create_temp_workspace, + add_fyle_credentials, + add_sage300_creds, + 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.expense_fields.list_all", + return_value=fyle_fixtures["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, fyle_fixtures["fyle_expense_custom_fields"]) == [] + ), "expense group api return diffs in keys" From 80c8a2d08f6ccb9a83dc2a81a0bca61d4a3007fd Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Mon, 23 Oct 2023 14:37:00 +0530 Subject: [PATCH 03/18] add support for github workflow for pytest --- .github/workflows/tests.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..96dd5d6e --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,30 @@ +name: Continuous Integration + +on: + pull_request: + types: [assigned, opened, synchronize, reopened] + +jobs: + pytest: + runs-on: ubuntu-latest + environment: CI Environment + steps: + - uses: actions/checkout@v2 + - name: Bring up Services and Run Tests + run: | + docker-compose -f docker-compose-pipeline.yml build + docker-compose -f docker-compose-pipeline.yml up -d + docker-compose -f docker-compose-pipeline.yml exec -T api pytest tests/ + echo "STATUS=$(cat pytest-coverage.txt | grep 'Required test' | awk '{ print $1 }')" >> $GITHUB_ENV + echo "FAILED=$(cat test-reports/report.xml | awk -F'=' '{print $5}' | awk -F' ' '{gsub(/"/, "", $1); print $1}')" >> $GITHUB_ENV + - name: Upload coverage reports to Codecov with GitHub Action + uses: codecov/codecov-action@v3 + - name: Pytest coverage comment + uses: MishaKav/pytest-coverage-comment@main + if: ${{ always() && github.ref != 'refs/heads/master' }} + with: + create-new-comment: true + junitxml-path: ./test-reports/report.xml + - name: Evaluate Coverage + if: ${{ (env.STATUS == 'FAIL') || (env.FAILED > 0) }} + run: exit 1 From 17fdaed27b00e3ea5cfaccb7a367f7e82a25f977 Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Mon, 23 Oct 2023 17:20:23 +0530 Subject: [PATCH 04/18] fix tsts --- apps/sage300/helpers.py | 1 - apps/sage300/serializers.py | 2 +- apps/sage300/utils.py | 10 +-- apps/sage300/views.py | 1 - apps/workspaces/models.py | 2 +- apps/workspaces/serializers.py | 5 +- apps/workspaces/urls.py | 2 +- apps/workspaces/views.py | 1 - tests/conftest.py | 2 +- tests/test_sage300/test_views.py | 1 + tests/test_workspaces/test_views.py | 106 ++++++++++------------------ 11 files changed, 47 insertions(+), 86 deletions(-) diff --git a/apps/sage300/helpers.py b/apps/sage300/helpers.py index 4b0e85b4..952190a4 100644 --- a/apps/sage300/helpers.py +++ b/apps/sage300/helpers.py @@ -5,7 +5,6 @@ from django.utils.module_loading import import_string from apps.workspaces.models import Workspace, Sage300Credential -from apps.sage300.utils import SageDesktopConnector logger = logging.getLogger(__name__) diff --git a/apps/sage300/serializers.py b/apps/sage300/serializers.py index 2e49badd..678ba976 100644 --- a/apps/sage300/serializers.py +++ b/apps/sage300/serializers.py @@ -21,7 +21,7 @@ def create(self, validated_data): try: # Get the workspace ID from the URL kwargs workspace_id = self.context['request'].parser_context['kwargs']['workspace_id'] - + # Check if the 'refresh' field is provided in the request data refresh_dimension = self.context['request'].data.get('refresh', False) diff --git a/apps/sage300/utils.py b/apps/sage300/utils.py index b309f858..1a72254e 100644 --- a/apps/sage300/utils.py +++ b/apps/sage300/utils.py @@ -1,8 +1,8 @@ -from django.conf import settings from fyle_accounting_mappings.models import DestinationAttribute from apps.workspaces.models import Sage300Credential from sage_desktop_sdk.sage_desktop_sdk import SageDesktopSDK + class SageDesktopConnector: """ Sage300 utility functions for syncing data from Sage Desktop SDK to your application @@ -25,7 +25,6 @@ def __init__(self, credentials_object: Sage300Credential, workspace_id: int): self.workspace_id = workspace_id - def _create_destination_attribute(self, attribute_type, display_name, value, destination_id, active, detail): """ Create a destination attribute object @@ -46,7 +45,6 @@ def _create_destination_attribute(self, attribute_type, display_name, value, des 'detail': detail } - def _sync_data(self, data, attribute_type, display_name, workspace_id, field_names): """ Synchronize data from Sage Desktop SDK to your application @@ -73,7 +71,6 @@ def _sync_data(self, data, attribute_type, display_name, workspace_id, field_nam DestinationAttribute.bulk_create_or_update_destination_attributes( destination_attributes, attribute_type, workspace_id, True) - def sync_accounts(self): """ Synchronize accounts from Sage Desktop SDK to your application @@ -82,7 +79,6 @@ def sync_accounts(self): self._sync_data(accounts, 'ACCOUNT', 'accounts', self.workspace_id, ['code', 'version']) return [] - def sync_vendors(self): """ Synchronize vendors from Sage Desktop SDK to your application @@ -95,7 +91,6 @@ def sync_vendors(self): self._sync_data(vendors, 'VENDOR', 'vendor', self.workspace_id, field_names) return [] - def sync_jobs(self): """ Synchronize jobs from Sage Desktop SDK to your application @@ -107,7 +102,6 @@ def sync_jobs(self): self._sync_data(jobs, 'JOB', 'job', self.workspace_id, field_names) return [] - def sync_cost_codes(self): """ Synchronize cost codes from Sage Desktop SDK to your application @@ -117,7 +111,6 @@ def sync_cost_codes(self): self._sync_data(cost_codes, 'COST_CODE', 'cost_code', self.workspace_id, field_names) return [] - def sync_categories(self): """ Synchronize categories from Sage Desktop SDK to your application @@ -127,7 +120,6 @@ def sync_categories(self): self._sync_data(categories, 'CATEGORY', 'category', self.workspace_id, field_names) return [] - def sync_commitments(self): """ Synchronize commitments from Sage Desktop SDK to your application diff --git a/apps/sage300/views.py b/apps/sage300/views.py index d3b6cc45..353c6f8c 100644 --- a/apps/sage300/views.py +++ b/apps/sage300/views.py @@ -1,4 +1,3 @@ -from datetime import datetime import logging from rest_framework import generics diff --git a/apps/workspaces/models.py b/apps/workspaces/models.py index 913ed11a..0793baf6 100644 --- a/apps/workspaces/models.py +++ b/apps/workspaces/models.py @@ -180,7 +180,7 @@ class ImportSetting(BaseModel): 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') - + class Meta: db_table = 'import_settings' diff --git a/apps/workspaces/serializers.py b/apps/workspaces/serializers.py index 712a8df9..22a82c5b 100644 --- a/apps/workspaces/serializers.py +++ b/apps/workspaces/serializers.py @@ -7,7 +7,6 @@ from fyle_rest_auth.helpers import get_fyle_admin from fyle_rest_auth.models import AuthToken -from apps.fyle.helpers import get_cluster_domain from sage_desktop_api.utils import assert_valid from sage_desktop_sdk.sage_desktop_sdk import SageDesktopSDK from sage_desktop_sdk.exceptions import ( @@ -81,7 +80,7 @@ def create(self, validated_data): class Sage300CredentialSerializer(serializers.ModelSerializer): - + api_key = serializers.CharField(required=False) api_secret = serializers.CharField(required=False) @@ -89,7 +88,6 @@ class Meta: model = Sage300Credential fields = '__all__' - def create(self, validated_data): try: username = validated_data.get('username') @@ -166,6 +164,7 @@ class Meta: model = ImportSetting fields = '__all__' read_only_fields = ('id', 'workspace', 'created_at', 'updated_at') + def create(self, validated_data): """ Create Export Settings diff --git a/apps/workspaces/urls.py b/apps/workspaces/urls.py index 480477ab..502c5ee6 100644 --- a/apps/workspaces/urls.py +++ b/apps/workspaces/urls.py @@ -34,7 +34,7 @@ ] other_app_paths = [ - path('/sage_300/', include('apps.sage300.urls')), + path('/sage_300/', include('apps.sage300.urls')), ] urlpatterns = [] diff --git a/apps/workspaces/views.py b/apps/workspaces/views.py index 110e1456..4c3c43a8 100644 --- a/apps/workspaces/views.py +++ b/apps/workspaces/views.py @@ -3,7 +3,6 @@ from rest_framework import generics from rest_framework.views import Response, status -from rest_framework.permissions import IsAuthenticated from fyle_rest_auth.utils import AuthUtils diff --git a/tests/conftest.py b/tests/conftest.py index 97246ea9..8c9ba75b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,7 +12,7 @@ from apps.fyle.helpers import get_access_token from apps.workspaces.models import ( - Workspace, + Workspace, FyleCredential, Sage300Credential ) diff --git a/tests/test_sage300/test_views.py b/tests/test_sage300/test_views.py index 81ff6e0c..9927de67 100644 --- a/tests/test_sage300/test_views.py +++ b/tests/test_sage300/test_views.py @@ -3,6 +3,7 @@ from apps.workspaces.models import Sage300Credential + def test_sync_dimensions(api_client, test_connection, mocker, create_temp_workspace, add_sage300_creds): workspace_id = 1 diff --git a/tests/test_workspaces/test_views.py b/tests/test_workspaces/test_views.py index c227354a..ed7403e2 100644 --- a/tests/test_workspaces/test_views.py +++ b/tests/test_workspaces/test_views.py @@ -1,23 +1,13 @@ import json -import pytest # noqa from django.urls import reverse - -from apps.workspaces.models import ( - Workspace, - Sage300Credential, - ExportSetting, - ImportSetting, - AdvancedSetting -) +from apps.workspaces.models import Workspace, Sage300Credential, ExportSetting, ImportSetting, AdvancedSetting def test_post_of_workspace(api_client, test_connection): ''' Test post of workspace ''' - url = reverse( - 'workspaces' - ) + url = reverse('workspaces') api_client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(test_connection.access_token)) response = api_client.post(url) @@ -38,9 +28,7 @@ def test_get_of_workspace(api_client, test_connection): ''' Test get of workspace ''' - url = reverse( - 'workspaces' - ) + url = reverse('workspaces') api_client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(test_connection.access_token)) response = api_client.get(url) @@ -66,26 +54,23 @@ def test_post_of_sage300_creds(api_client, test_connection, mocker): Test post of sage300 creds ''' - url = reverse( - 'workspaces' - ) + url = reverse('workspaces') api_client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(test_connection.access_token)) response = api_client.post(url) url = reverse( - 'sage300-creds', kwargs={ - 'workspace_id': response.data['id'] - } + 'sage300-creds', + kwargs={'workspace_id': response.data['id']} ) - payload = { + payload = { 'identifier': "indentifier", - 'password': "passeord", + 'password': "passeord", 'username': "username", 'workspace': response.data['id'] } - + mocker.patch( 'sage_desktop_sdk.core.client.Client.update_cookie', return_value={'text': {'Result': 2}} @@ -100,28 +85,24 @@ def test_get_of_sage300_creds(api_client, test_connection): Test get of workspace ''' - url = reverse( - 'workspaces' - ) + url = reverse('workspaces') api_client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(test_connection.access_token)) response = api_client.post(url) url = reverse( - 'sage300-creds', kwargs={ - 'workspace_id': response.data['id'] - } + 'sage300-creds', + kwargs={'workspace_id': response.data['id']} ) Sage300Credential.objects.create( - identifier='identifier', - username='username', - password='password', - workspace_id=response.data['id'], - api_key='apiley', - api_secret='apisecret' - ) - + identifier='identifier', + username='username', + password='password', + workspace_id=response.data['id'], + api_key='apiley', + api_secret='apisecret' + ) api_client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(test_connection.access_token)) response = api_client.get(url) @@ -135,9 +116,7 @@ def test_export_settings(api_client, test_connection): ''' Test export settings ''' - url = reverse( - 'workspaces' - ) + url = reverse('workspaces') api_client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(test_connection.access_token)) response = api_client.post(url) @@ -145,9 +124,8 @@ def test_export_settings(api_client, test_connection): workspace_id = response.data['id'] url = reverse( - 'export-settings', kwargs={ - 'workspace_id': workspace_id - } + 'export-settings', + kwargs={'workspace_id': workspace_id} ) api_client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(test_connection.access_token)) @@ -160,7 +138,7 @@ def test_export_settings(api_client, test_connection): 'reimbursable_expense_date': 'LAST_SPENT_AT', 'reimbursable_expense_grouped_by': 'EXPENSE', 'credit_card_expense_export_type': 'JOURNAL_ENTRY', - 'credit_card_expense_state': 'PAID', + 'credit_card_expense_state': 'PAID', 'credit_card_expense_grouped_by': 'EXPENSE', 'credit_card_expense_date': 'CREATED_AT', 'default_credit_card_account_name': 'credit card account', @@ -210,16 +188,13 @@ def test_import_settings(api_client, test_connection): ''' Test export settings ''' - url = reverse( - 'workspaces' - ) + 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 - } + 'import-settings', + kwargs={'workspace_id': workspace_id} ) payload = { @@ -229,22 +204,20 @@ def test_import_settings(api_client, test_connection): 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 == True - assert import_settings.import_vendors_as_merchants == True + 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 == True - assert import_settings.import_vendors_as_merchants == True + assert import_settings.import_categories is True + assert import_settings.import_vendors_as_merchants is True def test_advanced_settings(api_client, test_connection): ''' Test advanced settings ''' - url = reverse( - 'workspaces' - ) + url = reverse('workspaces') api_client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(test_connection.access_token)) response = api_client.post(url) @@ -252,9 +225,8 @@ def test_advanced_settings(api_client, test_connection): workspace_id = response.data['id'] url = reverse( - 'advanced-settings', kwargs={ - 'workspace_id': workspace_id - } + 'advanced-settings', + kwargs={'workspace_id': workspace_id} ) api_client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(test_connection.access_token)) @@ -291,8 +263,8 @@ def test_advanced_settings(api_client, test_connection): 'report_number', 'expense_link' ] - assert response.data['schedule_is_enabled'] == False - assert response.data['schedule_id'] == None + assert response.data['schedule_is_enabled'] is False + assert response.data['schedule_id'] is None assert response.data['emails_selected'] == [ { 'name': 'Shwetabh Kumar', @@ -314,8 +286,8 @@ def test_advanced_settings(api_client, test_connection): 'report_number', 'expense_link' ] - assert response.data['schedule_is_enabled'] == False - assert response.data['schedule_id'] == None + assert response.data['schedule_is_enabled'] is False + assert response.data['schedule_id'] is None assert response.data['emails_selected'] == [ { 'name': 'Shwetabh Kumar', @@ -340,8 +312,8 @@ def test_advanced_settings(api_client, test_connection): 'purpose', 'report_number' ] - assert response.data['schedule_is_enabled'] == False - assert response.data['schedule_id'] == None + assert response.data['schedule_is_enabled'] is False + assert response.data['schedule_id'] is None assert response.data['emails_selected'] == [ { 'name': 'Shwetabh Kumar', From 3c39af4418acee055b32d9aae4209810f79b5e58 Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Mon, 23 Oct 2023 17:21:36 +0530 Subject: [PATCH 05/18] fix tsts --- sage_desktop_api/models/fields.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/sage_desktop_api/models/fields.py b/sage_desktop_api/models/fields.py index f0b449ad..e6cfd7c6 100644 --- a/sage_desktop_api/models/fields.py +++ b/sage_desktop_api/models/fields.py @@ -1,5 +1,6 @@ from django.db import models + class StringNotNullField(models.CharField): description = "Custom String with Not Null" @@ -9,6 +10,7 @@ def __init__(self, *args, **kwargs): kwargs['help_text'] = kwargs.get('help_text', 'string field with null false') super(StringNotNullField, self).__init__(*args, **kwargs) + class StringNullField(models.CharField): description = "Custom String with Null" @@ -18,6 +20,7 @@ def __init__(self, *args, **kwargs): kwargs['help_text'] = kwargs.get('help_text', 'string field with null True') super(StringNullField, self).__init__(*args, **kwargs) + class IntegerNullField(models.IntegerField): description = "Custom Integer with Null" @@ -26,6 +29,7 @@ def __init__(self, *args, **kwargs): kwargs['help_text'] = kwargs.get('help_text', 'Integer field with null True') super(IntegerNullField, self).__init__(*args, **kwargs) + class IntegerNotNullField(models.IntegerField): description = "Custom Integer with Not Null" @@ -34,6 +38,7 @@ def __init__(self, *args, **kwargs): kwargs['help_text'] = kwargs.get('help_text', 'Integer field with null false') super(IntegerNotNullField, self).__init__(*args, **kwargs) + class CustomJsonField(models.JSONField): description = "Custom Json Field with Null" @@ -43,6 +48,7 @@ def __init__(self, *args, **kwargs): kwargs['help_text'] = kwargs.get('help_text', 'Json field with null true') super(CustomJsonField, self).__init__(*args, **kwargs) + class CustomDateTimeField(models.DateTimeField): description = "Custom DateTime Field with Auto-Add Now" @@ -50,6 +56,7 @@ def __init__(self, *args, **kwargs): kwargs['null'] = True # Allow the field to be nullable super(CustomDateTimeField, self).__init__(*args, **kwargs) + class TextNotNullField(models.TextField): description = "Custom Text Field with Not Null" @@ -58,6 +65,7 @@ def __init__(self, *args, **kwargs): kwargs['help_text'] = kwargs.get('help_text', 'text field with null false') super(TextNotNullField, self).__init__(*args, **kwargs) + class StringOptionsField(models.CharField): description = "Custom String Field with Options" @@ -69,6 +77,7 @@ def __init__(self, *args, **kwargs): kwargs['null'] = True # Allow the field to be nullable super(StringOptionsField, self).__init__(max_length=max_length, choices=choices, default=default, **kwargs) + class BooleanFalseField(models.BooleanField): description = "Custom Boolean Field with Default True" @@ -81,6 +90,7 @@ def toggle(self, instance): setattr(instance, self.attname, not value) instance.save() + class BooleanTrueField(models.BooleanField): description = "Custom Boolean Field with Default True" From dc785cce5e2aae16744719789c5f20b9d1c678c6 Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Mon, 23 Oct 2023 17:44:46 +0530 Subject: [PATCH 06/18] add support --- apps/fyle/urls.py | 2 +- tests/test_fyle/test_views.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/fyle/urls.py b/apps/fyle/urls.py index a0789ae0..f06ca770 100644 --- a/apps/fyle/urls.py +++ b/apps/fyle/urls.py @@ -3,5 +3,5 @@ from apps.fyle.views import CustomFieldView urlpatterns = [ - path("expense_fields/", CustomFieldView.as_view(), name="fyle-expense-fields"), + path('expense_fields/', CustomFieldView.as_view(), name='fyle-expense-fields'), ] diff --git a/tests/test_fyle/test_views.py b/tests/test_fyle/test_views.py index 2df61536..038a60fc 100644 --- a/tests/test_fyle/test_views.py +++ b/tests/test_fyle/test_views.py @@ -6,7 +6,7 @@ from tests.test_fyle.fixtures import fixtures as fyle_fixtures -@pytest.mark.django_db(databases=["default"]) +@pytest.mark.django_db(databases=['default']) def test_fyle_expense_fields( api_client, test_connection, @@ -18,11 +18,11 @@ def test_fyle_expense_fields( workspace_id = 1 access_token = test_connection.access_token - url = reverse("fyle-expense-fields", kwargs={"workspace_id": workspace_id}) + url = reverse('fyle-expense-fields', kwargs={'workspace_id': workspace_id}) mocker.patch( - "fyle.platform.apis.v1beta.admin.expense_fields.list_all", - return_value=fyle_fixtures["get_all_custom_fields"], + 'fyle.platform.apis.v1beta.admin.expense_fields.list_all', + return_value=fyle_fixtures['get_all_custom_fields'], ) api_client.credentials(HTTP_AUTHORIZATION="Bearer {}".format(access_token)) @@ -32,5 +32,5 @@ def test_fyle_expense_fields( response = json.loads(response.content) assert ( - dict_compare_keys(response, fyle_fixtures["fyle_expense_custom_fields"]) == [] - ), "expense group api return diffs in keys" + dict_compare_keys(response, fyle_fixtures['fyle_expense_custom_fields']) == [] + ), 'expense group api return diffs in keys' From 434f8de5b87b3f2b78bf2806693e3f1208b6c33d Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Mon, 23 Oct 2023 17:54:12 +0530 Subject: [PATCH 07/18] er --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 96dd5d6e..52d31384 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ jobs: run: | docker-compose -f docker-compose-pipeline.yml build docker-compose -f docker-compose-pipeline.yml up -d - docker-compose -f docker-compose-pipeline.yml exec -T api pytest tests/ + docker-compose -f docker-compose-pipeline.yml exec -T api pytest tests/ --cov='.' echo "STATUS=$(cat pytest-coverage.txt | grep 'Required test' | awk '{ print $1 }')" >> $GITHUB_ENV echo "FAILED=$(cat test-reports/report.xml | awk -F'=' '{print $5}' | awk -F' ' '{gsub(/"/, "", $1); print $1}')" >> $GITHUB_ENV - name: Upload coverage reports to Codecov with GitHub Action From 0509baf327aa6dd8195bf4c2bf943350395aa823 Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Mon, 23 Oct 2023 17:58:37 +0530 Subject: [PATCH 08/18] minor change --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 52d31384..e7d2c1b6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ jobs: run: | docker-compose -f docker-compose-pipeline.yml build docker-compose -f docker-compose-pipeline.yml up -d - docker-compose -f docker-compose-pipeline.yml exec -T api pytest tests/ --cov='.' + docker-compose -f docker-compose-pipeline.yml exec -T api pytest tests/ --cov --junit-xml=test-reports/report.xml --cov-report=xml --cov-fail-under=70 echo "STATUS=$(cat pytest-coverage.txt | grep 'Required test' | awk '{ print $1 }')" >> $GITHUB_ENV echo "FAILED=$(cat test-reports/report.xml | awk -F'=' '{print $5}' | awk -F' ' '{gsub(/"/, "", $1); print $1}')" >> $GITHUB_ENV - name: Upload coverage reports to Codecov with GitHub Action From 31d42d78fa7d8e9a6b4158fb6d5216b4957553aa Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Mon, 23 Oct 2023 18:02:12 +0530 Subject: [PATCH 09/18] minor fix --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index af3c5034..8c0792ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,7 +24,7 @@ gevent==23.9.1 gunicorn==20.1.0 # Platform SDK -fyle==0.30.0 +fyle==0.34.0 # Reusable Fyle Packages fyle-rest-auth==1.4.0 From 4cfd159541381adfa9386573242227d773804395 Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Mon, 23 Oct 2023 18:12:02 +0530 Subject: [PATCH 10/18] fix --- sage_desktop_api/settings.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/sage_desktop_api/settings.py b/sage_desktop_api/settings.py index 77232cdb..bd2974a3 100644 --- a/sage_desktop_api/settings.py +++ b/sage_desktop_api/settings.py @@ -173,9 +173,6 @@ DATABASE_ROUTERS = ['sage_desktop_api.cache_router.CacheRouter'] -# Password validation -# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators - AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', @@ -203,12 +200,10 @@ FYLE_APP_URL = os.environ.get('APP_URL') FYLE_EXPENSE_URL = os.environ.get('FYLE_APP_URL') - # Sage300 Settings SD_API_KEY = os.environ.get('SD_API_KEY') SD_API_SECRET = os.environ.get('SD_API_SECRET') - # Internationalization # https://docs.djangoproject.com/en/3.1/topics/i18n/ @@ -222,7 +217,6 @@ USE_TZ = True - # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.1/howto/static-files/ From 7d614d0004b4709568d47997a01f3a7d6f5304f6 Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Mon, 23 Oct 2023 18:15:14 +0530 Subject: [PATCH 11/18] ad --- sage_desktop_api/tests/settings.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/sage_desktop_api/tests/settings.py b/sage_desktop_api/tests/settings.py index d1a16b6e..e66e841d 100644 --- a/sage_desktop_api/tests/settings.py +++ b/sage_desktop_api/tests/settings.py @@ -155,12 +155,6 @@ } } -# DATABASE_ROUTERS = ['sage_desktop_api.cache_router.CacheRouter'] - - -# Password validation -# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators - AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', @@ -176,7 +170,6 @@ }, ] - # Fyle Settings API_URL = os.environ.get('API_URL') FYLE_TOKEN_URI = os.environ.get('FYLE_TOKEN_URI') @@ -205,7 +198,6 @@ USE_TZ = True - # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.1/howto/static-files/ From da70cab183d12310bb85e75bc1ed6c8e4bb274f3 Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Mon, 23 Oct 2023 18:19:30 +0530 Subject: [PATCH 12/18] minor dix --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 24ba3c01..86c20b00 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,7 @@ COPY . /fyle-sage-desktop-api/ WORKDIR /fyle-sage-desktop-api # Do linting checks -RUN flake8 . +# RUN flake8 . # Expose development port EXPOSE 8000 From bbf69ae5904088a24cb3ad1bf95cfe442d15adf0 Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Mon, 23 Oct 2023 18:29:12 +0530 Subject: [PATCH 13/18] asd --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 86c20b00..24ba3c01 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,7 @@ COPY . /fyle-sage-desktop-api/ WORKDIR /fyle-sage-desktop-api # Do linting checks -# RUN flake8 . +RUN flake8 . # Expose development port EXPOSE 8000 From 65286c56a308548a5ac9582e98078dfc9fcb3dcb Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Mon, 23 Oct 2023 18:32:12 +0530 Subject: [PATCH 14/18] asd --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 24ba3c01..86c20b00 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,7 @@ COPY . /fyle-sage-desktop-api/ WORKDIR /fyle-sage-desktop-api # Do linting checks -RUN flake8 . +# RUN flake8 . # Expose development port EXPOSE 8000 From f1e287a712a5bfcaee3e75ca1a795d21a1f944f0 Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Mon, 23 Oct 2023 18:33:56 +0530 Subject: [PATCH 15/18] minor fx; git ou --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 86c20b00..24ba3c01 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,7 @@ COPY . /fyle-sage-desktop-api/ WORKDIR /fyle-sage-desktop-api # Do linting checks -# RUN flake8 . +RUN flake8 . # Expose development port EXPOSE 8000 From 7c281c83216b451a6cd2c7a148591a9db05e6d4c Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Wed, 25 Oct 2023 11:01:18 +0530 Subject: [PATCH 16/18] lint fix --- sage_desktop_api/settings.py | 2 -- sage_desktop_api/tests/settings.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/sage_desktop_api/settings.py b/sage_desktop_api/settings.py index e5c96f6e..1e12e8a1 100644 --- a/sage_desktop_api/settings.py +++ b/sage_desktop_api/settings.py @@ -176,8 +176,6 @@ } } - - CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.db.DatabaseCache', diff --git a/sage_desktop_api/tests/settings.py b/sage_desktop_api/tests/settings.py index d24f0082..2f274aa3 100644 --- a/sage_desktop_api/tests/settings.py +++ b/sage_desktop_api/tests/settings.py @@ -179,11 +179,9 @@ FYLE_APP_URL = os.environ.get('APP_URL') FYLE_EXPENSE_URL = os.environ.get('FYLE_APP_URL') - SD_API_KEY = os.environ.get('SD_API_KEY') SD_API_SECRET = os.environ.get('SD_API_SECRET') - # Internationalization # https://docs.djangoproject.com/en/3.1/topics/i18n/ From 5e9eef2ad27a849a737856c64183a986cc4c3d3c Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Wed, 25 Oct 2023 11:26:13 +0530 Subject: [PATCH 17/18] fix test --- tests/test_fyle/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_fyle/test_views.py b/tests/test_fyle/test_views.py index 038a60fc..25515700 100644 --- a/tests/test_fyle/test_views.py +++ b/tests/test_fyle/test_views.py @@ -21,7 +21,7 @@ def test_fyle_expense_fields( url = reverse('fyle-expense-fields', kwargs={'workspace_id': workspace_id}) mocker.patch( - 'fyle.platform.apis.v1beta.admin.expense_fields.list_all', + 'fyle.platform.apis.v1beta.admin.ExpenseFields.list_all', return_value=fyle_fixtures['get_all_custom_fields'], ) From 44567411725069a3715d4cfc2d48f99e35047ffd Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Wed, 25 Oct 2023 12:37:49 +0530 Subject: [PATCH 18/18] resolve comments --- apps/fyle/helpers.py | 29 +++++++++++++++++++++++++++ apps/fyle/serializers.py | 20 +++++++++++++++++++ apps/fyle/views.py | 37 +++++------------------------------ tests/test_fyle/test_views.py | 2 +- 4 files changed, 55 insertions(+), 33 deletions(-) diff --git a/apps/fyle/helpers.py b/apps/fyle/helpers.py index 6cfb45df..a65641d7 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 e69de29b..5204823b 100644 --- a/apps/fyle/serializers.py +++ b/apps/fyle/serializers.py @@ -0,0 +1,20 @@ +from rest_framework import serializers + +from apps.fyle.helpers import get_expense_fields + + +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/views.py b/apps/fyle/views.py index 2338ae0e..76600d64 100644 --- a/apps/fyle/views.py +++ b/apps/fyle/views.py @@ -1,40 +1,13 @@ -from rest_framework.views import status from rest_framework import generics -from rest_framework.response import Response -from fyle_integrations_platform_connector import PlatformConnector +from apps.workspaces.models import Workspace +from apps.fyle.serializers import ExpenseFieldSerializer -from apps.workspaces.models import FyleCredential -from apps.fyle.constants import DEFAULT_FYLE_CONDITIONS - -class CustomFieldView(generics.RetrieveAPIView): +class CustomFieldView(generics.ListAPIView): """ Custom Field view """ - def get(self, request, *args, **kwargs): - """ - Get Custom Fields - """ - workspace_id = self.kwargs['workspace_id'] - - 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( - data=response, - status=status.HTTP_200_OK - ) + serializer_class = ExpenseFieldSerializer + queryset = Workspace.objects.all() diff --git a/tests/test_fyle/test_views.py b/tests/test_fyle/test_views.py index 25515700..c0481c0d 100644 --- a/tests/test_fyle/test_views.py +++ b/tests/test_fyle/test_views.py @@ -32,5 +32,5 @@ def test_fyle_expense_fields( response = json.loads(response.content) assert ( - dict_compare_keys(response, fyle_fixtures['fyle_expense_custom_fields']) == [] + dict_compare_keys(response['results'], fyle_fixtures['fyle_expense_custom_fields']) == [] ), 'expense group api return diffs in keys'