diff --git a/.pylintrc b/.pylintrc index b11c35c..04e0911 100644 --- a/.pylintrc +++ b/.pylintrc @@ -155,7 +155,8 @@ disable=print-statement, too-many-locals, too-few-public-methods, super-with-arguments, - too-many-instance-attributes + too-many-instance-attributes, + consider-using-f-string # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/apps/fyle/actions.py b/apps/fyle/actions.py new file mode 100644 index 0000000..dfe0959 --- /dev/null +++ b/apps/fyle/actions.py @@ -0,0 +1,10 @@ +from apps.mappings.connector import PlatformConnector + + +def sync_fyle_dimensions(workspace_id: int): + """ + Sync Attributes will be called for syncing the attribute's data from the fyle to qbd db + """ + + qbd_connection = PlatformConnector(workspace_id=workspace_id) + qbd_connection.sync_corporate_card() diff --git a/apps/fyle/tasks.py b/apps/fyle/tasks.py index 06bbb96..1f92562 100644 --- a/apps/fyle/tasks.py +++ b/apps/fyle/tasks.py @@ -94,7 +94,7 @@ def import_credit_card_expenses(workspace_id, accounting_export: AccountingExpor state=export_settings.credit_card_expense_state, settled_at=last_synced_at if export_settings.credit_card_expense_state == 'PAYMENT_PROCESSING' else None, approved_at=last_synced_at if export_settings.credit_card_expense_state == 'APPROVED' else None, - filter_credit_expenses=True, + filter_credit_expenses=False, last_paid_at=last_synced_at if export_settings.credit_card_expense_state == 'PAID' else None ) diff --git a/apps/fyle/urls.py b/apps/fyle/urls.py index e69de29..a21108a 100644 --- a/apps/fyle/urls.py +++ b/apps/fyle/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from apps.fyle.views import SyncFyleDimensionView + + +urlpatterns = [ + path('sync_dimensions/', SyncFyleDimensionView.as_view(), name='sync-fyle-dimensions'), +] diff --git a/apps/fyle/views.py b/apps/fyle/views.py index e69de29..4a09c88 100644 --- a/apps/fyle/views.py +++ b/apps/fyle/views.py @@ -0,0 +1,20 @@ +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import generics +from rest_framework.response import Response +from rest_framework.views import status + +from .actions import sync_fyle_dimensions + + +class SyncFyleDimensionView(generics.ListCreateAPIView): + """ + Sync Fyle Dimensions View + """ + + def post(self, request, *args, **kwargs): + """ + Sync Data From Fyle + """ + sync_fyle_dimensions(workspace_id=kwargs['workspace_id']) + + return Response(status=status.HTTP_200_OK) diff --git a/apps/mappings/__init__.py b/apps/mappings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/mappings/actions.py b/apps/mappings/actions.py new file mode 100644 index 0000000..a04a3c6 --- /dev/null +++ b/apps/mappings/actions.py @@ -0,0 +1,21 @@ +from .models import QBDMapping + + +def get_qbd_mapping_stat(source_type: str, workspace_id: int): + """ + get qbd mapping stat will return the count of total mappings available and unmapped mappings + """ + + total_attributes_count = QBDMapping.objects.filter( + workspace_id=workspace_id, + attribute_type = source_type).count() + + unmapped_attributes_count = QBDMapping.objects.filter( + workspace_id=workspace_id, + attribute_type = source_type, + destination_value__isnull=True).count() + + return { + 'all_attributes_count': total_attributes_count, + 'unmapped_attributes_count': unmapped_attributes_count + } diff --git a/apps/mappings/admin.py b/apps/mappings/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/apps/mappings/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/mappings/apps.py b/apps/mappings/apps.py new file mode 100644 index 0000000..1153d82 --- /dev/null +++ b/apps/mappings/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class MappingsConfig(AppConfig): + name = 'apps.mappings' diff --git a/apps/mappings/connector.py b/apps/mappings/connector.py new file mode 100644 index 0000000..eb0f195 --- /dev/null +++ b/apps/mappings/connector.py @@ -0,0 +1,52 @@ +from django.conf import settings +from fyle.platform import Platform + +from apps.workspaces.models import FyleCredential + +from .models import QBDMapping + + +class PlatformConnector: + + def __init__(self, workspace_id: int): + + fyle_credentials = FyleCredential.objects.get(workspace_id=workspace_id) + + self.platform = Platform( + server_url='{}/platform/v1beta'.format(fyle_credentials.cluster_domain), + token_url=settings.FYLE_TOKEN_URI, + client_id=settings.FYLE_CLIENT_ID, + client_secret=settings.FYLE_CLIENT_SECRET, + refresh_token=fyle_credentials.refresh_token, + ) + + self.workspace_id = workspace_id + + + def sync_corporate_card(self): + """ + Sync Cards will sync the corporate cards details from fyle to qbd db + """ + + query = { + 'order': 'updated_at.desc', + } + generator = self.platform.v1beta.admin.corporate_cards.list_all(query) + for items in generator: + card_attributes = [] + unique_card_numbers = [] + for card in items['data']: + value = '{} - {}'.format( + card['bank_name'], + card['card_number'][-6:].replace('-', '') + ) + + if value not in unique_card_numbers: + unique_card_numbers.append(value) + card_attributes.append({ + 'attribute_type': 'CORPORATE_CARD', + 'value': value, + 'source_id': card['id'], + }) + if len(card_attributes) > 0: + QBDMapping.update_or_create_mapping_objects(card_attributes, self.workspace_id) diff --git a/apps/mappings/migrations/0001_initial.py b/apps/mappings/migrations/0001_initial.py new file mode 100644 index 0000000..f79d6ad --- /dev/null +++ b/apps/mappings/migrations/0001_initial.py @@ -0,0 +1,34 @@ +# Generated by Django 3.1.14 on 2023-08-25 11:14 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('workspaces', '0025_auto_20230823_1118'), + ] + + operations = [ + migrations.CreateModel( + name='QBDMapping', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('attribute_type', models.CharField(help_text='Type of expense attribute', max_length=255)), + ('source_value', models.CharField(help_text='Value of expense attribute', max_length=1000)), + ('source_id', models.CharField(help_text='Fyle ID', max_length=255)), + ('destination_value', models.CharField(blank=True, + help_text='Value of destination attribute', max_length=1000, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True, help_text='Created at datetime')), + ('updated_at', models.DateTimeField(auto_now=True, help_text='Updated at datetime')), + ('workspace', models.ForeignKey(help_text='Reference to Workspace model', + on_delete=django.db.models.deletion.PROTECT, to='workspaces.workspace')), + ], + options={ + 'db_table': 'qbd_mappings', + }, + ), + ] diff --git a/apps/mappings/migrations/__init__.py b/apps/mappings/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/mappings/models.py b/apps/mappings/models.py new file mode 100644 index 0000000..0f77889 --- /dev/null +++ b/apps/mappings/models.py @@ -0,0 +1,33 @@ +from typing import List, Dict +from django.db import models + +from apps.workspaces.models import Workspace + +class QBDMapping(models.Model): + """ + Fyle Expense Attributes + """ + id = models.AutoField(primary_key=True) + attribute_type = models.CharField(max_length=255, help_text='Type of expense attribute') + source_value = models.CharField(max_length=1000, help_text='Value of expense attribute') + source_id = models.CharField(max_length=255, help_text='Fyle ID') + destination_value = models.CharField(max_length=1000, + null=True, blank=True, help_text='Value of destination attribute') + workspace = models.ForeignKey(Workspace, on_delete=models.PROTECT, help_text='Reference to Workspace model') + created_at = models.DateTimeField(auto_now_add=True, help_text='Created at datetime') + updated_at = models.DateTimeField(auto_now=True, help_text='Updated at datetime') + + class Meta: + db_table = 'qbd_mappings' + + @staticmethod + def update_or_create_mapping_objects(qbd_mapping_objects: List[Dict],workspace_id: int): + for qbd_mapping_object in qbd_mapping_objects: + QBDMapping.objects.update_or_create( + workspace_id= workspace_id, + source_value= qbd_mapping_object['value'], + attribute_type= qbd_mapping_object['attribute_type'], + defaults={ + 'source_id': qbd_mapping_object['source_id'], + } + ) diff --git a/apps/mappings/serializers.py b/apps/mappings/serializers.py new file mode 100644 index 0000000..da3289c --- /dev/null +++ b/apps/mappings/serializers.py @@ -0,0 +1,22 @@ +from rest_framework import serializers + +from .models import QBDMapping + + +class QBDMappingSerializer(serializers.ModelSerializer): + class Meta: + model = QBDMapping + fields = '__all__' + read_only_fields = ('workspace', 'created_at', 'updated_at') + + def create(self, validated_data): + workspace_id = self.context['request'].parser_context.get('kwargs').get('workspace_id') + qbd_mapping, _ = QBDMapping.objects.update_or_create( + source_id=validated_data['source_id'], + workspace_id=workspace_id, + defaults={ + 'destination_value': validated_data['destination_value'] + } + ) + + return qbd_mapping diff --git a/apps/mappings/tasks.py b/apps/mappings/tasks.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/mappings/urls.py b/apps/mappings/urls.py new file mode 100644 index 0000000..e22922d --- /dev/null +++ b/apps/mappings/urls.py @@ -0,0 +1,26 @@ +"""quickbooks_desktop_api URL Configuration +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/3.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.urls import path + +from .views import ( + QBDMappingView, + QBDMappingStatsView +) + + +urlpatterns = [ + path('', QBDMappingView.as_view(), name='qbd-mapping'), + path('stats/', QBDMappingStatsView.as_view(), name='qbd-mapping-stats'), +] diff --git a/apps/mappings/utils.py b/apps/mappings/utils.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/mappings/views.py b/apps/mappings/views.py new file mode 100644 index 0000000..264dec4 --- /dev/null +++ b/apps/mappings/views.py @@ -0,0 +1,44 @@ +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import generics +from rest_framework.response import Response +from rest_framework.views import status + +from apps.mappings.serializers import QBDMappingSerializer +from quickbooks_desktop_api.utils import assert_valid, LookupFieldMixin + +from .models import QBDMapping +from .actions import get_qbd_mapping_stat + + +class QBDMappingView(LookupFieldMixin, generics.ListCreateAPIView): + """ + QBD Mapping Update View + """ + + queryset = QBDMapping.objects.all() + serializer_class = QBDMappingSerializer + lookup_field = 'workspace_id' + lookup_url_kwarg = 'workspace_id' + filter_backends = (DjangoFilterBackend,) + filterset_fields = {'attribute_type': {'exact'},'destination_value': {'isnull'}} + ordering_fields = ('source_value',) + + +#mapping stats view +class QBDMappingStatsView(generics.RetrieveAPIView): + """ + Stats for total mapped and unmapped count for a given attribute type + """ + + def get(self, request, *args, **kwargs): + source_type = self.request.query_params.get('source_type') + workspace_id= self.kwargs['workspace_id'] + + assert_valid(source_type is not None, 'query param source_type not found') + + stat_response = get_qbd_mapping_stat(source_type, workspace_id) + + return Response( + data=stat_response, + status=status.HTTP_200_OK + ) diff --git a/apps/qbd/models.py b/apps/qbd/models.py index 167645b..e430128 100644 --- a/apps/qbd/models.py +++ b/apps/qbd/models.py @@ -9,6 +9,7 @@ AdvancedSetting, ExportSettings, FieldMapping, FyleCredential, Workspace ) +from apps.mappings.models import QBDMapping def get_class_and_project_name(field_mappings: FieldMapping, expense: Expense): @@ -125,6 +126,18 @@ def get_top_purpose(workspace_id: str, expense: Expense, default: str) -> str: return memo +def get_corporate_card_name(corporate_card_id: str, workspace_id: int, export_settings: ExportSettings): + corporate_card_name = export_settings.credit_card_account_name + + qbd_mapping = QBDMapping.objects.filter(workspace_id=workspace_id, source_id=corporate_card_id) + + if len(qbd_mapping) > 0 and qbd_mapping[0].destination_value: + corporate_card_name = qbd_mapping[0].destination_value + + return corporate_card_name + + + class Bill(models.Model): """ Bills Table Model Class @@ -386,13 +399,15 @@ def create_credit_card_purchase( date_preference = export_settings.credit_card_expense_date + corporate_card_name = get_corporate_card_name(expenses[0].corporate_card_id, workspace_id, export_settings) + if export_settings.credit_card_entity_name_preference == 'EMPLOYEE': name = expenses[0].employee_name credit_card_purchase = CreditCardPurchase.objects.create( transaction_type='CREDIT CARD', date=get_transaction_date(expenses, date_preference), - account=export_settings.credit_card_account_name, + account=corporate_card_name, name=name, class_name='', amount=sum([expense.amount for expense in expenses]) * -1, @@ -583,13 +598,15 @@ def create_journal( date_preference = export_settings.credit_card_expense_date + corporate_card_name = get_corporate_card_name(expenses[0].corporate_card_id, workspace_id, export_settings) + default_memo = f'Credit Card Expenses by {expenses[0].employee_email}' \ if fund_source == 'CCC' else f'Reimbursable Expenses by {expenses[0].employee_email}' journal = Journal.objects.create( transaction_type='GENERAL JOURNAL', date=get_transaction_date(expenses, date_preference=date_preference), - account=export_settings.credit_card_account_name if fund_source == 'CCC' else export_settings.bank_account_name, + account=corporate_card_name if fund_source == 'CCC' else export_settings.bank_account_name, name=name, amount=sum([expense.amount for expense in expenses]), memo=get_top_purpose( diff --git a/apps/workspaces/urls.py b/apps/workspaces/urls.py index 6e93db5..7b88a3e 100644 --- a/apps/workspaces/urls.py +++ b/apps/workspaces/urls.py @@ -31,5 +31,7 @@ path('/advanced_settings/', AdvancedSettingView.as_view(), name='advanced-settings'), path('/field_mappings/', FieldMappingView.as_view(), name='field-mappings'), path('/trigger_export/', TriggerExportView.as_view(), name='trigger-export'), - path('/accounting_exports/', include('apps.tasks.urls')) + path('/accounting_exports/', include('apps.tasks.urls')), + path('/qbd_mappings/', include('apps.mappings.urls')), + path('/fyle/', include('apps.fyle.urls')), ] diff --git a/quickbooks_desktop_api/settings.py b/quickbooks_desktop_api/settings.py index f6937fb..85e339e 100644 --- a/quickbooks_desktop_api/settings.py +++ b/quickbooks_desktop_api/settings.py @@ -51,13 +51,15 @@ 'fyle_rest_auth', 'fyle_accounting_mappings', 'django_q', + 'django_filters', # Created Apps 'apps.users', 'apps.workspaces', 'apps.fyle', 'apps.tasks', - 'apps.qbd' + 'apps.qbd', + 'apps.mappings' ] MIDDLEWARE = [ @@ -111,6 +113,7 @@ 'fyle_rest_auth.authentication.FyleJWTAuthentication', ), 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', + 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], 'PAGE_SIZE': 100 } diff --git a/quickbooks_desktop_api/tests/settings.py b/quickbooks_desktop_api/tests/settings.py index 1159013..efab4e7 100644 --- a/quickbooks_desktop_api/tests/settings.py +++ b/quickbooks_desktop_api/tests/settings.py @@ -47,12 +47,14 @@ 'fyle_rest_auth', 'fyle_accounting_mappings', 'django_q', + 'django_filters', # Created Apps 'apps.users', 'apps.workspaces', 'apps.fyle', 'apps.tasks', + 'apps.mappings', 'apps.qbd' ] diff --git a/quickbooks_desktop_api/utils.py b/quickbooks_desktop_api/utils.py index fe00760..c34d901 100644 --- a/quickbooks_desktop_api/utils.py +++ b/quickbooks_desktop_api/utils.py @@ -13,3 +13,13 @@ def assert_valid(condition: bool, message: str) -> Response or None: raise ValidationError(detail={ 'message': message }) + +class LookupFieldMixin: + lookup_field = 'workspace_id' + + def filter_queryset(self, queryset): + if self.lookup_field in self.kwargs: + lookup_value = self.kwargs[self.lookup_field] + filter_kwargs = {self.lookup_field: lookup_value} + queryset = queryset.filter(**filter_kwargs) + return super().filter_queryset(queryset) diff --git a/requirements.txt b/requirements.txt index a3f9786..4e7c4f6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,7 @@ django-rest-framework==0.1.0 djangorestframework==3.14.0 django-db-geventpool==4.0.1 django-request-logging==0.7.1 +django-filter==21.1 # DjangoQ for running async tasks django-q==1.3.4 diff --git a/tests/conftest.py b/tests/conftest.py index 566af84..940837c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,9 +18,11 @@ AdvancedSetting ) from apps.tasks.models import AccountingExport +from apps.mappings.models import QBDMapping from quickbooks_desktop_api.tests import settings from .test_fyle.fixtures import fixtures as fyle_fixtures +from .test_mapping.fixture import fixture as mapping_fixtures @pytest.fixture @@ -268,3 +270,24 @@ def add_expenses(): expense['id'] = expense['id'] + str(workspace_id) Expense.create_expense_objects(expenses, workspace_id) + + +@pytest.fixture() +@pytest.mark.django_db(databases=['default']) +def add_ccc_mapping(): + """ + Add Expense to a workspace + """ + mappings = mapping_fixtures['create_qbd_mapping'] + + for workspace_id in [1, 2, 3]: + for mapping in mappings: + QBDMapping.objects.update_or_create( + workspace_id= workspace_id, + source_value= mapping['value'], + attribute_type= mapping['attribute_type'], + defaults={ + 'source_id': mapping['source_id'], + 'destination_value': 'mastercard' if mapping['value'] == 'American Express - 055470' else None + } + ) diff --git a/tests/test_fyle/test_view.py b/tests/test_fyle/test_view.py new file mode 100644 index 0000000..bb3a7d2 --- /dev/null +++ b/tests/test_fyle/test_view.py @@ -0,0 +1,35 @@ +from django.urls import reverse +import pytest + +from apps.mappings.models import QBDMapping + +from tests.test_mapping.fixture import fixture + +@pytest.mark.django_db(databases=['default']) +def test_sync_fyle_dimension_view(api_client, test_connection, mocker): + mocker.patch( + 'fyle.platform.apis.v1beta.admin.corporate_cards.list_all', + return_value=fixture['credit_card_sdk'] + ) + 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( + 'sync-fyle-dimensions', kwargs={ + 'workspace_id': workspace_id + } + ) + + response = api_client.post(url) + + qbd_mapping = QBDMapping.objects.filter(workspace_id=workspace_id) + + assert response.status_code == 200 + assert len(qbd_mapping) == fixture['get_qbd_CCC_mapping']['count'] + assert qbd_mapping[0].source_value == fixture['get_qbd_CCC_mapping']['results'][0]['source_value'] diff --git a/tests/test_mapping/__init__.py b/tests/test_mapping/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_mapping/fixture.py b/tests/test_mapping/fixture.py new file mode 100644 index 0000000..53b912d --- /dev/null +++ b/tests/test_mapping/fixture.py @@ -0,0 +1,76 @@ +fixture = { + 'credit_card_sdk': + [ + { + 'count': 16, + 'data': + [ + { + "id":"bacc1DHywC3YAd", + "bank_name": "American Express", + "card_number": "55470055470", + "updated_at": "2023-08-28T05:46:59.330073Z", + }, + { + "bank_name": "American Express", + "id": "baccEg1AzugNxZ", + "card_number": "55470077674", + "updated_at": "2023-08-28T05:46:59.330073Z", + }, + ] + } + ], + 'post_qbd_CCC_mapping': { + "id": 1, + "attribute_type": "CORPORATE_CARD", + "source_value": "AMERICAN EXPRESS - 4818", + "source_id": "baccwOdiRCcWWQ", + "destination_value": "Mastercard", + "created_at": "2023-08-25T11:19:09.608035Z", + "updated_at": "2023-08-28T10:07:30.524503Z", + "workspace": 1 + }, + 'get_qbd_CCC_mapping_state': { + "all_attributes_count":2, + "unmapped_attributes_count":2 + }, + 'create_qbd_mapping': [ + { + 'attribute_type': 'CORPORATE_CARD', + 'value': 'American Express - 055470', + 'source_id': 'bacc1DHywC3YAd', + }, + { + 'attribute_type': 'CORPORATE_CARD', + 'value': 'American Express - 077674', + 'source_id': 'baccEg1AzugNxZ', + } + ], + 'get_qbd_CCC_mapping': { + "count": 2, + "next": "http://localhost:8008/api/workspaces/1/qbd_mappings/?attribute_type=CORPORATE_CARD", + "previous": '', + "results": [ + { + "id": 3, + "attribute_type": "CORPORATE_CARD", + "source_value": "American Express - 055470", + "source_id": "bacc1DHywC3YAd", + "destination_value": "", + "created_at": "2023-08-25T11:19:09.625706Z", + "updated_at": "2023-08-28T05:46:59.326614Z", + "workspace": 1 + }, + { + "id": 4, + "attribute_type": "CORPORATE_CARD", + "source_value": "American Express - 077674", + "source_id": "baccEg1AzugNxZ", + "destination_value": "", + "created_at": "2023-08-25T11:19:09.627857Z", + "updated_at": "2023-08-28T05:46:59.330073Z", + "workspace": 1 + }, + ] + } +} diff --git a/tests/test_mapping/test_connector.py b/tests/test_mapping/test_connector.py new file mode 100644 index 0000000..35ce020 --- /dev/null +++ b/tests/test_mapping/test_connector.py @@ -0,0 +1,23 @@ +import pytest +from apps.mappings.connector import PlatformConnector +from apps.mappings.models import QBDMapping +from .fixture import fixture + + +@pytest.mark.django_db(databases=['default'], transaction=True) +def test_sync_corporate_card(create_temp_workspace, + add_accounting_export_expenses, mocker, + add_fyle_credentials, add_export_settings): + + workspace_id = 1 + mocker.patch( + 'fyle.platform.apis.v1beta.admin.corporate_cards.list_all', + return_value=fixture['credit_card_sdk'] + ) + qbd_connection = PlatformConnector(workspace_id=workspace_id) + qbd_connection.sync_corporate_card() + qbd_mappings = QBDMapping.objects.filter(workspace_id=workspace_id, attribute_type = 'CORPORATE_CARD') + assert len(qbd_mappings) == len(fixture['get_qbd_CCC_mapping']['results']) + for i, item in enumerate(qbd_mappings): + assert qbd_mappings[i].source_value == fixture['get_qbd_CCC_mapping']['results'][i]['source_value'] + diff --git a/tests/test_mapping/test_view.py b/tests/test_mapping/test_view.py new file mode 100644 index 0000000..7a6440e --- /dev/null +++ b/tests/test_mapping/test_view.py @@ -0,0 +1,99 @@ +from django.urls import reverse +import pytest + +from apps.mappings.models import QBDMapping + +from .fixture import fixture + +@pytest.mark.django_db(databases=['default']) +def test_qbd_mapping_view(api_client, test_connection): + url = reverse( + 'workspaces' + ) + + api_client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(test_connection.access_token)) + response = api_client.post(url) + + workspace_id = response.data['id'] + + QBDMapping.update_or_create_mapping_objects(fixture['create_qbd_mapping'], workspace_id) + + url = reverse( + 'qbd-mapping', kwargs={ + 'workspace_id': workspace_id + } + ) + + # get all rows of qbo mapping table + response = api_client.get(url, {'attribute_type': 'CORPORATE_CARD', 'limit':10, 'offset':0}) + + assert response.status_code == 200 + assert len(response.data['results']) == len(fixture['get_qbd_CCC_mapping']['results']) + assert response.data['count'] == fixture['get_qbd_CCC_mapping']['count'] + + # post qbd mapping + url = reverse( + 'qbd-mapping', kwargs={ + 'workspace_id': workspace_id + } + ) + payload = { + "id": 3, + "destination_value": "Mastercard", + "attribute_type": "CORPORATE_CARD", + "source_id": "bacc1DHywC3YAd", + "source_value": "American Express - 055470", + "workspace": 1 + } + response = api_client.post(url,payload) + + post_value = QBDMapping.objects.filter(workspace_id=workspace_id, + attribute_type = payload['attribute_type'], + source_id = payload['source_id']) + assert response.status_code == 201 + assert post_value[0].destination_value == payload['destination_value'] + + # get all mapped rows (destination_value__isnull is false) + + param = {'attribute_type': 'CORPORATE_CARD', 'limit':10, 'offset':0, 'destination_value__isnull': 'false'} + response = api_client.get(url, param) + + assert response.status_code == 200 + assert len(response.data['results']) == len(fixture['get_qbd_CCC_mapping']['results'])-1 + assert response.data['count'] == fixture['get_qbd_CCC_mapping']['count']-1 + assert response.data['results'][0]['source_value'] == fixture['get_qbd_CCC_mapping']['results'][0]['source_value'] + + # get all unmapped rows (destination_value__isnull is true) + + param = {'attribute_type': 'CORPORATE_CARD', 'limit':10, 'offset':0, 'destination_value__isnull': 'true'} + response = api_client.get(url, param) + + assert response.status_code == 200 + assert len(response.data['results']) == len(fixture['get_qbd_CCC_mapping']['results'])-1 + assert response.data['count'] == fixture['get_qbd_CCC_mapping']['count']-1 + assert response.data['results'][0]['source_value'] == fixture['get_qbd_CCC_mapping']['results'][1]['source_value'] + +@pytest.mark.django_db(databases=['default']) +def test_qbd_mapping_stats_view(api_client, test_connection): + url = reverse( + 'workspaces' + ) + + api_client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(test_connection.access_token)) + response = api_client.post(url) + + workspace_id = response.data['id'] + + QBDMapping.update_or_create_mapping_objects(fixture['create_qbd_mapping'], workspace_id) + + url = reverse( + 'qbd-mapping-stats', kwargs={ + 'workspace_id': workspace_id + } + ) + + response = api_client.get(url, {'source_type': 'CORPORATE_CARD'}) + + assert response.status_code==200 + assert response.data['all_attributes_count']==fixture['get_qbd_CCC_mapping_state']['all_attributes_count'] + assert response.data['unmapped_attributes_count']==fixture['get_qbd_CCC_mapping_state']['unmapped_attributes_count'] diff --git a/tests/test_qbd/test_tasks.py b/tests/test_qbd/test_tasks.py index 6a54dff..a0dd25c 100644 --- a/tests/test_qbd/test_tasks.py +++ b/tests/test_qbd/test_tasks.py @@ -260,7 +260,7 @@ def test_create_credit_card_purchases_iif_file_expense_vendor( create_temp_workspace, add_accounting_export_bills, add_accounting_export_expenses, add_fyle_credentials, add_export_settings, add_field_mappings, add_advanced_settings, - mocker + add_ccc_mapping, mocker ): """ Test create credit card purchases iif file @@ -321,7 +321,7 @@ def test_create_credit_card_purchases_iif_file_expense_employee( create_temp_workspace, add_accounting_export_bills, add_accounting_export_expenses, add_fyle_credentials, add_export_settings, add_field_mappings, add_advanced_settings, - mocker + add_ccc_mapping, mocker ): """ Test create credit card purchases iif file @@ -361,6 +361,11 @@ def test_create_credit_card_purchases_iif_file_expense_employee( workspace_id=workspace_id, type='EXPORT_CREDIT_CARD_PURCHASES', status='ENQUEUED' ) + expense = Expense.objects.filter(workspace_id=workspace_id, exported=False).first() + + expense.corporate_card_id = 'bacc1DHywC3YAd' + expense.save() + create_credit_card_purchases_iif_file(workspace_id, accounting_export) expenses = Expense.objects.filter(workspace_id=workspace_id, exported=False) @@ -380,9 +385,10 @@ def test_create_credit_card_purchases_iif_file_expense_employee( @pytest.mark.django_db(databases=['default']) def test_create_credit_card_purchases_iif_file_expense_fail( - create_temp_workspace, add_accounting_export_bills, - add_accounting_export_expenses, add_fyle_credentials, - add_export_settings, add_advanced_settings, mocker + create_temp_workspace, add_accounting_export_bills, + add_accounting_export_expenses, add_fyle_credentials, + add_export_settings, add_advanced_settings, + add_ccc_mapping, mocker ): """ Test create credit card purchases iif file @@ -435,7 +441,7 @@ def test_create_credit_card_purchases_iif_file_expense_fatal( create_temp_workspace, add_accounting_export_bills, add_accounting_export_expenses, add_fyle_credentials, add_export_settings, add_field_mappings, add_advanced_settings, - mocker + add_ccc_mapping, mocker ): """ Test create credit card purchases iif file @@ -647,6 +653,11 @@ def test_create_journals_iif_file_ccc_report_employee( workspace_id=workspace_id, type='EXPORT_JOURNALS', status='ENQUEUED' ) + expense = Expense.objects.filter(workspace_id=workspace_id, exported=False).first() + + expense.corporate_card_id = 'bacc1DHywC3YAd' + expense.save() + # Testing for entity preference as Vendor create_journals_iif_file(workspace_id, accounting_export, 'CCC') @@ -785,7 +796,7 @@ def test_email_failure( create_temp_workspace, add_accounting_export_bills, add_accounting_export_expenses, add_fyle_credentials, add_export_settings, add_field_mappings, add_advanced_settings, - mocker + add_ccc_mapping, mocker ): """ Test create journals iif file Failed for email failure