diff --git a/apps/accounting_exports/models.py b/apps/accounting_exports/models.py index c146fec5..e6843db9 100644 --- a/apps/accounting_exports/models.py +++ b/apps/accounting_exports/models.py @@ -47,7 +47,7 @@ def _group_expenses(expenses: List[Expense], export_setting: ExportSetting, fund reimbursable_expense_date = export_setting.reimbursable_expense_date default_fields = ['employee_email', 'fund_source'] - report_grouping_fields = ['report_id', 'claim_number'] + report_grouping_fields = ['report_id', 'claim_number', 'corporate_card_id'] expense_grouping_fields = ['expense_id', 'expense_number'] # Define a mapping for fund sources and their associated group fields diff --git a/apps/fyle/signals.py b/apps/fyle/signals.py index 71d6f365..b18abf3f 100644 --- a/apps/fyle/signals.py +++ b/apps/fyle/signals.py @@ -3,15 +3,13 @@ """ import logging -from django.db.models.signals import post_save, pre_save +from django.db.models.signals import pre_save from django.dispatch import receiver from fyle_integrations_platform_connector import PlatformConnector from apps.workspaces.models import FyleCredential from apps.fyle.models import DependentFieldSetting from apps.sage300.dependent_fields import create_dependent_custom_field_in_fyle -from apps.mappings.imports.schedules import schedule_or_delete_dependent_field_tasks - logger = logging.getLogger(__name__) logger.level = logging.INFO @@ -51,13 +49,3 @@ def run_pre_save_dependent_field_settings_triggers(sender, instance: DependentFi parent_field_id=instance.cost_code_field_id, ) instance.cost_category_field_id = cost_category['data']['id'] - - -@receiver(post_save, sender=DependentFieldSetting) -def run_post_save_dependent_field_settings_triggers(sender, instance: DependentFieldSetting, **kwargs): - """ - :param sender: Sender Class - :param instance: Row instance of Sender Class - :return: None - """ - schedule_or_delete_dependent_field_tasks(instance.workspace_id) diff --git a/apps/mappings/imports/queues.py b/apps/mappings/imports/queues.py index acadcb6d..232cc356 100644 --- a/apps/mappings/imports/queues.py +++ b/apps/mappings/imports/queues.py @@ -1,6 +1,7 @@ from django_q.tasks import Chain from fyle_accounting_mappings.models import MappingSetting from apps.workspaces.models import ImportSetting +from apps.fyle.models import DependentFieldSetting def chain_import_fields_to_fyle(workspace_id): @@ -11,6 +12,9 @@ def chain_import_fields_to_fyle(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() + chain = Chain() if import_settings.import_categories: @@ -47,5 +51,11 @@ def chain_import_fields_to_fyle(workspace_id): True ) + if project_mapping and dependent_field_settings: + chain.append( + 'apps.mappings.imports.tasks.auto_import_and_map_fyle_fields', + workspace_id + ) + if chain.length() > 0: chain.run() diff --git a/apps/mappings/imports/schedules.py b/apps/mappings/imports/schedules.py index 2de4bb55..698f13d6 100644 --- a/apps/mappings/imports/schedules.py +++ b/apps/mappings/imports/schedules.py @@ -1,41 +1,9 @@ from datetime import datetime from django_q.models import Schedule from fyle_accounting_mappings.models import MappingSetting - -from apps.fyle.models import DependentFieldSetting from apps.workspaces.models import ImportSetting -def schedule_or_delete_dependent_field_tasks(workspace_id: int): - """ - :param configuration: Workspace Configuration Instance - :return: None - """ - project_mapping = MappingSetting.objects.filter( - source_field='PROJECT', - workspace_id=workspace_id, - import_to_fyle=True - ).first() - dependent_fields = DependentFieldSetting.objects.filter(workspace_id=workspace_id, is_import_enabled=True).first() - - if project_mapping and dependent_fields: - start_datetime = datetime.now() - Schedule.objects.update_or_create( - func='apps.mappings.imports.tasks.auto_import_and_map_fyle_fields', - args='{}'.format(workspace_id), - defaults={ - 'schedule_type': Schedule.MINUTES, - 'minutes': 24 * 60, - 'next_run': start_datetime - } - ) - elif not (project_mapping and dependent_fields): - Schedule.objects.filter( - func='apps.mappings.imports.tasks.auto_import_and_map_fyle_fields', - args='{}'.format(workspace_id) - ).delete() - - def schedule_or_delete_fyle_import_tasks(import_settings: ImportSetting, mapping_setting_instance: MappingSetting = None): """ Schedule or delete Fyle import tasks based on the import settingss. @@ -78,6 +46,3 @@ def schedule_or_delete_fyle_import_tasks(import_settings: ImportSetting, mapping func='apps.mappings.imports.queues.chain_import_fields_to_fyle', args='{}'.format(import_settings.workspace_id) ).delete() - - # Schedule or delete dependent field tasks - schedule_or_delete_dependent_field_tasks(import_settings.workspace_id) diff --git a/apps/mappings/imports/tasks.py b/apps/mappings/imports/tasks.py index 6166cb6b..c3e0a102 100644 --- a/apps/mappings/imports/tasks.py +++ b/apps/mappings/imports/tasks.py @@ -1,14 +1,15 @@ +import logging from django_q.tasks import Chain -from fyle_accounting_mappings.models import MappingSetting - from apps.mappings.models import ImportLog from apps.mappings.imports.modules.categories import Category from apps.mappings.imports.modules.projects import Project from apps.mappings.imports.modules.cost_centers import CostCenter from apps.mappings.imports.modules.merchants import Merchant from apps.mappings.imports.modules.expense_custom_fields import ExpenseCustomField -from apps.fyle.models import DependentFieldSetting + +logger = logging.getLogger(__name__) +logger.level = logging.INFO SOURCE_FIELD_CLASS_MAP = { 'CATEGORY': Category, @@ -41,20 +42,20 @@ def auto_import_and_map_fyle_fields(workspace_id): """ Auto import and map fyle fields """ - project_mapping = MappingSetting.objects.filter( - source_field='PROJECT', + import_log = ImportLog.objects.filter( workspace_id=workspace_id, - import_to_fyle=True + attribute_type = 'PROJECT' ).first() - dependent_fields = DependentFieldSetting.objects.filter(workspace_id=workspace_id, is_import_enabled=True).first() chain = Chain() - if project_mapping and dependent_fields: - chain.append('apps.mappings.tasks.sync_sage300_attributes', 'JOB', workspace_id) - chain.append('apps.mappings.tasks.sync_sage300_attributes', 'COST_CODE', workspace_id) - chain.append('apps.mappings.tasks.sync_sage300_attributes', 'COST_CATEGORY', workspace_id) - chain.append('apps.sage300.dependent_fields.import_dependent_fields_to_fyle', workspace_id) + chain.append('apps.mappings.tasks.sync_sage300_attributes', 'JOB', workspace_id) + chain.append('apps.mappings.tasks.sync_sage300_attributes', 'COST_CODE', workspace_id) + chain.append('apps.mappings.tasks.sync_sage300_attributes', 'COST_CATEGORY', workspace_id) + chain.append('apps.sage300.dependent_fields.import_dependent_fields_to_fyle', workspace_id) + + if import_log and import_log.status != 'COMPLETE': + logger.error(f"Project Import is in {import_log.status} state in WORKSPACE_ID: {workspace_id} with error {str(import_log.error_log)}") if chain.length() > 0: chain.run() diff --git a/apps/sage300/exports/base_model.py b/apps/sage300/exports/base_model.py index c6575a52..f76f4e7e 100644 --- a/apps/sage300/exports/base_model.py +++ b/apps/sage300/exports/base_model.py @@ -3,7 +3,7 @@ from django.db import models from django.db.models import Sum -from fyle_accounting_mappings.models import ExpenseAttribute, Mapping, MappingSetting, EmployeeMapping, DestinationAttribute +from fyle_accounting_mappings.models import ExpenseAttribute, Mapping, MappingSetting, EmployeeMapping from apps.accounting_exports.models import AccountingExport from apps.fyle.models import DependentFieldSetting, Expense @@ -60,7 +60,6 @@ def get_expense_purpose(workspace_id, lineitem: Expense, category: str, advance_ def get_vendor_id(accounting_export: AccountingExport): # Retrieve export settings for the given workspace export_settings = ExportSetting.objects.get(workspace_id=accounting_export.workspace_id) - # Extract the description from the accounting export description = accounting_export.description @@ -81,22 +80,22 @@ def get_vendor_id(accounting_export: AccountingExport): # Check if the fund source is 'CCC' elif accounting_export.fund_source == 'CCC': # Retrieve the vendor from the first expense - expense_vendor = accounting_export.expenses.first().vendor + vendor_id = None + corporate_card_id = accounting_export.expenses.first().corporate_card_id - # Query DestinationAttribute for the vendor with case-insensitive search - if expense_vendor: - vendor = DestinationAttribute.objects.filter( + if corporate_card_id: + vendor_mapping = Mapping.objects.filter( workspace_id=accounting_export.workspace_id, - value__icontains=expense_vendor, - attribute_type='VENDOR' - ).values_list('destination_id', flat=True).first() - if not vendor: - vendor = export_settings.default_vendor_id - else: - vendor = export_settings.default_vendor_id + source_type='CORPORATE_CARD', + destination_type='VENDOR', + source__source_id=corporate_card_id + ).first() - # Update vendor_id with the retrieved vendor or default to export settings - vendor_id = vendor + if vendor_mapping: + vendor_id = vendor_mapping.destination.destination_id + + if not vendor_id: + vendor_id = export_settings.default_vendor_id # Return the determined vendor_id return vendor_id diff --git a/apps/workspaces/serializers.py b/apps/workspaces/serializers.py index 2aa114df..8199f1de 100644 --- a/apps/workspaces/serializers.py +++ b/apps/workspaces/serializers.py @@ -158,6 +158,17 @@ def create(self, validated_data): # Update workspace onboarding state workspace = export_settings.workspace + if export_settings.credit_card_expense_export_type == 'PURCHASE_INVOICE': + MappingSetting.objects.update_or_create( + workspace_id = workspace_id, + defaults={ + 'source_field':'CORPORATE_CARD', + 'destination_field':'VENDOR', + 'import_to_fyle': False, + 'is_custom': False + } + ) + if workspace.onboarding_state == 'EXPORT_SETTINGS': workspace.onboarding_state = 'IMPORT_SETTINGS' workspace.save() diff --git a/scripts/001_delete_dep_fields_schedule.sql b/scripts/001_delete_dep_fields_schedule.sql new file mode 100644 index 00000000..0d318597 --- /dev/null +++ b/scripts/001_delete_dep_fields_schedule.sql @@ -0,0 +1,4 @@ +rollback; +begin; + +delete from django_q_schedule where func = 'apps.mappings.imports.tasks.auto_import_and_map_fyle_fields'; \ No newline at end of file diff --git a/scripts/002_add_vendor_fyle_card_mapping.sql b/scripts/002_add_vendor_fyle_card_mapping.sql new file mode 100644 index 00000000..a39f2db2 --- /dev/null +++ b/scripts/002_add_vendor_fyle_card_mapping.sql @@ -0,0 +1,20 @@ +rollback; +begin; + +insert into mapping_settings (source_field, destination_field, import_to_fyle, is_custom, workspace_id, created_at, updated_at) +select 'CORPORATE_CARD', 'VENDOR', 'f', 'f', ws.id, now(), now() +from workspaces ws +where not exists ( + select 1 + from mapping_settings ms + inner join export_settings es + on es.workspace_id = ms.workspace_id + where ms.source_field = 'CORPORATE_CARD' + and ms.destination_field = 'VENDOR' + and ms.workspace_id = ws.id + and es.credit_card_expense_export_type = 'PURCHASE_INVOICE' +); + +select COUNT(*) from export_settings where credit_card_expense_export_type = 'PURCHASE_INVOICE'; + +select COUNT(distinct(workspace_id)) from mapping_settings where workspace_id not in (select distinct(workspace_id) from mapping_settings where source_field = 'CORPORATE_CARD' and destination_field = 'VENDOR'); diff --git a/tests/conftest.py b/tests/conftest.py index 5e7aab11..e061b2d0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -624,6 +624,7 @@ def add_dependent_field_setting(create_temp_workspace): workspace_ids = [ 1, 2, 3 ] + for workspace_id in workspace_ids: DependentFieldSetting.objects.create( is_import_enabled=True, diff --git a/tests/test_sage300/test_exports/test_base_model.py b/tests/test_sage300/test_exports/test_base_model.py index 2927ce06..22969b16 100644 --- a/tests/test_sage300/test_exports/test_base_model.py +++ b/tests/test_sage300/test_exports/test_base_model.py @@ -4,10 +4,13 @@ from apps.fyle.models import Expense, DependentFieldSetting from fyle_accounting_mappings.models import ( ExpenseAttribute, + DestinationAttribute, Mapping, MappingSetting, EmployeeMapping ) +from apps.workspaces.models import ExportSetting +from apps.accounting_exports.models import _group_expenses def test_base_model_get_invoice_date( @@ -186,8 +189,10 @@ def test_get_vendor_id_2( accounting_export=accounting_export ) + export_settings = ExportSetting.objects.filter(workspace_id=accounting_export.workspace_id).first() + assert return_value is not None - assert return_value == 'dest_vendor123' + assert return_value == export_settings.default_vendor_id def test_get_vendor_id_3( @@ -220,6 +225,145 @@ def test_get_vendor_id_3( assert return_value == '1' +def test_get_vendor_id_4( + db, + create_temp_workspace, + create_expense_objects, + add_export_settings, + add_accounting_export_expenses, + create_employee_mapping_with_vendor +): + workspace_id = 1 + base_model = PurchaseInvoice + + corporate_card, _ = ExpenseAttribute.objects.update_or_create( + workspace_id=workspace_id, + defaults = { + 'attribute_type':'CORPORATE_CARD', + 'display_name':'Corporate Card', + 'value':'Bank of Fyle - T1711', + 'source_id':'bankoffyle123', + 'detail': {'cardholder_name': None} + } + ) + + vendor = DestinationAttribute.objects.filter(workspace_id=workspace_id, attribute_type='VENDOR').first() + + Mapping.objects.update_or_create( + workspace_id=workspace_id, + defaults = { + 'source_type':'CORPORATE_CARD', + 'destination_type':'VENDOR', + 'source':corporate_card, + 'destination':vendor + } + ) + + expense = Expense.objects.filter(workspace_id=workspace_id).first() + expense.fund_source = 'CCC' + expense.corporate_card_id = corporate_card.source_id + expense.save() + + accounting_export = AccountingExport.objects.filter(workspace_id=workspace_id).first() + accounting_export.expenses.set([expense]) + accounting_export.fund_source = 'CCC' + accounting_export.description = {'employee_email': 'ashwin.t@fyle.in'} + accounting_export.save() + + return_value = base_model.get_vendor_id( + accounting_export=accounting_export + ) + + assert return_value is not None + assert return_value == vendor.destination_id + + +def test_group_expenses( + db, + create_temp_workspace, + create_expense_objects, + add_export_settings, + add_accounting_export_expenses, + create_employee_mapping_with_vendor +): + workspace_id = 1 + + corporate_card_x, _ = ExpenseAttribute.objects.update_or_create( + workspace_id=workspace_id, + defaults = { + 'attribute_type':'CORPORATE_CARD', + 'display_name':'Corporate Card', + 'value':'Bank of Fyle - X', + 'source_id':'bankoffyle123X', + 'detail': {'cardholder_name': None} + } + ) + + corporate_card_y, _ = ExpenseAttribute.objects.update_or_create( + workspace_id=workspace_id, + defaults = { + 'attribute_type':'CORPORATE_CARD', + 'display_name':'Corporate Card', + 'value':'Bank of Fyle - Y', + 'source_id':'bankoffyle123Y', + 'detail': {'cardholder_name': None} + } + ) + + vendors = DestinationAttribute.objects.filter(workspace_id=workspace_id, attribute_type='VENDOR')[0:2] + + Mapping.objects.update_or_create( + workspace_id=workspace_id, + defaults = { + 'source_type':'CORPORATE_CARD', + 'destination_type':'VENDOR', + 'source':corporate_card_x, + 'destination':vendors[0] + } + ) + + Mapping.objects.update_or_create( + workspace_id=workspace_id, + defaults = { + 'source_type':'CORPORATE_CARD', + 'destination_type':'VENDOR', + 'source':corporate_card_y, + 'destination':vendors[0] + } + ) + + expenses = [] + for _ in range(3): + expense = Expense.objects.filter(workspace_id=workspace_id).first() + expense.pk = None + expenses.append(expense) + + expenses[0].fund_source = 'CCC' + expenses[0].expense_id = 'tx4ziVSA124' + expenses[0].corporate_card_id = None + expenses[0].save() + + expenses[1].fund_source = 'CCC' + expenses[1].expense_id = 'tx4ziVSAsfsf' + expenses[1].corporate_card_id = corporate_card_x.source_id + expenses[1].save() + + expenses[2].fund_source = 'CCC' + expenses[2].expense_id = 'tx4zisdAssda' + expenses[2].corporate_card_id = corporate_card_y.source_id + expenses[2].save() + + export_settings = ExportSetting.objects.filter(workspace_id=workspace_id).first() + export_settings.credit_card_expense_grouped_by = 'REPORT' + export_settings.credit_card_expense_date = 'POSTED_AT' + export_settings.reimbursable_expense_grouped_by = 'REPORT' + export_settings.reimbursable_expense_date = 'PAYMENT_PROCESSING' + export_settings.save() + + accounting_export = _group_expenses(expenses=expenses, export_setting=export_settings, fund_source='CCC') + assert len(accounting_export) == 3 + + def test_get_job_id( db, mocker,