Skip to content

Commit

Permalink
Merge branch 'master' into fyle_import_attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
ruuushhh committed Oct 26, 2023
2 parents 1b0b916 + b8487cc commit 543a46a
Show file tree
Hide file tree
Showing 23 changed files with 390 additions and 106 deletions.
30 changes: 30 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Continuous Integration

on:
pull_request:
types: [assigned, opened, synchronize, reopened]

jobs:
pytest:
runs-on: ubuntu-latest
environment: CI Environment
steps:
- uses: actions/checkout@v2
- name: Bring up Services and Run Tests
run: |
docker-compose -f docker-compose-pipeline.yml build
docker-compose -f docker-compose-pipeline.yml up -d
docker-compose -f docker-compose-pipeline.yml exec -T api pytest tests/ --cov --junit-xml=test-reports/report.xml --cov-report=xml --cov-fail-under=70
echo "STATUS=$(cat pytest-coverage.txt | grep 'Required test' | awk '{ print $1 }')" >> $GITHUB_ENV
echo "FAILED=$(cat test-reports/report.xml | awk -F'=' '{print $5}' | awk -F' ' '{gsub(/"/, "", $1); print $1}')" >> $GITHUB_ENV
- name: Upload coverage reports to Codecov with GitHub Action
uses: codecov/codecov-action@v3
- name: Pytest coverage comment
uses: MishaKav/pytest-coverage-comment@main
if: ${{ always() && github.ref != 'refs/heads/master' }}
with:
create-new-comment: true
junitxml-path: ./test-reports/report.xml
- name: Evaluate Coverage
if: ${{ (env.STATUS == 'FAIL') || (env.FAILED > 0) }}
run: exit 1
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Pull python base image
FROM python:3.10-slim
FROM python:3.11-slim

# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
Expand Down
22 changes: 22 additions & 0 deletions apps/fyle/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
DEFAULT_FYLE_CONDITIONS = [
{
'field_name': 'employee_email',
'type': 'SELECT',
'is_custom': False
},
{
'field_name': 'claim_number',
'type': 'TEXT',
'is_custom': False
},
{
'field_name': 'report_title',
'type': 'TEXT',
'is_custom': False
},
{
'field_name': 'spent_at',
'type': 'DATE',
'is_custom': False
}
]
29 changes: 29 additions & 0 deletions apps/fyle/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
import requests
from django.conf import settings

from fyle_integrations_platform_connector import PlatformConnector

from apps.workspaces.models import FyleCredential
from apps.fyle.constants import DEFAULT_FYLE_CONDITIONS


def post_request(url, body, refresh_token=None):
"""
Expand Down Expand Up @@ -50,3 +55,27 @@ def get_cluster_domain(refresh_token: str) -> str:
cluster_api_url = '{0}/oauth/cluster/'.format(settings.FYLE_BASE_URL)

return post_request(cluster_api_url, {}, refresh_token)['cluster_domain']


def get_expense_fields(workspace_id: int):
"""
Get expense custom fields from fyle
:param workspace_id: (int)
:return: list of custom expense fields
"""

fyle_credentails = FyleCredential.objects.get(workspace_id=workspace_id)
platform = PlatformConnector(fyle_credentails)
custom_fields = platform.expense_custom_fields.list_all()

response = []
response.extend(DEFAULT_FYLE_CONDITIONS)
for custom_field in custom_fields:
if custom_field['type'] in ('SELECT', 'NUMBER', 'TEXT', 'BOOLEAN'):
response.append({
'field_name': custom_field['field_name'],
'type': custom_field['type'],
'is_custom': custom_field['is_custom']
})

return response
2 changes: 1 addition & 1 deletion apps/fyle/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class Meta:

class Reimbursement:
"""
Creating a dummy class to be able to use
Creating a dummy class to be able to user
fyle_integrations_platform_connector correctly
"""
pass
18 changes: 18 additions & 0 deletions apps/fyle/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from datetime import datetime, timezone
from apps.workspaces.models import Workspace, FyleCredential
from apps.fyle.models import ExpenseFilter
from apps.fyle.helpers import get_expense_fields


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -73,3 +74,20 @@ def create(self, validated_data):
expense_filter, _ = ExpenseFilter.objects.update_or_create(workspace_id=workspace_id, rank=validated_data['rank'], defaults=validated_data)

return expense_filter


class ExpenseFieldSerializer(serializers.Serializer):
"""
Workspace Admin Serializer
"""
expense_fields = serializers.SerializerMethodField()

def get_expense_fields(self, validated_data):
"""
Get Expense Fields
"""

workspace_id = self.context['request'].parser_context.get('kwargs').get('workspace_id')
expense_fields = get_expense_fields(workspace_id=workspace_id)

return expense_fields
3 changes: 2 additions & 1 deletion apps/fyle/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@
"""

