Skip to content

Commit

Permalink
add support for prepending code to COA (#655)
Browse files Browse the repository at this point in the history
* add support for prepending code to COA

* fix breaking test_cases

* add test cases

* change docker-compose to docker compose

* add config api, add prepend for refresh dimension method

* update requirements, submodule

* add seperator for the account imported with code

* fix workspace_id error
  • Loading branch information
Hrishabh17 authored Sep 4, 2024
1 parent 94f4a67 commit e07a88b
Show file tree
Hide file tree
Showing 21 changed files with 1,553 additions and 1,123 deletions.
9 changes: 9 additions & 0 deletions apps/mappings/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,12 @@ def get_auto_sync_permission(workspace_general_settings: WorkspaceGeneralSetting
is_auto_sync_status_allowed = True

return is_auto_sync_status_allowed


def prepend_code_to_name(prepend_code_in_name: bool, value: str, code: str = None) -> str:
"""
Format the attribute name based on the use_code_in_naming flag
"""
if prepend_code_in_name and code:
return "{}: {}".format(code, value)
return value
1 change: 1 addition & 0 deletions apps/mappings/queues.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ def construct_tasks_and_chain_import_fields_to_fyle(workspace_id):
'is_auto_sync_enabled': get_auto_sync_permission(workspace_general_settings),
'is_3d_mapping': False,
'charts_of_accounts': workspace_general_settings.charts_of_accounts if 'accounts' in destination_sync_methods else None,
'prepend_code_to_name': True if 'ACCOUNT' in workspace_general_settings.import_code_fields else False
}

if workspace_general_settings.import_tax_codes:
Expand Down
2 changes: 2 additions & 0 deletions apps/quickbooks_online/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ def refresh_quickbooks_dimensions(workspace_id: int):
False,
workspace_general_settings.charts_of_accounts if 'accounts' in destination_sync_methods else None,
False,
True,
True if 'ACCOUNT' in workspace_general_settings.import_code_fields else False,
q_options={'cluster': 'import'}
)

Expand Down
39 changes: 35 additions & 4 deletions apps/quickbooks_online/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ def get_last_synced_time(workspace_id: int, attribute_type: str):


CHARTS_OF_ACCOUNTS = ['Expense', 'Other Expense', 'Fixed Asset', 'Cost of Goods Sold', 'Current Liability', 'Equity', 'Other Current Asset', 'Other Current Liability', 'Long Term Liability', 'Current Asset', 'Income', 'Other Income', 'Other Asset']
ATTRIBUTE_CALLBACK_PATH = {
'ACCOUNT': 'fyle_integrations_imports.modules.categories.disable_categories'
}


class QBOConnector:
Expand Down Expand Up @@ -190,10 +193,21 @@ def sync_accounts(self):
account_attributes = {'account': [], 'credit_card_account': [], 'bank_account': [], 'accounts_payable': []}
for account in accounts:
value = format_special_characters(account['Name'] if category_sync_version == 'v1' else account['FullyQualifiedName'])

code = ' '.join(account['AcctNum'].split()) if 'AcctNum' in account and account['AcctNum'] else None
if general_settings and account['AccountType'] in CHARTS_OF_ACCOUNTS and value:
account_attributes['account'].append(
{'attribute_type': 'ACCOUNT', 'display_name': 'Account', 'value': value, 'destination_id': account['Id'], 'active': True, 'detail': {'fully_qualified_name': account['FullyQualifiedName'], 'account_type': account['AccountType']}}
{
'attribute_type': 'ACCOUNT',
'display_name': 'Account',
'value': value,
'destination_id': account['Id'],
'active': True,
'detail': {
'fully_qualified_name': account['FullyQualifiedName'],
'account_type': account['AccountType']
},
'code': code
}
)

elif account['AccountType'] == 'Credit Card' and value:
Expand Down Expand Up @@ -234,7 +248,14 @@ def sync_accounts(self):

for attribute_type, attribute in account_attributes.items():
if attribute:
DestinationAttribute.bulk_create_or_update_destination_attributes(attribute, attribute_type.upper(), self.workspace_id, True, attribute_type.title().replace('_', ' '))
DestinationAttribute.bulk_create_or_update_destination_attributes(
attribute,
attribute_type.upper(),
self.workspace_id,
True,
attribute_type.title().replace('_', ' '),
attribute_disable_callback_path=ATTRIBUTE_CALLBACK_PATH.get(attribute_type.upper())
)

last_synced_time = get_last_synced_time(self.workspace_id, 'CATEGORY')

