Skip to content
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

Expenses Sync APIs #32

Merged
merged 15 commits into from
Nov 30, 2023
61 changes: 61 additions & 0 deletions apps/business_central/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@

from datetime import datetime, timezone
import logging

from django.utils.module_loading import import_string

from apps.workspaces.models import Workspace, BusinessCentralCredentials


logger = logging.getLogger(__name__)
logger.level = logging.INFO


# Import your Workspace and BusinessCentralCredentials models here
# Also, make sure you have 'logger' defined and imported from a logging module
def check_interval_and_sync_dimension(workspace: Workspace, business_central_credential: BusinessCentralCredentials) -> bool:
"""
Check the synchronization interval and trigger dimension synchronization if needed.

:param workspace: Workspace Instance
:param business_central_credential: BusinessCentralCredentials Instance

:return: True if synchronization is triggered, False if not
"""

if workspace.destination_synced_at:
# Calculate the time interval since the last destination sync
time_interval = datetime.now(timezone.utc) - workspace.destination_synced_at

if workspace.destination_synced_at is None or time_interval.days > 0:
# If destination_synced_at is None or the time interval is greater than 0 days, trigger synchronization
sync_dimensions(business_central_credential, workspace.id)
return True

return False


def sync_dimensions(business_central_credential: BusinessCentralCredentials, workspace_id: int) -> None:
"""
Synchronize various dimensions with Business Central using the provided credentials.

:param business_central_credential: BusinessCentralCredentials Instance
:param workspace_id: ID of the workspace

This function syncs dimensions like accounts, vendors, commitments, jobs, categories, and cost codes.
"""

# Initialize the Business Central connection using the provided credentials and workspace ID
business_central_connection = import_string('apps.business_central.utils.BusinessCentralConnector')(business_central_credential, workspace_id)

# List of dimensions to sync
dimensions = ['accounts', 'vendors', 'employees', 'locations']

for dimension in dimensions:
try:
# Dynamically call the sync method based on the dimension
sync = getattr(business_central_connection, 'sync_{}'.format(dimension))
sync()
except Exception as exception:
# Log any exceptions that occur during synchronization
logger.info(exception)
96 changes: 96 additions & 0 deletions apps/business_central/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import logging
from django.db.models import Q
from datetime import datetime
from rest_framework import serializers
from rest_framework.response import Response
from rest_framework.views import status

from fyle_accounting_mappings.models import DestinationAttribute

from apps.workspaces.models import Workspace, BusinessCentralCredentials
from apps.business_central.helpers import sync_dimensions, check_interval_and_sync_dimension


logger = logging.getLogger(__name__)
logger.level = logging.INFO


class ImportBusinessCentralAttributesSerializer(serializers.Serializer):
"""
Import Business Central Attributes serializer
"""

def create(self, validated_data):
try:
# Get the workspace ID from the URL kwargs
workspace_id = self.context['request'].parser_context['kwargs']['workspace_id']

# Check if the 'refresh' field is provided in the request data
refresh_dimension = self.context['request'].data.get('refresh', False)

# Retrieve the workspace and Business Central credentials
workspace = Workspace.objects.get(pk=workspace_id)
business_central_credentials = BusinessCentralCredentials.objects.get(
workspace_id=workspace.id
)

if refresh_dimension:
# If 'refresh' is true, perform a full sync of dimensions
sync_dimensions(business_central_credentials, workspace.id)
else:
# If 'refresh' is false, check the interval and sync dimension accordingly
check_interval_and_sync_dimension(workspace, business_central_credentials)

# Update the destination_synced_at field and save the workspace
workspace.destination_synced_at = datetime.now()
workspace.save(update_fields=['destination_synced_at'])

# Return a success response
return Response(status=status.HTTP_200_OK)

except BusinessCentralCredentials.DoesNotExist:
# Handle the case when business central credentials are not found or invalid
raise serializers.ValidationError(
{'message': 'Business Central credentials not found / invalid in workspace'}
)

except Exception as exception:
# Handle unexpected exceptions and log the error
logger.error(
'Something unexpected happened workspace_id: %s %s',
workspace_id,
exception,
)
# Raise a custom exception or re-raise the original exception
raise


class BusinessCentralFieldSerializer(serializers.Serializer):
"""
Business Central Fields Serializer
"""

attribute_type = serializers.CharField()
display_name = serializers.CharField()

def format_business_central_fields(self, workspace_id):
attribute_types = [
"VENDOR",
"ACCOUNT",
"EMPLOYEE",
"LOCATION",
]
attributes = (
DestinationAttribute.objects.filter(
~Q(attribute_type__in=attribute_types),
workspace_id=workspace_id,
)
.values("attribute_type", "display_name")
.distinct()
)

serialized_attributes = BusinessCentralFieldSerializer(attributes, many=True).data

attributes_list = list(serialized_attributes)

return attributes_list
13 changes: 13 additions & 0 deletions apps/business_central/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django.urls import path

from apps.business_central.views import ImportBusinessCentralAttributesView, BusinessCentralFieldsView