from django.urls import path
from apps.fyle.views import ImportFyleAttributesView, ExpenseFilterView, ExpenseFilterDeleteView
from apps.fyle.views import ImportFyleAttributesView, ExpenseFilterView, ExpenseFilterDeleteView, CustomFieldView


urlpatterns = [
path('import_attributes/', ImportFyleAttributesView.as_view(), name='import-fyle-attributes'),
path('expense_filters/<int:pk>/', ExpenseFilterDeleteView.as_view(), name='expense-filters'),
path('expense_filters/', ExpenseFilterView.as_view(), name='expense-filters'),
path('expense_fields/', CustomFieldView.as_view(), name='fyle-expense-fields'),
]
12 changes: 11 additions & 1 deletion apps/fyle/views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import logging
from rest_framework import generics
from sage_desktop_api.utils import LookupFieldMixin
from apps.fyle.serializers import ImportFyleAttributesSerializer, ExpenseFilterSerializer
from apps.workspaces.models import Workspace
from apps.fyle.serializers import ImportFyleAttributesSerializer, ExpenseFilterSerializer, ExpenseFieldSerializer
from apps.fyle.models import ExpenseFilter

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -32,3 +33,12 @@ class ExpenseFilterDeleteView(generics.DestroyAPIView):

queryset = ExpenseFilter.objects.all()
serializer_class = ExpenseFilterSerializer


class CustomFieldView(generics.ListAPIView):
"""
Custom Field view
"""

serializer_class = ExpenseFieldSerializer
queryset = Workspace.objects.all()
1 change: 0 additions & 1 deletion apps/sage300/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from django.utils.module_loading import import_string

from apps.workspaces.models import Workspace, Sage300Credential
from apps.sage300.utils import SageDesktopConnector


logger = logging.getLogger(__name__)
Expand Down
61 changes: 56 additions & 5 deletions apps/sage300/serializers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
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, Sage300Credential
from apps.sage300.helpers import sync_dimensions, check_interval_and_sync_dimension

Expand All @@ -21,13 +24,15 @@ 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 Sage 300 credentials
workspace = Workspace.objects.get(pk=workspace_id)
sage_intacct_credentials = Sage300Credential.objects.get(workspace_id=workspace.id)
sage_intacct_credentials = Sage300Credential.objects.get(
workspace_id=workspace.id
)

if refresh_dimension:
# If 'refresh' is true, perform a full sync of dimensions
Expand All @@ -45,10 +50,56 @@ def create(self, validated_data):

