diff --git a/apps/mappings/imports/modules/categories.py b/apps/mappings/imports/modules/categories.py index 5863003..1167ff5 100644 --- a/apps/mappings/imports/modules/categories.py +++ b/apps/mappings/imports/modules/categories.py @@ -1,7 +1,14 @@ +import logging from datetime import datetime -from typing import List +from typing import List, Dict +from apps.workspaces.models import ImportSetting, FyleCredential from apps.mappings.imports.modules.base import Base -from fyle_accounting_mappings.models import DestinationAttribute, CategoryMapping +from apps.mappings.helpers import prepend_code_to_name +from fyle_integrations_platform_connector import PlatformConnector +from fyle_accounting_mappings.models import DestinationAttribute, ExpenseAttribute, CategoryMapping + +logger = logging.getLogger(__name__) +logger.level = logging.INFO class Category(Base): @@ -82,3 +89,70 @@ def create_mappings(self): self.destination_field, self.workspace_id, ) + + +def disable_categories(workspace_id: int, categories_to_disable: Dict, *args, **kwargs): + """ + categories_to_disable object format: + { + 'destination_id': { + 'value': 'old_category_name', + 'updated_value': 'new_category_name', + 'code': 'old_code', + 'update_code': 'new_code' ---- if the code is updated else same as code + } + } + """ + fyle_credentials = FyleCredential.objects.get(workspace_id=workspace_id) + platform = PlatformConnector(fyle_credentials=fyle_credentials) + + use_code_in_naming = ImportSetting.objects.filter(workspace_id=workspace_id, import_code_fields__contains=['ACCOUNT']).first() + category_account_mapping = CategoryMapping.objects.filter( + workspace_id=workspace_id, + destination_account__destination_id__in=categories_to_disable.keys() + ) + + logger.info(f"Deleting Category-Account Mappings | WORKSPACE_ID: {workspace_id} | COUNT: {category_account_mapping.count()}") + category_account_mapping.delete() + + category_values = [] + for category_map in categories_to_disable.values(): + category_name = prepend_code_to_name(prepend_code_in_name=use_code_in_naming, value=category_map['value'], code=category_map['code']) + category_values.append(category_name) + + filters = { + 'workspace_id': workspace_id, + 'attribute_type': 'CATEGORY', + 'value__in': category_values, + 'active': True + } + + # Expense attribute value map is as follows: {old_category_name: destination_id} + expense_attribute_value_map = {} + for k, v in categories_to_disable.items(): + category_name = prepend_code_to_name(prepend_code_in_name=use_code_in_naming, value=v['value'], code=v['code']) + expense_attribute_value_map[category_name] = k + + expense_attributes = ExpenseAttribute.objects.filter(**filters) + + bulk_payload = [] + for expense_attribute in expense_attributes: + code = expense_attribute_value_map.get(expense_attribute.value, None) + if code: + payload = { + 'name': expense_attribute.value, + 'code': code, + 'is_enabled': False, + 'id': expense_attribute.source_id + } + bulk_payload.append(payload) + else: + logger.error(f"Category not found in categories_to_disable: {expense_attribute.value}") + + if bulk_payload: + logger.info(f"Disabling Category in Fyle | WORKSPACE_ID: {workspace_id} | COUNT: {len(bulk_payload)}") + platform.categories.post_bulk(bulk_payload) + else: + logger.info(f"No Categories to Disable in Fyle | WORKSPACE_ID: {workspace_id}") + + return bulk_payload diff --git a/apps/mappings/imports/modules/cost_centers.py b/apps/mappings/imports/modules/cost_centers.py index 0623fcb..99ea891 100644 --- a/apps/mappings/imports/modules/cost_centers.py +++ b/apps/mappings/imports/modules/cost_centers.py @@ -1,7 +1,14 @@ +import logging from datetime import datetime -from typing import List +from typing import List, Dict from apps.mappings.imports.modules.base import Base -from fyle_accounting_mappings.models import DestinationAttribute +from fyle_accounting_mappings.models import DestinationAttribute, ExpenseAttribute, MappingSetting, Mapping +from apps.workspaces.models import FyleCredential, ImportSetting +from fyle_integrations_platform_connector import PlatformConnector +from apps.mappings.helpers import prepend_code_to_name + +logger = logging.getLogger(__name__) +logger.level = logging.INFO class CostCenter(Base): @@ -56,3 +63,77 @@ def construct_fyle_payload( payload.append(cost_center) return payload + + +def disable_cost_centers(workspace_id: int, cost_centers_to_disable: Dict, *args, **kwargs): + """ + cost_centers_to_disable object format: + { + 'destination_id': { + 'value': 'old_cost_center_name', + 'updated_value': 'new_cost_center_name', + 'code': 'old_code', + 'update_code': 'new_code' ---- if the code is updated else same as code + } + } + """ + destination_type = MappingSetting.objects.get(workspace_id=workspace_id, source_field='COST_CENTER').destination_field + use_code_in_naming = ImportSetting.objects.filter(workspace_id=workspace_id, import_code_fields__contains=[destination_type]).first() + + cost_center_mappings = Mapping.objects.filter( + workspace_id=workspace_id, + source_type='COST_CENTER', + destination_type=destination_type, + destination_id__destination_id__in=cost_centers_to_disable.keys() + ) + + logger.info(f"Deleting Cost Center Mappings | WORKSPACE_ID: {workspace_id} | COUNT: {cost_center_mappings.count()}") + cost_center_mappings.delete() + + fyle_credentials = FyleCredential.objects.get(workspace_id=workspace_id) + platform = PlatformConnector(fyle_credentials=fyle_credentials) + + cost_center_values = [] + for cost_center_map in cost_centers_to_disable.values(): + cost_center_name = prepend_code_to_name(prepend_code_in_name=use_code_in_naming, value=cost_center_map['value'], code=cost_center_map['code']) + cost_center_values.append(cost_center_name) + + filters = { + 'workspace_id': workspace_id, + 'attribute_type': 'COST_CENTER', + 'value__in': cost_center_values, + 'active': True + } + + expense_attribute_value_map = {} + for k, v in cost_centers_to_disable.items(): + cost_center_name = prepend_code_to_name(prepend_code_in_name=use_code_in_naming, value=v['value'], code=v['code']) + expense_attribute_value_map[cost_center_name] = k + + expense_attributes = ExpenseAttribute.objects.filter(**filters) + + bulk_payload = [] + for expense_attribute in expense_attributes: + code = expense_attribute_value_map.get(expense_attribute.value, None) + if code: + payload = { + 'name': expense_attribute.value, + 'code': code, + 'is_enabled': False, + 'id': expense_attribute.source_id, + 'description': 'Cost Center - {0}, Id - {1}'.format( + expense_attribute.value, + code + ) + } + bulk_payload.append(payload) + else: + logger.error(f"Cost Center with value {expense_attribute.value} not found | WORKSPACE_ID: {workspace_id}") + + if bulk_payload: + logger.info(f"Disabling Cost Center in Fyle | WORKSPACE_ID: {workspace_id} | COUNT: {len(bulk_payload)}") + platform.cost_centers.post_bulk(bulk_payload) + else: + logger.info(f"No Cost Center to Disable in Fyle | WORKSPACE_ID: {workspace_id}") + + return bulk_payload diff --git a/apps/mappings/imports/modules/expense_custom_fields.py b/apps/mappings/imports/modules/expense_custom_fields.py index 73a5f33..97891b3 100644 --- a/apps/mappings/imports/modules/expense_custom_fields.py +++ b/apps/mappings/imports/modules/expense_custom_fields.py @@ -1,9 +1,11 @@ +import logging from datetime import datetime from typing import List, Dict from apps.mappings.imports.modules.base import Base from fyle_accounting_mappings.models import ( DestinationAttribute, - ExpenseAttribute + ExpenseAttribute, + Mapping ) from apps.mappings.exceptions import handle_import_exceptions from apps.mappings.models import ImportLog @@ -11,6 +13,9 @@ from apps.workspaces.models import FyleCredential from apps.mappings.constants import FYLE_EXPENSE_SYSTEM_FIELDS +logger = logging.getLogger(__name__) +logger.level = logging.INFO + class ExpenseCustomField(Base): """ @@ -176,3 +181,28 @@ def import_destination_attribute_to_fyle(self, import_log: ImportLog): self.sync_expense_attributes(platform) self.create_mappings() + + +def disable_custom_attributes(workspace_id: int, custom_fields_to_disable: Dict, *args, **kwargs): + """ + custom_fields_to_disable object format: + { + 'destination_id': { + 'value': 'old_custom_field_name', + 'updated_value': 'new_custom_field_name', + 'code': 'old_code', + 'update_code': 'new_code' ---- if the code is updated else same as code + } + } + + Currently JOB is only field that can be imported as Custom Field, may need to + update this function if more fields are added in future + """ + custom_field_mappings = Mapping.objects.filter( + workspace_id=workspace_id, + destination_type='JOB', + destination_id__destination_id__in=custom_fields_to_disable.keys() + ) + + logger.info(f"Deleting Custom Field Mappings | WORKSPACE_ID: {workspace_id} | COUNT: {custom_field_mappings.count()}") + custom_field_mappings.delete() diff --git a/apps/mappings/imports/modules/merchants.py b/apps/mappings/imports/modules/merchants.py index e8ac6e3..325d1fc 100644 --- a/apps/mappings/imports/modules/merchants.py +++ b/apps/mappings/imports/modules/merchants.py @@ -1,11 +1,16 @@ +import logging from datetime import datetime -from typing import List +from typing import List, Dict from apps.mappings.imports.modules.base import Base -from fyle_accounting_mappings.models import DestinationAttribute +from fyle_accounting_mappings.models import DestinationAttribute, ExpenseAttribute from apps.mappings.models import ImportLog from apps.mappings.exceptions import handle_import_exceptions -from apps.workspaces.models import FyleCredential +from apps.workspaces.models import FyleCredential, ImportSetting from fyle_integrations_platform_connector import PlatformConnector +from apps.mappings.helpers import prepend_code_to_name + +logger = logging.getLogger(__name__) +logger.level = logging.INFO class Merchant(Base): @@ -68,3 +73,42 @@ def import_destination_attribute_to_fyle(self, import_log: ImportLog): self.construct_payload_and_import_to_fyle(platform, import_log) self.sync_expense_attributes(platform) + + +def disable_merchants(workspace_id: int, merchants_to_disable: Dict, *args, **kwargs): + """ + merchants_to_disable object format: + { + 'destination_id': { + 'value': 'old_merchant_name', + 'updated_value': 'new_merchant_name', + 'code': 'old_code', + 'update_code': 'new_code' ---- if the code is updated else same as code + } + } + """ + fyle_credentials = FyleCredential.objects.get(workspace_id=workspace_id) + platform = PlatformConnector(fyle_credentials=fyle_credentials) + use_code_in_naming = ImportSetting.objects.filter(workspace_id = workspace_id, import_code_fields__contains=['VENDOR']).first() + + merchant_values = [] + for merchant_map in merchants_to_disable.values(): + merchant_name = prepend_code_to_name(prepend_code_in_name=use_code_in_naming, value=merchant_map['value'], code=merchant_map['code']) + merchant_values.append(merchant_name) + + filters = { + 'workspace_id': workspace_id, + 'attribute_type': 'MERCHANT', + 'value__in': merchant_values, + 'active': True + } + + bulk_payload = ExpenseAttribute.objects.filter(**filters).values_list('value', flat=True) + + if bulk_payload: + logger.info(f"Disabling Merchants in Fyle | WORKSPACE_ID: {workspace_id} | COUNT: {len(bulk_payload)}") + platform.merchants.post(bulk_payload, delete_merchants=True) + else: + logger.info(f"No Merchants to Disable in Fyle | WORKSPACE_ID: {workspace_id}") + + return bulk_payload diff --git a/apps/mappings/imports/queues.py b/apps/mappings/imports/queues.py index 64e3015..5ba216e 100644 --- a/apps/mappings/imports/queues.py +++ b/apps/mappings/imports/queues.py @@ -45,7 +45,9 @@ def chain_import_fields_to_fyle(workspace_id): 'apps.mappings.imports.tasks.trigger_import_via_schedule', workspace_id, 'ACCOUNT', - 'CATEGORY' + 'CATEGORY', + False, + True if 'ACCOUNT' in import_code_fields else False ) if import_settings.import_vendors_as_merchants: @@ -53,7 +55,9 @@ def chain_import_fields_to_fyle(workspace_id): 'apps.mappings.imports.tasks.trigger_import_via_schedule', workspace_id, 'VENDOR', - 'MERCHANT' + 'MERCHANT', + False, + True if 'VENDOR' in import_code_fields else False ) for mapping_setting in mapping_settings: diff --git a/apps/sage300/helpers.py b/apps/sage300/helpers.py index 6a7b64d..de5f256 100644 --- a/apps/sage300/helpers.py +++ b/apps/sage300/helpers.py @@ -135,11 +135,10 @@ def disable_projects(workspace_id: int, projects_to_disable: Dict, *args, **kwar 'is_enabled': False, 'id': expense_attribute.source_id } + bulk_payload.append(payload) else: logger.error(f"Project with value {expense_attribute.value} not found | WORKSPACE_ID: {workspace_id}") - bulk_payload.append(payload) - if bulk_payload: logger.info(f"Disabling Projects in Fyle | WORKSPACE_ID: {workspace_id} | COUNT: {len(bulk_payload)}") platform.projects.post_bulk(bulk_payload) diff --git a/apps/sage300/utils.py b/apps/sage300/utils.py index def534c..873575b 100644 --- a/apps/sage300/utils.py +++ b/apps/sage300/utils.py @@ -1,6 +1,6 @@ import logging from django.utils.module_loading import import_string -from fyle_accounting_mappings.models import DestinationAttribute +from fyle_accounting_mappings.models import DestinationAttribute, MappingSetting from apps.workspaces.models import Sage300Credential from sage_desktop_sdk.sage_desktop_sdk import SageDesktopSDK from apps.sage300.models import CostCategory @@ -11,6 +11,15 @@ logger.level = logging.INFO +ATTRIBUTE_CALLBACK_MAP = { + 'PROJECT': 'apps.sage300.helpers.disable_projects', + 'CATEGORY': 'apps.mappings.imports.modules.categories.disable_categories', + 'MERCHANT': 'apps.mappings.imports.modules.merchants.disable_merchants', + 'COST_CENTER': 'apps.mappings.imports.modules.cost_centers.disable_cost_centers', + 'CUSTOM': 'apps.mappings.imports.modules.expense_custom_fields.disable_custom_attributes' +} + + class SageDesktopConnector: """ Sage300 utility functions for syncing data from Sage Desktop SDK to your application @@ -132,6 +141,15 @@ def _sync_data(self, data_gen, attribute_type, display_name, workspace_id, field :param workspace_id: ID of the workspace :param field_names: Names of fields to include in detail """ + source_type = None + mapping_setting = MappingSetting.objects.filter(workspace_id=workspace_id, destination_field=attribute_type).first() + if mapping_setting: + if attribute_type == 'VENDOR': + source_type = 'MERCHANT' + elif mapping_setting.is_custom: + source_type = 'CUSTOM' + else: + source_type = mapping_setting.source_field if is_generator: for data in data_gen: @@ -144,14 +162,13 @@ def _sync_data(self, data_gen, attribute_type, display_name, workspace_id, field if destination_attr: destination_attributes.append(destination_attr) - if attribute_type == 'JOB': - project_disable_callback_path = 'apps.sage300.helpers.disable_projects' + if source_type in ATTRIBUTE_CALLBACK_MAP.keys(): DestinationAttribute.bulk_create_or_update_destination_attributes( destination_attributes, attribute_type, workspace_id, True, - attribute_disable_callback_path=project_disable_callback_path + attribute_disable_callback_path=ATTRIBUTE_CALLBACK_MAP[source_type] ) else: DestinationAttribute.bulk_create_or_update_destination_attributes( diff --git a/tests/test_mappings/test_imports/test_modules/conftest.py b/tests/test_mappings/test_imports/test_modules/conftest.py index 8b5ae2d..20e3f93 100644 --- a/tests/test_mappings/test_imports/test_modules/conftest.py +++ b/tests/test_mappings/test_imports/test_modules/conftest.py @@ -120,6 +120,44 @@ def add_cost_center_mappings(): detail='Cost Center - Platform APIs, Id - 10081', active=True ) + DestinationAttribute.objects.create( + workspace_id=workspace_id, + attribute_type='JOB', + display_name='CRE Platform', + value='CRE Platform', + destination_id='10065', + detail='Sage 300 Project - CRE Platform, Id - 10065', + active=True, + code='123' + ) + DestinationAttribute.objects.create( + workspace_id=workspace_id, + attribute_type='JOB', + display_name='Integrations CRE', + value='Integrations CRE', + destination_id='10082', + detail='Sage 300 Project - Integrations CRE, Id - 10082', + active=True, + code='123' + ) + ExpenseAttribute.objects.create( + workspace_id=workspace_id, + attribute_type='COST_CENTER', + display_name='CRE Platform', + value='123 CRE Platform', + source_id='10065', + detail='Sage 300 Cost_Center - 123 CRE Platform, Id - 10065', + active=True + ) + ExpenseAttribute.objects.create( + workspace_id=workspace_id, + attribute_type='COST_CENTER', + display_name='Integrations CRE', + value='123 Integrations CRE', + source_id='10082', + detail='Sage 300 Cost_Center - 123 Integrations CRE, Id - 10082', + active=True + ) @pytest.fixture() @@ -150,6 +188,44 @@ def add_merchant_mappings(): detail='Merchant - Platform APIs, Id - 10081', active=True ) + DestinationAttribute.objects.create( + workspace_id=workspace_id, + attribute_type='VENDOR', + display_name='CRE Platform', + value='CRE Platform', + destination_id='10065', + detail='Sage 300 Merchant - CRE Platform, Id - 10065', + active=True, + code='123' + ) + DestinationAttribute.objects.create( + workspace_id=workspace_id, + attribute_type='VENDOR', + display_name='Integrations CRE', + value='Integrations CRE', + destination_id='10082', + detail='Sage 300 Merchant - Integrations CRE, Id - 10082', + active=True, + code='123' + ) + ExpenseAttribute.objects.create( + workspace_id=workspace_id, + attribute_type='MERCHANT', + display_name='CRE Platform', + value='123 CRE Platform', + source_id='10065', + detail='Sage 300 Merchant - 123 CRE Platform, Id - 10065', + active=True + ) + ExpenseAttribute.objects.create( + workspace_id=workspace_id, + attribute_type='MERCHANT', + display_name='Integrations CRE', + value='123 Integrations CRE', + source_id='10082', + detail='Sage 300 Merchant - 123 Integrations CRE, Id - 10082', + active=True + ) @pytest.fixture() @@ -260,3 +336,28 @@ def add_expense_destination_attributes_2(): }, 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 Sage300", + source_id='10095', + detail='Merchant - Platform APIs, Id - 10085', + active=True + ) + + DestinationAttribute.objects.create( + workspace_id=1, + attribute_type='ACCOUNT', + display_name='Account', + value="Sage300", + destination_id='10085', + detail='Merchant - Platform APIs, Id - 10085', + active=True, + code='123' + ) diff --git a/tests/test_mappings/test_imports/test_modules/fixtures.py b/tests/test_mappings/test_imports/test_modules/fixtures.py index 82f8496..26635a2 100644 --- a/tests/test_mappings/test_imports/test_modules/fixtures.py +++ b/tests/test_mappings/test_imports/test_modules/fixtures.py @@ -8201,5 +8201,36 @@ 'description': 'Sage 300 Project - 123 Integrations CRE, Id - 10082', 'is_enabled': True } + ], + "create_fyle_category_payload_with_code_create_new_case":[ + { + 'name': 'Internet', + 'code': 'Internet', + 'is_enabled': True + }, + { + 'name': 'Meals', + 'code': 'Meals', + 'is_enabled': True + }, + { + 'name': '123 Sage300', + 'code': '10085', + 'is_enabled': True + } + ], + "create_fyle_cost_center_payload_with_code_create_new_case":[ + { + "name": "123 CRE Platform", + "code": "10065", + "is_enabled": True, + "description": "Cost Center - 123 CRE Platform, Id - 10065" + }, + { + "name": "123 Integrations CRE", + "code": "10082", + "is_enabled": True, + "description": "Cost Center - 123 Integrations CRE, Id - 10082" + } ] } diff --git a/tests/test_mappings/test_imports/test_modules/test_categories.py b/tests/test_mappings/test_imports/test_modules/test_categories.py index 670055d..7fdd9b0 100644 --- a/tests/test_mappings/test_imports/test_modules/test_categories.py +++ b/tests/test_mappings/test_imports/test_modules/test_categories.py @@ -1,6 +1,8 @@ -from apps.mappings.imports.modules.categories import Category +from apps.workspaces.models import ImportSetting +from apps.mappings.imports.modules.categories import Category, disable_categories from fyle_accounting_mappings.models import CategoryMapping, DestinationAttribute, ExpenseAttribute from tests.test_mappings.test_imports.test_modules.fixtures import data as destination_attributes_data +from .fixtures import data def test_construct_fyle_payload( @@ -82,3 +84,144 @@ def test_create_mappings( assert category_mappings.count() == 2 assert category_mappings[0].destination_account.value == "Internet" assert category_mappings[1].destination_account.value == "Meals" + + +def test_get_existing_fyle_attributes( + db, + create_temp_workspace, + add_expense_destination_attributes_1, + add_expense_destination_attributes_3, + add_import_settings +): + category = Category(1, 'ACCOUNT', None) + + paginated_destination_attributes = DestinationAttribute.objects.filter(workspace_id=1, attribute_type='ACCOUNT') + paginated_destination_attributes_without_duplicates = category.remove_duplicate_attributes(paginated_destination_attributes) + paginated_destination_attribute_values = [attribute.value for attribute in paginated_destination_attributes_without_duplicates] + existing_fyle_attributes_map = category.get_existing_fyle_attributes(paginated_destination_attribute_values) + + assert existing_fyle_attributes_map == {'internet': '10091', 'meals': '10092'} + + # with code prepending + category.use_code_in_naming = True + paginated_destination_attributes = DestinationAttribute.objects.filter(workspace_id=1, attribute_type='ACCOUNT', code__isnull=False) + paginated_destination_attributes_without_duplicates = category.remove_duplicate_attributes(paginated_destination_attributes) + paginated_destination_attribute_values = [attribute.value for attribute in paginated_destination_attributes_without_duplicates] + existing_fyle_attributes_map = category.get_existing_fyle_attributes(paginated_destination_attribute_values) + + assert existing_fyle_attributes_map == {'123 sage300': '10095'} + + +def test_construct_fyle_payload_with_code( + db, + create_temp_workspace, + add_expense_destination_attributes_1, + add_expense_destination_attributes_3, + add_import_settings +): + category = Category(1, 'ACCOUNT', None, True) + + paginated_destination_attributes = DestinationAttribute.objects.filter(workspace_id=1, attribute_type='ACCOUNT') + paginated_destination_attributes_without_duplicates = category.remove_duplicate_attributes(paginated_destination_attributes) + paginated_destination_attribute_values = [attribute.value for attribute in paginated_destination_attributes_without_duplicates] + existing_fyle_attributes_map = category.get_existing_fyle_attributes(paginated_destination_attribute_values) + + # already exists + fyle_payload = category.construct_fyle_payload( + paginated_destination_attributes, + existing_fyle_attributes_map, + True + ) + + assert fyle_payload == [] + + # create new case + existing_fyle_attributes_map = {} + fyle_payload = category.construct_fyle_payload( + paginated_destination_attributes, + existing_fyle_attributes_map, + True + ) + + assert fyle_payload == data["create_fyle_category_payload_with_code_create_new_case"] + + +def test_disable_categories( + db, + mocker, + create_temp_workspace, + add_fyle_credentials, + add_expense_destination_attributes_1, + add_import_settings +): + workspace_id = 1 + + projects_to_disable = { + 'destination_id': { + 'value': 'old_category', + 'updated_value': 'new_category', + 'code': 'old_category_code', + 'updated_code': 'old_category_code' + } + } + + ExpenseAttribute.objects.create( + workspace_id=workspace_id, + attribute_type='CATEGORY', + display_name='Category', + value='old_category', + source_id='source_id', + active=True + ) + + mock_platform = mocker.patch('apps.mappings.imports.modules.categories.PlatformConnector') + bulk_post_call = mocker.patch.object(mock_platform.return_value.categories, 'post_bulk') + + disable_categories(workspace_id, projects_to_disable) + + assert bulk_post_call.call_count == 1 + + projects_to_disable = { + 'destination_id': { + 'value': 'old_category_2', + 'updated_value': 'new_category', + 'code': 'old_category_code', + 'updated_code': 'new_category_code' + } + } + + disable_categories(workspace_id, projects_to_disable) + assert bulk_post_call.call_count == 1 + + # Test disable projects with code in naming + import_settings = ImportSetting.objects.get(workspace_id=workspace_id) + import_settings.import_code_fields = ['ACCOUNT'] + import_settings.save() + + ExpenseAttribute.objects.create( + workspace_id=workspace_id, + attribute_type='CATEGORY', + display_name='Category', + value='old_category_code old_category', + source_id='source_id_123', + active=True + ) + + projects_to_disable = { + 'destination_id': { + 'value': 'old_category', + 'updated_value': 'new_category', + 'code': 'old_category_code', + 'updated_code': 'old_category_code' + } + } + + payload = [{ + 'name': 'old_category_code old_category', + 'code': 'destination_id', + 'is_enabled': False, + 'id': 'source_id_123' + }] + + bulk_payload = disable_categories(workspace_id, projects_to_disable) + assert bulk_payload == payload diff --git a/tests/test_mappings/test_imports/test_modules/test_cost_centers.py b/tests/test_mappings/test_imports/test_modules/test_cost_centers.py index 1a1dbed..ff2ad08 100644 --- a/tests/test_mappings/test_imports/test_modules/test_cost_centers.py +++ b/tests/test_mappings/test_imports/test_modules/test_cost_centers.py @@ -1,5 +1,5 @@ -from apps.mappings.imports.modules.cost_centers import CostCenter -from fyle_accounting_mappings.models import DestinationAttribute +from apps.mappings.imports.modules.cost_centers import CostCenter, disable_cost_centers, ImportSetting +from fyle_accounting_mappings.models import DestinationAttribute, ExpenseAttribute, MappingSetting from .fixtures import data @@ -19,3 +19,153 @@ def test_construct_fyle_payload(api_client, test_connection, mocker, create_temp ) assert fyle_payload == data['create_fyle_cost_center_payload_create_new_case'] + + +def test_get_existing_fyle_attributes( + db, + create_temp_workspace, + add_cost_center_mappings, + add_import_settings +): + cost_center = CostCenter(1, 'JOB', None) + + paginated_destination_attributes = DestinationAttribute.objects.filter(workspace_id=1, attribute_type='JOB') + paginated_destination_attributes_without_duplicates = cost_center.remove_duplicate_attributes(paginated_destination_attributes) + paginated_destination_attribute_values = [attribute.value for attribute in paginated_destination_attributes_without_duplicates] + existing_fyle_attributes_map = cost_center.get_existing_fyle_attributes(paginated_destination_attribute_values) + + assert existing_fyle_attributes_map == {} + + # with code prepending + cost_center.use_code_in_naming = True + paginated_destination_attributes = DestinationAttribute.objects.filter(workspace_id=1, attribute_type='JOB', code__isnull=False) + paginated_destination_attributes_without_duplicates = cost_center.remove_duplicate_attributes(paginated_destination_attributes) + paginated_destination_attribute_values = [attribute.value for attribute in paginated_destination_attributes_without_duplicates] + existing_fyle_attributes_map = cost_center.get_existing_fyle_attributes(paginated_destination_attribute_values) + + assert existing_fyle_attributes_map == {'123 cre platform': '10065', '123 integrations cre': '10082'} + + +def test_construct_fyle_payload_with_code( + db, + create_temp_workspace, + add_cost_center_mappings, + add_import_settings +): + cost_center = CostCenter(1, 'JOB', None, True) + + paginated_destination_attributes = DestinationAttribute.objects.filter(workspace_id=1, attribute_type='JOB') + paginated_destination_attributes_without_duplicates = cost_center.remove_duplicate_attributes(paginated_destination_attributes) + paginated_destination_attribute_values = [attribute.value for attribute in paginated_destination_attributes_without_duplicates] + existing_fyle_attributes_map = cost_center.get_existing_fyle_attributes(paginated_destination_attribute_values) + + # already exists + fyle_payload = cost_center.construct_fyle_payload( + paginated_destination_attributes, + existing_fyle_attributes_map, + True + ) + + assert fyle_payload == [] + + # create new case + existing_fyle_attributes_map = {} + fyle_payload = cost_center.construct_fyle_payload( + paginated_destination_attributes, + existing_fyle_attributes_map, + True + ) + + assert fyle_payload == data["create_fyle_cost_center_payload_with_code_create_new_case"] + + +def test_disable_cost_centers( + db, + mocker, + create_temp_workspace, + add_fyle_credentials, + add_cost_center_mappings, + add_import_settings +): + workspace_id = 1 + + MappingSetting.objects.create( + workspace_id=workspace_id, + source_field='COST_CENTER', + destination_field='JOB', + import_to_fyle=True, + is_custom=False + ) + + cost_centers_to_disable = { + 'destination_id': { + 'value': 'old_cost_center', + 'updated_value': 'new_cost_center', + 'code': 'old_cost_center_code', + 'updated_code': 'old_cost_center_code' + } + } + + ExpenseAttribute.objects.create( + workspace_id=workspace_id, + attribute_type='COST_CENTER', + display_name='CostCenter', + value='old_cost_center', + source_id='source_id', + active=True + ) + + mock_platform = mocker.patch('apps.mappings.imports.modules.cost_centers.PlatformConnector') + bulk_post_call = mocker.patch.object(mock_platform.return_value.cost_centers, 'post_bulk') + + disable_cost_centers(workspace_id, cost_centers_to_disable) + + assert bulk_post_call.call_count == 1 + + cost_centers_to_disable = { + 'destination_id': { + 'value': 'old_cost_center_2', + 'updated_value': 'new_cost_center', + 'code': 'old_cost_center_code', + 'updated_code': 'new_cost_center_code' + } + } + + disable_cost_centers(workspace_id, cost_centers_to_disable) + assert bulk_post_call.call_count == 1 + + # Test disable projects with code in naming + import_settings = ImportSetting.objects.get(workspace_id=workspace_id) + import_settings.import_code_fields = ['JOB'] + import_settings.save() + + ExpenseAttribute.objects.create( + workspace_id=workspace_id, + attribute_type='COST_CENTER', + display_name='CostCenter', + value='old_cost_center_code old_cost_center', + source_id='source_id_123', + active=True + ) + + cost_centers_to_disable = { + 'destination_id': { + 'value': 'old_cost_center', + 'updated_value': 'new_cost_center', + 'code': 'old_cost_center_code', + 'updated_code': 'old_cost_center_code' + } + } + + payload = [ + { + 'name': 'old_cost_center_code old_cost_center', + 'code': 'destination_id', + 'is_enabled': False, + 'id': 'source_id_123', + 'description': 'Cost Center - old_cost_center_code old_cost_center, Id - destination_id' + } + ] + + bulk_payload = disable_cost_centers(workspace_id, cost_centers_to_disable) + assert bulk_payload == payload diff --git a/tests/test_mappings/test_imports/test_modules/test_expense_fields.py b/tests/test_mappings/test_imports/test_modules/test_expense_fields.py index 6027e01..9d5f0ef 100644 --- a/tests/test_mappings/test_imports/test_modules/test_expense_fields.py +++ b/tests/test_mappings/test_imports/test_modules/test_expense_fields.py @@ -1,5 +1,5 @@ -from apps.mappings.imports.modules.expense_custom_fields import ExpenseCustomField -from fyle_accounting_mappings.models import DestinationAttribute +from apps.mappings.imports.modules.expense_custom_fields import ExpenseCustomField, disable_custom_attributes +from fyle_accounting_mappings.models import DestinationAttribute, ExpenseAttribute, Mapping from apps.mappings.models import ImportLog @@ -202,3 +202,50 @@ def test_post_to_fyle_and_sync( 'is_enabled': True }] ) + + +def test_disable_custom_attributes(db, create_temp_workspace): + destination_attribute = DestinationAttribute.objects.create( + workspace_id=1, + attribute_type='JOB', + value='old_custom_field_name', + code='old_code', + destination_id='123', + active=True + ) + + expense_attribute = ExpenseAttribute.objects.create( + workspace_id=1, + attribute_type='CUSTOM', + value='old_code old_custom_field_name', + source_id='456', + active=True + ) + + mapping = Mapping.objects.create( + workspace_id=1, + source_id=expense_attribute.id, + source_type='CUSTOM', + destination_id=destination_attribute.id, + destination_type='JOB' + ) + + custom_fields_to_disable = { + '123': { + 'value': 'old_custom_field_name', + 'updated_value': 'new_custom_field_name', + 'code': 'old_code', + 'updated_code': 'new_code' + } + } + + disable_custom_attributes(1, custom_fields_to_disable) + + count = 0 + try: + mapping.refresh_from_db() + except Exception as e: + count += 1 + assert str(e) == 'Mapping matching query does not exist.' + + assert count == 1 diff --git a/tests/test_mappings/test_imports/test_modules/test_merchants.py b/tests/test_mappings/test_imports/test_modules/test_merchants.py index 4281c13..f50507d 100644 --- a/tests/test_mappings/test_imports/test_modules/test_merchants.py +++ b/tests/test_mappings/test_imports/test_modules/test_merchants.py @@ -1,6 +1,7 @@ -from apps.mappings.imports.modules.merchants import Merchant -from fyle_accounting_mappings.models import DestinationAttribute from apps.mappings.models import ImportLog +from apps.workspaces.models import ImportSetting +from apps.mappings.imports.modules.merchants import Merchant, disable_merchants +from fyle_accounting_mappings.models import DestinationAttribute, ExpenseAttribute def test_construct_fyle_payload(api_client, test_connection, mocker, create_temp_workspace, add_sage300_creds, add_fyle_credentials, add_merchant_mappings): @@ -63,3 +64,137 @@ def test_import_destination_attribute_to_fyle( merchant.import_destination_attribute_to_fyle(import_log) assert True + + +def test_get_existing_fyle_attributes( + db, + create_temp_workspace, + add_merchant_mappings, + add_import_settings +): + merchant = Merchant(1, 'VENDOR', None) + + paginated_destination_attributes = DestinationAttribute.objects.filter(workspace_id=1, attribute_type='VENDOR') + paginated_destination_attributes_without_duplicates = merchant.remove_duplicate_attributes(paginated_destination_attributes) + paginated_destination_attribute_values = [attribute.value for attribute in paginated_destination_attributes_without_duplicates] + existing_fyle_attributes_map = merchant.get_existing_fyle_attributes(paginated_destination_attribute_values) + + assert existing_fyle_attributes_map == {} + + # with code prepending + merchant.use_code_in_naming = True + paginated_destination_attributes = DestinationAttribute.objects.filter(workspace_id=1, attribute_type='VENDOR', code__isnull=False) + paginated_destination_attributes_without_duplicates = merchant.remove_duplicate_attributes(paginated_destination_attributes) + paginated_destination_attribute_values = [attribute.value for attribute in paginated_destination_attributes_without_duplicates] + existing_fyle_attributes_map = merchant.get_existing_fyle_attributes(paginated_destination_attribute_values) + + assert existing_fyle_attributes_map == {'123 cre platform': '10065', '123 integrations cre': '10082'} + + +def test_construct_fyle_payload_with_code( + db, + create_temp_workspace, + add_merchant_mappings, + add_import_settings +): + merchant = Merchant(1, 'VENDOR', None, True) + + paginated_destination_attributes = DestinationAttribute.objects.filter(workspace_id=1, attribute_type='VENDOR') + paginated_destination_attributes_without_duplicates = merchant.remove_duplicate_attributes(paginated_destination_attributes) + paginated_destination_attribute_values = [attribute.value for attribute in paginated_destination_attributes_without_duplicates] + existing_fyle_attributes_map = merchant.get_existing_fyle_attributes(paginated_destination_attribute_values) + + # already exists + fyle_payload = merchant.construct_fyle_payload( + paginated_destination_attributes, + existing_fyle_attributes_map, + True + ) + + assert fyle_payload == [] + + # create new case + existing_fyle_attributes_map = {} + fyle_payload = merchant.construct_fyle_payload( + paginated_destination_attributes, + existing_fyle_attributes_map, + True + ) + + assert fyle_payload == ['123 CRE Platform', '123 Integrations CRE'] + + +def test_disable_merchants( + db, + mocker, + create_temp_workspace, + add_fyle_credentials, + add_merchant_mappings, + add_import_settings +): + workspace_id = 1 + + projects_to_disable = { + 'destination_id': { + 'value': 'old_merchant', + 'updated_value': 'new_merchant', + 'code': 'old_merchant_code', + 'updated_code': 'old_merchant_code' + } + } + + ExpenseAttribute.objects.create( + workspace_id=workspace_id, + attribute_type='MERCHANT', + display_name='Merchant', + value='old_merchant', + source_id='source_id', + active=True + ) + + mock_platform = mocker.patch('apps.mappings.imports.modules.merchants.PlatformConnector') + bulk_post_call = mocker.patch.object(mock_platform.return_value.merchants, 'post') + + disable_merchants(workspace_id, projects_to_disable) + + assert bulk_post_call.call_count == 1 + + projects_to_disable = { + 'destination_id': { + 'value': 'old_merchant_2', + 'updated_value': 'new_merchant', + 'code': 'old_merchant_code', + 'updated_code': 'new_merchant_code' + } + } + + disable_merchants(workspace_id, projects_to_disable) + assert bulk_post_call.call_count == 1 + + # Test disable projects with code in naming + import_settings = ImportSetting.objects.get(workspace_id=workspace_id) + import_settings.import_code_fields = ['VENDOR'] + import_settings.save() + + ExpenseAttribute.objects.create( + workspace_id=workspace_id, + attribute_type='MERCHANT', + display_name='Merchant', + value='old_merchant_code old_merchant', + source_id='source_id_123', + active=True + ) + + projects_to_disable = { + 'destination_id': { + 'value': 'old_merchant', + 'updated_value': 'new_merchant', + 'code': 'old_merchant_code', + 'updated_code': 'old_merchant_code' + } + } + + payload = ['old_merchant_code old_merchant'] + + bulk_payload = disable_merchants(workspace_id, projects_to_disable) + assert bulk_payload[0] == payload[0]