urlpatterns = [
path(
"import_attributes/",
ImportBusinessCentralAttributesView.as_view(),
name="import-business-central-attributes",
),
path("fields/", BusinessCentralFieldsView.as_view(), name="business-central-fields"),
]
21 changes: 21 additions & 0 deletions apps/business_central/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from rest_framework import generics

from apps.business_central.serializers import ImportBusinessCentralAttributesSerializer, BusinessCentralFieldSerializer


class ImportBusinessCentralAttributesView(generics.CreateAPIView):
"""
Import Business Central Attributes View
"""
serializer_class = ImportBusinessCentralAttributesSerializer


class BusinessCentralFieldsView(generics.ListAPIView):
"""
Business Central Fields View
"""
serializer_class = BusinessCentralFieldSerializer
pagination_class = None

def get_queryset(self):
return BusinessCentralFieldSerializer().format_business_central_fields(self.kwargs["workspace_id"])
37 changes: 37 additions & 0 deletions apps/fyle/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import logging
import traceback
from functools import wraps

from fyle.platform.exceptions import NoPrivilegeError

from apps.workspaces.models import FyleCredential

logger = logging.getLogger(__name__)
logger.level = logging.INFO


def handle_exceptions(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except FyleCredential.DoesNotExist:
logger.info('Fyle credentials not found %s', args[0]) # args[1] is workspace_id
args[1].detail = {'message': 'Fyle credentials do not exist in workspace'}
args[1].status = 'FAILED'
args[1].save()

except NoPrivilegeError:
logger.info('Invalid Fyle Credentials / Admin is disabled')
args[1].detail = {'message': 'Invalid Fyle Credentials / Admin is disabled'}
args[1].status = 'FAILED'
args[1].save()

except Exception:
error = traceback.format_exc()
args[1].detail = {'error': error}
args[1].status = 'FATAL'
args[1].save()
logger.exception('Something unexpected happened workspace_id: %s %s', args[0], args[1].detail)

return wrapper
Comment on lines +13 to +37
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handle_exceptions decorator assumes a specific order and type of arguments, which could lead to issues if the decorated function does not follow this pattern. Consider making the decorator more flexible to handle different argument orders or types, and ensure that exceptions are re-raised after logging and updating the status to notify the calling code of the exception.

+            raise

Committable suggestion

IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
def handle_exceptions(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except FyleCredential.DoesNotExist:
logger.info('Fyle credentials not found %s', args[0]) # args[1] is workspace_id
args[1].detail = {'message': 'Fyle credentials do not exist in workspace'}
args[1].status = 'FAILED'
args[1].save()
except NoPrivilegeError:
logger.info('Invalid Fyle Credentials / Admin is disabled')
args[1].detail = {'message': 'Invalid Fyle Credentials / Admin is disabled'}
args[1].status = 'FAILED'
args[1].save()
except Exception:
error = traceback.format_exc()
args[1].detail = {'error': error}
args[1].status = 'FATAL'
args[1].save()
logger.exception('Something unexpected happened workspace_id: %s %s', args[0], args[1].detail)
return wrapper
def handle_exceptions(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except FyleCredential.DoesNotExist:
logger.info('Fyle credentials not found %s', args[0]) # args[1] is workspace_id
args[1].detail = {'message': 'Fyle credentials do not exist in workspace'}
args[1].status = 'FAILED'
args[1].save()
raise
except NoPrivilegeError:
logger.info('Invalid Fyle Credentials / Admin is disabled')
args[1].detail = {'message': 'Invalid Fyle Credentials / Admin is disabled'}
args[1].status = 'FAILED'
args[1].save()
raise
except Exception:
error = traceback.format_exc()
args[1].detail = {'error': error}
args[1].status = 'FATAL'
args[1].save()
logger.exception('Something unexpected happened workspace_id: %s %s', args[0], args[1].detail)
raise
return wrapper

27 changes: 25 additions & 2 deletions apps/fyle/helpers.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import json

import requests
from django.conf import settings

from fyle_integrations_platform_connector import PlatformConnector

from apps.workspaces.models import FyleCredential
from apps.accounting_exports.models import AccountingExport
from apps.fyle.constants import DEFAULT_FYLE_CONDITIONS
from apps.workspaces.models import ExportSetting, FyleCredential


def post_request(url, body, refresh_token=None):
Expand Down Expand Up @@ -79,3 +80,25 @@ def get_expense_fields(workspace_id: int):
})

return response


def get_exportable_accounting_exports_ids(workspace_id: int):
"""
Get List of accounting exports ids
"""

export_setting = ExportSetting.objects.get(workspace_id=workspace_id)
fund_source = []

if export_setting.reimbursable_expenses_export_type:
fund_source.append('PERSONAL')
if export_setting.credit_card_expense_export_type:
fund_source.append('CCC')

accounting_export_ids = AccountingExport.objects.filter(
workspace_id=workspace_id,
exported_at__isnull=True,
fund_source__in=fund_source
).values_list('id', flat=True)

return accounting_export_ids
Loading
Loading