except Sage300Credential.DoesNotExist:
# Handle the case when Sage 300 credentials are not found or invalid
raise serializers.ValidationError({'message': 'Sage300 credentials not found / invalid in workspace'})
raise serializers.ValidationError(
{'message': 'Sage300 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)
logger.error(
'Something unexpected happened workspace_id: %s %s',
workspace_id,
exception,
)
# Raise a custom exception or re-raise the original exception
raise Exception()
raise


class DestinationAttributeSerializer(serializers.Serializer):
attribute_type = serializers.CharField()
display_name = serializers.CharField()


class Sage300FieldSerializer(serializers.Serializer):
"""
Sage300 Expense Fields Serializer
"""

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

def format_sage300_fields(self, workspace_id):
attribute_types = [
"VENDOR",
"ACCOUNT",
"JOB",
"CATEGORY",
"COST_CODE",
"PAYMENT",
]
attributes = (
DestinationAttribute.objects.filter(
~Q(attribute_type__in=attribute_types),
workspace_id=workspace_id,
)
.values("attribute_type", "display_name")
.distinct()
)

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

# Adding "Job" by default since it can be supported even if it doesn't exist
attributes_list = list(serialized_attributes)
attributes_list.append({"attribute_type": "JOB", "display_name": "Job"})

return attributes_list
10 changes: 7 additions & 3 deletions apps/sage300/urls.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@

from django.urls import path

from apps.sage300.views import ImportSage300AttributesView
from apps.sage300.views import ImportSage300AttributesView, Sage300FieldsView


urlpatterns = [
path('import_attributes/', ImportSage300AttributesView.as_view(), name='import-sage300-attributes'),
path(
"import_attributes/",
ImportSage300AttributesView.as_view(),
name="import-sage300-attributes",
),
path("fields/", Sage300FieldsView.as_view(), name="sage300-fields"),
]
10 changes: 1 addition & 9 deletions apps/sage300/utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from django.conf import settings
from fyle_accounting_mappings.models import DestinationAttribute
from apps.workspaces.models import Sage300Credential
from sage_desktop_sdk.sage_desktop_sdk import SageDesktopSDK


class SageDesktopConnector:
"""
Sage300 utility functions for syncing data from Sage Desktop SDK to your application
Expand All @@ -25,7 +25,6 @@ def __init__(self, credentials_object: Sage300Credential, workspace_id: int):

self.workspace_id = workspace_id


def _create_destination_attribute(self, attribute_type, display_name, value, destination_id, active, detail):
"""
Create a destination attribute object
Expand All @@ -46,7 +45,6 @@ def _create_destination_attribute(self, attribute_type, display_name, value, des
'detail': detail
}


def _sync_data(self, data, attribute_type, display_name, workspace_id, field_names):
"""
Synchronize data from Sage Desktop SDK to your application
Expand All @@ -73,7 +71,6 @@ def _sync_data(self, data, attribute_type, display_name, workspace_id, field_nam
DestinationAttribute.bulk_create_or_update_destination_attributes(
destination_attributes, attribute_type, workspace_id, True)


def sync_accounts(self):
"""
Synchronize accounts from Sage Desktop SDK to your application
Expand All @@ -82,7 +79,6 @@ def sync_accounts(self):
self._sync_data(accounts, 'ACCOUNT', 'accounts', self.workspace_id, ['code', 'version'])
return []


def sync_vendors(self):
"""
Synchronize vendors from Sage Desktop SDK to your application
Expand All @@ -95,7 +91,6 @@ def sync_vendors(self):
self._sync_data(vendors, 'VENDOR', 'vendor', self.workspace_id, field_names)
return []


def sync_jobs(self):
"""
Synchronize jobs from Sage Desktop SDK to your application
Expand All @@ -107,7 +102,6 @@ def sync_jobs(self):
self._sync_data(jobs, 'JOB', 'job', self.workspace_id, field_names)
return []


def sync_cost_codes(self):
"""
Synchronize cost codes from Sage Desktop SDK to your application
Expand All @@ -117,7 +111,6 @@ def sync_cost_codes(self):
self._sync_data(cost_codes, 'COST_CODE', 'cost_code', self.workspace_id, field_names)
return []


def sync_categories(self):
"""
Synchronize categories from Sage Desktop SDK to your application
Expand All @@ -127,7 +120,6 @@ def sync_categories(self):
self._sync_data(categories, 'CATEGORY', 'category', self.workspace_id, field_names)
return []


def sync_commitments(self):
"""
Synchronize commitments from Sage Desktop SDK to your application
Expand Down
14 changes: 12 additions & 2 deletions apps/sage300/views.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from datetime import datetime
import logging

from rest_framework import generics

from apps.sage300.serializers import Sage300FieldSerializer
from apps.sage300.serializers import ImportSage300AttributesSerializer


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

Expand All @@ -13,5 +14,14 @@ class ImportSage300AttributesView(generics.CreateAPIView):
"""
Import Sage300 Attributes View
"""

serializer_class = ImportSage300AttributesSerializer


class Sage300FieldsView(generics.ListAPIView):
"""
Sage300 Expense Fields View
"""
serializer_class = Sage300FieldSerializer

def get_queryset(self):
return Sage300FieldSerializer().format_sage300_fields(self.kwargs["workspace_id"])
Loading

0 comments on commit 543a46a

Please sign in to comment.