diff --git a/apps/mappings/tasks.py b/apps/mappings/tasks.py index 6c0564fd..673cfdd7 100644 --- a/apps/mappings/tasks.py +++ b/apps/mappings/tasks.py @@ -477,6 +477,7 @@ def auto_create_category_mappings(workspace_id): configuration: Configuration = Configuration.objects.get(workspace_id=workspace_id) reimbursable_expenses_object = configuration.reimbursable_expenses_object + employee_field_mapping = configuration.employee_field_mapping corporate_credit_card_expenses_object = configuration.corporate_credit_card_expenses_object if configuration.reimbursable_expenses_object == 'EXPENSE_REPORT' or configuration.corporate_credit_card_expenses_object == 'EXPENSE_REPORT': diff --git a/apps/tasks/migrations/0009_error.py b/apps/tasks/migrations/0009_error.py new file mode 100644 index 00000000..5e987736 --- /dev/null +++ b/apps/tasks/migrations/0009_error.py @@ -0,0 +1,35 @@ +# Generated by Django 3.1.14 on 2023-11-06 12:20 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('fyle_accounting_mappings', '0023_auto_20231010_1139'), + ('fyle', '0026_auto_20231025_0913'), + ('workspaces', '0036_auto_20231027_0709'), + ('tasks', '0008_auto_20220301_1300'), + ] + + operations = [ + migrations.CreateModel( + name='Error', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('type', models.CharField(choices=[('EMPLOYEE_MAPPING', 'EMPLOYEE_MAPPING'), ('CATEGORY_MAPPING', 'CATEGORY_MAPPING'), ('TAX_MAPPING', 'TAX_MAPPING'), ('NETSUITE_ERROR', 'NETSUITE_ERROR')], help_text='Error type', max_length=50)), + ('is_resolved', models.BooleanField(default=False, help_text='Is resolved')), + ('error_title', models.CharField(help_text='Error title', max_length=255)), + ('error_detail', models.TextField(help_text='Error detail')), + ('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')), + ('expense_attribute', models.OneToOneField(help_text='Reference to Expense Attribute', null=True, on_delete=django.db.models.deletion.PROTECT, to='fyle_accounting_mappings.expenseattribute')), + ('expense_group', models.ForeignKey(help_text='Reference to Expense group', null=True, on_delete=django.db.models.deletion.PROTECT, to='fyle.expensegroup')), + ('workspace', models.ForeignKey(help_text='Reference to Workspace model', on_delete=django.db.models.deletion.PROTECT, to='workspaces.workspace')), + ], + options={ + 'db_table': 'errors', + }, + ), + ] diff --git a/apps/tasks/models.py b/apps/tasks/models.py index 4068834d..ce51519b 100644 --- a/apps/tasks/models.py +++ b/apps/tasks/models.py @@ -5,6 +5,8 @@ from apps.workspaces.models import Workspace from apps.fyle.models import ExpenseGroup +from fyle_accounting_mappings.models import ExpenseAttribute + def get_default(): return { @@ -30,6 +32,7 @@ def get_default(): ('ENQUEUED', 'ENQUEUED') ) +ERROR_TYPE_CHOICES = (('EMPLOYEE_MAPPING', 'EMPLOYEE_MAPPING'), ('CATEGORY_MAPPING', 'CATEGORY_MAPPING'), ('TAX_MAPPING', 'TAX_MAPPING'), ('NETSUITE_ERROR', 'NETSUITE_ERROR')) class TaskLog(models.Model): """ @@ -57,3 +60,28 @@ class TaskLog(models.Model): class Meta: db_table = 'task_logs' + + +class Error(models.Model): + """ + Table to store errors + """ + id = models.AutoField(primary_key=True) + workspace = models.ForeignKey(Workspace, on_delete=models.PROTECT, help_text='Reference to Workspace model') + type = models.CharField(max_length=50, choices=ERROR_TYPE_CHOICES, help_text='Error type') + expense_group = models.ForeignKey( + ExpenseGroup, on_delete=models.PROTECT, + null=True, help_text='Reference to Expense group' + ) + expense_attribute = models.OneToOneField( + ExpenseAttribute, on_delete=models.PROTECT, + null=True, help_text='Reference to Expense Attribute' + ) + is_resolved = models.BooleanField(default=False, help_text='Is resolved') + error_title = models.CharField(max_length=255, help_text='Error title') + error_detail = models.TextField(help_text='Error detail') + 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 = 'errors' diff --git a/apps/workspaces/apis/errors/__init__.py b/apps/workspaces/apis/errors/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/workspaces/apis/errors/serializers.py b/apps/workspaces/apis/errors/serializers.py new file mode 100644 index 00000000..3628d69e --- /dev/null +++ b/apps/workspaces/apis/errors/serializers.py @@ -0,0 +1,41 @@ +from fyle_accounting_mappings.models import ExpenseAttribute +from rest_framework import serializers + +from apps.fyle.models import ExpenseGroup +from apps.fyle.serializers import ExpenseSerializer +from apps.tasks.models import Error + + +class ExpenseAttributeSerializer(serializers.ModelSerializer): + """ + Serializer for Expense Attribute + """ + + class Meta: + model = ExpenseAttribute + fields = '__all__' + + +class ExpenseGroupSerializer(serializers.ModelSerializer): + """ + Serializer for Expense Group + """ + + expenses = ExpenseSerializer(many=True) + + class Meta: + model = ExpenseGroup + fields = '__all__' + + +class ErrorSerializer(serializers.ModelSerializer): + """ + Serializer for the Errors + """ + + expense_attribute = ExpenseAttributeSerializer() + expense_group = ExpenseGroupSerializer() + + class Meta: + model = Error + fields = '__all__' diff --git a/apps/workspaces/apis/errors/views.py b/apps/workspaces/apis/errors/views.py new file mode 100644 index 00000000..a07240db --- /dev/null +++ b/apps/workspaces/apis/errors/views.py @@ -0,0 +1,16 @@ +from django_filters.rest_framework import DjangoFilterBackend +from fyle_netsuite_api.utils import LookupFieldMixin +from rest_framework import generics +from apps.tasks.models import Error +from apps.workspaces.apis.errors.serializers import ErrorSerializer + + +class ErrorsView(LookupFieldMixin, generics.ListAPIView): + + queryset = Error.objects.all() + serializer_class = ErrorSerializer + pagination_class = None + filter_backends = (DjangoFilterBackend,) + filterset_fields = {'type':{'exact'}, 'is_resolved':{'exact'}} + + diff --git a/apps/workspaces/apis/urls.py b/apps/workspaces/apis/urls.py index 0b68bf80..24813b47 100644 --- a/apps/workspaces/apis/urls.py +++ b/apps/workspaces/apis/urls.py @@ -4,6 +4,7 @@ from apps.workspaces.apis.map_employees.views import MapEmployeesView from apps.workspaces.apis.export_settings.views import ExportSettingsView from apps.workspaces.apis.import_settings.views import ImportSettingsView +from apps.workspaces.apis.errors.views import ErrorsView @@ -12,4 +13,5 @@ path('/export_settings/', ExportSettingsView.as_view(), name='export-settings'), path('/import_settings/', ImportSettingsView.as_view(), name='import-settings'), path('/advanced_settings/', AdvancedSettingsView.as_view(), name='advanced-settings'), + path('/errors/', ErrorsView.as_view(), name='errors') ] diff --git a/fyle_netsuite_api/settings.py b/fyle_netsuite_api/settings.py index 72704778..8235bfa4 100644 --- a/fyle_netsuite_api/settings.py +++ b/fyle_netsuite_api/settings.py @@ -56,7 +56,8 @@ 'apps.tasks', 'apps.mappings', 'apps.netsuite', - 'django_q' + 'django_q', + 'django_filters', ] MIDDLEWARE = [ diff --git a/fyle_netsuite_api/utils.py b/fyle_netsuite_api/utils.py index fe007608..e973f5ab 100644 --- a/fyle_netsuite_api/utils.py +++ b/fyle_netsuite_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 9678ed2c..06358460 100644 --- a/requirements.txt +++ b/requirements.txt @@ -62,3 +62,4 @@ urllib3==1.26.11 wcwidth==0.1.8 wrapt==1.12.1 zeep==4.1.0 +django-filter diff --git a/tests/test_mappings/test_tasks.py b/tests/test_mappings/test_tasks.py index 314202c2..112fa431 100644 --- a/tests/test_mappings/test_tasks.py +++ b/tests/test_mappings/test_tasks.py @@ -298,6 +298,7 @@ def test_upload_categories_to_fyle(mocker, db): assert count_of_accounts == 137 + configuration.employee_field_mapping = 'EMPLOYEE' configuration.reimbursable_expenses_object = 'BILL' configuration.corporate_credit_card_expenses_object = 'BILL' configuration.save() diff --git a/tests/test_workspaces/test_apis/test_errors/__init__.py b/tests/test_workspaces/test_apis/test_errors/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_workspaces/test_apis/test_errors/fixtures.py b/tests/test_workspaces/test_apis/test_errors/fixtures.py new file mode 100644 index 00000000..4c8e2e17 --- /dev/null +++ b/tests/test_workspaces/test_apis/test_errors/fixtures.py @@ -0,0 +1,62 @@ +data = { + 'errors_response': [ + { + "id": 1, + "expense_attribute": { + "id": 13044, + "attribute_type": "CATEGORY", + "display_name": "Category", + "value": "Internet", + "source_id": "142069", + "auto_mapped": False, + "auto_created": False, + "active": None, + "detail": None, + "created_at": "2022-10-07T06:02:54.076426Z", + "updated_at": "2022-10-07T06:02:54.076429Z", + "workspace": 8 + }, + "expense_group": None, + "type": "CATEGORY_MAPPING", + "is_resolved": False, + "error_title": "Internet", + "error_detail": "Category mapping is missing", + "created_at": "2022-10-07T06:07:32.823778Z", + "updated_at": "2022-10-07T06:31:53.211657Z", + "workspace": 1 + }, + { + "id": 2, + "expense_attribute": { + "id": 13020, + "attribute_type": "EMPLOYEE", + "display_name": "Employee", + "value": "ashwin.t@fyle.in", + "source_id": "ouQmTCQE26dc", + "auto_mapped": False, + "auto_created": False, + "active": None, + "detail": { + "user_id": "usqywo0f3nBY", + "location": None, + "full_name": "Joanna", + "department": None, + "department_id": None, + "employee_code": None, + "department_code": None + }, + "created_at": "2022-10-07T06:02:53.810543Z", + "updated_at": "2022-10-07T06:02:53.810548Z", + "workspace": 8 + }, + "expense_group": None, + "type": "EMPLOYEE_MAPPING", + "is_resolved": False, + "error_title": "ashwin.t@fyle.in", + "error_detail": "Employee mapping is missing", + "created_at": "2022-10-07T06:31:48.338064Z", + "updated_at": "2022-10-07T06:31:48.338082Z", + "workspace": 1 + } + ] +} diff --git a/tests/test_workspaces/test_apis/test_errors/test_views.py b/tests/test_workspaces/test_apis/test_errors/test_views.py new file mode 100644 index 00000000..d269814d --- /dev/null +++ b/tests/test_workspaces/test_apis/test_errors/test_views.py @@ -0,0 +1,60 @@ +import json +from datetime import datetime,timezone +from apps.tasks.models import Error +from .fixtures import data +from tests.helper import dict_compare_keys +import pytest +from django.urls import reverse + +@pytest.mark.django_db(databases=['default']) +def test_errors(api_client, access_token): + + Error.objects.create( + workspace_id=1, + type = 'EMPLOYEE_MAPPING', + expense_attribute_id=8, + is_resolved = False, + error_title = 'ashwin.t@fyle.in', + error_detail = 'Employee mapping is missing', + created_at = datetime.now(tz=timezone.utc), + updated_at = datetime.now(tz=timezone.utc) + ) + + url = reverse( + 'errors', kwargs={ + 'workspace_id': 1 + } + ) + api_client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(access_token)) + response = api_client.get( + url, + format='json' + ) + + assert response.status_code == 200 + + response = json.loads(response.content) + assert dict_compare_keys(response, data['errors_response']) == [], 'errors api returns a diff in the keys' + + url = '/api/v2/workspaces/1/errors/?is_resolved=False&type=CATEGORY_MAPPING' + response = api_client.get( + url, + format='json' + ) + + assert response.status_code == 200 + + Error.objects.filter( + workspace_id=1, + type='EMPLOYEE_MAPPING', + error_detail = 'Employee mapping is missing', + is_resolved=False + ).update(is_resolved=True) + + url = '/api/v2/workspaces/1/errors/?is_resolved=true&type=EMPLOYEE_MAPPING' + response = api_client.get( + url, + format='json' + ) + + assert response.status_code == 200