Expand All @@ -246,6 +267,8 @@ def sync_accounts(self):
for inactive_account in inactive_accounts:
value = inactive_account['Name'].replace(" (deleted)", "").rstrip() if category_sync_version == 'v1' else inactive_account['FullyQualifiedName'].replace(" (deleted)", "").rstrip()
full_qualified_name = inactive_account['FullyQualifiedName'].replace(" (deleted)", "").rstrip()
code = ' '.join(inactive_account['AcctNum'].split()) if 'AcctNum' in inactive_account and inactive_account['AcctNum'] else None

inactive_account_attributes['account'].append(
{
'attribute_type': 'ACCOUNT',
Expand All @@ -254,12 +277,20 @@ def sync_accounts(self):
'destination_id': inactive_account['Id'],
'active': False,
'detail': {'fully_qualified_name': full_qualified_name, 'account_type': inactive_account['AccountType']},
'code': code
}
)

for attribute_type, attribute in inactive_account_attributes.items():
if attribute:
DestinationAttribute.bulk_create_or_update_destination_attributes(attribute, attribute_type.upper(), self.workspace_id, True, attribute_type.title().replace('_', ' '))
DestinationAttribute.bulk_create_or_update_destination_attributes(
attribute,
attribute_type.upper(),
self.workspace_id,
True,
attribute_type.title().replace('_', ' '),
attribute_disable_callback_path=ATTRIBUTE_CALLBACK_PATH.get(attribute_type.upper())
)

return []

Expand Down
28 changes: 27 additions & 1 deletion apps/workspaces/apis/import_settings/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class Meta:
class WorkspaceGeneralSettingsSerializer(serializers.ModelSerializer):
class Meta:
model = WorkspaceGeneralSettings
fields = ['import_categories', 'import_items', 'charts_of_accounts', 'import_tax_codes', 'import_vendors_as_merchants']
fields = ['import_categories', 'import_items', 'charts_of_accounts', 'import_tax_codes', 'import_vendors_as_merchants', 'import_code_fields']


class GeneralMappingsSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -100,6 +100,7 @@ def update(self, instance, validated):
'charts_of_accounts': workspace_general_settings.get('charts_of_accounts'),
'import_tax_codes': workspace_general_settings.get('import_tax_codes'),
'import_vendors_as_merchants': workspace_general_settings.get('import_vendors_as_merchants'),
'import_code_fields': workspace_general_settings.get('import_code_fields'),
},
)

Expand Down Expand Up @@ -145,4 +146,29 @@ def validate(self, data):

if not data.get('general_mappings'):
raise serializers.ValidationError('General mappings are required')

workspace_id = getattr(self.instance, 'id', None)
if not workspace_id:
workspace_id = self.context['request'].parser_context.get('kwargs').get('workspace_id')
general_settings = WorkspaceGeneralSettings.objects.filter(workspace_id=workspace_id).first()
import_logs = ImportLog.objects.filter(workspace_id=workspace_id).values_list('attribute_type', flat=True)

is_errored = False
old_code_pref_list = set()

if general_settings:
old_code_pref_list = set(general_settings.import_code_fields)

new_code_pref_list = set(data.get('workspace_general_settings', {}).get('import_code_fields', []))
diff_code_pref_list = list(old_code_pref_list.symmetric_difference(new_code_pref_list))

if 'ACCOUNT' in diff_code_pref_list and 'CATEGORY' in import_logs:
is_errored = True

if not old_code_pref_list.issubset(new_code_pref_list):
is_errored = True

if is_errored:
raise serializers.ValidationError('Cannot change the code fields once they are imported')

return data
23 changes: 21 additions & 2 deletions apps/workspaces/apis/import_settings/views.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
from rest_framework import generics

from rest_framework import generics, status
from rest_framework.response import Response
from apps.workspaces.apis.import_settings.serializers import ImportSettingsSerializer
from apps.workspaces.models import Workspace
from fyle_integrations_imports.models import ImportLog


class ImportSettingsView(generics.RetrieveUpdateAPIView):
serializer_class = ImportSettingsSerializer

def get_object(self):
return Workspace.objects.filter(id=self.kwargs['workspace_id']).first()


class ImportCodeFieldView(generics.GenericAPIView):
"""
Import Code Field View
"""
def get(self, request, *args, **kwargs):
workspace_id = kwargs['workspace_id']
category_import_log = ImportLog.objects.filter(workspace_id=workspace_id, attribute_type='CATEGORY').first()

response_data = {
'ACCOUNT': False if category_import_log else True,
}

return Response(
data=response_data,
status=status.HTTP_200_OK
)
3 changes: 2 additions & 1 deletion apps/workspaces/apis/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@
from apps.workspaces.apis.advanced_configurations.views import AdvancedConfigurationsView
from apps.workspaces.apis.errors.views import ErrorsView
from apps.workspaces.apis.export_settings.views import ExportSettingsView
from apps.workspaces.apis.import_settings.views import ImportSettingsView
from apps.workspaces.apis.import_settings.views import ImportSettingsView, ImportCodeFieldView
from apps.workspaces.apis.map_employees.views import MapEmployeesView
from apps.workspaces.apis.clone_settings.views import CloneSettingsView

