-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add support for skipped expenses api #53
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
Comment on lines
+79
to
+99
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
|
||
|
||
|
||
def post_request(url, body, refresh_token=None): | ||
""" | ||
Create a HTTP post request. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The addition of the |
||
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') | ||
Comment on lines
79
to
85
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The # Inside the defaults dictionary of update_or_create method, add:
'is_skipped': expense.get('is_skipped', False), |
||
|
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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'] | ||||||||||
Comment on lines
+135
to
+136
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The 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',
+ 'is_skipped'
] Committable suggestion
Suggested change
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
Comment on lines
+22
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider combining the update and retrieval of |
||
|
||
|
||
@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) | ||
Comment on lines
+82
to
+85
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Verify that overwriting |
||
AccountingExport.create_accounting_export( | ||
expenses_object, | ||
expense_objects, | ||
fund_source=fund_source_key, | ||
workspace_id=workspace_id | ||
) | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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 = [ | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The use of - urlpatterns = list(itertools.chain(accounting_exports_path, fyle_dimension_paths, other_paths))
+ urlpatterns = accounting_exports_path + fyle_dimension_paths + other_paths Committable suggestion
Suggested change
|
||||||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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 | ||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding error handling for the case where + try:
+ org_id = Workspace.objects.get(id=self.kwargs['workspace_id']).org_id
+ except Workspace.DoesNotExist:
+ return Response(
+ data={'error': 'Workspace not found.'},
+ status=status.HTTP_404_NOT_FOUND
+ ) Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
filters = { | ||||||||||||||||||||||||||||||||||
'org_id': org_id, | ||||||||||||||||||||||||||||||||||
'is_skipped': True | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
if start_date and end_date: | ||||||||||||||||||||||||||||||||||
filters['updated_at__range'] = [start_date, end_date] | ||||||||||||||||||||||||||||||||||
Comment on lines
+119
to
+120
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure that the + from django.core.exceptions import ValidationError
+ from django.utils.dateparse import parse_date
+ if start_date and end_date:
+ try:
+ start_date = parse_date(start_date)
+ end_date = parse_date(end_date)
+ if not start_date or not end_date:
+ raise ValidationError
+ except ValidationError:
+ return Response(
+ data={'error': 'Invalid date format for start_date or end_date.'},
+ status=status.HTTP_400_BAD_REQUEST
+ )
+ filters['updated_at__range'] = [start_date, end_date] Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
queryset = Expense.objects.filter(**filters).order_by('-updated_at') | ||||||||||||||||||||||||||||||||||
return queryset |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
construct_expense_filter
function correctly handles the construction of filters for different custom field types and operators. However, consider the following points:BOOLEAN
type, the conversion from string to boolean should be case-insensitive.expense_filter.values
list always contains the expected data types before performing operations like integer conversion or boolean checks.expense_filter.condition
andexpense_filter.operator
values are sanitized and do not introduce any security vulnerabilities such as SQL injection.