From f7b28b07ac1801f4005b97a11b08fea94de71067 Mon Sep 17 00:00:00 2001 From: Hrishabh Tiwari Date: Tue, 16 Jul 2024 19:11:35 +0530 Subject: [PATCH 01/13] Job, Dep Field code-prepending support --- apps/mappings/imports/modules/base.py | 11 ++- apps/mappings/imports/modules/categories.py | 3 +- apps/mappings/imports/modules/cost_centers.py | 3 +- .../imports/modules/expense_custom_fields.py | 5 +- apps/mappings/imports/modules/merchants.py | 5 +- apps/mappings/imports/modules/projects.py | 3 +- apps/mappings/imports/queues.py | 9 +- apps/mappings/imports/tasks.py | 12 ++- apps/sage300/dependent_fields.py | 90 ++++++++++++++++--- apps/sage300/helpers.py | 40 +++++++-- ...ostcategory_cost_category_code_and_more.py | 28 ++++++ apps/sage300/models.py | 49 ++++++++-- apps/sage300/utils.py | 10 ++- requirements.txt | 4 +- 14 files changed, 221 insertions(+), 51 deletions(-) create mode 100644 apps/sage300/migrations/0007_costcategory_cost_category_code_and_more.py diff --git a/apps/mappings/imports/modules/base.py b/apps/mappings/imports/modules/base.py index 7193d464..bc2efebb 100644 --- a/apps/mappings/imports/modules/base.py +++ b/apps/mappings/imports/modules/base.py @@ -36,12 +36,14 @@ def __init__( destination_field: str, platform_class_name: str, sync_after:datetime, + use_code_in_naming: bool = False ): self.workspace_id = workspace_id self.source_field = source_field self.destination_field = destination_field self.platform_class_name = platform_class_name self.sync_after = sync_after + self.use_code_in_naming = use_code_in_naming def get_platform_class(self, platform: PlatformConnector): """ @@ -92,9 +94,14 @@ def remove_duplicate_attributes(self, destination_attributes: List[DestinationAt attribute_values = [] for destination_attribute in destination_attributes: - if destination_attribute.value.lower() not in attribute_values: + attribute_value = destination_attribute.value + if self.use_code_in_naming: + attribute_value = '{} {}'.format(destination_attribute.code, destination_attribute.value) + + if attribute_value.lower() not in attribute_values: + destination_attribute.value = attribute_value unique_attributes.append(destination_attribute) - attribute_values.append(destination_attribute.value.lower()) + attribute_values.append(attribute_value.lower()) return unique_attributes diff --git a/apps/mappings/imports/modules/categories.py b/apps/mappings/imports/modules/categories.py index 9721ff1f..58630039 100644 --- a/apps/mappings/imports/modules/categories.py +++ b/apps/mappings/imports/modules/categories.py @@ -9,13 +9,14 @@ class Category(Base): Class for Category module """ - def __init__(self, workspace_id: int, destination_field: str, sync_after: datetime): + def __init__(self, workspace_id: int, destination_field: str, sync_after: datetime, use_code_in_naming: bool = False): super().__init__( workspace_id=workspace_id, source_field="CATEGORY", destination_field=destination_field, platform_class_name="categories", sync_after=sync_after, + use_code_in_naming=use_code_in_naming ) def trigger_import(self): diff --git a/apps/mappings/imports/modules/cost_centers.py b/apps/mappings/imports/modules/cost_centers.py index 18a7d29b..0623fcb0 100644 --- a/apps/mappings/imports/modules/cost_centers.py +++ b/apps/mappings/imports/modules/cost_centers.py @@ -9,13 +9,14 @@ class CostCenter(Base): Class for Cost Center module """ - def __init__(self, workspace_id: int, destination_field: str, sync_after: datetime): + def __init__(self, workspace_id: int, destination_field: str, sync_after: datetime, use_code_in_naming: bool = False): super().__init__( workspace_id=workspace_id, source_field="COST_CENTER", destination_field=destination_field, platform_class_name="cost_centers", sync_after=sync_after, + use_code_in_naming=use_code_in_naming ) def trigger_import(self): diff --git a/apps/mappings/imports/modules/expense_custom_fields.py b/apps/mappings/imports/modules/expense_custom_fields.py index c3678467..73a5f332 100644 --- a/apps/mappings/imports/modules/expense_custom_fields.py +++ b/apps/mappings/imports/modules/expense_custom_fields.py @@ -16,13 +16,14 @@ class ExpenseCustomField(Base): """ Class for ExepenseCustomField module """ - def __init__(self, workspace_id: int, source_field: str, destination_field: str, sync_after: datetime): + def __init__(self, workspace_id: int, source_field: str, destination_field: str, sync_after: datetime, use_code_in_naming: bool = False): super().__init__( workspace_id=workspace_id, source_field=source_field, destination_field=destination_field, platform_class_name='expense_custom_fields', - sync_after=sync_after + sync_after=sync_after, + use_code_in_naming=use_code_in_naming ) def trigger_import(self): diff --git a/apps/mappings/imports/modules/merchants.py b/apps/mappings/imports/modules/merchants.py index 586149ad..e8ac6e32 100644 --- a/apps/mappings/imports/modules/merchants.py +++ b/apps/mappings/imports/modules/merchants.py @@ -12,13 +12,14 @@ class Merchant(Base): """ Class for Merchant module """ - def __init__(self, workspace_id: int, destination_field: str, sync_after: datetime): + def __init__(self, workspace_id: int, destination_field: str, sync_after: datetime, use_code_in_naming: bool = False): super().__init__( workspace_id=workspace_id, source_field='MERCHANT', destination_field=destination_field, platform_class_name='merchants', - sync_after=sync_after + sync_after=sync_after, + use_code_in_naming=use_code_in_naming ) def trigger_import(self): diff --git a/apps/mappings/imports/modules/projects.py b/apps/mappings/imports/modules/projects.py index 86214250..89b2354d 100644 --- a/apps/mappings/imports/modules/projects.py +++ b/apps/mappings/imports/modules/projects.py @@ -10,13 +10,14 @@ class Project(Base): Class for Project module """ - def __init__(self, workspace_id: int, destination_field: str, sync_after: datetime): + def __init__(self, workspace_id: int, destination_field: str, sync_after: datetime, use_code_in_naming: bool = False): super().__init__( workspace_id=workspace_id, source_field="PROJECT", destination_field=destination_field, platform_class_name="projects", sync_after=sync_after, + use_code_in_naming=use_code_in_naming ) def trigger_import(self): diff --git a/apps/mappings/imports/queues.py b/apps/mappings/imports/queues.py index f15c2c83..c6d96bfe 100644 --- a/apps/mappings/imports/queues.py +++ b/apps/mappings/imports/queues.py @@ -16,6 +16,8 @@ def chain_import_fields_to_fyle(workspace_id): dependent_field_settings = DependentFieldSetting.objects.filter(workspace_id=workspace_id, is_import_enabled=True).first() project_mapping = MappingSetting.objects.filter(workspace_id=workspace_id, source_field='PROJECT', import_to_fyle=True).first() + import_code_fields = import_settings.import_code_fields + chain = Chain() if project_mapping and dependent_field_settings: @@ -47,7 +49,9 @@ def chain_import_fields_to_fyle(workspace_id): 'apps.mappings.imports.tasks.trigger_import_via_schedule', workspace_id, mapping_setting.destination_field, - mapping_setting.source_field + mapping_setting.source_field, + False, + True if mapping_setting.destination_field in import_code_fields else False ) for custom_fields_mapping_setting in custom_field_mapping_settings: @@ -56,7 +60,8 @@ def chain_import_fields_to_fyle(workspace_id): workspace_id, custom_fields_mapping_setting.destination_field, custom_fields_mapping_setting.source_field, - True + True, + True if custom_fields_mapping_setting.destination_field in import_code_fields else False ) if project_mapping and dependent_field_settings: diff --git a/apps/mappings/imports/tasks.py b/apps/mappings/imports/tasks.py index 28a3da6f..4f7dfefc 100644 --- a/apps/mappings/imports/tasks.py +++ b/apps/mappings/imports/tasks.py @@ -19,7 +19,13 @@ } -def trigger_import_via_schedule(workspace_id: int, destination_field: str, source_field: str, is_custom: bool = False): +def trigger_import_via_schedule( + workspace_id: int, + destination_field: str, + source_field: str, + is_custom: bool = False, + use_code_in_naming: bool = False +): """ Trigger import via schedule :param workspace_id: Workspace id @@ -30,9 +36,9 @@ def trigger_import_via_schedule(workspace_id: int, destination_field: str, sourc sync_after = import_log.last_successful_run_at if import_log else None if is_custom: - item = ExpenseCustomField(workspace_id, source_field, destination_field, sync_after) + item = ExpenseCustomField(workspace_id, source_field, destination_field, sync_after, use_code_in_naming) item.trigger_import() else: module_class = SOURCE_FIELD_CLASS_MAP[source_field] - item = module_class(workspace_id, destination_field, sync_after) + item = module_class(workspace_id, destination_field, sync_after, use_code_in_naming) item.trigger_import() diff --git a/apps/sage300/dependent_fields.py b/apps/sage300/dependent_fields.py index f122a9e5..f8a56e64 100644 --- a/apps/sage300/dependent_fields.py +++ b/apps/sage300/dependent_fields.py @@ -2,7 +2,9 @@ from datetime import datetime from typing import Dict, List from time import sleep -from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.aggregates import JSONBAgg +from django.contrib.postgres.fields import JSONField +from django.db.models import F, Func, Value from fyle_accounting_mappings.models import ExpenseAttribute from fyle_integrations_platform_connector import PlatformConnector @@ -13,6 +15,7 @@ from apps.fyle.helpers import connect_to_platform from apps.mappings.models import ImportLog from apps.mappings.exceptions import handle_import_exceptions +from apps.workspaces.models import ImportSetting logger = logging.getLogger(__name__) logger.level = logging.INFO @@ -72,12 +75,40 @@ def create_dependent_custom_field_in_fyle(workspace_id: int, fyle_attribute_type @handle_import_exceptions def post_dependent_cost_code(import_log: ImportLog, dependent_field_setting: DependentFieldSetting, platform: PlatformConnector, filters: Dict, is_enabled: bool = True) -> List[str]: - projects = CostCategory.objects.filter(**filters).values('job_name').annotate(cost_codes=ArrayAgg('cost_code_name', distinct=True)) - projects_from_categories = [project['job_name'] for project in projects] + import_settings = ImportSetting.objects.filter(workspace_id=import_log.workspace.id).first() + use_job_code_in_naming = False + use_cost_code_in_naming = False + + if 'JOB' in import_settings.import_code_fields: + use_job_code_in_naming = True + if 'COST_CODE' in import_settings.import_code_fields: + use_cost_code_in_naming = True + + projects = ( + CostCategory.objects.filter(**filters) + .values('job_name', 'job_code') + .annotate( + cost_codes=JSONBAgg( + Func( + Value('cost_code_name'), F('cost_code_name'), + Value('cost_code_code'), F('cost_code_code'), + function='jsonb_build_object' + ), + output_field=JSONField(), + distinct=True + ) + ) + ) + + projects_from_categories = [] posted_cost_codes = [] processed_batches = 0 is_errored = False + for project in projects: + project_name = "{} {}".format(project['job_code'], project['job_name']) if use_job_code_in_naming else project['job_name'] + projects_from_categories.append(project_name) + existing_projects_in_fyle = ExpenseAttribute.objects.filter( workspace_id=dependent_field_setting.workspace_id, attribute_type='PROJECT', @@ -91,17 +122,19 @@ def post_dependent_cost_code(import_log: ImportLog, dependent_field_setting: Dep for project in projects: payload = [] cost_code_names = [] + project_name = "{} {}".format(project['job_code'], project['job_name']) if use_job_code_in_naming else project['job_name'] for cost_code in project['cost_codes']: - if project['job_name'] in existing_projects_in_fyle: + if project_name in existing_projects_in_fyle: + cost_code_name = "{} {}".format(cost_code['cost_code_code'], cost_code['cost_code_name']) if use_cost_code_in_naming else cost_code['cost_code_name'] payload.append({ 'parent_expense_field_id': dependent_field_setting.project_field_id, - 'parent_expense_field_value': project['job_name'], + 'parent_expense_field_value': project_name, 'expense_field_id': dependent_field_setting.cost_code_field_id, - 'expense_field_value': cost_code, + 'expense_field_value': cost_code_name, 'is_enabled': is_enabled }) - cost_code_names.append(cost_code) + cost_code_names.append(cost_code['cost_code_name']) if payload: sleep(0.2) @@ -116,6 +149,7 @@ def post_dependent_cost_code(import_log: ImportLog, dependent_field_setting: Dep import_log.status = 'PARTIALLY_FAILED' if is_errored else 'COMPLETE' import_log.error_log = [] import_log.processed_batches_count = processed_batches + import_log.last_successful_run_at = datetime.now() if not is_errored else import_log.last_successful_run_at import_log.save() return posted_cost_codes, is_errored @@ -123,7 +157,31 @@ def post_dependent_cost_code(import_log: ImportLog, dependent_field_setting: Dep @handle_import_exceptions def post_dependent_cost_type(import_log: ImportLog, dependent_field_setting: DependentFieldSetting, platform: PlatformConnector, filters: Dict, posted_cost_codes: List = []): - cost_categories = CostCategory.objects.filter(is_imported=False, **filters).values('cost_code_name').annotate(cost_categories=ArrayAgg('name', distinct=True)) + import_settings = ImportSetting.objects.filter(workspace_id=import_log.workspace.id).first() + use_cost_code_in_naming = False + use_category_code_in_naming = False + + if 'COST_CODE' in import_settings.import_code_fields: + use_cost_code_in_naming = True + if 'COST_CATEGORY' in import_settings.import_code_fields: + use_category_code_in_naming = True + + cost_categories = ( + CostCategory.objects.filter(is_imported=False, **filters) + .values('cost_code_name', 'cost_code_code') + .annotate( + cost_categories=JSONBAgg( + Func( + Value('cost_category_name'), F('name'), + Value('cost_category_code'), F('cost_category_code'), + function='jsonb_build_object' + ), + output_field=JSONField(), + distinct=True + ) + ) + ) + is_errored = False processed_batches = 0 @@ -132,15 +190,18 @@ def post_dependent_cost_type(import_log: ImportLog, dependent_field_setting: Dep for category in cost_categories: if category['cost_code_name'] in posted_cost_codes: - payload = [ - { + cost_code_name = "{} {}".format(category['cost_code_code'], category['cost_code_name']) if use_cost_code_in_naming else category['cost_code_name'] + payload = [] + + for cost_type in category['cost_categories']: + cost_type_name = "{} {}".format(cost_type['cost_category_code'], cost_type['cost_category_name']) if use_category_code_in_naming else cost_type['cost_category_name'] + payload.append({ 'parent_expense_field_id': dependent_field_setting.cost_code_field_id, - 'parent_expense_field_value': category['cost_code_name'], + 'parent_expense_field_value': cost_code_name, 'expense_field_id': dependent_field_setting.cost_category_field_id, - 'expense_field_value': cost_type, + 'expense_field_value': cost_type_name, 'is_enabled': True - } for cost_type in category['cost_categories'] - ] + }) if payload: sleep(0.2) @@ -155,6 +216,7 @@ def post_dependent_cost_type(import_log: ImportLog, dependent_field_setting: Dep import_log.status = 'PARTIALLY_FAILED' if is_errored else 'COMPLETE' import_log.error_log = [] import_log.processed_batches_count = processed_batches + import_log.last_successful_run_at = datetime.now() if not is_errored else import_log.last_successful_run_at import_log.save() return is_errored diff --git a/apps/sage300/helpers.py b/apps/sage300/helpers.py index 0a302a5a..35ddad1f 100644 --- a/apps/sage300/helpers.py +++ b/apps/sage300/helpers.py @@ -5,7 +5,7 @@ from typing import Dict from django.utils.module_loading import import_string -from apps.workspaces.models import Workspace, Sage300Credential, FyleCredential +from apps.workspaces.models import Workspace, Sage300Credential, FyleCredential, ImportSetting from fyle_accounting_mappings.models import ExpenseAttribute from fyle_integrations_platform_connector import PlatformConnector from apps.sage300.models import CostCategory @@ -74,7 +74,9 @@ def disable_projects(workspace_id: int, projects_to_disable: Dict): { 'destination_id': { 'value': 'old_project_name', - 'updated_value': 'new_project_name' + 'updated_value': 'new_project_name', + 'code': 'old_project_code', + 'updated_code': 'new_project_code' } } @@ -83,15 +85,32 @@ def disable_projects(workspace_id: int, projects_to_disable: Dict): platform = PlatformConnector(fyle_credentials=fyle_credentials) platform.projects.sync() + use_code_in_naming = ImportSetting.objects.filter( + workspace_id = workspace_id, + import_code_fields__contains=['JOB'] + ).first() + + project_values = [] + for projects_map in projects_to_disable.values(): + project_name = projects_map['value'] + if use_code_in_naming: + project_name = "{} {}".format(projects_map['code'], project_name) + project_values.append(project_name) + filters = { 'workspace_id': workspace_id, 'attribute_type': 'PROJECT', - 'value__in': [projects_map['value'] for projects_map in projects_to_disable.values()], + 'value__in': project_values, 'active': True } # Expense attribute value map is as follows: {old_project_name: destination_id} - expense_attribute_value_map = {v['value']: k for k, v in projects_to_disable.items()} + expense_attribute_value_map = {} + for k, v in projects_to_disable.items(): + project_name = v['value'] + if use_code_in_naming: + project_name = "{} {}".format(v['code'], project_name) + expense_attribute_value_map[project_name] = k expense_attributes = ExpenseAttribute.objects.filter(**filters) @@ -120,11 +139,11 @@ def disable_projects(workspace_id: int, projects_to_disable: Dict): else: logger.info(f"No Projects to Disable in Fyle | WORKSPACE_ID: {workspace_id}") - update_and_disable_cost_code(workspace_id, projects_to_disable, platform) + update_and_disable_cost_code(workspace_id, projects_to_disable, platform, use_code_in_naming) platform.projects.sync() -def update_and_disable_cost_code(workspace_id: int, cost_codes_to_disable: Dict, platform: PlatformConnector): +def update_and_disable_cost_code(workspace_id: int, cost_codes_to_disable: Dict, platform: PlatformConnector, use_code_in_naming: bool): """ Update the job_name in CostCategory and disable the old cost code in Fyle """ @@ -144,12 +163,17 @@ def update_and_disable_cost_code(workspace_id: int, cost_codes_to_disable: Dict, # here we are updating the CostCategory with the new project name bulk_update_payload = [] for destination_id, value in cost_codes_to_disable.items(): + updated_job_name = value['updated_value'] + if use_code_in_naming: + updated_job_name = "{} {}".format(value['updated_code'], value['updated_value']) + cost_categories = CostCategory.objects.filter( workspace_id=workspace_id, job_id=destination_id - ).exclude(job_name=value['updated_value']) + ).exclude(job_name=updated_job_name) for cost_category in cost_categories: + cost_category.job_code = value['updated_code'] cost_category.job_name = value['updated_value'] cost_category.updated_at = datetime.now(timezone.utc) cost_category.is_imported = False @@ -157,4 +181,4 @@ def update_and_disable_cost_code(workspace_id: int, cost_codes_to_disable: Dict, if bulk_update_payload: logger.info(f"Updating Cost Categories | WORKSPACE_ID: {workspace_id} | COUNT: {len(bulk_update_payload)}") - CostCategory.objects.bulk_update(bulk_update_payload, ['job_name', 'updated_at', 'is_imported'], batch_size=50) + CostCategory.objects.bulk_update(bulk_update_payload, ['job_name', 'job_code', 'updated_at', 'is_imported'], batch_size=50) diff --git a/apps/sage300/migrations/0007_costcategory_cost_category_code_and_more.py b/apps/sage300/migrations/0007_costcategory_cost_category_code_and_more.py new file mode 100644 index 00000000..94257bb2 --- /dev/null +++ b/apps/sage300/migrations/0007_costcategory_cost_category_code_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.1.2 on 2024-07-16 07:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sage300', '0006_alter_purchaseinvoicelineitems_expense_account_id'), + ] + + operations = [ + migrations.AddField( + model_name='costcategory', + name='cost_category_code', + field=models.CharField(help_text='Cost_Category - Code', max_length=255, null=True), + ), + migrations.AddField( + model_name='costcategory', + name='cost_code_code', + field=models.CharField(help_text='Cost_Code - Code', max_length=255, null=True), + ), + migrations.AddField( + model_name='costcategory', + name='job_code', + field=models.CharField(help_text='Job - Code', max_length=255, null=True), + ), + ] diff --git a/apps/sage300/models.py b/apps/sage300/models.py index 3ce3fd87..0493a4e7 100644 --- a/apps/sage300/models.py +++ b/apps/sage300/models.py @@ -32,6 +32,9 @@ class CostCategory(BaseForeignWorkspaceModel): cost_category_id = StringNotNullField(help_text='Sage300 Category Id') status = BooleanFalseField(help_text='Sage300 Cost Type Status') is_imported = models.BooleanField(default=False, help_text='Is Imported') + job_code = models.CharField(max_length=255, null=True, help_text='Job - Code') + cost_code_code = models.CharField(max_length=255, null=True, help_text='Cost_Code - Code') + cost_category_code = models.CharField(max_length=255, null=True, help_text='Cost_Category - Code') class Meta: db_table = 'cost_category' @@ -56,9 +59,11 @@ def bulk_create_or_update(categories: List[Dict], workspace_id: int): 'id', 'cost_category_id', 'name', - 'status' + 'status', + 'job_code', + 'cost_code_code', + 'cost_category_code' ) - existing_cost_type_record_numbers = [] primary_key_map = {} @@ -68,6 +73,9 @@ def bulk_create_or_update(categories: List[Dict], workspace_id: int): 'id': existing_category['id'], 'name': existing_category['name'], 'status': existing_category['status'], + 'job_code': existing_category['job_code'], + 'cost_code_code': existing_category['cost_code_code'], + 'cost_category_code': existing_category['cost_category_code'], } cost_category_to_be_created = [] @@ -78,12 +86,28 @@ def bulk_create_or_update(categories: List[Dict], workspace_id: int): cost_code_ids = [category.cost_code_id for category in list_of_categories] job_ids = [category.job_id for category in list_of_categories] - job_name_mapping = {attr.destination_id: attr.value for attr in DestinationAttribute.objects.filter(destination_id__in=job_ids, workspace_id=workspace_id)} - cost_code_name_mapping = {attr.destination_id: attr.value for attr in DestinationAttribute.objects.filter(destination_id__in=cost_code_ids, workspace_id=workspace_id)} + jobs = DestinationAttribute.objects.filter(destination_id__in=job_ids, workspace_id=workspace_id) + cost_codes = DestinationAttribute.objects.filter(destination_id__in=cost_code_ids, workspace_id=workspace_id) + + job_mapping = {} + cost_code_mapping = {} + + for job in jobs: + job_mapping[job.destination_id] = { + 'job_name': job.value, + 'code': job.code + } + + for cost_code in cost_codes: + cost_code_mapping[cost_code.destination_id] = { + 'cost_code_name': cost_code.value, + 'code': cost_code.code + } for category in list_of_categories: - job_name = job_name_mapping.get(category.job_id) - cost_code_name = cost_code_name_mapping.get(category.cost_code_id) + job_name = job_mapping.get(category.job_id)['job_name'] + cost_code_name = cost_code_mapping.get(category.cost_code_id)['cost_code_name'] + cost_category_code = " ".join(category.code.split()) if category.code is not None else None if job_name and cost_code_name: jobs_to_be_updated.add(category.job_id) category_object = CostCategory( @@ -94,7 +118,10 @@ def bulk_create_or_update(categories: List[Dict], workspace_id: int): name=" ".join(category.name.split()), status=category.is_active, cost_category_id=category.id, - workspace_id=workspace_id + workspace_id=workspace_id, + job_code=job_mapping.get(category.job_id)['code'], + cost_code_code=cost_code_mapping.get(category.cost_code_id)['code'], + cost_category_code=cost_category_code ) if category.id not in existing_cost_type_record_numbers: @@ -102,8 +129,11 @@ def bulk_create_or_update(categories: List[Dict], workspace_id: int): elif category.id in primary_key_map.keys() and ( category.name != primary_key_map[category.id]['name'] or category.is_active != primary_key_map[category.id]['status'] + or job_mapping.get(category.job_id)['code'] != primary_key_map[category.id]['job_code'] + or cost_code_mapping.get(category.cost_code_id)['code'] != primary_key_map[category.id]['cost_code_code'] + or cost_category_code != primary_key_map[category.id]['cost_category_code'] ): - category_object.id = primary_key_map[category.id]['cost_category_id'] + category_object.id = primary_key_map[category.id]['id'] cost_category_to_be_updated.append(category_object) else: @@ -116,7 +146,8 @@ def bulk_create_or_update(categories: List[Dict], workspace_id: int): CostCategory.objects.bulk_update( cost_category_to_be_updated, fields=[ 'job_id', 'job_name', 'cost_code_id', 'cost_code_name', - 'name', 'status', 'cost_category_id' + 'name', 'status', 'cost_category_id', + 'job_code', 'cost_code_code', 'cost_category_code' ], batch_size=2000 ) diff --git a/apps/sage300/utils.py b/apps/sage300/utils.py index dfa08b25..60fc1e30 100644 --- a/apps/sage300/utils.py +++ b/apps/sage300/utils.py @@ -33,7 +33,7 @@ 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): + def _create_destination_attribute(self, attribute_type, display_name, value, destination_id, active, detail, code): """ Create a destination attribute object :param attribute_type: Type of the attribute @@ -50,7 +50,8 @@ def _create_destination_attribute(self, attribute_type, display_name, value, des 'value': value, 'destination_id': destination_id, 'active': active, - 'detail': detail + 'detail': detail, + 'code': code } def _update_latest_version(self, attribute_type: str): @@ -88,7 +89,8 @@ def _add_to_destination_attributes(self, item, attribute_type, display_name, fie " ".join(item.name.split()), item.id, item.is_active, - detail + detail, + item.code ) def _get_attribute_class(self, attribute_type: str): @@ -149,7 +151,7 @@ def _sync_data(self, data_gen, attribute_type, display_name, workspace_id, field attribute_type, workspace_id, True, - project_disable_callback_path=project_disable_callback_path + attribute_disable_callback_path=project_disable_callback_path ) else: DestinationAttribute.bulk_create_or_update_destination_attributes( diff --git a/requirements.txt b/requirements.txt index 762a5039..67a19114 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,8 +28,8 @@ fyle==0.37.0 # Reusable Fyle Packages fyle-rest-auth==1.7.2 -fyle-accounting-mappings==1.33.1 -fyle-integrations-platform-connector==1.38.4 +fyle-accounting-mappings==1.34.0 +fyle-integrations-platform-connector==1.39.0 # Postgres Dependincies From d8d6be32fd617ac61d72c105baef2c3907594958 Mon Sep 17 00:00:00 2001 From: Hrishabh Tiwari Date: Tue, 16 Jul 2024 19:42:28 +0530 Subject: [PATCH 02/13] auto map feature --- apps/sage300/helpers.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/sage300/helpers.py b/apps/sage300/helpers.py index 35ddad1f..675ed077 100644 --- a/apps/sage300/helpers.py +++ b/apps/sage300/helpers.py @@ -6,7 +6,7 @@ from django.utils.module_loading import import_string from apps.workspaces.models import Workspace, Sage300Credential, FyleCredential, ImportSetting -from fyle_accounting_mappings.models import ExpenseAttribute +from fyle_accounting_mappings.models import ExpenseAttribute, Mapping from fyle_integrations_platform_connector import PlatformConnector from apps.sage300.models import CostCategory from apps.fyle.models import DependentFieldSetting @@ -85,6 +85,16 @@ def disable_projects(workspace_id: int, projects_to_disable: Dict): platform = PlatformConnector(fyle_credentials=fyle_credentials) platform.projects.sync() + project_job_mapping = Mapping.objects.filter( + workspace_id=workspace_id, + source_type='PROJECT', + destination_type='JOB', + destination_id__destination_id__in=projects_to_disable.keys() + ) + + logger.info(f"Deleting Project-Job Mappings | WORKSPACE_ID: {workspace_id} | COUNT: {project_job_mapping.count()}") + project_job_mapping.delete() + use_code_in_naming = ImportSetting.objects.filter( workspace_id = workspace_id, import_code_fields__contains=['JOB'] From c6abe7ec2cd23ea77c7d7d4862999b1e242bd432 Mon Sep 17 00:00:00 2001 From: Hrishabh Tiwari Date: Wed, 17 Jul 2024 10:55:12 +0530 Subject: [PATCH 03/13] fix comments - add util func, handle code in VENDOR_TYPE --- apps/mappings/helpers.py | 8 ++++++++ apps/mappings/imports/modules/base.py | 7 +++---- apps/sage300/dependent_fields.py | 20 ++++++++++++-------- apps/sage300/helpers.py | 13 ++++--------- apps/sage300/utils.py | 2 +- 5 files changed, 28 insertions(+), 22 deletions(-) diff --git a/apps/mappings/helpers.py b/apps/mappings/helpers.py index e69de29b..49e2f502 100644 --- a/apps/mappings/helpers.py +++ b/apps/mappings/helpers.py @@ -0,0 +1,8 @@ + +def format_attribute_name(use_code_in_naming: bool, attribute_name: str, attribute_code: str = None) -> str: + """ + Format the attribute name based on the use_code_in_naming + """ + if use_code_in_naming: + return "{} {}".format(attribute_code, attribute_name) + return attribute_name diff --git a/apps/mappings/imports/modules/base.py b/apps/mappings/imports/modules/base.py index bc2efebb..a89c61ee 100644 --- a/apps/mappings/imports/modules/base.py +++ b/apps/mappings/imports/modules/base.py @@ -19,7 +19,7 @@ from apps.sage300.utils import SageDesktopConnector from apps.mappings.exceptions import handle_import_exceptions from apps.accounting_exports.models import Error - +from apps.mappings.helpers import format_attribute_name logger = logging.getLogger(__name__) logger.level = logging.INFO @@ -95,13 +95,12 @@ def remove_duplicate_attributes(self, destination_attributes: List[DestinationAt for destination_attribute in destination_attributes: attribute_value = destination_attribute.value - if self.use_code_in_naming: - attribute_value = '{} {}'.format(destination_attribute.code, destination_attribute.value) + attribute_value = format_attribute_name(self.use_code_in_naming, destination_attribute.value, destination_attribute.code) if attribute_value.lower() not in attribute_values: destination_attribute.value = attribute_value unique_attributes.append(destination_attribute) - attribute_values.append(attribute_value.lower()) + attribute_values.append(destination_attribute.value.lower()) return unique_attributes diff --git a/apps/sage300/dependent_fields.py b/apps/sage300/dependent_fields.py index f8a56e64..fbac55d1 100644 --- a/apps/sage300/dependent_fields.py +++ b/apps/sage300/dependent_fields.py @@ -16,6 +16,7 @@ from apps.mappings.models import ImportLog from apps.mappings.exceptions import handle_import_exceptions from apps.workspaces.models import ImportSetting +from apps.mappings.helpers import format_attribute_name logger = logging.getLogger(__name__) logger.level = logging.INFO @@ -78,7 +79,7 @@ def post_dependent_cost_code(import_log: ImportLog, dependent_field_setting: Dep import_settings = ImportSetting.objects.filter(workspace_id=import_log.workspace.id).first() use_job_code_in_naming = False use_cost_code_in_naming = False - + last_successful_run_at = datetime.now() if 'JOB' in import_settings.import_code_fields: use_job_code_in_naming = True if 'COST_CODE' in import_settings.import_code_fields: @@ -106,7 +107,7 @@ def post_dependent_cost_code(import_log: ImportLog, dependent_field_setting: Dep is_errored = False for project in projects: - project_name = "{} {}".format(project['job_code'], project['job_name']) if use_job_code_in_naming else project['job_name'] + project_name = format_attribute_name(use_code_in_naming=use_job_code_in_naming, attribute_name=project['job_name'], attribute_code=project['job_code']) projects_from_categories.append(project_name) existing_projects_in_fyle = ExpenseAttribute.objects.filter( @@ -122,11 +123,11 @@ def post_dependent_cost_code(import_log: ImportLog, dependent_field_setting: Dep for project in projects: payload = [] cost_code_names = [] - project_name = "{} {}".format(project['job_code'], project['job_name']) if use_job_code_in_naming else project['job_name'] + project_name = format_attribute_name(use_code_in_naming=use_job_code_in_naming, attribute_name=project['job_name'], attribute_code=project['job_code']) for cost_code in project['cost_codes']: if project_name in existing_projects_in_fyle: - cost_code_name = "{} {}".format(cost_code['cost_code_code'], cost_code['cost_code_name']) if use_cost_code_in_naming else cost_code['cost_code_name'] + cost_code_name = format_attribute_name(use_code_in_naming=use_cost_code_in_naming, attribute_name=cost_code['cost_code_name'], attribute_code=cost_code['cost_code_code']) payload.append({ 'parent_expense_field_id': dependent_field_setting.project_field_id, 'parent_expense_field_value': project_name, @@ -149,7 +150,8 @@ def post_dependent_cost_code(import_log: ImportLog, dependent_field_setting: Dep import_log.status = 'PARTIALLY_FAILED' if is_errored else 'COMPLETE' import_log.error_log = [] import_log.processed_batches_count = processed_batches - import_log.last_successful_run_at = datetime.now() if not is_errored else import_log.last_successful_run_at + if not is_errored: + import_log.last_successful_run_at = last_successful_run_at import_log.save() return posted_cost_codes, is_errored @@ -160,6 +162,7 @@ def post_dependent_cost_type(import_log: ImportLog, dependent_field_setting: Dep import_settings = ImportSetting.objects.filter(workspace_id=import_log.workspace.id).first() use_cost_code_in_naming = False use_category_code_in_naming = False + last_successful_run_at = datetime.now() if 'COST_CODE' in import_settings.import_code_fields: use_cost_code_in_naming = True @@ -190,11 +193,11 @@ def post_dependent_cost_type(import_log: ImportLog, dependent_field_setting: Dep for category in cost_categories: if category['cost_code_name'] in posted_cost_codes: - cost_code_name = "{} {}".format(category['cost_code_code'], category['cost_code_name']) if use_cost_code_in_naming else category['cost_code_name'] + cost_code_name = format_attribute_name(use_code_in_naming=use_cost_code_in_naming, attribute_name=category['cost_code_name'], attribute_code=category['cost_code_code']) payload = [] for cost_type in category['cost_categories']: - cost_type_name = "{} {}".format(cost_type['cost_category_code'], cost_type['cost_category_name']) if use_category_code_in_naming else cost_type['cost_category_name'] + cost_type_name = format_attribute_name(use_code_in_naming=use_category_code_in_naming, attribute_name=cost_type['cost_category_name'], attribute_code=cost_type['cost_category_code']) payload.append({ 'parent_expense_field_id': dependent_field_setting.cost_code_field_id, 'parent_expense_field_value': cost_code_name, @@ -216,7 +219,8 @@ def post_dependent_cost_type(import_log: ImportLog, dependent_field_setting: Dep import_log.status = 'PARTIALLY_FAILED' if is_errored else 'COMPLETE' import_log.error_log = [] import_log.processed_batches_count = processed_batches - import_log.last_successful_run_at = datetime.now() if not is_errored else import_log.last_successful_run_at + if not is_errored: + import_log.last_successful_run_at = last_successful_run_at import_log.save() return is_errored diff --git a/apps/sage300/helpers.py b/apps/sage300/helpers.py index 675ed077..c5a57e70 100644 --- a/apps/sage300/helpers.py +++ b/apps/sage300/helpers.py @@ -12,6 +12,7 @@ from apps.fyle.models import DependentFieldSetting from apps.sage300.dependent_fields import post_dependent_cost_code from apps.mappings.models import ImportLog +from apps.mappings.helpers import format_attribute_name logger = logging.getLogger(__name__) logger.level = logging.INFO @@ -102,9 +103,7 @@ def disable_projects(workspace_id: int, projects_to_disable: Dict): project_values = [] for projects_map in projects_to_disable.values(): - project_name = projects_map['value'] - if use_code_in_naming: - project_name = "{} {}".format(projects_map['code'], project_name) + project_name = format_attribute_name(use_code_in_naming=use_code_in_naming, attribute_name=projects_map['value'], attribute_code=projects_map['code']) project_values.append(project_name) filters = { @@ -117,9 +116,7 @@ def disable_projects(workspace_id: int, projects_to_disable: Dict): # Expense attribute value map is as follows: {old_project_name: destination_id} expense_attribute_value_map = {} for k, v in projects_to_disable.items(): - project_name = v['value'] - if use_code_in_naming: - project_name = "{} {}".format(v['code'], project_name) + project_name = format_attribute_name(use_code_in_naming=use_code_in_naming, attribute_name=v['value'], attribute_code=v['code']) expense_attribute_value_map[project_name] = k expense_attributes = ExpenseAttribute.objects.filter(**filters) @@ -173,9 +170,7 @@ def update_and_disable_cost_code(workspace_id: int, cost_codes_to_disable: Dict, # here we are updating the CostCategory with the new project name bulk_update_payload = [] for destination_id, value in cost_codes_to_disable.items(): - updated_job_name = value['updated_value'] - if use_code_in_naming: - updated_job_name = "{} {}".format(value['updated_code'], value['updated_value']) + updated_job_name = format_attribute_name(use_code_in_naming=use_code_in_naming, attribute_name=value['updated_value'], attribute_code=value['updated_code']) cost_categories = CostCategory.objects.filter( workspace_id=workspace_id, diff --git a/apps/sage300/utils.py b/apps/sage300/utils.py index 60fc1e30..def534cf 100644 --- a/apps/sage300/utils.py +++ b/apps/sage300/utils.py @@ -90,7 +90,7 @@ def _add_to_destination_attributes(self, item, attribute_type, display_name, fie item.id, item.is_active, detail, - item.code + item.code if hasattr(item, 'code') else None ) def _get_attribute_class(self, attribute_type: str): From ab91b34feff0a7b569b69e910d6bdbc2a224c008 Mon Sep 17 00:00:00 2001 From: Hrishabh Tiwari Date: Wed, 17 Jul 2024 11:42:08 +0530 Subject: [PATCH 04/13] fix existing test cases --- apps/mappings/helpers.py | 4 ++-- apps/sage300/dependent_fields.py | 2 ++ tests/conftest.py | 3 ++- tests/test_mappings/test_helpers.py | 19 +++++++++++++++ tests/test_sage300/test_dependent_fields.py | 12 ++++++---- tests/test_sage300/test_helpers.py | 26 +++++++++++++++------ tests/test_sage300/test_utils.py | 7 ++++-- 7 files changed, 57 insertions(+), 16 deletions(-) create mode 100644 tests/test_mappings/test_helpers.py diff --git a/apps/mappings/helpers.py b/apps/mappings/helpers.py index 49e2f502..4fa3d15f 100644 --- a/apps/mappings/helpers.py +++ b/apps/mappings/helpers.py @@ -1,8 +1,8 @@ def format_attribute_name(use_code_in_naming: bool, attribute_name: str, attribute_code: str = None) -> str: """ - Format the attribute name based on the use_code_in_naming + Format the attribute name based on the use_code_in_naming flag """ - if use_code_in_naming: + if use_code_in_naming and attribute_code: return "{} {}".format(attribute_code, attribute_name) return attribute_name diff --git a/apps/sage300/dependent_fields.py b/apps/sage300/dependent_fields.py index fbac55d1..0d2fad8b 100644 --- a/apps/sage300/dependent_fields.py +++ b/apps/sage300/dependent_fields.py @@ -109,6 +109,7 @@ def post_dependent_cost_code(import_log: ImportLog, dependent_field_setting: Dep for project in projects: project_name = format_attribute_name(use_code_in_naming=use_job_code_in_naming, attribute_name=project['job_name'], attribute_code=project['job_code']) projects_from_categories.append(project_name) + print(projects_from_categories) existing_projects_in_fyle = ExpenseAttribute.objects.filter( workspace_id=dependent_field_setting.workspace_id, @@ -117,6 +118,7 @@ def post_dependent_cost_code(import_log: ImportLog, dependent_field_setting: Dep active=True ).values_list('value', flat=True) + print(existing_projects_in_fyle) import_log.total_batches_count = len(existing_projects_in_fyle) import_log.save() diff --git a/tests/conftest.py b/tests/conftest.py index a7caf6e8..89324c82 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -464,7 +464,8 @@ def add_import_settings(): ImportSetting.objects.create( workspace_id=workspace_id, import_categories=False, - import_vendors_as_merchants=False + import_vendors_as_merchants=False, + import_code_fields = [] ) diff --git a/tests/test_mappings/test_helpers.py b/tests/test_mappings/test_helpers.py new file mode 100644 index 00000000..b1b20bcc --- /dev/null +++ b/tests/test_mappings/test_helpers.py @@ -0,0 +1,19 @@ +from apps.mappings.helpers import format_attribute_name + + +def test_format_attribute_name(): + # Test case 1: use_code_in_naming is True and attribute_code is not None + result = format_attribute_name(True, "attribute_name", "attribute_code") + assert result == "attribute_code attribute_name" + + # Test case 2: use_code_in_naming is True but attribute_code is None + result = format_attribute_name(True, "attribute_name", None) + assert result == "attribute_name" + + # Test case 3: use_code_in_naming is False and attribute_code is not None + result = format_attribute_name(False, "attribute_name", "attribute_code") + assert result == "attribute_name" + + # Test case 4: use_code_in_naming is False and attribute_code is None + result = format_attribute_name(False, "attribute_name", None) + assert result == "attribute_name" diff --git a/tests/test_sage300/test_dependent_fields.py b/tests/test_sage300/test_dependent_fields.py index 30245d90..fe775ead 100644 --- a/tests/test_sage300/test_dependent_fields.py +++ b/tests/test_sage300/test_dependent_fields.py @@ -50,7 +50,8 @@ def test_post_dependent_cost_code( create_temp_workspace, add_cost_category, add_dependent_field_setting, - add_project_mappings + add_project_mappings, + add_import_settings ): workspace_id = 1 @@ -92,7 +93,8 @@ def test_post_dependent_cost_type( create_temp_workspace, add_cost_category, add_dependent_field_setting, - add_project_mappings + add_project_mappings, + add_import_settings ): workspace_id = 1 @@ -136,7 +138,8 @@ def test_post_dependent_expense_field_values( create_temp_workspace, add_cost_category, add_dependent_field_setting, - add_project_mappings + add_project_mappings, + add_import_settings ): workspace_id = 1 @@ -167,7 +170,8 @@ def test_import_dependent_fields_to_fyle( create_temp_workspace, add_cost_category, add_dependent_field_setting, - add_project_mappings + add_project_mappings, + add_import_settings ): workspace_id = 1 diff --git a/tests/test_sage300/test_helpers.py b/tests/test_sage300/test_helpers.py index e8fffc0a..512c17e8 100644 --- a/tests/test_sage300/test_helpers.py +++ b/tests/test_sage300/test_helpers.py @@ -5,7 +5,7 @@ disable_projects, update_and_disable_cost_code ) -from apps.workspaces.models import Workspace, Sage300Credential +from apps.workspaces.models import Workspace, Sage300Credential, ImportSetting from fyle_accounting_mappings.models import ExpenseAttribute from apps.fyle.models import DependentFieldSetting from apps.sage300.models import CostCategory @@ -101,7 +101,9 @@ def test_disable_projects( projects_to_disable = { 'destination_id': { 'value': 'old_project', - 'updated_value': 'new_project' + 'updated_value': 'new_project', + 'code': 'old_project_code', + 'updated_code': 'old_project_code' } } @@ -129,7 +131,9 @@ def test_disable_projects( projects_to_disable = { 'destination_id': { 'value': 'old_project_2', - 'updated_value': 'new_project' + 'updated_value': 'new_project', + 'code': 'old_project_code', + 'updated_code': 'new_project_code' } } @@ -145,17 +149,25 @@ def test_update_and_disable_cost_code( create_temp_workspace, add_fyle_credentials, add_dependent_field_setting, - add_cost_category + add_cost_category, + add_import_settings ): workspace_id = 1 projects_to_disable = { 'destination_id': { 'value': 'old_project', - 'updated_value': 'new_project' + 'updated_value': 'new_project', + 'code': 'old_project_code', + 'updated_code': 'old_project_code' } } + import_settings = ImportSetting.objects.get(workspace_id=workspace_id) + use_code_in_naming = False + if 'JOB' in import_settings.import_code_fields: + use_code_in_naming = True + cost_category = CostCategory.objects.filter(workspace_id=workspace_id).first() cost_category.job_name = 'old_project' cost_category.job_id = 'destination_id' @@ -175,7 +187,7 @@ def test_update_and_disable_cost_code( mocker.patch.object(mock_platform.return_value.cost_centers, 'sync') mocker.patch.object(mock_platform.return_value.dependent_fields, 'bulk_post_dependent_expense_field_values') - update_and_disable_cost_code(workspace_id, projects_to_disable, mock_platform) + update_and_disable_cost_code(workspace_id, projects_to_disable, mock_platform, use_code_in_naming) updated_cost_category = CostCategory.objects.filter(workspace_id=workspace_id, job_id='destination_id').first() assert updated_cost_category.job_name == 'new_project' @@ -185,7 +197,7 @@ def test_update_and_disable_cost_code( DependentFieldSetting.objects.get(workspace_id=workspace_id).delete() - update_and_disable_cost_code(workspace_id, projects_to_disable, mock_platform) + update_and_disable_cost_code(workspace_id, projects_to_disable, mock_platform, use_code_in_naming) updated_cost_category = CostCategory.objects.filter(workspace_id=workspace_id, job_id='destination_id').first() assert updated_cost_category.job_name == 'old_project' diff --git a/tests/test_sage300/test_utils.py b/tests/test_sage300/test_utils.py index acec17a6..92418bb5 100644 --- a/tests/test_sage300/test_utils.py +++ b/tests/test_sage300/test_utils.py @@ -46,6 +46,7 @@ def test__create_destination_attribute( destination_id = 1 active = True detail = 'test' + code = '123' expected_result = { 'attribute_type': attribute_type, @@ -53,7 +54,8 @@ def test__create_destination_attribute( 'value': value, 'destination_id': destination_id, 'active': active, - 'detail': detail + 'detail': detail, + 'code': code } assert sage_connector._create_destination_attribute( @@ -62,7 +64,8 @@ def test__create_destination_attribute( value=value, destination_id=destination_id, active=active, - detail=detail + detail=detail, + code=code ) == expected_result From 2a6c29a86b7d1762d9121b4dca6bfe586607668d Mon Sep 17 00:00:00 2001 From: Hrishabh Tiwari Date: Wed, 17 Jul 2024 16:59:29 +0530 Subject: [PATCH 05/13] Add check for syncing jobs, deps and add test cases --- apps/mappings/helpers.py | 20 +++++++ apps/mappings/imports/queues.py | 10 +++- apps/sage300/dependent_fields.py | 4 +- apps/sage300/helpers.py | 1 + tests/conftest.py | 60 ++++++++++++++++++++ tests/test_mappings/test_helpers.py | 36 +++++++++++- tests/test_sage300/test_dependent_fields.py | 30 ++++++++++ tests/test_sage300/test_helpers.py | 61 ++++++++++++++++++++- 8 files changed, 213 insertions(+), 9 deletions(-) diff --git a/apps/mappings/helpers.py b/apps/mappings/helpers.py index 4fa3d15f..a2084c9c 100644 --- a/apps/mappings/helpers.py +++ b/apps/mappings/helpers.py @@ -1,3 +1,6 @@ +from datetime import datetime, timedelta, timezone +from apps.mappings.models import ImportLog + def format_attribute_name(use_code_in_naming: bool, attribute_name: str, attribute_code: str = None) -> str: """ @@ -6,3 +9,20 @@ def format_attribute_name(use_code_in_naming: bool, attribute_name: str, attribu if use_code_in_naming and attribute_code: return "{} {}".format(attribute_code, attribute_name) return attribute_name + + +def allow_job_sync(import_log: ImportLog = None) -> bool: + """ + Check if job sync is allowed + """ + time_difference = datetime.now(timezone.utc) - timedelta(minutes=30) + + if ( + not import_log + or import_log.status != 'COMPLETE' + or import_log.last_successful_run_at is None + or import_log.last_successful_run_at < time_difference + ): + return True + + return False diff --git a/apps/mappings/imports/queues.py b/apps/mappings/imports/queues.py index c6d96bfe..3a8b6b4b 100644 --- a/apps/mappings/imports/queues.py +++ b/apps/mappings/imports/queues.py @@ -3,6 +3,7 @@ from apps.workspaces.models import ImportSetting from apps.fyle.models import DependentFieldSetting from apps.mappings.models import ImportLog +from apps.mappings.helpers import allow_job_sync def chain_import_fields_to_fyle(workspace_id): @@ -17,10 +18,15 @@ def chain_import_fields_to_fyle(workspace_id): project_mapping = MappingSetting.objects.filter(workspace_id=workspace_id, source_field='PROJECT', import_to_fyle=True).first() import_code_fields = import_settings.import_code_fields + project_import_log = ImportLog.objects.filter(workspace_id=workspace_id, attribute_type='PROJECT').first() + + # We'll only sync job when the time_difference > 30 minutes to avoid + # any dependent field import issue due to timestamp on job name update + is_sync_allowed = allow_job_sync(project_import_log) chain = Chain() - if project_mapping and dependent_field_settings: + if project_mapping and dependent_field_settings and is_sync_allowed: cost_code_import_log = ImportLog.create('COST_CODE', workspace_id) cost_category_import_log = ImportLog.create('COST_CATEGORY', workspace_id) chain.append('apps.mappings.tasks.sync_sage300_attributes', 'JOB', workspace_id) @@ -64,7 +70,7 @@ def chain_import_fields_to_fyle(workspace_id): True if custom_fields_mapping_setting.destination_field in import_code_fields else False ) - if project_mapping and dependent_field_settings: + if project_mapping and dependent_field_settings and is_sync_allowed: chain.append('apps.sage300.dependent_fields.import_dependent_fields_to_fyle', workspace_id) if chain.length() > 0: diff --git a/apps/sage300/dependent_fields.py b/apps/sage300/dependent_fields.py index 0d2fad8b..6d1889c1 100644 --- a/apps/sage300/dependent_fields.py +++ b/apps/sage300/dependent_fields.py @@ -109,7 +109,6 @@ def post_dependent_cost_code(import_log: ImportLog, dependent_field_setting: Dep for project in projects: project_name = format_attribute_name(use_code_in_naming=use_job_code_in_naming, attribute_name=project['job_name'], attribute_code=project['job_code']) projects_from_categories.append(project_name) - print(projects_from_categories) existing_projects_in_fyle = ExpenseAttribute.objects.filter( workspace_id=dependent_field_setting.workspace_id, @@ -118,7 +117,6 @@ def post_dependent_cost_code(import_log: ImportLog, dependent_field_setting: Dep active=True ).values_list('value', flat=True) - print(existing_projects_in_fyle) import_log.total_batches_count = len(existing_projects_in_fyle) import_log.save() @@ -253,7 +251,7 @@ def post_dependent_expense_field_values(workspace_id: int, dependent_field_setti return else: is_cost_type_errored = post_dependent_cost_type(cost_category_import_log, dependent_field_setting, platform, filters, posted_cost_codes) - if not is_cost_type_errored and not is_cost_code_errored: + if not is_cost_type_errored and not is_cost_code_errored and cost_category_import_log.processed_batches_count > 0: DependentFieldSetting.objects.filter(workspace_id=workspace_id).update(last_successful_import_at=datetime.now()) diff --git a/apps/sage300/helpers.py b/apps/sage300/helpers.py index c5a57e70..79fa70f3 100644 --- a/apps/sage300/helpers.py +++ b/apps/sage300/helpers.py @@ -148,6 +148,7 @@ def disable_projects(workspace_id: int, projects_to_disable: Dict): update_and_disable_cost_code(workspace_id, projects_to_disable, platform, use_code_in_naming) platform.projects.sync() + return bulk_payload def update_and_disable_cost_code(workspace_id: int, cost_codes_to_disable: Dict, platform: PlatformConnector, use_code_in_naming: bool): diff --git a/tests/conftest.py b/tests/conftest.py index 89324c82..eb7029a8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -339,6 +339,26 @@ def add_project_mappings(): detail='Sage 300 Project - 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='10064', + detail='Sage 300 Project - CRE Platform, Id - 10064', + active=True, + code='123' + ) + DestinationAttribute.objects.create( + workspace_id=workspace_id, + attribute_type='JOB', + display_name='Integrations CRE', + value='Integrations CRE', + destination_id='10081', + detail='Sage 300 Project - Integrations CRE, Id - 10081', + active=True, + code='123' + ) ExpenseAttribute.objects.create( workspace_id=workspace_id, attribute_type='PROJECT', @@ -357,6 +377,24 @@ def add_project_mappings(): detail='Sage 300 Project - Platform APIs, Id - 10081', active=True ) + ExpenseAttribute.objects.create( + workspace_id=workspace_id, + attribute_type='PROJECT', + display_name='CRE Platform', + value='123 CRE Platform', + source_id='10065', + detail='Sage 300 Project - 123 CRE Platform, Id - 10065', + active=True + ) + ExpenseAttribute.objects.create( + workspace_id=workspace_id, + attribute_type='PROJECT', + display_name='Integrations CRE', + value='123 Integrations CRE', + source_id='10082', + detail='Sage 300 Project - 123 Integrations CRE, Id - 10082', + active=True + ) @pytest.fixture() @@ -616,6 +654,28 @@ def add_cost_category(create_temp_workspace): workspace = Workspace.objects.get(id=workspace_id), is_imported = False ) + CostCategory.objects.create( + job_id='10065', + job_name='Integrations CRE', + cost_code_id='cost_code_id_123', + cost_code_name='Integrations CRE', + name='Integrations', + cost_category_id='cost_category_id_456', + status=True, + workspace = Workspace.objects.get(id=workspace_id), + is_imported = False + ) + CostCategory.objects.create( + job_id='10082', + job_name='CRE Platform', + cost_code_id='cost_code_id_545', + cost_code_name='CRE Platform', + name='CRE', + cost_category_id='cost_category_id_583', + status=True, + workspace = Workspace.objects.get(id=workspace_id), + is_imported = False + ) @pytest.fixture() diff --git a/tests/test_mappings/test_helpers.py b/tests/test_mappings/test_helpers.py index b1b20bcc..3a642ee5 100644 --- a/tests/test_mappings/test_helpers.py +++ b/tests/test_mappings/test_helpers.py @@ -1,4 +1,6 @@ -from apps.mappings.helpers import format_attribute_name +from datetime import datetime, timedelta, timezone +from apps.mappings.helpers import format_attribute_name, allow_job_sync +from apps.mappings.models import ImportLog def test_format_attribute_name(): @@ -17,3 +19,35 @@ def test_format_attribute_name(): # Test case 4: use_code_in_naming is False and attribute_code is None result = format_attribute_name(False, "attribute_name", None) assert result == "attribute_name" + + +def test_allow_job_sync(db, create_temp_workspace): + import_log = ImportLog.create('PROJECT', 1) + + # Test case 1: import_log is None + result = allow_job_sync(None) + assert result is True + + # Test case 2: import_log is not None and last_successful_run_at is None + import_log.last_successful_run_at = None + import_log.status = 'COMPLETE' + result = allow_job_sync(import_log) + assert result is True + + # Test case 3: import_log is not None and status is not 'COMPLETE' + import_log.last_successful_run_at = '2021-01-01T00:00:00Z' + import_log.status = 'FATAL' + result = allow_job_sync(import_log) + assert result is True + + # Test case 4: import_log is not None and last_successful_run_at is less than 30 minutes + import_log.last_successful_run_at = datetime.now(timezone.utc) - timedelta(minutes=29) + import_log.status = 'COMPLETE' + result = allow_job_sync(import_log) + assert result is False + + # Test case 5: import_log is not None and last_successful_run_at is greater than 30 minutes + import_log.last_successful_run_at = datetime.now(timezone.utc) - timedelta(minutes=31) + import_log.status = 'COMPLETE' + result = allow_job_sync(import_log) + assert result is True diff --git a/tests/test_sage300/test_dependent_fields.py b/tests/test_sage300/test_dependent_fields.py index fe775ead..cca80b75 100644 --- a/tests/test_sage300/test_dependent_fields.py +++ b/tests/test_sage300/test_dependent_fields.py @@ -7,6 +7,8 @@ ) from apps.fyle.models import DependentFieldSetting from apps.mappings.models import ImportLog +from apps.sage300.models import CostCategory +from apps.workspaces.models import ImportSetting def test_construct_custom_field_placeholder(): @@ -86,6 +88,19 @@ def test_post_dependent_cost_code( ) assert cost_code_import_log.status == 'FATAL' + # Code pre-prepend case + ImportSetting.objects.filter(workspace_id=workspace_id).update(import_code_fields=['JOB', 'COST_CODE', 'COST_CATEGORY']) + CostCategory.objects.filter(workspace_id=workspace_id).update(job_code='123', cost_code_code='456', cost_category_code='789') + + result, is_errored = post_dependent_cost_code( + cost_code_import_log, + dependent_field_setting=dependent_field_settings, + platform=platform.return_value, + filters=filters + ) + assert result == ['CRE Platform', 'Integrations CRE'] + assert is_errored == False + def test_post_dependent_cost_type( db, @@ -131,6 +146,21 @@ def test_post_dependent_cost_type( ) assert cost_category_import_log.status == 'FATAL' + # Code pre-prepend case + ImportSetting.objects.filter(workspace_id=workspace_id).update(import_code_fields=['JOB', 'COST_CODE', 'COST_CATEGORY']) + CostCategory.objects.filter(workspace_id=workspace_id).update(job_code='123', cost_code_code='456', cost_category_code='789') + + post_dependent_cost_type( + cost_category_import_log, + dependent_field_setting=dependent_field_settings, + platform=platform.return_value, + filters=filters, + posted_cost_codes=['CRE Platform', 'Integrations CRE'] + ) + + assert platform.return_value.dependent_fields.bulk_post_dependent_expense_field_values.call_count == 4 + assert cost_category_import_log.status == 'COMPLETE' + def test_post_dependent_expense_field_values( db, diff --git a/tests/test_sage300/test_helpers.py b/tests/test_sage300/test_helpers.py index 512c17e8..bd35364e 100644 --- a/tests/test_sage300/test_helpers.py +++ b/tests/test_sage300/test_helpers.py @@ -94,7 +94,9 @@ def test_disable_projects( db, mocker, create_temp_workspace, - add_fyle_credentials + add_fyle_credentials, + add_project_mappings, + add_import_settings ): workspace_id = 1 @@ -142,6 +144,42 @@ def test_disable_projects( assert sync_call.call_count == 4 disable_cost_code_call.call_count == 2 + # 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='PROJECT', + display_name='Project', + value='old_project_code old_project', + source_id='source_id_123', + active=True + ) + + projects_to_disable = { + 'destination_id': { + 'value': 'old_project', + 'updated_value': 'new_project', + 'code': 'old_project_code', + 'updated_code': 'old_project_code' + } + } + + payload = [{ + 'name': 'old_project_code old_project', + 'code': 'destination_id', + 'description': 'Sage 300 Project - {0}, Id - {1}'.format( + 'old_project_code old_project', + 'destination_id' + ), + 'is_enabled': False, + 'id': 'source_id_123' + }] + + assert disable_projects(workspace_id, projects_to_disable) == payload + def test_update_and_disable_cost_code( db, @@ -158,8 +196,8 @@ def test_update_and_disable_cost_code( 'destination_id': { 'value': 'old_project', 'updated_value': 'new_project', - 'code': 'old_project_code', - 'updated_code': 'old_project_code' + 'code': 'new_project_code', + 'updated_code': 'new_project_code' } } @@ -192,6 +230,23 @@ def test_update_and_disable_cost_code( updated_cost_category = CostCategory.objects.filter(workspace_id=workspace_id, job_id='destination_id').first() assert updated_cost_category.job_name == 'new_project' + # Test with code in naming + use_code_in_naming = True + + ExpenseAttribute.objects.create( + workspace_id=workspace_id, + attribute_type='PROJECT', + display_name='Project', + value='old_project_code old_project', + source_id='source_id_123', + active=True + ) + + update_and_disable_cost_code(workspace_id, projects_to_disable, mock_platform, use_code_in_naming) + assert updated_cost_category.job_name == 'new_project' + assert updated_cost_category.job_code == 'new_project_code' + + # Delete dependent field setting updated_cost_category.job_name = 'old_project' updated_cost_category.save() From ad44ea79626f1e107ad2c11093d2e0f8f8c95d77 Mon Sep 17 00:00:00 2001 From: Hrishabh Tiwari Date: Wed, 17 Jul 2024 17:13:25 +0530 Subject: [PATCH 06/13] update func name --- apps/mappings/helpers.py | 2 +- apps/mappings/imports/queues.py | 4 ++-- tests/test_mappings/test_helpers.py | 14 +++++++------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/mappings/helpers.py b/apps/mappings/helpers.py index a2084c9c..fa7d21b0 100644 --- a/apps/mappings/helpers.py +++ b/apps/mappings/helpers.py @@ -11,7 +11,7 @@ def format_attribute_name(use_code_in_naming: bool, attribute_name: str, attribu return attribute_name -def allow_job_sync(import_log: ImportLog = None) -> bool: +def is_job_sync_allowed(import_log: ImportLog = None) -> bool: """ Check if job sync is allowed """ diff --git a/apps/mappings/imports/queues.py b/apps/mappings/imports/queues.py index 3a8b6b4b..9fa82bc0 100644 --- a/apps/mappings/imports/queues.py +++ b/apps/mappings/imports/queues.py @@ -3,7 +3,7 @@ from apps.workspaces.models import ImportSetting from apps.fyle.models import DependentFieldSetting from apps.mappings.models import ImportLog -from apps.mappings.helpers import allow_job_sync +from apps.mappings.helpers import is_job_sync_allowed def chain_import_fields_to_fyle(workspace_id): @@ -22,7 +22,7 @@ def chain_import_fields_to_fyle(workspace_id): # We'll only sync job when the time_difference > 30 minutes to avoid # any dependent field import issue due to timestamp on job name update - is_sync_allowed = allow_job_sync(project_import_log) + is_sync_allowed = is_job_sync_allowed(project_import_log) chain = Chain() diff --git a/tests/test_mappings/test_helpers.py b/tests/test_mappings/test_helpers.py index 3a642ee5..e89b214f 100644 --- a/tests/test_mappings/test_helpers.py +++ b/tests/test_mappings/test_helpers.py @@ -1,5 +1,5 @@ from datetime import datetime, timedelta, timezone -from apps.mappings.helpers import format_attribute_name, allow_job_sync +from apps.mappings.helpers import format_attribute_name, is_job_sync_allowed from apps.mappings.models import ImportLog @@ -21,33 +21,33 @@ def test_format_attribute_name(): assert result == "attribute_name" -def test_allow_job_sync(db, create_temp_workspace): +def test_is_job_sync_allowed(db, create_temp_workspace): import_log = ImportLog.create('PROJECT', 1) # Test case 1: import_log is None - result = allow_job_sync(None) + result = is_job_sync_allowed(None) assert result is True # Test case 2: import_log is not None and last_successful_run_at is None import_log.last_successful_run_at = None import_log.status = 'COMPLETE' - result = allow_job_sync(import_log) + result = is_job_sync_allowed(import_log) assert result is True # Test case 3: import_log is not None and status is not 'COMPLETE' import_log.last_successful_run_at = '2021-01-01T00:00:00Z' import_log.status = 'FATAL' - result = allow_job_sync(import_log) + result = is_job_sync_allowed(import_log) assert result is True # Test case 4: import_log is not None and last_successful_run_at is less than 30 minutes import_log.last_successful_run_at = datetime.now(timezone.utc) - timedelta(minutes=29) import_log.status = 'COMPLETE' - result = allow_job_sync(import_log) + result = is_job_sync_allowed(import_log) assert result is False # Test case 5: import_log is not None and last_successful_run_at is greater than 30 minutes import_log.last_successful_run_at = datetime.now(timezone.utc) - timedelta(minutes=31) import_log.status = 'COMPLETE' - result = allow_job_sync(import_log) + result = is_job_sync_allowed(import_log) assert result is True From 4413e5609c06d8a0a1678daba8950e08e1303280 Mon Sep 17 00:00:00 2001 From: Hrishabh Tiwari Date: Wed, 17 Jul 2024 20:39:59 +0530 Subject: [PATCH 07/13] added unit tests --- tests/conftest.py | 8 +++---- tests/test_sage300/test_models.py | 35 ++++++++++++++++++++++++++++++- tests/test_sage300/test_utils.py | 2 -- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index eb7029a8..ad07d669 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -344,8 +344,8 @@ def add_project_mappings(): attribute_type='JOB', display_name='CRE Platform', value='CRE Platform', - destination_id='10064', - detail='Sage 300 Project - CRE Platform, Id - 10064', + destination_id='10065', + detail='Sage 300 Project - CRE Platform, Id - 10065', active=True, code='123' ) @@ -354,8 +354,8 @@ def add_project_mappings(): attribute_type='JOB', display_name='Integrations CRE', value='Integrations CRE', - destination_id='10081', - detail='Sage 300 Project - Integrations CRE, Id - 10081', + destination_id='10082', + detail='Sage 300 Project - Integrations CRE, Id - 10082', active=True, code='123' ) diff --git a/tests/test_sage300/test_models.py b/tests/test_sage300/test_models.py index 9ab5887e..1a76da52 100644 --- a/tests/test_sage300/test_models.py +++ b/tests/test_sage300/test_models.py @@ -1,11 +1,14 @@ from apps.sage300.models import CostCategory +from apps.workspaces.models import ImportSetting +from fyle_accounting_mappings.models import DestinationAttribute def test_bulk_create_or_update( db, mocker, create_temp_workspace, - add_project_mappings + add_project_mappings, + add_cost_code_mappings ): workspace_id = 1 @@ -36,3 +39,33 @@ def test_bulk_create_or_update( assert category.cost_code_id == category_data['CostCodeId'] assert category.name == category_data['Name'] assert category.status == category_data['IsActive'] + + # Test create new categories with code in naming + categories_gen_data = [{ + "Id": 3, + "JobId": "10065", + "CostCodeId": "10064", + "Name": "Test Category 2", + "IsActive": True, + "Code": "456", + }] + + ImportSetting.objects.filter(workspace_id=workspace_id).update(import_code_fields=['JOB', 'COST_CODE', 'COST_CATEGORY']) + DestinationAttribute.objects.filter(workspace_id=workspace_id, destination_id='10065', attribute_type = 'JOB').update(code='10065') + DestinationAttribute.objects.filter(workspace_id=workspace_id, destination_id='10064', attribute_type = 'COST_CODE').update(code='10064') + + categories_generator = categories_gen_data + CostCategory.bulk_create_or_update(categories_generator, workspace_id) + + created_categories = CostCategory.objects.all() + assert len(created_categories) == 3 + + for category_data in categories_gen_data: + category = CostCategory.objects.get(cost_category_id=category_data['Id']) + assert category.job_id == category_data['JobId'] + assert category.cost_code_id == category_data['CostCodeId'] + assert category.name == category_data['Name'] + assert category.status == category_data['IsActive'] + assert category.cost_category_code == category_data['Code'] + assert category.cost_code_code == '10064' + assert category.job_code == '10065' diff --git a/tests/test_sage300/test_utils.py b/tests/test_sage300/test_utils.py index 92418bb5..d7984085 100644 --- a/tests/test_sage300/test_utils.py +++ b/tests/test_sage300/test_utils.py @@ -490,8 +490,6 @@ def test_sync_cost_categories( assert Version.objects.get(workspace_id=workspace_id).cost_category == 2 - assert Version.objects.get(workspace_id=workspace_id).cost_category == 2 - def test_sync_cost_codes( db, From 37445f9f3246622511a39062487b42c44a3189f0 Mon Sep 17 00:00:00 2001 From: Hrishabh Tiwari Date: Wed, 17 Jul 2024 20:50:55 +0530 Subject: [PATCH 08/13] fix failing test --- tests/conftest.py | 9 +++++++++ tests/test_sage300/test_models.py | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index ad07d669..ffed5b7b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -455,6 +455,15 @@ def add_cost_code_mappings(): detail='Cost Center - Platform APIs, Id - 10081', active=True ) + DestinationAttribute.objects.create( + workspace_id=workspace_id, + attribute_type='COST_CODE', + display_name='New Cost Code', + value='New Cost Code', + destination_id='10065', + detail='Cost Code - New Cost Code, Id - 10065', + active=True + ) @pytest.fixture() diff --git a/tests/test_sage300/test_models.py b/tests/test_sage300/test_models.py index 1a76da52..3a4bfa60 100644 --- a/tests/test_sage300/test_models.py +++ b/tests/test_sage300/test_models.py @@ -44,7 +44,7 @@ def test_bulk_create_or_update( categories_gen_data = [{ "Id": 3, "JobId": "10065", - "CostCodeId": "10064", + "CostCodeId": "10065", "Name": "Test Category 2", "IsActive": True, "Code": "456", @@ -52,7 +52,7 @@ def test_bulk_create_or_update( ImportSetting.objects.filter(workspace_id=workspace_id).update(import_code_fields=['JOB', 'COST_CODE', 'COST_CATEGORY']) DestinationAttribute.objects.filter(workspace_id=workspace_id, destination_id='10065', attribute_type = 'JOB').update(code='10065') - DestinationAttribute.objects.filter(workspace_id=workspace_id, destination_id='10064', attribute_type = 'COST_CODE').update(code='10064') + DestinationAttribute.objects.filter(workspace_id=workspace_id, destination_id='10065', attribute_type = 'COST_CODE').update(code='10065') categories_generator = categories_gen_data CostCategory.bulk_create_or_update(categories_generator, workspace_id) @@ -67,5 +67,5 @@ def test_bulk_create_or_update( assert category.name == category_data['Name'] assert category.status == category_data['IsActive'] assert category.cost_category_code == category_data['Code'] - assert category.cost_code_code == '10064' + assert category.cost_code_code == '10065' assert category.job_code == '10065' From bfcc8b015d43fc7e04426537d8cbc46f0c8599d9 Mon Sep 17 00:00:00 2001 From: Hrishabh Tiwari Date: Thu, 18 Jul 2024 12:20:09 +0530 Subject: [PATCH 09/13] add project import related test cases --- .../test_imports/test_modules/conftest.py | 38 ++++++ .../test_imports/test_modules/fixtures.py | 14 +++ .../test_modules/test_projects.py | 49 ++++++++ tests/test_sage300/test_utils.py | 109 ++++++++++++++++++ 4 files changed, 210 insertions(+) diff --git a/tests/test_mappings/test_imports/test_modules/conftest.py b/tests/test_mappings/test_imports/test_modules/conftest.py index 21dd910f..8b5ae2d9 100644 --- a/tests/test_mappings/test_imports/test_modules/conftest.py +++ b/tests/test_mappings/test_imports/test_modules/conftest.py @@ -33,6 +33,44 @@ def add_project_mappings(): detail='Sage 300 Project - 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='PROJECT', + display_name='CRE Platform', + value='123 CRE Platform', + source_id='10065', + detail='Sage 300 Project - 123 CRE Platform, Id - 10065', + active=True + ) + ExpenseAttribute.objects.create( + workspace_id=workspace_id, + attribute_type='PROJECT', + display_name='Integrations CRE', + value='123 Integrations CRE', + source_id='10082', + detail='Sage 300 Project - 123 Integrations CRE, Id - 10082', + active=True + ) @pytest.fixture() diff --git a/tests/test_mappings/test_imports/test_modules/fixtures.py b/tests/test_mappings/test_imports/test_modules/fixtures.py index 7b8dc933..82f8496d 100644 --- a/tests/test_mappings/test_imports/test_modules/fixtures.py +++ b/tests/test_mappings/test_imports/test_modules/fixtures.py @@ -8188,4 +8188,18 @@ "offset": 0, } ], + "create_fyle_project_payload_with_code_create_new_case":[ + { + 'name': '123 CRE Platform', + 'code': '10065', + 'description': 'Sage 300 Project - 123 CRE Platform, Id - 10065', + 'is_enabled': True + }, + { + 'name': '123 Integrations CRE', + 'code': '10082', + 'description': 'Sage 300 Project - 123 Integrations CRE, Id - 10082', + 'is_enabled': True + } + ] } diff --git a/tests/test_mappings/test_imports/test_modules/test_projects.py b/tests/test_mappings/test_imports/test_modules/test_projects.py index c019244b..c0892aff 100644 --- a/tests/test_mappings/test_imports/test_modules/test_projects.py +++ b/tests/test_mappings/test_imports/test_modules/test_projects.py @@ -1,4 +1,5 @@ from apps.mappings.imports.modules.projects import Project +from apps.workspaces.models import ImportSetting from fyle_accounting_mappings.models import DestinationAttribute from .fixtures import data @@ -57,3 +58,51 @@ def test_construct_fyle_payload(api_client, test_connection, mocker, create_temp ) assert fyle_payload == data['create_fyle_project_payload_create_disable_case2'] + + +def test_get_existing_fyle_attributes(db, create_temp_workspace, add_project_mappings, add_import_settings): + project = Project(1, 'PROJECT', None) + + paginated_destination_attributes = DestinationAttribute.objects.filter(workspace_id=1, attribute_type='JOB') + paginated_destination_attributes_without_duplicates = project.remove_duplicate_attributes(paginated_destination_attributes) + paginated_destination_attribute_values = [attribute.value for attribute in paginated_destination_attributes_without_duplicates] + existing_fyle_attributes_map = project.get_existing_fyle_attributes(paginated_destination_attribute_values) + + assert existing_fyle_attributes_map == {} + + # with code prepending + project.use_code_in_naming = True + paginated_destination_attributes_without_duplicates = project.remove_duplicate_attributes(paginated_destination_attributes) + paginated_destination_attribute_values = [attribute.value for attribute in paginated_destination_attributes_without_duplicates] + existing_fyle_attributes_map = project.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_project_mappings, add_cost_category, add_import_settings): + project = Project(1, 'PROJECT', None, True) + project.use_code_in_naming = True + + paginated_destination_attributes = DestinationAttribute.objects.filter(workspace_id=1, attribute_type='JOB') + paginated_destination_attributes_without_duplicates = project.remove_duplicate_attributes(paginated_destination_attributes) + paginated_destination_attribute_values = [attribute.value for attribute in paginated_destination_attributes_without_duplicates] + existing_fyle_attributes_map = project.get_existing_fyle_attributes(paginated_destination_attribute_values) + + # already exists + fyle_payload = project.construct_fyle_payload( + paginated_destination_attributes, + existing_fyle_attributes_map, + True + ) + + assert fyle_payload == [] + + # create new case + existing_fyle_attributes_map = {} + fyle_payload = project.construct_fyle_payload( + paginated_destination_attributes, + existing_fyle_attributes_map, + True + ) + + assert fyle_payload == data["create_fyle_project_payload_with_code_create_new_case"] diff --git a/tests/test_sage300/test_utils.py b/tests/test_sage300/test_utils.py index d7984085..ffc1db95 100644 --- a/tests/test_sage300/test_utils.py +++ b/tests/test_sage300/test_utils.py @@ -530,3 +530,112 @@ def test_sync_cost_codes( result = sage_connector.sync_cost_codes(cost_code_import_log) assert result == [] + + +def test_bulk_create_or_update_destination_attributes_with_code(db, create_temp_workspace): + workspace_id = 1 + + # Dummy data for testing + attributes = [ + { + 'attribute_type': 'COST_CODE', + 'display_name': 'Display 1', + 'value': 'Value 1', + 'destination_id': 'DestID1', + 'detail': {'info': 'Detail 1'}, + 'active': True, + 'code': 'Code1' + }, + { + 'attribute_type': 'COST_CODE', + 'display_name': 'Display 2', + 'value': 'Value 2', + 'destination_id': 'DestID2', + 'detail': {'info': 'Detail 2'}, + 'active': False, + 'code': 'Code2' + } + ] + + # Call the function to test + DestinationAttribute.bulk_create_or_update_destination_attributes( + attributes=attributes, + attribute_type='COST_CODE', + workspace_id=workspace_id, + update=True + ) + + # Verify creation + assert DestinationAttribute.objects.filter(destination_id='DestID1').exists() + assert DestinationAttribute.objects.filter(destination_id='DestID2').exists() + + # Update data + attributes[0]['value'] = 'Updated Value 1' + attributes[1]['value'] = 'Updated Value 2' + attributes[0]['code'] = 'Updated Code1' + attributes[1]['code'] = 'Updated Code2' + DestinationAttribute.bulk_create_or_update_destination_attributes( + attributes=attributes, + attribute_type='COST_CODE', + workspace_id=workspace_id, + update=True + ) + + # Verify update + destination_attributes = DestinationAttribute.objects.filter(destination_id__in=['DestID1', 'DestID2']) + destination_attributes_dict = {attr.destination_id: attr for attr in destination_attributes} + + assert destination_attributes_dict['DestID1'].value == 'Updated Value 1' + assert destination_attributes_dict['DestID2'].value == 'Updated Value 2' + assert destination_attributes_dict['DestID1'].code == 'Updated Code1' + assert destination_attributes_dict['DestID2'].code == 'Updated Code2' + + +def test_bulk_create_or_update_destination_attributes_without_code(db, create_temp_workspace): + workspace_id = 1 + + # Dummy data for testing + attributes = [ + { + 'attribute_type': 'COST_CODE', + 'display_name': 'Display 1', + 'value': 'Value 1', + 'destination_id': 'DestID1', + 'detail': {'info': 'Detail 1'}, + 'active': True, + }, + { + 'attribute_type': 'COST_CODE', + 'display_name': 'Display 2', + 'value': 'Value 2', + 'destination_id': 'DestID2', + 'detail': {'info': 'Detail 2'}, + 'active': False, + } + ] + + # Call the function to test + DestinationAttribute.bulk_create_or_update_destination_attributes( + attributes=attributes, + attribute_type='COST_CODE', + workspace_id=workspace_id, + update=True + ) + + # Verify creation + assert DestinationAttribute.objects.filter(destination_id='DestID1').exists() + assert DestinationAttribute.objects.filter(destination_id='DestID2').exists() + + # Update data + attributes[0]['value'] = 'Updated Value 1' + attributes[1]['value'] = 'Updated Value 2' + DestinationAttribute.bulk_create_or_update_destination_attributes( + attributes=attributes, + attribute_type='COST_CODE', + workspace_id=workspace_id, + update=True + ) + + # Verify update + assert DestinationAttribute.objects.get(destination_id='DestID1').value == 'Updated Value 1' + assert DestinationAttribute.objects.get(destination_id='DestID2').value == 'Updated Value 2' From 5d832de9d5fec813ad137f7959d44cf5fea0f110 Mon Sep 17 00:00:00 2001 From: Hrishabh Tiwari Date: Thu, 18 Jul 2024 12:23:06 +0530 Subject: [PATCH 10/13] fix lint --- tests/test_mappings/test_imports/test_modules/test_projects.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_mappings/test_imports/test_modules/test_projects.py b/tests/test_mappings/test_imports/test_modules/test_projects.py index c0892aff..b49ef2d9 100644 --- a/tests/test_mappings/test_imports/test_modules/test_projects.py +++ b/tests/test_mappings/test_imports/test_modules/test_projects.py @@ -1,5 +1,4 @@ from apps.mappings.imports.modules.projects import Project -from apps.workspaces.models import ImportSetting from fyle_accounting_mappings.models import DestinationAttribute from .fixtures import data From 7da3f6e94b7f9ae4ec8ee71b9078a811c049d802 Mon Sep 17 00:00:00 2001 From: Hrishabh Tiwari Date: Thu, 18 Jul 2024 19:21:13 +0530 Subject: [PATCH 11/13] remove redundant db calls --- apps/mappings/imports/queues.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/mappings/imports/queues.py b/apps/mappings/imports/queues.py index 9fa82bc0..64e30159 100644 --- a/apps/mappings/imports/queues.py +++ b/apps/mappings/imports/queues.py @@ -12,10 +12,8 @@ def chain_import_fields_to_fyle(workspace_id): :param workspace_id: Workspace Id """ mapping_settings = MappingSetting.objects.filter(workspace_id=workspace_id, import_to_fyle=True) - custom_field_mapping_settings = MappingSetting.objects.filter(workspace_id=workspace_id, is_custom=True, import_to_fyle=True) import_settings = ImportSetting.objects.get(workspace_id=workspace_id) dependent_field_settings = DependentFieldSetting.objects.filter(workspace_id=workspace_id, is_import_enabled=True).first() - project_mapping = MappingSetting.objects.filter(workspace_id=workspace_id, source_field='PROJECT', import_to_fyle=True).first() import_code_fields = import_settings.import_code_fields project_import_log = ImportLog.objects.filter(workspace_id=workspace_id, attribute_type='PROJECT').first() @@ -24,6 +22,15 @@ def chain_import_fields_to_fyle(workspace_id): # any dependent field import issue due to timestamp on job name update is_sync_allowed = is_job_sync_allowed(project_import_log) + custom_field_mapping_settings = [] + project_mapping = None + + for setting in mapping_settings: + if setting.is_custom: + custom_field_mapping_settings.append(setting) + if setting.source_field == 'PROJECT': + project_mapping = setting + chain = Chain() if project_mapping and dependent_field_settings and is_sync_allowed: From 2cc22940fabd0b7d0939d455b6e1a7050f8b4e4e Mon Sep 17 00:00:00 2001 From: Hrishabh Tiwari Date: Sat, 20 Jul 2024 19:33:14 +0530 Subject: [PATCH 12/13] rename helper func, fix is_job_sync_allowed method --- apps/mappings/helpers.py | 10 +++++----- apps/mappings/imports/modules/base.py | 4 ++-- apps/sage300/dependent_fields.py | 12 ++++++------ apps/sage300/helpers.py | 10 +++++----- tests/test_mappings/test_helpers.py | 24 +++++++++--------------- 5 files changed, 27 insertions(+), 33 deletions(-) diff --git a/apps/mappings/helpers.py b/apps/mappings/helpers.py index fa7d21b0..2be075fd 100644 --- a/apps/mappings/helpers.py +++ b/apps/mappings/helpers.py @@ -2,13 +2,13 @@ from apps.mappings.models import ImportLog -def format_attribute_name(use_code_in_naming: bool, attribute_name: str, attribute_code: str = None) -> str: +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 use_code_in_naming and attribute_code: - return "{} {}".format(attribute_code, attribute_name) - return attribute_name + if prepend_code_in_name and code: + return "{} {}".format(code, value) + return value def is_job_sync_allowed(import_log: ImportLog = None) -> bool: @@ -16,10 +16,10 @@ def is_job_sync_allowed(import_log: ImportLog = None) -> bool: Check if job sync is allowed """ time_difference = datetime.now(timezone.utc) - timedelta(minutes=30) + time_difference = time_difference.replace(tzinfo=timezone.utc) if ( not import_log - or import_log.status != 'COMPLETE' or import_log.last_successful_run_at is None or import_log.last_successful_run_at < time_difference ): diff --git a/apps/mappings/imports/modules/base.py b/apps/mappings/imports/modules/base.py index a89c61ee..d247d67c 100644 --- a/apps/mappings/imports/modules/base.py +++ b/apps/mappings/imports/modules/base.py @@ -19,7 +19,7 @@ from apps.sage300.utils import SageDesktopConnector from apps.mappings.exceptions import handle_import_exceptions from apps.accounting_exports.models import Error -from apps.mappings.helpers import format_attribute_name +from apps.mappings.helpers import prepend_code_to_name logger = logging.getLogger(__name__) logger.level = logging.INFO @@ -95,7 +95,7 @@ def remove_duplicate_attributes(self, destination_attributes: List[DestinationAt for destination_attribute in destination_attributes: attribute_value = destination_attribute.value - attribute_value = format_attribute_name(self.use_code_in_naming, destination_attribute.value, destination_attribute.code) + attribute_value = prepend_code_to_name(self.use_code_in_naming, destination_attribute.value, destination_attribute.code) if attribute_value.lower() not in attribute_values: destination_attribute.value = attribute_value diff --git a/apps/sage300/dependent_fields.py b/apps/sage300/dependent_fields.py index 6d1889c1..649a6788 100644 --- a/apps/sage300/dependent_fields.py +++ b/apps/sage300/dependent_fields.py @@ -16,7 +16,7 @@ from apps.mappings.models import ImportLog from apps.mappings.exceptions import handle_import_exceptions from apps.workspaces.models import ImportSetting -from apps.mappings.helpers import format_attribute_name +from apps.mappings.helpers import prepend_code_to_name logger = logging.getLogger(__name__) logger.level = logging.INFO @@ -107,7 +107,7 @@ def post_dependent_cost_code(import_log: ImportLog, dependent_field_setting: Dep is_errored = False for project in projects: - project_name = format_attribute_name(use_code_in_naming=use_job_code_in_naming, attribute_name=project['job_name'], attribute_code=project['job_code']) + project_name = prepend_code_to_name(prepend_code_in_name=use_job_code_in_naming, value=project['job_name'], code=project['job_code']) projects_from_categories.append(project_name) existing_projects_in_fyle = ExpenseAttribute.objects.filter( @@ -123,11 +123,11 @@ def post_dependent_cost_code(import_log: ImportLog, dependent_field_setting: Dep for project in projects: payload = [] cost_code_names = [] - project_name = format_attribute_name(use_code_in_naming=use_job_code_in_naming, attribute_name=project['job_name'], attribute_code=project['job_code']) + project_name = prepend_code_to_name(prepend_code_in_name=use_job_code_in_naming, value=project['job_name'], code=project['job_code']) for cost_code in project['cost_codes']: if project_name in existing_projects_in_fyle: - cost_code_name = format_attribute_name(use_code_in_naming=use_cost_code_in_naming, attribute_name=cost_code['cost_code_name'], attribute_code=cost_code['cost_code_code']) + cost_code_name = prepend_code_to_name(prepend_code_in_name=use_cost_code_in_naming, value=cost_code['cost_code_name'], code=cost_code['cost_code_code']) payload.append({ 'parent_expense_field_id': dependent_field_setting.project_field_id, 'parent_expense_field_value': project_name, @@ -193,11 +193,11 @@ def post_dependent_cost_type(import_log: ImportLog, dependent_field_setting: Dep for category in cost_categories: if category['cost_code_name'] in posted_cost_codes: - cost_code_name = format_attribute_name(use_code_in_naming=use_cost_code_in_naming, attribute_name=category['cost_code_name'], attribute_code=category['cost_code_code']) + cost_code_name = prepend_code_to_name(prepend_code_in_name=use_cost_code_in_naming, value=category['cost_code_name'], code=category['cost_code_code']) payload = [] for cost_type in category['cost_categories']: - cost_type_name = format_attribute_name(use_code_in_naming=use_category_code_in_naming, attribute_name=cost_type['cost_category_name'], attribute_code=cost_type['cost_category_code']) + cost_type_name = prepend_code_to_name(prepend_code_in_name=use_category_code_in_naming, value=cost_type['cost_category_name'], code=cost_type['cost_category_code']) payload.append({ 'parent_expense_field_id': dependent_field_setting.cost_code_field_id, 'parent_expense_field_value': cost_code_name, diff --git a/apps/sage300/helpers.py b/apps/sage300/helpers.py index 79fa70f3..6a7b64d7 100644 --- a/apps/sage300/helpers.py +++ b/apps/sage300/helpers.py @@ -12,7 +12,7 @@ from apps.fyle.models import DependentFieldSetting from apps.sage300.dependent_fields import post_dependent_cost_code from apps.mappings.models import ImportLog -from apps.mappings.helpers import format_attribute_name +from apps.mappings.helpers import prepend_code_to_name logger = logging.getLogger(__name__) logger.level = logging.INFO @@ -67,7 +67,7 @@ def sync_dimensions(sage300_credential: Sage300Credential, workspace_id: int) -> logger.info(exception) -def disable_projects(workspace_id: int, projects_to_disable: Dict): +def disable_projects(workspace_id: int, projects_to_disable: Dict, *args, **kwargs): """ Disable projects in Fyle when the projects are updated in Sage 300. This is a callback function that is triggered from accounting_mappings. @@ -103,7 +103,7 @@ def disable_projects(workspace_id: int, projects_to_disable: Dict): project_values = [] for projects_map in projects_to_disable.values(): - project_name = format_attribute_name(use_code_in_naming=use_code_in_naming, attribute_name=projects_map['value'], attribute_code=projects_map['code']) + project_name = prepend_code_to_name(prepend_code_in_name=use_code_in_naming, value=projects_map['value'], code=projects_map['code']) project_values.append(project_name) filters = { @@ -116,7 +116,7 @@ def disable_projects(workspace_id: int, projects_to_disable: Dict): # Expense attribute value map is as follows: {old_project_name: destination_id} expense_attribute_value_map = {} for k, v in projects_to_disable.items(): - project_name = format_attribute_name(use_code_in_naming=use_code_in_naming, attribute_name=v['value'], attribute_code=v['code']) + project_name = prepend_code_to_name(prepend_code_in_name=use_code_in_naming, value=v['value'], code=v['code']) expense_attribute_value_map[project_name] = k expense_attributes = ExpenseAttribute.objects.filter(**filters) @@ -171,7 +171,7 @@ def update_and_disable_cost_code(workspace_id: int, cost_codes_to_disable: Dict, # here we are updating the CostCategory with the new project name bulk_update_payload = [] for destination_id, value in cost_codes_to_disable.items(): - updated_job_name = format_attribute_name(use_code_in_naming=use_code_in_naming, attribute_name=value['updated_value'], attribute_code=value['updated_code']) + updated_job_name = prepend_code_to_name(prepend_code_in_name=use_code_in_naming, value=value['updated_value'], code=value['updated_code']) cost_categories = CostCategory.objects.filter( workspace_id=workspace_id, diff --git a/tests/test_mappings/test_helpers.py b/tests/test_mappings/test_helpers.py index e89b214f..309dd934 100644 --- a/tests/test_mappings/test_helpers.py +++ b/tests/test_mappings/test_helpers.py @@ -1,23 +1,23 @@ from datetime import datetime, timedelta, timezone -from apps.mappings.helpers import format_attribute_name, is_job_sync_allowed +from apps.mappings.helpers import prepend_code_to_name, is_job_sync_allowed from apps.mappings.models import ImportLog -def test_format_attribute_name(): +def test_prepend_code_to_name(): # Test case 1: use_code_in_naming is True and attribute_code is not None - result = format_attribute_name(True, "attribute_name", "attribute_code") + result = prepend_code_to_name(True, "attribute_name", "attribute_code") assert result == "attribute_code attribute_name" # Test case 2: use_code_in_naming is True but attribute_code is None - result = format_attribute_name(True, "attribute_name", None) + result = prepend_code_to_name(True, "attribute_name", None) assert result == "attribute_name" # Test case 3: use_code_in_naming is False and attribute_code is not None - result = format_attribute_name(False, "attribute_name", "attribute_code") + result = prepend_code_to_name(False, "attribute_name", "attribute_code") assert result == "attribute_name" # Test case 4: use_code_in_naming is False and attribute_code is None - result = format_attribute_name(False, "attribute_name", None) + result = prepend_code_to_name(False, "attribute_name", None) assert result == "attribute_name" @@ -34,19 +34,13 @@ def test_is_job_sync_allowed(db, create_temp_workspace): result = is_job_sync_allowed(import_log) assert result is True - # Test case 3: import_log is not None and status is not 'COMPLETE' - import_log.last_successful_run_at = '2021-01-01T00:00:00Z' + # Test case 3: import_log is not None and last_successful_run_at is less than 30 minutes + import_log.last_successful_run_at = (datetime.now(timezone.utc) - timedelta(minutes=29)).replace(tzinfo=timezone.utc) import_log.status = 'FATAL' result = is_job_sync_allowed(import_log) - assert result is True - - # Test case 4: import_log is not None and last_successful_run_at is less than 30 minutes - import_log.last_successful_run_at = datetime.now(timezone.utc) - timedelta(minutes=29) - import_log.status = 'COMPLETE' - result = is_job_sync_allowed(import_log) assert result is False - # Test case 5: import_log is not None and last_successful_run_at is greater than 30 minutes + # Test case 4: import_log is not None and last_successful_run_at is greater than 30 minutes import_log.last_successful_run_at = datetime.now(timezone.utc) - timedelta(minutes=31) import_log.status = 'COMPLETE' result = is_job_sync_allowed(import_log) From 320f0bf69fa6a844830e572ef7aeb96711f1115c Mon Sep 17 00:00:00 2001 From: Hrishabh Tiwari <74908943+Hrishabh17@users.noreply.github.com> Date: Tue, 23 Jul 2024 12:27:09 +0530 Subject: [PATCH 13/13] add support for code prepending in CATEGORY (#208) * add support for code prepending in CATEGORY * rename the helper method * rename the helper method * add support for code prepending in MERCHANT (#209) * add support for code prepending in MERCHANT * change the callback methods are sent * bug fix * rename the helper method * fix callback method mapping of vendor * Code naming support cost center (#210) * add support for code prepending in COST_CENTER * rename the helper method * add support for code prepending in CUSTOM attribute (#211) * add support for code prepending in CUSTOM attribute * improve test case --- apps/mappings/imports/modules/categories.py | 78 ++++++++- apps/mappings/imports/modules/cost_centers.py | 85 +++++++++- .../imports/modules/expense_custom_fields.py | 32 +++- apps/mappings/imports/modules/merchants.py | 50 +++++- apps/mappings/imports/queues.py | 8 +- apps/sage300/helpers.py | 3 +- apps/sage300/utils.py | 25 ++- .../test_imports/test_modules/conftest.py | 101 ++++++++++++ .../test_imports/test_modules/fixtures.py | 31 ++++ .../test_modules/test_categories.py | 145 ++++++++++++++++- .../test_modules/test_cost_centers.py | 154 +++++++++++++++++- .../test_modules/test_expense_fields.py | 51 +++++- .../test_modules/test_merchants.py | 139 +++++++++++++++- 13 files changed, 879 insertions(+), 23 deletions(-) diff --git a/apps/mappings/imports/modules/categories.py b/apps/mappings/imports/modules/categories.py index 58630039..1167ff53 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 0623fcb0..99ea8911 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 73a5f332..97891b39 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 e8ac6e32..325d1fc8 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 64e30159..5ba216e2 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 6a7b64d7..de5f256d 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 def534cf..873575b3 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 8b5ae2d9..20e3f93f 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 82f8496d..26635a2c 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 670055d7..7fdd9b0a 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 1a1dbed3..ff2ad084 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 6027e013..9d5f0ef6 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 4281c133..f50507db 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]