From ea66c231cffffb1c38ed0cca4499d64635d2e184 Mon Sep 17 00:00:00 2001 From: Ashutosh singh <55102089+Ashutosh619-sudo@users.noreply.github.com> Date: Mon, 26 Feb 2024 14:58:23 +0530 Subject: [PATCH] Advance search (#573) * Advance search for QBO * advanced search bug fix * test case for advanced search * removed unused code * comment resolved * remove flake errors --- apps/fyle/helpers.py | 64 ++++++++++++++++++++++++++++++++++- apps/fyle/views.py | 12 +++---- tests/test_fyle/test_views.py | 54 ++++++++++++++++++++++++++++- 3 files changed, 122 insertions(+), 8 deletions(-) diff --git a/apps/fyle/helpers.py b/apps/fyle/helpers.py index d012ad93..25bf9f2e 100644 --- a/apps/fyle/helpers.py +++ b/apps/fyle/helpers.py @@ -4,13 +4,16 @@ from typing import List, Union import requests + +import django_filters from django.conf import settings from django.db.models import Q -from apps.fyle.models import ExpenseFilter, ExpenseGroupSettings, Expense +from apps.fyle.models import ExpenseFilter, ExpenseGroup, ExpenseGroupSettings, Expense from apps.tasks.models import TaskLog from apps.workspaces.models import WorkspaceGeneralSettings + logger = logging.getLogger(__name__) logger.level = logging.INFO @@ -245,3 +248,62 @@ def get_batched_expenses(batched_payload: List[dict], workspace_id: int) -> List """ expense_ids = [expense['id'] for expense in batched_payload] return Expense.objects.filter(expense_id__in=expense_ids, workspace_id=workspace_id) + + +class AdvanceSearchFilter(django_filters.FilterSet): + def filter_queryset(self, queryset): + or_filtered_queryset = queryset.none() + or_filter_fields = getattr(self.Meta, 'or_fields', []) + or_field_present = False + + for field_name in self.Meta.fields: + value = self.data.get(field_name) + if value: + if field_name == 'is_skipped': + value = True if str(value) == 'true' else False + filter_instance = self.filters[field_name] + queryset = filter_instance.filter(queryset, value) + + for field_name in or_filter_fields: + value = self.data.get(field_name) + if value: + or_field_present = True + filter_instance = self.filters[field_name] + field_filtered_queryset = filter_instance.filter(queryset, value) + or_filtered_queryset |= field_filtered_queryset + + if or_field_present: + queryset = queryset & or_filtered_queryset + return queryset + + return queryset + + +class ExpenseGroupSearchFilter(AdvanceSearchFilter): + exported_at__gte = django_filters.DateTimeFilter(lookup_expr='gte', field_name='exported_at') + exported_at__lte = django_filters.DateTimeFilter(lookup_expr='lte', field_name='exported_at') + tasklog__status = django_filters.CharFilter() + expenses__expense_number = django_filters.CharFilter(field_name='expenses__expense_number', lookup_expr='icontains') + expenses__employee_name = django_filters.CharFilter(field_name='expenses__employee_name', lookup_expr='icontains') + expenses__employee_email = django_filters.CharFilter(field_name='expenses__employee_email', lookup_expr='icontains') + expenses__claim_number = django_filters.CharFilter(field_name='expenses__claim_number', lookup_expr='icontains') + + class Meta: + model = ExpenseGroup + fields = ['exported_at__gte', 'exported_at__lte', 'tasklog__status'] + or_fields = ['expenses__expense_number', 'expenses__employee_name', 'expenses__employee_email', 'expenses__claim_number'] + + +class ExpenseSearchFilter(AdvanceSearchFilter): + org_id = django_filters.CharFilter() + is_skipped = django_filters.BooleanFilter() + updated_at = django_filters.DateTimeFromToRangeFilter() + expense_number = django_filters.CharFilter(field_name='expense_number', lookup_expr='icontains') + employee_name = django_filters.CharFilter(field_name='employee_name', lookup_expr='icontains') + employee_email = django_filters.CharFilter(field_name='employee_email', lookup_expr='icontains') + claim_number = django_filters.CharFilter(field_name='claim_number', lookup_expr='icontains') + + class Meta: + model = Expense + fields = ['org_id', 'is_skipped', 'updated_at'] + or_fields = ['expense_number', 'employee_name', 'employee_email', 'claim_number'] diff --git a/apps/fyle/views.py b/apps/fyle/views.py index 6c117c7d..b3111673 100644 --- a/apps/fyle/views.py +++ b/apps/fyle/views.py @@ -1,4 +1,5 @@ import logging +from apps.fyle.helpers import ExpenseGroupSearchFilter, ExpenseSearchFilter from django_filters.rest_framework import DjangoFilterBackend from rest_framework import generics @@ -34,10 +35,10 @@ class ExpenseGroupView(LookupFieldMixin, generics.ListCreateAPIView): List Fyle Expenses """ - queryset = ExpenseGroup.objects.all().order_by("-exported_at") + queryset = ExpenseGroup.objects.all().order_by("-exported_at").distinct() serializer_class = ExpenseGroupSerializer filter_backends = (DjangoFilterBackend,) - filterset_fields = {"exported_at": {"gte", "lte"}, "tasklog__status": {"exact"}} + filterset_class = ExpenseGroupSearchFilter class ExportableExpenseGroupsView(generics.RetrieveAPIView): @@ -158,16 +159,15 @@ class ExpenseFilterDeleteView(generics.DestroyAPIView): serializer_class = ExpenseFilterSerializer -class ExpenseView(generics.ListAPIView): +class ExpenseView(LookupFieldMixin, generics.ListAPIView): """ Expense view """ - queryset = Expense.objects.all() + queryset = Expense.objects.all().order_by("-updated_at").distinct() serializer_class = ExpenseSerializer filter_backends = (DjangoFilterBackend,) - filterset_fields = {'org_id': {'exact'}, 'is_skipped': {'exact'}, 'updated_at': {'gte', 'lte'}} - ordering_fields = ('-updated_at',) + filterset_class = ExpenseSearchFilter class CustomFieldView(generics.RetrieveAPIView): diff --git a/tests/test_fyle/test_views.py b/tests/test_fyle/test_views.py index 5baaae78..ba1c4618 100644 --- a/tests/test_fyle/test_views.py +++ b/tests/test_fyle/test_views.py @@ -17,7 +17,7 @@ def test_expense_group_view(api_client, test_connection): api_client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(access_token)) - response = api_client.get(url, {'exported_at__gte': '2022-05-23 13:03:06', 'exported_at__lte': '2022-05-23 13:03:48'}) + response = api_client.get(url, {'exported_at__gte': '2022-05-23T13:03:06Z', 'exported_at__lte': '2022-05-23T13:03:48Z'}) assert response.status_code == 200 response = json.loads(response.content) @@ -26,6 +26,58 @@ def test_expense_group_view(api_client, test_connection): TaskLog.objects.update_or_create(workspace_id=3, type='FETCHING_EXPENSES', defaults={'status': 'IN_PROGRESS'}) +def test_export_log_advanced_search(api_client, test_connection): + access_token = test_connection.access_token + + url = reverse('expense-groups', kwargs={'workspace_id': 3}) + + api_client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(access_token)) + + response = api_client.get(url,{'tasklog__status':'COMPLETE'}) + assert response.status_code == 200 + + response = json.loads(response.content) + assert response['count'] == 17 + + response = api_client.get(url, {'expenses__expense_number': 'E/2021/04/T/277'}) + assert response.status_code == 200 + + response = json.loads(response.content) + assert response['count'] == 1 + + response = api_client.get(url, {'expenses__expense_number': 'E/2021/04/T/'}) + assert response.status_code == 200 + + response = json.loads(response.content) + assert response['count'] == 11 + + response = api_client.get(url, {'expenses__claim_number': 'C/2021/04/R/38'}) + assert response.status_code == 200 + + response = json.loads(response.content) + assert response['count'] == 4 + + response = api_client.get(url, {'expenses__employee_email': 'user8@fyleforgotham.in'}) + assert response.status_code == 200 + + response = json.loads(response.content) + assert response['count'] == 4 + + response = api_client.get(url, {'expenses__employee_name': 'Joanna'}) + assert response.status_code == 200 + + response = json.loads(response.content) + assert response['count'] == 5 + + response = api_client.get(url, {'tasklog__status':'COMPLETE', 'expenses__employee_name': 'Joanna', 'expenses__employee_email': 'user8@fyleforgotham.in'}) + assert response.status_code == 200 + + response = json.loads(response.content) + assert response['count'] == 9 + + TaskLog.objects.update_or_create(workspace_id=3, type='FETCHING_EXPENSES', defaults={'status': 'IN_PROGRESS'}) + + def test_expense_group_settings(api_client, test_connection): access_token = test_connection.access_token