urlpatterns = [
path('<int:workspace_id>/export_settings/', ExportSettingsView.as_view()),
path('<int:workspace_id>/map_employees/', MapEmployeesView.as_view()),
path('<int:workspace_id>/import_settings/import_code_fields_config/', ImportCodeFieldView.as_view(), name='import-code-fields-config'),
path('<int:workspace_id>/import_settings/', ImportSettingsView.as_view()),
path('<int:workspace_id>/advanced_configurations/', AdvancedConfigurationsView.as_view()),
path('<int:workspace_id>/clone_settings/', CloneSettingsView.as_view()),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 3.2.14 on 2024-08-01 10:27

import django.contrib.postgres.fields
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('workspaces', '0045_alter_workspacegeneralsettings_is_tax_override_enabled'),
]

operations = [
migrations.AddField(
model_name='workspacegeneralsettings',
name='import_code_fields',
field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(choices=[('ACCOUNT', 'ACCOUNT')], max_length=255), blank=True, default=list, help_text='Code Preference List', size=None),
),
]
13 changes: 13 additions & 0 deletions apps/workspaces/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
('EMPLOYEE', 'EMPLOYEE')
)

CODE_CHOICES = (
('ACCOUNT', 'ACCOUNT'),
)


def get_default_onboarding_state():
return 'CONNECTION'
Expand Down Expand Up @@ -120,6 +124,15 @@ class WorkspaceGeneralSettings(models.Model):
max_length=100,
help_text='Name in journal entry for ccc expense only',
default='EMPLOYEE',choices=NAME_IN_JOURNAL_ENTRY)
import_code_fields = ArrayField(
base_field=models.CharField(
max_length=255,
choices=CODE_CHOICES
),
help_text='Code Preference List',
blank=True,
default=list
)
created_at = models.DateTimeField(auto_now_add=True, help_text='Created at')
updated_at = models.DateTimeField(auto_now=True, help_text='Updated at')

Expand Down
8 changes: 8 additions & 0 deletions apps/workspaces/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,11 @@ def async_update_workspace_name(workspace: Workspace, access_token: str):

workspace.name = org_name
workspace.save()


def get_import_configuration_model_path():
return 'apps.workspaces.models.WorkspaceGeneralSettings'


def get_error_model_path():
return 'apps.tasks.models.Error'
2 changes: 1 addition & 1 deletion fyle_integrations_imports
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ django-sendgrid-v5==1.2.0
enum34==1.1.10
future==0.18.2
fyle==0.37.0
fyle-accounting-mappings==1.32.1
fyle-accounting-mappings==1.34.2
fyle-integrations-platform-connector==1.38.4
fyle-rest-auth==1.7.2
flake8==4.0.1
Expand Down
57 changes: 57 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from apps.fyle.helpers import get_access_token
from fyle_qbo_api.tests import settings
from tests.test_workspaces.fixtures import data as fyle_data
from fyle_accounting_mappings.models import ExpenseAttribute, DestinationAttribute


def pytest_configure():
Expand Down Expand Up @@ -79,3 +80,59 @@ def default_session_fixture(request):

patched_5 = mock.patch('fyle.platform.apis.v1beta.spender.MyProfile.get', return_value=fyle_data['admin_user'])
patched_5.__enter__()


@pytest.fixture()
@pytest.mark.django_db(databases=['default'])
def add_expense_destination_attributes_1():
"""
Pytest fixture to add expense & destination attributes to a workspace
"""
values = ['Internet','Meals']
count = 0

for value in values:
count += 1
ExpenseAttribute.objects.create(
workspace_id=1,
attribute_type='CATEGORY',
display_name='Category',
value= value,
source_id='1009{0}'.format(count),
detail='Merchant - Platform APIs, Id - 1008',
active=True
)
DestinationAttribute.objects.create(
workspace_id=1,
attribute_type='ACCOUNT',
display_name='Account',
value= value,
destination_id=value,
detail='Merchant - Platform APIs, Id - 10081',
active=True
)


@pytest.fixture()
@pytest.mark.django_db(databases=['default'])
def add_expense_destination_attributes_3():
ExpenseAttribute.objects.create(
workspace_id=1,
attribute_type='CATEGORY',
display_name='Category',
value="123: QBO",
source_id='10095',
detail='Merchant - Platform APIs, Id - 10085',
active=True
)

DestinationAttribute.objects.create(
workspace_id=1,
attribute_type='ACCOUNT',
display_name='Account',
value="QBO",
destination_id='10085',
detail='Merchant - Platform APIs, Id - 10085',
active=True,
code='123'
)
Loading

0 comments on commit e07a88b

Please sign in to comment.