diff --git a/apps/fyle/helpers.py b/apps/fyle/helpers.py index 776c99b..03b8692 100644 --- a/apps/fyle/helpers.py +++ b/apps/fyle/helpers.py @@ -1,14 +1,104 @@ import json - import requests +from typing import List from django.conf import settings +from django.db.models import Q + from fyle_integrations_platform_connector import PlatformConnector from apps.accounting_exports.models import AccountingExport from apps.fyle.constants import DEFAULT_FYLE_CONDITIONS +from apps.fyle.models import ExpenseFilter from apps.workspaces.models import ExportSetting, FyleCredential +def construct_expense_filter(expense_filter): + constructed_expense_filter = {} + # If the expense filter is a custom field + if expense_filter.is_custom: + # If the operator is not isnull + if expense_filter.operator != 'isnull': + # If the custom field is of type SELECT and the operator is not_in + if expense_filter.custom_field_type == 'SELECT' and expense_filter.operator == 'not_in': + # Construct the filter for the custom property + filter1 = { + f'custom_properties__{expense_filter.condition}__in': expense_filter.values + } + # Invert the filter using the ~Q operator and assign it to the constructed expense filter + constructed_expense_filter = ~Q(**filter1) + else: + # If the custom field is of type NUMBER, convert the values to integers + if expense_filter.custom_field_type == 'NUMBER': + expense_filter.values = [int(value) for value in expense_filter.values] + # If the expense filter is a custom field and the operator is yes or no(checkbox) + if expense_filter.custom_field_type == 'BOOLEAN': + expense_filter.values[0] = True if expense_filter.values[0] == 'true' else False + # Construct the filter for the custom property + filter1 = { + f'custom_properties__{expense_filter.condition}__{expense_filter.operator}': + expense_filter.values[0] if len(expense_filter.values) == 1 and expense_filter.operator != 'in' + else expense_filter.values + } + # Assign the constructed filter to the constructed expense filter + constructed_expense_filter = Q(**filter1) + + # If the expense filter is a custom field and the operator is isnull + elif expense_filter.operator == 'isnull': + # Determine the value for the isnull filter based on the first value in the values list + expense_filter_value: bool = True if expense_filter.values[0].lower() == 'true' else False + # Construct the isnull filter for the custom property + filter1 = { + f'custom_properties__{expense_filter.condition}__isnull': expense_filter_value + } + # Construct the exact filter for the custom property + filter2 = { + f'custom_properties__{expense_filter.condition}__exact': None + } + if expense_filter_value: + # If the isnull filter value is True, combine the two filters using the | operator and assign it to the constructed expense filter + constructed_expense_filter = Q(**filter1) | Q(**filter2) + else: + # If the isnull filter value is False, invert the exact filter using the ~Q operator and assign it to the constructed expense filter + constructed_expense_filter = ~Q(**filter2) + + # For all non-custom fields + else: + # Construct the filter for the non-custom field + filter1 = { + f'{expense_filter.condition}__{expense_filter.operator}': + expense_filter.values[0] if len(expense_filter.values) == 1 and expense_filter.operator != 'in' + else expense_filter.values + } + # Assign the constructed filter to the constructed expense filter + constructed_expense_filter = Q(**filter1) + + # Return the constructed expense filter + return constructed_expense_filter + + +def construct_expense_filter_query(expense_filters: List[ExpenseFilter]): + final_filter = None + join_by = None + for expense_filter in expense_filters: + constructed_expense_filter = construct_expense_filter(expense_filter) + + # If this is the first filter, set it as the final filter + if expense_filter.rank == 1: + final_filter = (constructed_expense_filter) + + # If join by is AND, OR + elif expense_filter.rank != 1: + if join_by == 'AND': + final_filter = final_filter & (constructed_expense_filter) + else: + final_filter = final_filter | (constructed_expense_filter) + + # Set the join type for the additonal filter + join_by = expense_filter.join_by + + return final_filter + + def post_request(url, body, refresh_token=None): """ Create a HTTP post request. diff --git a/apps/fyle/models.py b/apps/fyle/models.py index 70f1c01..e51ef27 100644 --- a/apps/fyle/models.py +++ b/apps/fyle/models.py @@ -79,6 +79,7 @@ class Expense(BaseForeignWorkspaceModel): spent_at = CustomDateTimeField(help_text='Expense spent at') approved_at = CustomDateTimeField(help_text='Expense approved at') posted_at = CustomDateTimeField(help_text='Date when the money is taken from the bank') + is_skipped = models.BooleanField(null=True, default=False, help_text='Expense is skipped or not') expense_created_at = CustomDateTimeField(help_text='Expense created at') expense_updated_at = CustomDateTimeField(help_text='Expense created at') fund_source = StringNotNullField(help_text='Expense fund source') diff --git a/apps/fyle/serializers.py b/apps/fyle/serializers.py index 1ea4a65..6f6c48a 100644 --- a/apps/fyle/serializers.py +++ b/apps/fyle/serializers.py @@ -13,7 +13,7 @@ from rest_framework.views import status from apps.fyle.helpers import get_expense_fields -from apps.fyle.models import ExpenseFilter +from apps.fyle.models import ExpenseFilter, Expense from apps.workspaces.models import FyleCredential, Workspace logger = logging.getLogger(__name__) @@ -124,3 +124,13 @@ def get_expense_fields(self, workspace_id:int): expense_fields = get_expense_fields(workspace_id=workspace_id) return expense_fields + + +class ExpenseSerializer(serializers.ModelSerializer): + """ + Expense serializer + """ + + class Meta: + model = Expense + fields = ['updated_at', 'claim_number', 'employee_email', 'employee_name', 'fund_source', 'expense_number', 'payment_number', 'vendor', 'category', 'amount', 'report_id', 'settlement_id', 'expense_id'] diff --git a/apps/fyle/tasks.py b/apps/fyle/tasks.py index aab3070..8e7a8bf 100644 --- a/apps/fyle/tasks.py +++ b/apps/fyle/tasks.py @@ -6,7 +6,8 @@ from apps.accounting_exports.models import AccountingExport from apps.fyle.exceptions import handle_exceptions -from apps.fyle.models import Expense +from apps.fyle.models import Expense, ExpenseFilter +from apps.fyle.helpers import construct_expense_filter_query from apps.workspaces.models import ExportSetting, FyleCredential, Workspace SOURCE_ACCOUNT_MAP = { @@ -18,6 +19,31 @@ logger.level = logging.INFO +def get_filtered_expenses(workspace: int, expense_objects: list, expense_filters: list): + """ + function to get filtered expense objects + """ + + expenses_object_ids = [expense_object.id for expense_object in expense_objects] + final_query = construct_expense_filter_query(expense_filters) + + Expense.objects.filter( + final_query, + id__in=expenses_object_ids, + accountingexport__isnull=True, + org_id=workspace.org_id + ).update(is_skipped=True) + + filtered_expenses = Expense.objects.filter( + is_skipped=False, + id__in=expenses_object_ids, + accountingexport__isnull=True, + org_id=workspace.org_id + ) + + return filtered_expenses + + @handle_exceptions def import_expenses(workspace_id, accounting_export: AccountingExport, source_account_type, fund_source_key): """ @@ -53,9 +79,12 @@ def import_expenses(workspace_id, accounting_export: AccountingExport, source_ac workspace.save() with transaction.atomic(): - expenses_object = Expense.create_expense_objects(expenses, workspace_id) + expense_objects = Expense.create_expense_objects(expenses, workspace_id) + expense_filters = ExpenseFilter.objects.filter(workspace_id=workspace_id).order_by('rank') + if expense_filters: + expense_objects = get_filtered_expenses(workspace, expense_objects, expense_filters) AccountingExport.create_accounting_export( - expenses_object, + expense_objects, fund_source=fund_source_key, workspace_id=workspace_id ) diff --git a/apps/fyle/urls.py b/apps/fyle/urls.py index 8cc7cfd..41a8118 100644 --- a/apps/fyle/urls.py +++ b/apps/fyle/urls.py @@ -26,6 +26,7 @@ ExportableExpenseGroupsView, FyleFieldsView, ImportFyleAttributesView, + SkippedExpenseView ) accounting_exports_path = [ @@ -38,6 +39,7 @@ path('expense_filters/', ExpenseFilterView.as_view(), name='expense-filters'), path('fields/', FyleFieldsView.as_view(), name='fyle-fields'), path('expense_fields/', CustomFieldView.as_view(), name='fyle-expense-fields'), + path('expenses/', SkippedExpenseView.as_view(), name='expenses'), ] fyle_dimension_paths = [ diff --git a/apps/fyle/views.py b/apps/fyle/views.py index b1e1407..2198660 100644 --- a/apps/fyle/views.py +++ b/apps/fyle/views.py @@ -5,14 +5,16 @@ from rest_framework.views import status from apps.fyle.helpers import get_exportable_accounting_exports_ids -from apps.fyle.models import ExpenseFilter +from apps.fyle.models import ExpenseFilter, Expense from apps.fyle.queue import queue_import_credit_card_expenses, queue_import_reimbursable_expenses from apps.fyle.serializers import ( ExpenseFieldSerializer, ExpenseFilterSerializer, FyleFieldsSerializer, ImportFyleAttributesSerializer, + ExpenseSerializer ) +from apps.workspaces.models import Workspace from ms_business_central_api.utils import LookupFieldMixin logger = logging.getLogger(__name__) @@ -96,3 +98,26 @@ def post(self, request, *args, **kwargs): return Response( status=status.HTTP_200_OK ) + + +class SkippedExpenseView(generics.ListAPIView): + """ + List Skipped Expenses + """ + serializer_class = ExpenseSerializer + + def get_queryset(self): + start_date = self.request.query_params.get('start_date', None) + end_date = self.request.query_params.get('end_date', None) + org_id = Workspace.objects.get(id=self.kwargs['workspace_id']).org_id + + filters = { + 'org_id': org_id, + 'is_skipped': True + } + + if start_date and end_date: + filters['updated_at__range'] = [start_date, end_date] + + queryset = Expense.objects.filter(**filters).order_by('-updated_at') + return queryset