diff --git a/apps/fyle/actions.py b/apps/fyle/actions.py index d1e6dc89..5430bea8 100644 --- a/apps/fyle/actions.py +++ b/apps/fyle/actions.py @@ -1,13 +1,22 @@ + +import logging from datetime import datetime, timezone +from typing import List +from django.conf import settings from django.db.models import Q +from fyle.platform.exceptions import InternalServerError, RetryException +from fyle.platform.internals.decorators import retry from fyle_accounting_mappings.models import ExpenseAttribute from fyle_integrations_platform_connector import PlatformConnector +from apps.fyle.enums import FundSourceEnum, FyleAttributeEnum +from apps.fyle.helpers import get_batched_expenses, get_updated_accounting_export_summary +from apps.fyle.models import Expense, ExpenseGroup from apps.workspaces.models import FyleCredential, Workspace, WorkspaceGeneralSettings -from .enums import FyleAttributeEnum, FundSourceEnum -from .models import ExpenseGroup +logger = logging.getLogger(__name__) +logger.level = logging.INFO def get_expense_field(workspace_id): @@ -78,3 +87,182 @@ def exportable_expense_group(workspace_id): ).values_list("id", flat=True) return expense_group_ids + + +def __bulk_update_expenses(expense_to_be_updated: List[Expense]) -> None: + """ + Bulk update expenses + :param expense_to_be_updated: expenses to be updated + :return: None + """ + if expense_to_be_updated: + Expense.objects.bulk_update(expense_to_be_updated, ['accounting_export_summary'], batch_size=50) + + +def update_expenses_in_progress(in_progress_expenses: List[Expense]) -> None: + """ + Update expenses in progress in bulk + :param in_progress_expenses: in progress expenses + :return: None + """ + expense_to_be_updated = [] + for expense in in_progress_expenses: + expense_to_be_updated.append( + Expense( + id=expense.id, + accounting_export_summary=get_updated_accounting_export_summary( + expense.expense_id, + 'IN_PROGRESS', + None, + '{}/workspaces/main/dashboard'.format(settings.XERO_INTEGRATION_APP_URL), + False + ) + ) + ) + + __bulk_update_expenses(expense_to_be_updated) + + +def mark_accounting_export_summary_as_synced(expenses: List[Expense]) -> None: + """ + Mark accounting export summary as synced in bulk + :param expenses: List of expenses + :return: None + """ + # Mark all expenses as synced + expense_to_be_updated = [] + for expense in expenses: + expense.accounting_export_summary['synced'] = True + updated_accounting_export_summary = expense.accounting_export_summary + expense_to_be_updated.append( + Expense( + id=expense.id, + accounting_export_summary=updated_accounting_export_summary, + previous_export_state=updated_accounting_export_summary['state'] + ) + ) + + Expense.objects.bulk_update(expense_to_be_updated, ['accounting_export_summary', 'previous_export_state'], batch_size=50) + + +def update_failed_expenses(failed_expenses: List[Expense], is_mapping_error: bool) -> None: + """ + Update failed expenses + :param failed_expenses: Failed expenses + """ + expense_to_be_updated = [] + for expense in failed_expenses: + error_type = 'MAPPING' if is_mapping_error else 'ACCOUNTING_INTEGRATION_ERROR' + + # Skip dummy updates (if it is already in error state with the same error type) + if not (expense.accounting_export_summary.get('state') == 'ERROR' and \ + expense.accounting_export_summary.get('error_type') == error_type): + expense_to_be_updated.append( + Expense( + id=expense.id, + accounting_export_summary=get_updated_accounting_export_summary( + expense.expense_id, + 'ERROR', + error_type, + '{}/workspaces/main/dashboard'.format(settings.XERO_INTEGRATION_APP_URL), + False + ) + ) + ) + + __bulk_update_expenses(expense_to_be_updated) + + +def update_complete_expenses(exported_expenses: List[Expense], url: str) -> None: + """ + Update complete expenses + :param exported_expenses: Exported expenses + :param url: Export url + :return: None + """ + expense_to_be_updated = [] + for expense in exported_expenses: + expense_to_be_updated.append( + Expense( + id=expense.id, + accounting_export_summary=get_updated_accounting_export_summary( + expense.expense_id, + 'COMPLETE', + None, + url, + False + ) + ) + ) + + __bulk_update_expenses(expense_to_be_updated) + + +def __handle_post_accounting_export_summary_exception(exception: Exception, workspace_id: int) -> None: + """ + Handle post accounting export summary exception + :param exception: Exception + :param workspace_id: Workspace id + :return: None + """ + error_response = exception.__dict__ + expense_to_be_updated = [] + if ( + 'message' in error_response and error_response['message'] == 'Some of the parameters are wrong' + and 'response' in error_response and 'data' in error_response['response'] and error_response['response']['data'] + ): + logger.info('Error while syncing workspace %s %s',workspace_id, error_response) + for expense in error_response['response']['data']: + if expense['message'] == 'Permission denied to perform this action.': + expense_instance = Expense.objects.get(expense_id=expense['key'], workspace_id=workspace_id) + expense_to_be_updated.append( + Expense( + id=expense_instance.id, + accounting_export_summary=get_updated_accounting_export_summary( + expense_instance.expense_id, + 'DELETED', + None, + '{}/workspaces/main/dashboard'.format(settings.XERO_INTEGRATION_APP_URL), + True + ) + ) + ) + if expense_to_be_updated: + Expense.objects.bulk_update(expense_to_be_updated, ['accounting_export_summary'], batch_size=50) + else: + logger.error('Error while syncing accounting export summary, workspace_id: %s %s', workspace_id, str(error_response)) + + +@retry(n=3, backoff=1, exceptions=InternalServerError) +def bulk_post_accounting_export_summary(platform: PlatformConnector, payload: List[dict]): + """ + Bulk post accounting export summary with retry of 3 times and backoff of 1 second which handles InternalServerError + :param platform: Platform connector object + :param payload: Payload + :return: None + """ + platform.expenses.post_bulk_accounting_export_summary(payload) + + +def create_generator_and_post_in_batches(accounting_export_summary_batches: List[dict], platform: PlatformConnector, workspace_id: int) -> None: + """ + Create generator and post in batches + :param accounting_export_summary_batches: Accounting export summary batches + :param platform: Platform connector object + :param workspace_id: Workspace id + :return: None + """ + for batched_payload in accounting_export_summary_batches: + try: + if batched_payload: + bulk_post_accounting_export_summary(platform, batched_payload) + + batched_expenses = get_batched_expenses(batched_payload, workspace_id) + mark_accounting_export_summary_as_synced(batched_expenses) + except RetryException: + logger.error( + 'Internal server error while posting accounting export summary to Fyle workspace_id: %s', + workspace_id + ) + except Exception as exception: + __handle_post_accounting_export_summary_exception(exception, workspace_id) diff --git a/apps/fyle/helpers.py b/apps/fyle/helpers.py index 3896b7ab..dc8b845c 100644 --- a/apps/fyle/helpers.py +++ b/apps/fyle/helpers.py @@ -1,8 +1,19 @@ import json +import logging +import traceback +from typing import List, Union import requests from django.conf import settings +from apps.fyle.models import Expense, ExpenseGroupSettings +from apps.tasks.models import TaskLog +from apps.workspaces.models import WorkspaceGeneralSettings + +logger = logging.getLogger(__name__) + +SOURCE_ACCOUNT_MAP = {'PERSONAL': 'PERSONAL_CASH_ACCOUNT', 'CCC': 'PERSONAL_CORPORATE_CREDIT_CARD_ACCOUNT'} + def post_request(url, body, refresh_token=None): """ @@ -18,12 +29,67 @@ def post_request(url, body, refresh_token=None): response = requests.post(url, headers=api_headers, data=body) - if response.status_code == 200: + if response.status_code in [200, 201]: return json.loads(response.text) else: raise Exception(response.text) +def get_source_account_type(fund_source: List[str]) -> List[str]: + """ + Get source account type + :param fund_source: fund source + :return: source account type + """ + source_account_type = [] + for source in fund_source: + source_account_type.append(SOURCE_ACCOUNT_MAP[source]) + + return source_account_type + + +def get_filter_credit_expenses(expense_group_settings: ExpenseGroupSettings) -> bool: + """ + Get filter credit expenses + :param expense_group_settings: expense group settings + :return: filter credit expenses + """ + filter_credit_expenses = True + if expense_group_settings.import_card_credits: + filter_credit_expenses = False + + return filter_credit_expenses + + +def get_fund_source(workspace_id: int) -> List[str]: + """ + Get fund source + :param workspace_id: workspace id + :return: fund source + """ + general_settings = WorkspaceGeneralSettings.objects.get(workspace_id=workspace_id) + fund_source = [] + if general_settings.reimbursable_expenses_object: + fund_source.append('PERSONAL') + if general_settings.corporate_credit_card_expenses_object: + fund_source.append('CCC') + + return fund_source + + +def handle_import_exception(task_log: TaskLog) -> None: + """ + Handle import exception + :param task_log: task log + :return: None + """ + error = traceback.format_exc() + task_log.detail = {'error': error} + task_log.status = 'FATAL' + task_log.save() + logger.error('Something unexpected happened workspace_id: %s %s', task_log.workspace_id, task_log.detail) + + def get_request(url, params, refresh_token): """ Create a HTTP get request. @@ -86,3 +152,34 @@ def get_cluster_domain(refresh_token: str) -> str: cluster_api_url = "{0}/oauth/cluster/".format(settings.FYLE_BASE_URL) return post_request(cluster_api_url, {}, refresh_token)["cluster_domain"] + + +def get_updated_accounting_export_summary( + expense_id: str, state: str, error_type: Union[str, None], url: Union[str, None], is_synced: bool) -> dict: + """ + Get updated accounting export summary + :param expense_id: expense id + :param state: state + :param error_type: error type + :param url: url + :param is_synced: is synced + :return: updated accounting export summary + """ + return { + 'id': expense_id, + 'state': state, + 'error_type': error_type, + 'url': url, + 'synced': is_synced + } + + +def get_batched_expenses(batched_payload: List[dict], workspace_id: int) -> List[Expense]: + """ + Get batched expenses + :param batched_payload: batched payload + :param workspace_id: workspace id + :return: batched expenses + """ + expense_ids = [expense['id'] for expense in batched_payload] + return Expense.objects.filter(expense_id__in=expense_ids, workspace_id=workspace_id) diff --git a/apps/fyle/migrations/0018_auto_20240213_0450.py b/apps/fyle/migrations/0018_auto_20240213_0450.py new file mode 100644 index 00000000..666a8c2c --- /dev/null +++ b/apps/fyle/migrations/0018_auto_20240213_0450.py @@ -0,0 +1,36 @@ +# Generated by Django 3.2.14 on 2024-02-13 04:50 + +import apps.fyle.models +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('workspaces', '0037_workspacegeneralsettings_import_suppliers_as_merchants'), + ('fyle', '0017_expense_posted_at'), + ] + + operations = [ + migrations.AddField( + model_name='expense', + name='accounting_export_summary', + field=models.JSONField(default=dict), + ), + migrations.AddField( + model_name='expense', + name='previous_export_state', + field=models.CharField(help_text='Previous export state', max_length=255, null=True), + ), + migrations.AddField( + model_name='expense', + name='workspace', + field=models.ForeignKey(help_text='To which workspace this expense belongs to', null=True, on_delete=django.db.models.deletion.PROTECT, to='workspaces.workspace'), + ), + migrations.AlterField( + model_name='expensegroupsettings', + name='ccc_expense_state', + field=models.CharField(choices=[('APPROVED', 'APPROVED'), ('PAYMENT_PROCESSING', 'PAYMENT_PROCESSING'), ('PAID', 'PAID')], default=apps.fyle.models.get_default_ccc_expense_state, help_text='state at which the ccc expenses are fetched (PAYMENT_PROCESSING /PAID)', max_length=100, null=True), + ), + ] diff --git a/apps/fyle/models.py b/apps/fyle/models.py index 58ece9aa..b15820e2 100644 --- a/apps/fyle/models.py +++ b/apps/fyle/models.py @@ -13,11 +13,9 @@ from django.db.models import Count, JSONField from fyle_accounting_mappings.models import ExpenseAttribute +from apps.fyle.enums import ExpenseStateEnum, FundSourceEnum, PlatformExpensesEnum from apps.workspaces.models import Workspace -from .enums import FundSourceEnum, PlatformExpensesEnum, ExpenseStateEnum - - logger = logging.getLogger(__name__) logger.level = logging.INFO @@ -149,6 +147,9 @@ class Expense(models.Model): ) tax_amount = models.FloatField(null=True, help_text="Tax Amount") tax_group_id = models.CharField(null=True, max_length=255, help_text="Tax Group ID") + accounting_export_summary = JSONField(default=dict) + previous_export_state = models.CharField(max_length=255, help_text='Previous export state', null=True) + workspace = models.ForeignKey(Workspace, on_delete=models.PROTECT, help_text='To which workspace this expense belongs to', null=True) class Meta: db_table = "expenses" @@ -209,6 +210,7 @@ def create_expense_objects(expenses: List[Dict], workspace_id: int): "billable": expense["billable"] if expense["billable"] else False, + 'workspace_id': workspace_id }, ) diff --git a/apps/fyle/queue.py b/apps/fyle/queue.py new file mode 100644 index 00000000..68cec648 --- /dev/null +++ b/apps/fyle/queue.py @@ -0,0 +1,24 @@ +from django_q.tasks import async_task + + +def async_post_accounting_export_summary(org_id: str, workspace_id: int) -> None: + """ + Async'ly post accounting export summary to Fyle + :param org_id: org id + :param workspace_id: workspace id + :return: None + """ + # This function calls post_accounting_export_summary asynchrously + async_task('apps.fyle.tasks.post_accounting_export_summary', org_id, workspace_id) + + +def async_import_and_export_expenses(body: dict) -> None: + """ + Async'ly import and export expenses + :param body: body + :return: None + """ + if body.get('action') == 'ACCOUNTING_EXPORT_INITIATED' and body.get('data'): + report_id = body['data']['id'] + org_id = body['data']['org_id'] + async_task('apps.fyle.tasks.import_and_export_expenses', report_id, org_id) diff --git a/apps/fyle/tasks.py b/apps/fyle/tasks.py index 00544056..267f4047 100644 --- a/apps/fyle/tasks.py +++ b/apps/fyle/tasks.py @@ -1,20 +1,22 @@ import logging import traceback from datetime import datetime -from typing import List +from typing import Dict, List from django.db import transaction from fyle.platform.exceptions import InvalidTokenError as FyleInvalidTokenError from fyle_integrations_platform_connector import PlatformConnector -from apps.tasks.models import TaskLog +from apps.fyle.actions import create_generator_and_post_in_batches +from apps.fyle.enums import ExpenseStateEnum, FundSourceEnum, PlatformExpensesEnum +from apps.fyle.helpers import get_filter_credit_expenses, get_fund_source, get_source_account_type, handle_import_exception +from apps.fyle.models import Expense, ExpenseGroup, ExpenseGroupSettings +from apps.fyle.queue import async_post_accounting_export_summary from apps.tasks.enums import TaskLogStatusEnum, TaskLogTypeEnum +from apps.tasks.models import TaskLog +from apps.workspaces.actions import export_to_xero from apps.workspaces.models import FyleCredential, Workspace, WorkspaceGeneralSettings -from .models import Expense, ExpenseGroup, ExpenseGroupSettings -from .enums import FundSourceEnum, PlatformExpensesEnum, ExpenseStateEnum - - logger = logging.getLogger(__name__) logger.level = logging.INFO @@ -159,3 +161,110 @@ def async_create_expense_groups( def sync_dimensions(fyle_credentials): platform = PlatformConnector(fyle_credentials) platform.import_fyle_dimensions() + + +def group_expenses_and_save(expenses: List[Dict], task_log: TaskLog, workspace: Workspace): + expense_objects = Expense.create_expense_objects(expenses, workspace.id) + configuration: WorkspaceGeneralSettings = WorkspaceGeneralSettings.objects.get(workspace_id=workspace.id) + filtered_expenses = expense_objects + expenses_object_ids = [expense_object.id for expense_object in expense_objects] + + async_post_accounting_export_summary(workspace.fyle_org_id, workspace.id) + + filtered_expenses = Expense.objects.filter( + is_skipped=False, + id__in=expenses_object_ids, + expensegroup__isnull=True, + org_id=workspace.fyle_org_id + ) + + ExpenseGroup.create_expense_groups_by_report_id_fund_source( + filtered_expenses, configuration, workspace.id + ) + + task_log.status = 'COMPLETE' + task_log.save() + + +def import_and_export_expenses(report_id: str, org_id: str) -> None: + """ + Import and export expenses + :param report_id: report id + :param org_id: org id + :return: None + """ + workspace = Workspace.objects.get(fyle_org_id=org_id) + fyle_credentials = FyleCredential.objects.get(workspace_id=workspace.id) + expense_group_settings = ExpenseGroupSettings.objects.get(workspace_id=workspace.id) + + try: + with transaction.atomic(): + task_log, _ = TaskLog.objects.update_or_create(workspace_id=workspace.id, type='FETCHING_EXPENSES', defaults={'status': 'IN_PROGRESS'}) + + fund_source = get_fund_source(workspace.id) + source_account_type = get_source_account_type(fund_source) + filter_credit_expenses = get_filter_credit_expenses(expense_group_settings) + + platform = PlatformConnector(fyle_credentials) + expenses = platform.expenses.get( + source_account_type, + filter_credit_expenses=filter_credit_expenses, + report_id=report_id + ) + + group_expenses_and_save(expenses, task_log, workspace) + + # Export only selected expense groups + expense_ids = Expense.objects.filter(report_id=report_id, org_id=org_id).values_list('id', flat=True) + expense_groups = ExpenseGroup.objects.filter(expenses__id__in=[expense_ids], workspace_id=workspace.id).distinct('id').values('id') + expense_group_ids = [expense_group['id'] for expense_group in expense_groups] + + if len(expense_group_ids): + export_to_xero(workspace.id, None, expense_group_ids) + + except Exception: + handle_import_exception(task_log) + + +def post_accounting_export_summary(org_id: str, workspace_id: int, fund_source: str = None) -> None: + """ + Post accounting export summary to Fyle + :param org_id: org id + :param workspace_id: workspace id + :param fund_source: fund source + :return: None + """ + # Iterate through all expenses which are not synced and post accounting export summary to Fyle in batches + fyle_credentials = FyleCredential.objects.get(workspace_id=workspace_id) + platform = PlatformConnector(fyle_credentials) + filters = { + 'org_id': org_id, + 'accounting_export_summary__synced': False + } + + if fund_source: + filters['fund_source'] = fund_source + + expenses_count = Expense.objects.filter(**filters).count() + + accounting_export_summary_batches = [] + page_size = 200 + for offset in range(0, expenses_count, page_size): + limit = offset + page_size + paginated_expenses = Expense.objects.filter(**filters).order_by('id')[offset:limit] + + payload = [] + + for expense in paginated_expenses: + accounting_export_summary = expense.accounting_export_summary + accounting_export_summary.pop('synced') + payload.append(expense.accounting_export_summary) + + accounting_export_summary_batches.append(payload) + + logger.info( + 'Posting accounting export summary to Fyle workspace_id: %s, payload: %s', + workspace_id, + accounting_export_summary_batches + ) + create_generator_and_post_in_batches(accounting_export_summary_batches, platform, workspace_id) diff --git a/apps/fyle/urls.py b/apps/fyle/urls.py index a4051bce..977c1732 100644 --- a/apps/fyle/urls.py +++ b/apps/fyle/urls.py @@ -6,6 +6,7 @@ ExpenseGroupSyncView, ExpenseGroupView, ExportableExpenseGroupsView, + ExportView, RefreshFyleDimensionView, SyncFyleDimensionView, ) @@ -26,4 +27,5 @@ path("expense_group_settings/", ExpenseGroupSettingsView.as_view()), path("sync_dimensions/", SyncFyleDimensionView.as_view()), path("refresh_dimensions/", RefreshFyleDimensionView.as_view()), + path('exports/', ExportView.as_view(), name='exports') ] diff --git a/apps/fyle/views.py b/apps/fyle/views.py index 029fb847..213b0711 100644 --- a/apps/fyle/views.py +++ b/apps/fyle/views.py @@ -5,6 +5,7 @@ from apps.exceptions import handle_view_exceptions from apps.fyle.actions import exportable_expense_group, get_expense_field, refresh_fyle_dimension, sync_fyle_dimension from apps.fyle.models import ExpenseGroup, ExpenseGroupSettings +from apps.fyle.queue import async_import_and_export_expenses from apps.fyle.serializers import ExpenseFieldSerializer, ExpenseGroupSerializer, ExpenseGroupSettingsSerializer from apps.fyle.tasks import async_create_expense_groups, get_task_log_and_fund_source from fyle_xero_api.utils import LookupFieldMixin @@ -106,3 +107,16 @@ def get(self, request, *args, **kwargs): data={"exportable_expense_group_ids": expense_group_ids}, status=status.HTTP_200_OK, ) + + +class ExportView(generics.CreateAPIView): + """ + Export View + """ + authentication_classes = [] + permission_classes = [] + + def post(self, request, *args, **kwargs): + async_import_and_export_expenses(request.data) + + return Response(data={}, status=status.HTTP_200_OK) diff --git a/apps/mappings/helpers.py b/apps/mappings/helpers.py index dce2a16b..c33824ac 100644 --- a/apps/mappings/helpers.py +++ b/apps/mappings/helpers.py @@ -3,9 +3,8 @@ from django_q.models import Schedule from fyle_accounting_mappings.models import MappingSetting -from apps.workspaces.models import WorkspaceGeneralSettings - from apps.fyle.enums import FyleAttributeEnum +from apps.workspaces.models import WorkspaceGeneralSettings def schedule_or_delete_fyle_import_tasks(configuration: WorkspaceGeneralSettings): diff --git a/apps/mappings/signals.py b/apps/mappings/signals.py index 679ac494..d54b6313 100644 --- a/apps/mappings/signals.py +++ b/apps/mappings/signals.py @@ -8,13 +8,13 @@ from django_q.tasks import async_task from fyle_accounting_mappings.models import Mapping, MappingSetting +from apps.fyle.enums import FyleAttributeEnum from apps.mappings.helpers import schedule_or_delete_fyle_import_tasks from apps.mappings.models import TenantMapping from apps.mappings.queue import schedule_cost_centers_creation, schedule_fyle_attributes_creation from apps.mappings.tasks import upload_attributes_to_fyle from apps.tasks.models import Error from apps.workspaces.models import WorkspaceGeneralSettings -from apps.fyle.enums import FyleAttributeEnum @receiver(post_save, sender=Mapping) diff --git a/apps/mappings/tasks.py b/apps/mappings/tasks.py index 2893af7c..fd98da31 100644 --- a/apps/mappings/tasks.py +++ b/apps/mappings/tasks.py @@ -5,13 +5,12 @@ from fyle_accounting_mappings.models import DestinationAttribute, ExpenseAttribute, Mapping, MappingSetting from fyle_integrations_platform_connector import PlatformConnector +from apps.fyle.enums import FyleAttributeEnum from apps.mappings.constants import FYLE_EXPENSE_SYSTEM_FIELDS from apps.mappings.exceptions import handle_import_exceptions from apps.tasks.models import Error from apps.workspaces.models import FyleCredential, WorkspaceGeneralSettings, XeroCredentials from apps.xero.utils import XeroConnector -from apps.fyle.enums import FyleAttributeEnum - logger = logging.getLogger(__name__) logger.level = logging.INFO diff --git a/apps/tasks/models.py b/apps/tasks/models.py index e17e5c89..e68e1e85 100644 --- a/apps/tasks/models.py +++ b/apps/tasks/models.py @@ -3,11 +3,10 @@ from fyle_accounting_mappings.models import ExpenseAttribute from apps.fyle.models import ExpenseGroup +from apps.tasks.enums import ErrorTypeEnum from apps.workspaces.models import Workspace from apps.xero.models import BankTransaction, Bill, Payment -from .enums import ErrorTypeEnum - def get_default(): return dict diff --git a/apps/workspaces/actions.py b/apps/workspaces/actions.py index 5f8fc4b5..c2887511 100644 --- a/apps/workspaces/actions.py +++ b/apps/workspaces/actions.py @@ -1,4 +1,5 @@ import logging +from datetime import datetime, timedelta from django.contrib.auth import get_user_model from django.core.cache import cache @@ -8,12 +9,21 @@ from fyle_rest_auth.models import AuthToken from xerosdk import exceptions as xero_exc +from apps.fyle.enums import FundSourceEnum from apps.fyle.helpers import get_cluster_domain -from apps.fyle.models import ExpenseGroupSettings +from apps.fyle.models import ExpenseGroup, ExpenseGroupSettings from apps.mappings.models import TenantMapping -from apps.workspaces.models import FyleCredential, LastExportDetail, Workspace, XeroCredentials +from apps.workspaces.models import ( + FyleCredential, + LastExportDetail, + Workspace, + WorkspaceGeneralSettings, + WorkspaceSchedule, + XeroCredentials, +) from apps.workspaces.signals import post_delete_xero_connection from apps.workspaces.utils import generate_xero_refresh_token +from apps.xero.queue import schedule_bank_transaction_creation, schedule_bills_creation from apps.xero.utils import XeroConnector logger = logging.getLogger(__name__) @@ -170,3 +180,60 @@ def get_workspace_admin(workspace_id): {"name": employee.detail["full_name"], "email": admin.email} ) return admin_email + + +def export_to_xero(workspace_id, export_mode="MANUAL", expense_group_ids=[]): + general_settings = WorkspaceGeneralSettings.objects.get(workspace_id=workspace_id) + last_export_detail = LastExportDetail.objects.get(workspace_id=workspace_id) + workspace_schedule = WorkspaceSchedule.objects.filter(workspace_id=workspace_id, interval_hours__gt=0, enabled=True).first() + + last_exported_at = datetime.now() + is_expenses_exported = False + export_mode = export_mode or 'MANUAL' + expense_group_filters = { + 'exported_at__isnull': True, + 'workspace_id': workspace_id + } + if expense_group_ids: + expense_group_filters['id__in'] = expense_group_ids + + if general_settings.reimbursable_expenses_object: + expense_group_ids = ExpenseGroup.objects.filter( + fund_source=FundSourceEnum.PERSONAL, + **expense_group_filters + ).values_list("id", flat=True) + + if len(expense_group_ids): + is_expenses_exported = True + + schedule_bills_creation( + workspace_id=workspace_id, + expense_group_ids=expense_group_ids, + is_auto_export=export_mode == 'AUTO', + fund_source='PERSONAL' + ) + + if general_settings.corporate_credit_card_expenses_object: + expense_group_ids = ExpenseGroup.objects.filter( + fund_source=FundSourceEnum.CCC, + **expense_group_filters + ).values_list("id", flat=True) + + if len(expense_group_ids): + is_expenses_exported = True + + schedule_bank_transaction_creation( + workspace_id=workspace_id, + expense_group_ids=expense_group_ids, + is_auto_export=export_mode == 'AUTO', + fund_source='CCC' + ) + + if is_expenses_exported: + last_export_detail.last_exported_at = last_exported_at + last_export_detail.export_mode = export_mode or 'MANUAL' + + if workspace_schedule: + last_export_detail.next_export_at = last_exported_at + timedelta(hours=workspace_schedule.interval_hours) + + last_export_detail.save() diff --git a/apps/workspaces/apis/advanced_settings/serializers.py b/apps/workspaces/apis/advanced_settings/serializers.py index 39f7d8ff..13aaf4c5 100644 --- a/apps/workspaces/apis/advanced_settings/serializers.py +++ b/apps/workspaces/apis/advanced_settings/serializers.py @@ -133,6 +133,8 @@ def update(self, instance, validated): if instance.onboarding_state == "ADVANCED_SETTINGS": instance.onboarding_state = "COMPLETE" instance.save() + # Commenting this for now, will uncomment once we expose to UI + # AdvancedConfigurationsTriggers.post_to_integration_settings(instance.id, True) return instance diff --git a/apps/workspaces/apis/advanced_settings/triggers.py b/apps/workspaces/apis/advanced_settings/triggers.py index 2430b9d1..cca29a77 100644 --- a/apps/workspaces/apis/advanced_settings/triggers.py +++ b/apps/workspaces/apis/advanced_settings/triggers.py @@ -29,3 +29,10 @@ def run_workspace_general_settings_triggers( sync_xero_to_fyle_payments=workspace_general_settings_instance.sync_xero_to_fyle_payments, workspace_id=workspace_general_settings_instance.workspace.id, ) + + @staticmethod + def post_to_integration_settings(workspace_id: int, active: bool): + """ + Post to integration settings + """ + post_to_integration_settings(workspace_id, active) diff --git a/apps/workspaces/email.py b/apps/workspaces/email.py index a1f4c5c9..9caceca5 100644 --- a/apps/workspaces/email.py +++ b/apps/workspaces/email.py @@ -6,15 +6,11 @@ from django.template.loader import render_to_string from fyle_accounting_mappings.models import ExpenseAttribute from sendgrid import SendGridAPIClient -from sendgrid.helpers.mail import ( - Mail, From -) +from sendgrid.helpers.mail import From, Mail from apps.mappings.models import TenantMapping - +from apps.tasks.enums import TaskLogStatusEnum, TaskLogTypeEnum from apps.tasks.models import Error, TaskLog -from apps.tasks.enums import TaskLogTypeEnum, TaskLogStatusEnum - from apps.workspaces.models import Workspace, WorkspaceSchedule diff --git a/apps/workspaces/signals.py b/apps/workspaces/signals.py index dd1ef0b8..886262a6 100644 --- a/apps/workspaces/signals.py +++ b/apps/workspaces/signals.py @@ -1,10 +1,9 @@ from fyle_accounting_mappings.models import DestinationAttribute, Mapping +from apps.fyle.enums import FyleAttributeEnum from apps.mappings.models import TenantMapping from apps.workspaces.models import Workspace -from apps.fyle.enums import FyleAttributeEnum - def post_delete_xero_connection(workspace_id): """ diff --git a/apps/workspaces/tasks.py b/apps/workspaces/tasks.py index 08e44e4e..aacfbae2 100644 --- a/apps/workspaces/tasks.py +++ b/apps/workspaces/tasks.py @@ -1,26 +1,21 @@ +import json import logging from datetime import datetime -from django.conf import settings +from django.conf import settings from fyle_integrations_platform_connector import PlatformConnector from fyle_rest_auth.helpers import get_fyle_admin -from apps.fyle.models import ExpenseGroup -from apps.fyle.tasks import async_create_expense_groups from apps.fyle.enums import FundSourceEnum - +from apps.fyle.helpers import post_request +from apps.fyle.tasks import async_create_expense_groups from apps.mappings.models import TenantMapping - -from apps.tasks.models import TaskLog from apps.tasks.enums import TaskLogStatusEnum, TaskLogTypeEnum - +from apps.tasks.models import TaskLog from apps.users.models import User - +from apps.workspaces.actions import export_to_xero from apps.workspaces.email import get_admin_name, get_errors, get_failed_task_logs_count, send_failure_notification_email -from apps.workspaces.models import FyleCredential, LastExportDetail, Workspace, WorkspaceGeneralSettings, WorkspaceSchedule - -from apps.xero.tasks import create_chain_and_export, schedule_bank_transaction_creation, schedule_bills_creation - +from apps.workspaces.models import FyleCredential, Workspace, WorkspaceGeneralSettings, WorkspaceSchedule logger = logging.getLogger(__name__) logger.level = logging.INFO @@ -55,37 +50,6 @@ def run_sync_schedule(workspace_id): export_to_xero(workspace_id, "AUTO") -def export_to_xero(workspace_id, export_mode="MANUAL"): - general_settings = WorkspaceGeneralSettings.objects.get(workspace_id=workspace_id) - last_export_detail = LastExportDetail.objects.get(workspace_id=workspace_id) - last_exported_at = datetime.now() - chaining_attributes = [] - - if general_settings.reimbursable_expenses_object: - expense_group_ids = ExpenseGroup.objects.filter( - fund_source=FundSourceEnum.PERSONAL, - workspace_id=workspace_id - ).values_list("id", flat=True) - chaining_attributes.extend( - schedule_bills_creation(workspace_id, expense_group_ids) - ) - - if general_settings.corporate_credit_card_expenses_object: - expense_group_ids = ExpenseGroup.objects.filter( - fund_source=FundSourceEnum.CCC, - workspace_id=workspace_id - ).values_list("id", flat=True) - chaining_attributes.extend( - schedule_bank_transaction_creation(workspace_id, expense_group_ids) - ) - - if chaining_attributes: - create_chain_and_export(chaining_attributes, workspace_id) - last_export_detail.last_exported_at = last_exported_at - last_export_detail.export_mode = export_mode - last_export_detail.save() - - def async_update_fyle_credentials(fyle_org_id: str, refresh_token: str): fyle_credentials = FyleCredential.objects.filter( workspace__fyle_org_id=fyle_org_id @@ -170,3 +134,23 @@ def async_create_admin_subcriptions(workspace_id: int) -> None: 'webhook_url': '{}/workspaces/{}/fyle/exports/'.format(settings.API_URL, workspace_id) } platform.subscriptions.post(payload) + + +def post_to_integration_settings(workspace_id: int, active: bool): + """ + Post to integration settings + """ + refresh_token = FyleCredential.objects.get(workspace_id=workspace_id).refresh_token + url = '{}/integrations/'.format(settings.INTEGRATIONS_SETTINGS_API) + payload = { + 'tpa_id': settings.FYLE_CLIENT_ID, + 'tpa_name': 'Fyle Xero Integration', + 'type': 'ACCOUNTING', + 'is_active': active, + 'connected_at': datetime.now().strftime('%Y-%m-%dT%H:%M:%S.%fZ') + } + + try: + post_request(url, json.dumps(payload), refresh_token) + except Exception as error: + logger.error(error) diff --git a/apps/workspaces/utils.py b/apps/workspaces/utils.py index dc66ac42..7d28fe7b 100644 --- a/apps/workspaces/utils.py +++ b/apps/workspaces/utils.py @@ -10,15 +10,11 @@ from fyle_accounting_mappings.models import MappingSetting from xerosdk import InternalServerError, InvalidTokenError, XeroSDK -from apps.fyle.models import ExpenseGroupSettings from apps.fyle.enums import FyleAttributeEnum - +from apps.fyle.models import ExpenseGroupSettings from apps.mappings.queue import schedule_auto_map_employees, schedule_tax_groups_creation - from apps.workspaces.models import Workspace, WorkspaceGeneralSettings - from apps.xero.queue import schedule_payment_creation, schedule_reimbursements_sync, schedule_xero_objects_status_sync - from fyle_xero_api.utils import assert_valid diff --git a/apps/workspaces/views.py b/apps/workspaces/views.py index f4c4fc2d..00cd099e 100644 --- a/apps/workspaces/views.py +++ b/apps/workspaces/views.py @@ -1,16 +1,15 @@ import logging from django.contrib.auth import get_user_model +from django_q.tasks import async_task from fyle_rest_auth.utils import AuthUtils from rest_framework import generics from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import status -from django_q.tasks import async_task - from apps.exceptions import handle_view_exceptions -from apps.workspaces.actions import connect_xero, get_workspace_admin, post_workspace, revoke_connections +from apps.workspaces.actions import connect_xero, export_to_xero, get_workspace_admin, post_workspace, revoke_connections from apps.workspaces.models import LastExportDetail, Workspace, WorkspaceGeneralSettings, XeroCredentials from apps.workspaces.serializers import ( LastExportDetailSerializer, @@ -18,7 +17,6 @@ WorkspaceSerializer, XeroCredentialSerializer, ) -from apps.workspaces.tasks import export_to_xero from apps.workspaces.utils import generate_xero_identity logger = logging.getLogger(__name__) diff --git a/apps/xero/actions.py b/apps/xero/actions.py index 7c604236..d6a7e13b 100644 --- a/apps/xero/actions.py +++ b/apps/xero/actions.py @@ -3,9 +3,9 @@ from django_q.tasks import Chain from fyle_accounting_mappings.models import MappingSetting +from apps.fyle.enums import FyleAttributeEnum from apps.workspaces.models import Workspace, WorkspaceGeneralSettings, XeroCredentials from apps.xero.utils import XeroConnector -from apps.fyle.enums import FyleAttributeEnum def get_xero_connector(workspace_id): diff --git a/apps/xero/exceptions.py b/apps/xero/exceptions.py index 518a672c..3d6c2c72 100644 --- a/apps/xero/exceptions.py +++ b/apps/xero/exceptions.py @@ -12,15 +12,12 @@ XeroSDKError, ) -from fyle_xero_api.exceptions import BulkError - +from apps.fyle.actions import update_failed_expenses from apps.fyle.models import ExpenseGroup - +from apps.tasks.enums import ErrorTypeEnum, TaskLogStatusEnum, TaskLogTypeEnum from apps.tasks.models import Error, TaskLog -from apps.tasks.enums import TaskLogStatusEnum, TaskLogTypeEnum, ErrorTypeEnum - from apps.workspaces.models import FyleCredential, LastExportDetail, XeroCredentials - +from fyle_xero_api.exceptions import BulkError logger = logging.getLogger(__name__) logger.level = logging.INFO @@ -127,6 +124,7 @@ def handle_xero_error(exception, expense_group: ExpenseGroup, task_log: TaskLog) task_log.detail = None task_log.status = TaskLogStatusEnum.FAILED + update_failed_expenses(expense_group.expenses.all(), False) task_log.save() @@ -148,6 +146,7 @@ def new_fn(*args): try: if not payment and not xero_connection: + update_failed_expenses(expense_group.expenses.all(), False) raise XeroCredentials.DoesNotExist func(*args) @@ -215,6 +214,7 @@ def new_fn(*args): ] task_log.save() + update_failed_expenses(expense_group.expenses.all(), False) except XeroCredentials.DoesNotExist: logger.info( @@ -227,6 +227,7 @@ def new_fn(*args): task_log.detail = detail task_log.save() + update_failed_expenses(expense_group.expenses.all(), False) except XeroSDKError as exception: logger.info(exception.response) @@ -236,6 +237,7 @@ def new_fn(*args): task_log.xero_errors = detail task_log.save() + update_failed_expenses(expense_group.expenses.all(), False) except BulkError as exception: logger.info(exception.response) @@ -243,6 +245,7 @@ def new_fn(*args): task_log.status = TaskLogStatusEnum.FAILED task_log.detail = detail task_log.save() + update_failed_expenses(expense_group.expenses.all(), True) except Exception as error: error = traceback.format_exc() @@ -254,6 +257,7 @@ def new_fn(*args): task_log.workspace_id, task_log.detail, ) + update_failed_expenses(expense_group.expenses.all(), False) if not payment and args[-1] == True: update_last_export_details(workspace_id=expense_group.workspace_id) diff --git a/apps/xero/models.py b/apps/xero/models.py index eda9f1ad..649b9a24 100644 --- a/apps/xero/models.py +++ b/apps/xero/models.py @@ -5,11 +5,9 @@ from django.db.models import JSONField from fyle_accounting_mappings.models import DestinationAttribute, ExpenseAttribute, Mapping, MappingSetting -from apps.fyle.models import Expense, ExpenseGroup from apps.fyle.enums import FyleAttributeEnum - +from apps.fyle.models import Expense, ExpenseGroup from apps.mappings.models import GeneralMapping - from apps.workspaces.models import FyleCredential, Workspace diff --git a/apps/xero/queue.py b/apps/xero/queue.py index 836219ee..7c25d42a 100644 --- a/apps/xero/queue.py +++ b/apps/xero/queue.py @@ -1,8 +1,17 @@ from datetime import datetime, timedelta +from typing import List +from django.db.models import Q from django_q.models import Schedule +from django_q.tasks import Chain +from xerosdk.exceptions import UnsuccessfulAuthentication +from apps.fyle.models import Expense, ExpenseGroup from apps.mappings.models import GeneralMapping +from apps.tasks.enums import TaskLogStatusEnum, TaskLogTypeEnum +from apps.tasks.models import TaskLog +from apps.workspaces.models import FyleCredential, XeroCredentials +from apps.xero.utils import XeroConnector def schedule_payment_creation(sync_fyle_to_xero_payments, workspace_id): @@ -72,3 +81,134 @@ def schedule_reimbursements_sync(sync_xero_to_fyle_payments, workspace_id): if schedule: schedule.delete() + + +def __create_chain_and_run(fyle_credentials: FyleCredential, xero_connection, in_progress_expenses: List[Expense], + workspace_id: int, chain_tasks: List[dict], fund_source: str) -> None: + """ + Create chain and run + :param fyle_credentials: Fyle credentials + :param in_progress_expenses: List of in progress expenses + :param workspace_id: workspace id + :param chain_tasks: List of chain tasks + :param fund_source: Fund source + :return: None + """ + chain = Chain() + chain.append("apps.fyle.tasks.sync_dimensions", fyle_credentials) + + chain.append('apps.xero.tasks.update_expense_and_post_summary', in_progress_expenses, workspace_id, fund_source) + + for task in chain_tasks: + chain.append(task['target'], task['expense_group_id'], task['task_log_id'], xero_connection, task['last_export']) + + chain.append('apps.fyle.tasks.post_accounting_export_summary', fyle_credentials.workspace.fyle_org_id, workspace_id, fund_source) + chain.run() + + +def schedule_bills_creation(workspace_id: int, expense_group_ids: List[str], is_auto_export: bool, fund_source: str) -> list: + """ + Schedule bills creation + :param expense_group_ids: List of expense group ids + :param workspace_id: workspace id + :return: List of chaining attributes + """ + if expense_group_ids: + expense_groups = ExpenseGroup.objects.filter( + Q(tasklog__id__isnull=True) + | ~Q(tasklog__status__in=[TaskLogStatusEnum.IN_PROGRESS, TaskLogStatusEnum.COMPLETE]), + workspace_id=workspace_id, + id__in=expense_group_ids, + bill__id__isnull=True, + exported_at__isnull=True, + ).all() + + chain_tasks = [] + in_progress_expenses = [] + + for index, expense_group in enumerate(expense_groups): + task_log, _ = TaskLog.objects.get_or_create( + workspace_id=expense_group.workspace_id, + expense_group=expense_group, + defaults={"status": TaskLogStatusEnum.ENQUEUED, "type": TaskLogTypeEnum.CREATING_BILL}, + ) + if task_log.status not in [TaskLogStatusEnum.IN_PROGRESS, TaskLogStatusEnum.ENQUEUED]: + task_log.status = TaskLogStatusEnum.ENQUEUED + task_log.save() + + last_export = False + if expense_groups.count() == index + 1: + last_export = True + + chain_tasks.append({ + 'target': 'apps.xero.tasks.create_bill', + 'expense_group_id': expense_group.id, + 'task_log_id': task_log.id, + 'last_export': last_export}) + + if not (is_auto_export and expense_group.expenses.first().previous_export_state == 'ERROR'): + in_progress_expenses.extend(expense_group.expenses.all()) + + if len(chain_tasks) > 0: + fyle_credentials = FyleCredential.objects.get(workspace_id=workspace_id) + try: + xero_credentials = XeroCredentials.get_active_xero_credentials(workspace_id) + xero_connection = XeroConnector(xero_credentials, workspace_id) + __create_chain_and_run(fyle_credentials, xero_connection, in_progress_expenses, workspace_id, chain_tasks, fund_source) + except (UnsuccessfulAuthentication, XeroCredentials.DoesNotExist): + xero_connection = None + + +def schedule_bank_transaction_creation( + workspace_id: int, expense_group_ids: List[str], is_auto_export: bool, fund_source: str +) -> list: + """ + Schedule bank transaction creation + :param expense_group_ids: List of expense group ids + :param workspace_id: workspace id + :return: List of chaining attributes + """ + if expense_group_ids: + expense_groups = ExpenseGroup.objects.filter( + Q(tasklog__id__isnull=True) + | ~Q(tasklog__status__in=[TaskLogStatusEnum.IN_PROGRESS, TaskLogStatusEnum.COMPLETE]), + workspace_id=workspace_id, + id__in=expense_group_ids, + banktransaction__id__isnull=True, + exported_at__isnull=True, + ).all() + + chain_tasks = [] + in_progress_expenses = [] + + for index, expense_group in enumerate(expense_groups): + task_log, _ = TaskLog.objects.get_or_create( + workspace_id=expense_group.workspace_id, + expense_group=expense_group, + defaults={"status": TaskLogStatusEnum.ENQUEUED, "type": TaskLogTypeEnum.CREATING_BANK_TRANSACTION}, + ) + if task_log.status not in [TaskLogStatusEnum.IN_PROGRESS, TaskLogStatusEnum.ENQUEUED]: + task_log.status = TaskLogStatusEnum.ENQUEUED + task_log.save() + + last_export = False + if expense_groups.count() == index + 1: + last_export = True + + chain_tasks.append({ + 'target': 'apps.xero.tasks.create_bank_transaction', + 'expense_group_id': expense_group.id, + 'task_log_id': task_log.id, + 'last_export': last_export}) + + if not (is_auto_export and expense_group.expenses.first().previous_export_state == 'ERROR'): + in_progress_expenses.extend(expense_group.expenses.all()) + + if len(chain_tasks) > 0: + fyle_credentials = FyleCredential.objects.get(workspace_id=workspace_id) + try: + xero_credentials = XeroCredentials.get_active_xero_credentials(workspace_id) + xero_connection = XeroConnector(xero_credentials, workspace_id) + except (UnsuccessfulAuthentication, XeroCredentials.DoesNotExist): + xero_connection = None + __create_chain_and_run(fyle_credentials, xero_connection, in_progress_expenses, workspace_id, chain_tasks, fund_source) diff --git a/apps/xero/tasks.py b/apps/xero/tasks.py index e1ba9c11..101f470c 100644 --- a/apps/xero/tasks.py +++ b/apps/xero/tasks.py @@ -5,32 +5,22 @@ from typing import List from django.db import transaction -from django.db.models import Q - -from django_q.tasks import Chain - from fyle_accounting_mappings.models import DestinationAttribute, ExpenseAttribute, Mapping - from fyle_integrations_platform_connector import PlatformConnector - -from fyle_xero_api.exceptions import BulkError - from xerosdk.exceptions import UnsuccessfulAuthentication, WrongParamsError -from apps.fyle.models import Expense, ExpenseGroup, Reimbursement +from apps.fyle.actions import update_complete_expenses, update_expenses_in_progress from apps.fyle.enums import FundSourceEnum, FyleAttributeEnum, PlatformExpensesEnum - +from apps.fyle.models import Expense, ExpenseGroup, Reimbursement +from apps.fyle.tasks import post_accounting_export_summary from apps.mappings.models import GeneralMapping, TenantMapping - +from apps.tasks.enums import ErrorTypeEnum, TaskLogStatusEnum, TaskLogTypeEnum from apps.tasks.models import Error, TaskLog -from apps.tasks.enums import TaskLogStatusEnum, TaskLogTypeEnum, ErrorTypeEnum - from apps.workspaces.models import FyleCredential, Workspace, WorkspaceGeneralSettings, XeroCredentials - from apps.xero.exceptions import handle_xero_exceptions from apps.xero.models import BankTransaction, BankTransactionLineItem, Bill, BillLineItem, Payment from apps.xero.utils import XeroConnector - +from fyle_xero_api.exceptions import BulkError logger = logging.getLogger(__name__) logger.level = logging.INFO @@ -191,6 +181,19 @@ def create_or_update_employee_mapping( ) +def update_expense_and_post_summary(in_progress_expenses: List[Expense], workspace_id: int, fund_source: str) -> None: + """ + Update expense and post accounting export summary + :param in_progress_expenses: List of expenses + :param workspace_id: Workspace ID + :param fund_source: Fund source + :return: None + """ + fyle_org_id = Workspace.objects.get(pk=workspace_id).fyle_org_id + update_expenses_in_progress(in_progress_expenses) + post_accounting_export_summary(fyle_org_id, workspace_id, fund_source) + + @handle_xero_exceptions(payment=False) def create_bill( expense_group_id: int, @@ -242,6 +245,7 @@ def create_bill( expense_group.response_logs = created_bill expense_group.save() resolve_errors_for_exported_expense_group(expense_group) + generate_export_url_and_update_expense(expense_group, 'BILL') # Assign billable expenses to customers if general_settings.import_customers: @@ -268,84 +272,6 @@ def create_bill( ) -def create_chain_and_export(chaining_attributes: list, workspace_id: int) -> None: - """ - Create a chain of expense groups and export them to Xero - :param chaining_attributes: - :param workspace_id: - :return: None - """ - try: - xero_credentials = XeroCredentials.get_active_xero_credentials(workspace_id) - xero_connection = XeroConnector(xero_credentials, workspace_id) - except (UnsuccessfulAuthentication, XeroCredentials.DoesNotExist): - xero_connection = None - - chain = Chain() - - fyle_credentials = FyleCredential.objects.get(workspace_id=workspace_id) - chain.append("apps.fyle.tasks.sync_dimensions", fyle_credentials) - - for group in chaining_attributes: - trigger_function = "apps.xero.tasks.create_{}".format(group["export_type"]) - chain.append( - trigger_function, - group["expense_group_id"], - group["task_log_id"], - xero_connection, - group["last_export"] - ) - - if chain.length() > 1: - chain.run() - - -def schedule_bills_creation(workspace_id: int, expense_group_ids: List[str]) -> list: - """ - Schedule bills creation - :param expense_group_ids: List of expense group ids - :param workspace_id: workspace id - :return: List of chaining attributes - """ - chaining_attributes = [] - if expense_group_ids: - expense_groups = ExpenseGroup.objects.filter( - Q(tasklog__id__isnull=True) - | ~Q(tasklog__status__in=[TaskLogStatusEnum.IN_PROGRESS, TaskLogStatusEnum.COMPLETE]), - workspace_id=workspace_id, - id__in=expense_group_ids, - bill__id__isnull=True, - exported_at__isnull=True, - ).all() - - for index, expense_group in enumerate(expense_groups): - task_log, _ = TaskLog.objects.get_or_create( - workspace_id=expense_group.workspace_id, - expense_group=expense_group, - defaults={"status": TaskLogStatusEnum.ENQUEUED, "type": TaskLogTypeEnum.CREATING_BILL}, - ) - if task_log.status not in [TaskLogStatusEnum.IN_PROGRESS, TaskLogStatusEnum.ENQUEUED]: - task_log.status = TaskLogStatusEnum.ENQUEUED - task_log.save() - - last_export = False - if expense_groups.count() == index + 1: - last_export = True - - chaining_attributes.append( - { - "expense_group_id": expense_group.id, - "task_log_id": task_log.id, - "export_type": "bill", - "last_export": last_export, - } - ) - - task_log.save() - - return chaining_attributes - - def get_linked_transaction_object(export_instance, line_items: list): """ Get linked transaction object @@ -487,6 +413,7 @@ def create_bank_transaction( expense_group.response_logs = created_bank_transaction expense_group.save() resolve_errors_for_exported_expense_group(expense_group) + generate_export_url_and_update_expense(expense_group, 'BANK TRANSACTION') # Assign billable expenses to customers if general_settings.import_customers: @@ -517,54 +444,6 @@ def create_bank_transaction( ) -def schedule_bank_transaction_creation( - workspace_id: int, expense_group_ids: List[str] -) -> list: - """ - Schedule bank transaction creation - :param expense_group_ids: List of expense group ids - :param workspace_id: workspace id - :return: List of chaining attributes - """ - chaining_attributes = [] - if expense_group_ids: - expense_groups = ExpenseGroup.objects.filter( - Q(tasklog__id__isnull=True) - | ~Q(tasklog__status__in=[TaskLogStatusEnum.IN_PROGRESS, TaskLogStatusEnum.COMPLETE]), - workspace_id=workspace_id, - id__in=expense_group_ids, - banktransaction__id__isnull=True, - exported_at__isnull=True, - ).all() - - for index, expense_group in enumerate(expense_groups): - task_log, _ = TaskLog.objects.get_or_create( - workspace_id=expense_group.workspace_id, - expense_group=expense_group, - defaults={"status": TaskLogStatusEnum.ENQUEUED, "type": TaskLogTypeEnum.CREATING_BANK_TRANSACTION}, - ) - if task_log.status not in [TaskLogStatusEnum.IN_PROGRESS, TaskLogStatusEnum.ENQUEUED]: - task_log.status = TaskLogStatusEnum.ENQUEUED - task_log.save() - - last_export = False - if expense_groups.count() == index + 1: - last_export = True - - chaining_attributes.append( - { - "expense_group_id": expense_group.id, - "task_log_id": task_log.id, - "export_type": "bank_transaction", - "last_export": last_export, - } - ) - - task_log.save() - - return chaining_attributes - - def __validate_expense_group(expense_group: ExpenseGroup): bulk_errors = [] row = 0 @@ -945,3 +824,32 @@ def update_xero_short_code(workspace_id: int): except Exception as exception: logger.exception("Error updating Xero short code", exception) + + +def generate_export_url_and_update_expense(expense_group: ExpenseGroup, export_type: str) -> None: + """ + Generate export url and update expense + :param expense_group: Expense Group + :return: None + """ + workspace = Workspace.objects.get(id=expense_group.workspace_id) + try: + if export_type == 'BILL': + export_id = expense_group.response_logs['Invoices'][0]['InvoiceID'] + if workspace.xero_short_code: + url = f'https://go.xero.com/organisationlogin/default.aspx?shortcode={workspace.xero_short_code}&redirecturl=/AccountsPayable/Edit.aspx?InvoiceID={export_id}' + else: + url = f'https://go.xero.com/AccountsPayable/View.aspx?invoiceID={export_id}' + else: + export_id = expense_group.response_logs['BankTransactions'][0]['BankTransactionID'] + account_id = expense_group.response_logs['BankTransactions'][0]['BankAccount']['AccountID'] + if workspace.xero_short_code: + url = f'https://go.xero.com/organisationlogin/default.aspx?shortcode={workspace.xero_short_code}&redirecturl=/Bank/ViewTransaction.aspx?bankTransactionID={export_id}&accountID={account_id}' + else: + url = f'https://go.xero.com/Bank/ViewTransaction.aspx?bankTransactionID={export_id}&accountID={account_id}' + except Exception as error: + # Defaulting it to Intacct app url, worst case scenario if we're not able to parse it properly + url = 'https://go.xero.com' + logger.error('Error while generating export url %s', error) + + update_complete_expenses(expense_group.expenses.all(), url) diff --git a/fyle_xero_api/settings.py b/fyle_xero_api/settings.py index a9e9db12..197f9c8f 100644 --- a/fyle_xero_api/settings.py +++ b/fyle_xero_api/settings.py @@ -264,6 +264,8 @@ FYLE_BASE_URL = os.environ.get("FYLE_BASE_URL") FYLE_APP_URL = os.environ.get("FYLE_APP_URL") XERO_APP_URL = os.environ.get("APP_URL") +INTEGRATIONS_SETTINGS_API = os.environ.get('INTEGRATIONS_SETTINGS_API') +XERO_INTEGRATION_APP_URL = os.environ.get('XERO_INTEGRATION_APP_URL') # XERO Settings diff --git a/fyle_xero_api/tests/settings.py b/fyle_xero_api/tests/settings.py index 1a7fe3c8..1e9eb21b 100644 --- a/fyle_xero_api/tests/settings.py +++ b/fyle_xero_api/tests/settings.py @@ -272,6 +272,8 @@ FYLE_REFRESH_TOKEN = os.environ.get("FYLE_REFRESH_TOKEN") FYLE_SERVER_URL = os.environ.get("FYLE_SERVER_URL") XERO_APP_URL = os.environ.get("APP_URL") +INTEGRATIONS_SETTINGS_API = os.environ.get('INTEGRATIONS_SETTINGS_API') +XERO_INTEGRATION_APP_URL = os.environ.get('XERO_INTEGRATION_APP_URL') # XERO Settings XERO_BASE_URL = os.environ.get("XERO_BASE_URL") diff --git a/requirements.txt b/requirements.txt index c65be721..110335ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,10 +17,10 @@ djangorestframework==3.11.2 django-sendgrid-v5==1.2.0 enum34==1.1.10 future==0.18.2 -fyle==0.30.0 -fyle-accounting-mappings==1.26.2 -fyle-integrations-platform-connector==1.32.2 -fyle-rest-auth==1.7.0 +fyle==0.35.0 +fyle-integrations-platform-connector==1.36.1 +fyle-accounting-mappings==1.26.0 +fyle-rest-auth==1.6.0 gevent==23.9.1 gunicorn==20.1.0 idna==2.8 diff --git a/scripts/python/create-admin-subscriptions.py b/scripts/python/create-admin-subscriptions.py index 9a17dd66..5b0e79cf 100644 --- a/scripts/python/create-admin-subscriptions.py +++ b/scripts/python/create-admin-subscriptions.py @@ -1,9 +1,9 @@ # Create admin subscriptions for existing workspaces -from apps.workspaces.tasks import async_create_admin_subcriptions -from apps.workspaces.models import Workspace from fyle.platform.exceptions import InvalidTokenError as FyleInvalidTokenError +from apps.workspaces.models import Workspace +from apps.workspaces.tasks import async_create_admin_subcriptions workspaces = Workspace.objects.all() @@ -11,7 +11,7 @@ try: async_create_admin_subcriptions(workspace.id) print('Admin subscriptions created for workspace - {} with ID - {}'.format(workspace.name, workspace.id)) - except FyleInvalidTokenError as e: + except FyleInvalidTokenError: print('Invalid Token for workspace - {} with ID - {}'.format(workspace.name, workspace.id)) except Exception as e: print('Error while creating admin subscriptions for workspace - {} with ID - {}'.format(workspace.name, workspace.id)) diff --git a/scripts/python/fill-accounting-export-summary.py b/scripts/python/fill-accounting-export-summary.py new file mode 100644 index 00000000..2b1ba2ec --- /dev/null +++ b/scripts/python/fill-accounting-export-summary.py @@ -0,0 +1,100 @@ +from datetime import datetime + +from django.conf import settings +from django.db.models import Q + +from apps.fyle.actions import __bulk_update_expenses +from apps.fyle.helpers import get_updated_accounting_export_summary +from apps.fyle.models import Expense +from apps.tasks.models import TaskLog +from apps.workspaces.models import Workspace + +# PLEASE RUN sql/scripts/022-fill-skipped-accounting-export-summary.sql BEFORE RUNNING THIS SCRIPT + + +export_types = ['CREATING_BILL', 'CREATING_BANK_TRANSACTION'] +task_statuses = ['COMPLETE', 'FAILED', 'FATAL'] + + +# We'll handle all COMPLETE, ERROR expenses in this script +workspaces = Workspace.objects.filter( + ~Q(name__icontains='fyle for') & ~Q(name__icontains='test') +) + +start_time = datetime.now() +number_of_expenses_without_accounting_export_summary = Expense.objects.filter( + accounting_export_summary__state__isnull=True +).count() +print('Number of expenses without accounting export summary - {}'.format(number_of_expenses_without_accounting_export_summary)) +for workspace in workspaces: + task_logs_count = TaskLog.objects.filter( + type__in=export_types, + workspace_id=workspace.id, + status__in=task_statuses + ).count() + print('Updating summary from workspace - {} with ID - {}'.format(workspace.name, workspace.id)) + print('Number of task logs to be updated - {}'.format(task_logs_count)) + page_size = 200 + for offset in range(0, task_logs_count, page_size): + expense_to_be_updated = [] + limit = offset + page_size + paginated_task_logs = TaskLog.objects.filter( + type__in=export_types, + workspace_id=workspace.id, + status__in=task_statuses + )[offset:limit] + for task_log in paginated_task_logs: + expense_group = task_log.expense_group + state = 'ERROR' if task_log.status == 'FAILED' or task_log.status == 'FATAL' else 'COMPLETE' + error_type = None + url = None + if task_log.status == 'FAILED' or task_log.status == 'FATAL': + for item in task_log.detail: + if item.get('type') and item.get('type') == 'Category Mapping': + error_type = 'MAPPING' + else: + error_type = 'ACCOUNTING_INTEGRATION_ERROR' + url = '{}/workspaces/main/dashboard'.format(settings.XERO_INTEGRATION_APP_URL) + else: + try: + if task_log.type == 'CREATING_BILL': + export_id = expense_group.response_logs['Invoices'][0]['InvoiceID'] + if workspace.xero_short_code: + url = f'https://go.xero.com/organisationlogin/default.aspx?shortcode={workspace.xero_short_code}&redirecturl=/AccountsPayable/Edit.aspx?InvoiceID={export_id}' + else: + url = f'https://go.xero.com/AccountsPayable/View.aspx?invoiceID={export_id}' + else: + export_id = expense_group.response_logs['BankTransactions'][0]['BankTransactionID'] + account_id = expense_group.response_logs['BankTransactions'][0]['BankAccount']['AccountID'] + if workspace.xero_short_code: + url = f'https://go.xero.com/organisationlogin/default.aspx?shortcode={workspace.xero_short_code}&redirecturl=/Bank/ViewTransaction.aspx?bankTransactionID={export_id}&accountID={account_id}' + else: + url = f'https://go.xero.com/Bank/ViewTransaction.aspx?bankTransactionID={export_id}&accountID={account_id}' + except Exception as error: + # Defaulting it to Intacct app url, worst case scenario if we're not able to parse it properly + url = 'https://go.xero.com' + print('Error while parsing url for task log - {}. Error - {}'.format(task_log.id, error)) + for expense in expense_group.expenses.filter(accounting_export_summary__state__isnull=True): + if url: + expense_to_be_updated.append( + Expense( + id=expense.id, + accounting_export_summary=get_updated_accounting_export_summary( + expense.expense_id, + state, + error_type, + url, + False + ) + ) + ) + print('Updating {} expenses in batches of 50'.format(len(expense_to_be_updated))) + __bulk_update_expenses(expense_to_be_updated) + + +number_of_expenses_without_accounting_export_summary = Expense.objects.filter( + accounting_export_summary__state__isnull=True +).count() +print('Number of expenses without accounting export summary - {}'.format(number_of_expenses_without_accounting_export_summary)) +end_time = datetime.now() +print('Time taken - {}'.format(end_time - start_time)) diff --git a/scripts/python/post-accounting-export-summary.py b/scripts/python/post-accounting-export-summary.py new file mode 100644 index 00000000..4713fc7c --- /dev/null +++ b/scripts/python/post-accounting-export-summary.py @@ -0,0 +1,43 @@ +from datetime import datetime +from time import sleep + +from django.db.models import Q + +from apps.fyle.models import Expense +from apps.fyle.tasks import post_accounting_export_summary +from apps.workspaces.models import Workspace + +# PLEASE RUN scripts/python/fill-accounting-export-summary.py BEFORE RUNNING THIS SCRIPT +workspaces = Workspace.objects.filter( + ~Q(name__icontains='fyle for') & ~Q(name__icontains='test') +) + +start_time = datetime.now() +number_of_expenses_to_be_posted = Expense.objects.filter( + accounting_export_summary__synced=False +).count() +print('Number of expenses to be posted - {}'.format(number_of_expenses_to_be_posted)) +for workspace in workspaces: + expenses_count = Expense.objects.filter( + accounting_export_summary__synced=False, + workspace_id=workspace.id + ).count() + print('Updating summary from workspace - {} with ID - {}'.format(workspace.name, workspace.id)) + print('Number of expenses_count to be posted for the current workspace - {}'.format(expenses_count)) + if expenses_count: + try: + sleep(1) + post_accounting_export_summary(workspace.fyle_org_id, workspace.id) + except Exception as e: + print('Error while posting accounting export summary for workspace - {} with ID - {}'.format(workspace.name, workspace.id)) + print(e.__dict__) + +number_of_expenses_posted = Expense.objects.filter( + accounting_export_summary__synced=True +).count() +print('Number of expenses posted to Fyle - {}'.format(number_of_expenses_posted)) +end_time = datetime.now() +print('Time taken - {}'.format(end_time - start_time)) + +# This query should return 0 rows +# select expense_group_id from task_logs where status not in ('ENQUEUED', 'IN_PROGRESS') and expense_group_id in (select expensegroup_id from expense_groups_expenses where expense_id in (select id from expenses where accounting_export_summary ='{}')); diff --git a/scripts/python/post-integrations.py b/scripts/python/post-integrations.py new file mode 100644 index 00000000..43c8292a --- /dev/null +++ b/scripts/python/post-integrations.py @@ -0,0 +1,12 @@ +from apps.workspaces.models import Workspace +from apps.workspaces.tasks import post_to_integration_settings + +workspaces = Workspace.objects.filter(onboarding_state='COMPLETE') + +for workspace in workspaces: + try: + print("Posting to integration settings for workspace: {}".format(workspace.id)) + post_to_integration_settings(workspace.id, True) + except Exception as e: + print("Error while posting to integration settings for workspace: {}".format(workspace.id)) + print(e) diff --git a/sql/scripts/021-fill-workspace-id-expenses.sql b/sql/scripts/021-fill-workspace-id-expenses.sql new file mode 100644 index 00000000..e59512be --- /dev/null +++ b/sql/scripts/021-fill-workspace-id-expenses.sql @@ -0,0 +1,11 @@ +rollback; +begin; + +with workspace as ( + select w.fyle_org_id, w.id, e.id as expense_pk from expenses e + join workspaces w on w.fyle_org_id = e.org_id +) +update expenses e +set workspace_id = w.id +from workspace w +where e.id = w.expense_pk; diff --git a/sql/scripts/022-fill-org-id-expenses.sql b/sql/scripts/022-fill-org-id-expenses.sql new file mode 100644 index 00000000..1df08fd6 --- /dev/null +++ b/sql/scripts/022-fill-org-id-expenses.sql @@ -0,0 +1,14 @@ +rollback; +begin; + +with expense_groups as ( + select w.fyle_org_id, e.id from expenses e + join expense_groups_expenses ege on e.id = ege.expense_id + join expense_groups eg on eg.id = ege.expensegroup_id + join workspaces w on w.id = eg.workspace_id + where e.org_id is null +) +update expenses e +set org_id = eg.fyle_org_id +from expense_groups eg +where e.id = eg.id; diff --git a/sql/scripts/023-fill-skipped-accounting-export-summary.sql b/sql/scripts/023-fill-skipped-accounting-export-summary.sql new file mode 100644 index 00000000..92672074 --- /dev/null +++ b/sql/scripts/023-fill-skipped-accounting-export-summary.sql @@ -0,0 +1,10 @@ +rollback; +begin; + +update expenses set accounting_export_summary = jsonb_build_object( + 'id', expense_id, + 'url', CONCAT('https://netsuite.fyleapps.com/workspaces/', workspace_id, '/expense_groups?page_number=0&page_size=10&state=SKIP'), + 'state', 'SKIPPED', + 'synced', false, + 'error_type', null +) where is_skipped = 't'; diff --git a/start_import_qcluster.sh b/start_import_qcluster.sh index 5bc0ffd8..cfad5e4c 100644 --- a/start_import_qcluster.sh +++ b/start_import_qcluster.sh @@ -3,4 +3,4 @@ # Creating the cache table python manage.py createcachetable --database cache_db -Q_CLUSTER_NAME=import python manage.py qcluster \ No newline at end of file +Q_CLUSTER_NAME=import python manage.py qcluster diff --git a/tests/sql_fixtures/reset_db_fixtures/reset_db.sql b/tests/sql_fixtures/reset_db_fixtures/reset_db.sql index 354f10ee..0c05291e 100644 --- a/tests/sql_fixtures/reset_db_fixtures/reset_db.sql +++ b/tests/sql_fixtures/reset_db_fixtures/reset_db.sql @@ -3,7 +3,7 @@ -- -- Dumped from database version 15.5 (Debian 15.5-1.pgdg120+1) --- Dumped by pg_dump version 15.5 (Debian 15.5-1.pgdg120+1) +-- Dumped by pg_dump version 15.6 (Debian 15.6-1.pgdg120+2) SET statement_timeout = 0; SET lock_timeout = 0; @@ -899,7 +899,10 @@ CREATE TABLE public.expenses ( tax_group_id character varying(255), billable boolean NOT NULL, employee_name character varying(255), - posted_at timestamp with time zone + posted_at timestamp with time zone, + accounting_export_summary jsonb NOT NULL, + previous_export_state character varying(255), + workspace_id integer ); @@ -2515,6 +2518,7 @@ COPY public.django_migrations (id, app, name, applied) FROM stdin; 142 django_q 0015_alter_schedule_schedule_type 2024-02-07 11:26:26.042653+00 143 django_q 0016_schedule_intended_date_kwarg 2024-02-07 11:26:26.046856+00 144 django_q 0017_task_cluster_alter 2024-02-07 11:26:26.055692+00 +145 fyle 0018_auto_20240213_0450 2024-02-13 04:54:22.357613+00 \. @@ -4747,17 +4751,17 @@ COPY public.expense_groups_expenses (id, expensegroup_id, expense_id) FROM stdin -- Data for Name: expenses; Type: TABLE DATA; Schema: public; Owner: postgres -- -COPY public.expenses (id, employee_email, category, sub_category, project, expense_id, expense_number, claim_number, amount, currency, foreign_amount, foreign_currency, settlement_id, reimbursable, state, vendor, cost_center, purpose, report_id, spent_at, approved_at, expense_created_at, expense_updated_at, created_at, updated_at, fund_source, verified_at, custom_properties, paid_on_xero, org_id, file_ids, corporate_card_id, tax_amount, tax_group_id, billable, employee_name, posted_at) FROM stdin; -1 ashwin.t@fyle.in Food \N \N txaaVBj3yKGW E/2022/06/T/4 C/2022/06/R/2 1 USD \N \N setrunCck8hLH t PAYMENT_PROCESSING \N \N \N rp9EvDF8Umk6 2022-06-27 17:00:00+00 2022-06-27 09:06:52.951+00 2022-06-27 09:06:13.135764+00 2022-06-27 09:08:23.340321+00 2022-08-02 20:26:22.81033+00 2022-08-02 20:26:22.810363+00 PERSONAL \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "Postman Field": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N -2 ashwin.t@fyle.in Food \N \N txB6D8k0Ws8a E/2022/06/T/2 C/2022/06/R/3 4 USD \N \N setrunCck8hLH t PAYMENT_PROCESSING \N \N \N rpNeZt3cv9wz 2022-06-27 17:00:00+00 2022-06-27 09:07:16.556+00 2022-06-27 09:05:45.738+00 2022-06-27 09:08:23.340321+00 2022-08-02 20:26:22.82716+00 2022-08-02 20:26:22.827194+00 PERSONAL \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "Postman Field": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N -3 sravan.kumar@fyle.in Food \N Bebe Rexha txGilVGolf60 E/2022/06/T/1 C/2022/06/R/1 10 USD \N \N setlpIUKpdvsT t PAYMENT_PROCESSING \N Adidas \N rpKuJtEv6h0n 2020-01-01 17:00:00+00 2022-06-08 04:28:30.61+00 2022-06-08 04:27:35.274447+00 2022-06-08 04:28:51.237261+00 2022-08-02 20:26:22.835984+00 2022-08-02 20:26:22.83608+00 PERSONAL \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N 1 tg0gTsClGjLp f \N \N -4 sravan.kumar@fyle.in Food \N \N txjIqTCtkkC8 E/2022/05/T/21 C/2022/05/R/18 100 USD \N \N set3ZMFXrDPL3 f PAYMENT_PROCESSING \N \N \N rpLawO11bFib 2022-05-25 17:00:00+00 2022-05-25 08:59:25.649+00 2022-05-25 08:59:07.718891+00 2022-05-25 09:04:05.66983+00 2022-08-02 20:26:22.844927+00 2022-08-02 20:26:22.844961+00 CCC \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N -5 sravan.kumar@fyle.in WIP \N Bebe Rexha txUPRc3VwxOP E/2022/05/T/19 C/2022/05/R/17 101 USD \N \N setb1pSLMIok8 f PAYMENT_PROCESSING \N Adidas \N rpv1txzAsgr3 2021-01-01 17:00:00+00 2022-05-25 07:24:12.987+00 2022-05-25 07:21:40.598113+00 2022-05-25 07:25:00.848892+00 2022-08-02 20:26:22.857516+00 2022-08-02 20:26:22.857675+00 CCC \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N -6 ashwin.t@fyle.in Food \N \N txUDvDmEV4ep E/2022/05/T/18 C/2022/05/R/16 5 USD \N \N set33iAVXO7BA t PAYMENT_PROCESSING \N \N \N rpE2JyATZhDe 2020-05-25 17:00:00+00 2022-05-25 06:05:23.362+00 2022-05-25 06:04:46.557927+00 2022-05-25 06:05:47.36985+00 2022-08-02 20:26:22.870854+00 2022-08-02 20:26:22.87089+00 PERSONAL \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N -7 sravan.kumar@fyle.in WIP \N Bebe Rexha tx1FW3uxYZG6 E/2022/05/T/16 C/2022/05/R/15 151 USD \N \N setzFn3FK5t80 f PAYMENT_PROCESSING \N Adidas \N rprwGgzOZyfR 2022-05-25 17:00:00+00 2022-05-25 03:41:49.042+00 2022-05-25 03:41:28.839711+00 2022-05-25 03:42:10.145663+00 2022-08-02 20:26:22.882803+00 2022-08-02 20:26:22.882836+00 CCC \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N -8 sravan.kumar@fyle.in WIP \N Bebe Rexha txVXhyVB8mgK E/2022/05/T/15 C/2022/05/R/14 45 USD \N \N setsN8cLD9KIn f PAYMENT_PROCESSING \N Adidas \N rpnG3lZYDsHU 2022-05-25 17:00:00+00 2022-05-25 02:48:53.791+00 2022-05-25 02:48:37.432989+00 2022-05-25 02:49:18.189037+00 2022-08-02 20:26:22.894793+00 2022-08-02 20:26:22.894827+00 CCC \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N -9 sravan.kumar@fyle.in WIP \N Bebe Rexha txBMQRkBQciI E/2022/05/T/14 C/2022/05/R/13 10 USD \N \N setanDKqMZfXB f PAYMENT_PROCESSING \N Adidas \N rpVvNQvE2wbm 2022-05-25 17:00:00+00 2022-05-25 02:38:40.858+00 2022-05-25 02:38:25.832419+00 2022-05-25 02:39:08.208877+00 2022-08-02 20:26:22.908632+00 2022-08-02 20:26:22.908661+00 CCC \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N -10 sravan.kumar@fyle.in WIP \N Bebe Rexha txkw3dt3umkN E/2022/05/T/12 C/2022/05/R/12 101 USD \N \N setBe6qAlNXPU f PAYMENT_PROCESSING \N Adidas \N rp5lITpxFLxE 2022-05-24 17:00:00+00 2022-05-24 15:59:13.26+00 2022-05-24 15:55:50.369024+00 2022-05-24 16:00:27.982+00 2022-08-02 20:26:22.921466+00 2022-08-02 20:26:22.9215+00 CCC \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N 1 tg0gTsClGjLp f \N \N +COPY public.expenses (id, employee_email, category, sub_category, project, expense_id, expense_number, claim_number, amount, currency, foreign_amount, foreign_currency, settlement_id, reimbursable, state, vendor, cost_center, purpose, report_id, spent_at, approved_at, expense_created_at, expense_updated_at, created_at, updated_at, fund_source, verified_at, custom_properties, paid_on_xero, org_id, file_ids, corporate_card_id, tax_amount, tax_group_id, billable, employee_name, posted_at, accounting_export_summary, previous_export_state, workspace_id) FROM stdin; +1 ashwin.t@fyle.in Food \N \N txaaVBj3yKGW E/2022/06/T/4 C/2022/06/R/2 1 USD \N \N setrunCck8hLH t PAYMENT_PROCESSING \N \N \N rp9EvDF8Umk6 2022-06-27 17:00:00+00 2022-06-27 09:06:52.951+00 2022-06-27 09:06:13.135764+00 2022-06-27 09:08:23.340321+00 2022-08-02 20:26:22.81033+00 2022-08-02 20:26:22.810363+00 PERSONAL \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "Postman Field": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N {} \N \N +2 ashwin.t@fyle.in Food \N \N txB6D8k0Ws8a E/2022/06/T/2 C/2022/06/R/3 4 USD \N \N setrunCck8hLH t PAYMENT_PROCESSING \N \N \N rpNeZt3cv9wz 2022-06-27 17:00:00+00 2022-06-27 09:07:16.556+00 2022-06-27 09:05:45.738+00 2022-06-27 09:08:23.340321+00 2022-08-02 20:26:22.82716+00 2022-08-02 20:26:22.827194+00 PERSONAL \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "Postman Field": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N {} \N \N +3 sravan.kumar@fyle.in Food \N Bebe Rexha txGilVGolf60 E/2022/06/T/1 C/2022/06/R/1 10 USD \N \N setlpIUKpdvsT t PAYMENT_PROCESSING \N Adidas \N rpKuJtEv6h0n 2020-01-01 17:00:00+00 2022-06-08 04:28:30.61+00 2022-06-08 04:27:35.274447+00 2022-06-08 04:28:51.237261+00 2022-08-02 20:26:22.835984+00 2022-08-02 20:26:22.83608+00 PERSONAL \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N 1 tg0gTsClGjLp f \N \N {} \N \N +4 sravan.kumar@fyle.in Food \N \N txjIqTCtkkC8 E/2022/05/T/21 C/2022/05/R/18 100 USD \N \N set3ZMFXrDPL3 f PAYMENT_PROCESSING \N \N \N rpLawO11bFib 2022-05-25 17:00:00+00 2022-05-25 08:59:25.649+00 2022-05-25 08:59:07.718891+00 2022-05-25 09:04:05.66983+00 2022-08-02 20:26:22.844927+00 2022-08-02 20:26:22.844961+00 CCC \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N {} \N \N +5 sravan.kumar@fyle.in WIP \N Bebe Rexha txUPRc3VwxOP E/2022/05/T/19 C/2022/05/R/17 101 USD \N \N setb1pSLMIok8 f PAYMENT_PROCESSING \N Adidas \N rpv1txzAsgr3 2021-01-01 17:00:00+00 2022-05-25 07:24:12.987+00 2022-05-25 07:21:40.598113+00 2022-05-25 07:25:00.848892+00 2022-08-02 20:26:22.857516+00 2022-08-02 20:26:22.857675+00 CCC \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N {} \N \N +6 ashwin.t@fyle.in Food \N \N txUDvDmEV4ep E/2022/05/T/18 C/2022/05/R/16 5 USD \N \N set33iAVXO7BA t PAYMENT_PROCESSING \N \N \N rpE2JyATZhDe 2020-05-25 17:00:00+00 2022-05-25 06:05:23.362+00 2022-05-25 06:04:46.557927+00 2022-05-25 06:05:47.36985+00 2022-08-02 20:26:22.870854+00 2022-08-02 20:26:22.87089+00 PERSONAL \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N {} \N \N +7 sravan.kumar@fyle.in WIP \N Bebe Rexha tx1FW3uxYZG6 E/2022/05/T/16 C/2022/05/R/15 151 USD \N \N setzFn3FK5t80 f PAYMENT_PROCESSING \N Adidas \N rprwGgzOZyfR 2022-05-25 17:00:00+00 2022-05-25 03:41:49.042+00 2022-05-25 03:41:28.839711+00 2022-05-25 03:42:10.145663+00 2022-08-02 20:26:22.882803+00 2022-08-02 20:26:22.882836+00 CCC \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N {} \N \N +8 sravan.kumar@fyle.in WIP \N Bebe Rexha txVXhyVB8mgK E/2022/05/T/15 C/2022/05/R/14 45 USD \N \N setsN8cLD9KIn f PAYMENT_PROCESSING \N Adidas \N rpnG3lZYDsHU 2022-05-25 17:00:00+00 2022-05-25 02:48:53.791+00 2022-05-25 02:48:37.432989+00 2022-05-25 02:49:18.189037+00 2022-08-02 20:26:22.894793+00 2022-08-02 20:26:22.894827+00 CCC \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N {} \N \N +9 sravan.kumar@fyle.in WIP \N Bebe Rexha txBMQRkBQciI E/2022/05/T/14 C/2022/05/R/13 10 USD \N \N setanDKqMZfXB f PAYMENT_PROCESSING \N Adidas \N rpVvNQvE2wbm 2022-05-25 17:00:00+00 2022-05-25 02:38:40.858+00 2022-05-25 02:38:25.832419+00 2022-05-25 02:39:08.208877+00 2022-08-02 20:26:22.908632+00 2022-08-02 20:26:22.908661+00 CCC \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N {} \N \N +10 sravan.kumar@fyle.in WIP \N Bebe Rexha txkw3dt3umkN E/2022/05/T/12 C/2022/05/R/12 101 USD \N \N setBe6qAlNXPU f PAYMENT_PROCESSING \N Adidas \N rp5lITpxFLxE 2022-05-24 17:00:00+00 2022-05-24 15:59:13.26+00 2022-05-24 15:55:50.369024+00 2022-05-24 16:00:27.982+00 2022-08-02 20:26:22.921466+00 2022-08-02 20:26:22.9215+00 CCC \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N 1 tg0gTsClGjLp f \N \N {} \N \N \. @@ -5036,7 +5040,7 @@ SELECT pg_catalog.setval('public.django_content_type_id_seq', 38, true); -- Name: django_migrations_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres -- -SELECT pg_catalog.setval('public.django_migrations_id_seq', 144, true); +SELECT pg_catalog.setval('public.django_migrations_id_seq', 145, true); -- @@ -6009,6 +6013,13 @@ CREATE INDEX expense_groups_workspace_id_21fcb4ac ON public.expense_groups USING CREATE INDEX expenses_expense_id_0e3511ea_like ON public.expenses USING btree (expense_id varchar_pattern_ops); +-- +-- Name: expenses_workspace_id_72fb819f; Type: INDEX; Schema: public; Owner: postgres +-- + +CREATE INDEX expenses_workspace_id_72fb819f ON public.expenses USING btree (workspace_id); + + -- -- Name: fyle_accounting_mappings_d_workspace_id_a6a3ab6a; Type: INDEX; Schema: public; Owner: postgres -- @@ -6359,6 +6370,14 @@ ALTER TABLE ONLY public.expense_groups ADD CONSTRAINT expense_groups_workspace_id_21fcb4ac_fk_workspaces_id FOREIGN KEY (workspace_id) REFERENCES public.workspaces(id) DEFERRABLE INITIALLY DEFERRED; +-- +-- Name: expenses expenses_workspace_id_72fb819f_fk_workspaces_id; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.expenses + ADD CONSTRAINT expenses_workspace_id_72fb819f_fk_workspaces_id FOREIGN KEY (workspace_id) REFERENCES public.workspaces(id) DEFERRABLE INITIALLY DEFERRED; + + -- -- Name: mappings fyle_accounting_mapp_destination_id_79497f6e_fk_fyle_acco; Type: FK CONSTRAINT; Schema: public; Owner: postgres -- @@ -6570,4 +6589,3 @@ ALTER TABLE ONLY public.xero_credentials -- -- PostgreSQL database dump complete -- - diff --git a/tests/test_fyle/test_actions.py b/tests/test_fyle/test_actions.py new file mode 100644 index 00000000..65a216dd --- /dev/null +++ b/tests/test_fyle/test_actions.py @@ -0,0 +1,149 @@ +from unittest import mock + +from django.conf import settings +from fyle.platform.exceptions import InternalServerError, RetryException, WrongParamsError +from fyle_integrations_platform_connector import PlatformConnector + +from apps.fyle.actions import ( + bulk_post_accounting_export_summary, + create_generator_and_post_in_batches, + mark_accounting_export_summary_as_synced, + update_complete_expenses, + update_expenses_in_progress, + update_failed_expenses, +) +from apps.fyle.helpers import get_updated_accounting_export_summary +from apps.fyle.models import Expense +from apps.workspaces.models import FyleCredential + + +def test_update_expenses_in_progress(db): + expenses = Expense.objects.filter(org_id='or79Cob97KSh') + update_expenses_in_progress(expenses) + + expenses = Expense.objects.filter(org_id='or79Cob97KSh') + + for expense in expenses: + assert expense.accounting_export_summary['synced'] == False + assert expense.accounting_export_summary['state'] == 'IN_PROGRESS' + assert expense.accounting_export_summary['url'] == '{}/workspaces/main/dashboard'.format( + settings.XERO_INTEGRATION_APP_URL + ) + assert expense.accounting_export_summary['error_type'] == None + assert expense.accounting_export_summary['id'] == expense.expense_id + + +def test_update_failed_expenses(db): + expenses = Expense.objects.filter(org_id='or79Cob97KSh') + update_failed_expenses(expenses, True) + + expenses = Expense.objects.filter(org_id='or79Cob97KSh') + + for expense in expenses: + assert expense.accounting_export_summary['synced'] == False + assert expense.accounting_export_summary['state'] == 'ERROR' + assert expense.accounting_export_summary['error_type'] == 'MAPPING' + assert expense.accounting_export_summary['url'] == '{}/workspaces/main/dashboard'.format( + settings.XERO_INTEGRATION_APP_URL + ) + assert expense.accounting_export_summary['id'] == expense.expense_id + + +def test_update_complete_expenses(db): + expenses = Expense.objects.filter(org_id='or79Cob97KSh') + + update_complete_expenses(expenses, 'https://intacct.google.com') + + expenses = Expense.objects.filter(org_id='or79Cob97KSh') + + for expense in expenses: + assert expense.accounting_export_summary['synced'] == False + assert expense.accounting_export_summary['state'] == 'COMPLETE' + assert expense.accounting_export_summary['error_type'] == None + assert expense.accounting_export_summary['url'] == 'https://intacct.google.com' + assert expense.accounting_export_summary['id'] == expense.expense_id + + +def test_create_generator_and_post_in_batches(db): + fyle_credentails = FyleCredential.objects.get(workspace_id=1) + platform = PlatformConnector(fyle_credentails) + + with mock.patch('fyle.platform.apis.v1beta.admin.Expenses.post_bulk_accounting_export_summary') as mock_call: + mock_call.side_effect = RetryException('Timeout') + try: + create_generator_and_post_in_batches([{ + 'id': 'txxTi9ZfdepC' + }], platform, 1) + + # Exception should be handled + assert True + except RetryException: + # This should not be reached + assert False + + +def test_handle_post_accounting_export_summary_exception(db): + fyle_credentails = FyleCredential.objects.get(workspace_id=1) + platform = PlatformConnector(fyle_credentails) + expense = Expense.objects.filter(org_id='orPJvXuoLqvJ').first() + expense.workspace_id = 1 + expense.save() + + expense_id = expense.expense_id + + with mock.patch('fyle.platform.apis.v1beta.admin.Expenses.post_bulk_accounting_export_summary') as mock_call: + mock_call.side_effect = WrongParamsError('Some of the parameters are wrong', { + 'data': [ + { + 'message': 'Permission denied to perform this action.', + 'key': expense_id + } + ] + }) + create_generator_and_post_in_batches([{ + 'id': expense_id + }], platform, 1) + + expense = Expense.objects.get(expense_id=expense_id) + + assert expense.accounting_export_summary['synced'] == True + assert expense.accounting_export_summary['state'] == 'DELETED' + assert expense.accounting_export_summary['error_type'] == None + assert expense.accounting_export_summary['url'] == '{}/workspaces/main/dashboard'.format( + settings.XERO_INTEGRATION_APP_URL + ) + assert expense.accounting_export_summary['id'] == expense_id + + +def test_mark_accounting_export_summary_as_synced(db): + expenses = Expense.objects.filter(org_id='or79Cob97KSh') + for expense in expenses: + expense.accounting_export_summary = get_updated_accounting_export_summary( + 'tx_123', + 'SKIPPED', + None, + '{}/workspaces/main/export_log'.format(settings.XERO_INTEGRATION_APP_URL), + True + ) + expense.save() + + expenses = Expense.objects.filter(org_id='or79Cob97KSh') + + mark_accounting_export_summary_as_synced(expenses) + + expenses = Expense.objects.filter(org_id='or79Cob97KSh') + + for expense in expenses: + assert expense.accounting_export_summary['synced'] == True + + +def test_bulk_post_accounting_export_summary(db): + fyle_credentails = FyleCredential.objects.get(workspace_id=1) + platform = PlatformConnector(fyle_credentails) + + with mock.patch('fyle.platform.apis.v1beta.admin.Expenses.post_bulk_accounting_export_summary') as mock_call: + mock_call.side_effect = InternalServerError('Timeout') + try: + bulk_post_accounting_export_summary(platform, {}) + except RetryException: + assert mock_call.call_count == 3 diff --git a/tests/test_fyle/test_helpers.py b/tests/test_fyle/test_helpers.py index 396764ec..e7be57fc 100644 --- a/tests/test_fyle/test_helpers.py +++ b/tests/test_fyle/test_helpers.py @@ -1,9 +1,12 @@ from asyncio.log import logger +from django.conf import settings from rest_framework.response import Response from rest_framework.views import status -from apps.fyle.helpers import get_fyle_orgs, get_request, post_request +from apps.fyle.actions import __bulk_update_expenses +from apps.fyle.helpers import get_fyle_orgs, get_request, get_updated_accounting_export_summary, post_request +from apps.fyle.models import Expense def test_post_request(mocker): @@ -59,3 +62,29 @@ def test_get_fyle_orgs(mocker): get_fyle_orgs(refresh_token="srtyu", cluster_domain="erty") except Exception: logger.info("Error in post request") + + +def test_bulk_update_expenses(db): + expenses = Expense.objects.filter(org_id='or79Cob97KSh') + for expense in expenses: + expense.accounting_export_summary = get_updated_accounting_export_summary( + expense.expense_id, + 'SKIPPED', + None, + '{}/workspaces/main/export_log'.format(settings.XERO_INTEGRATION_APP_URL), + True + ) + expense.save() + + __bulk_update_expenses(expenses) + + expenses = Expense.objects.filter(org_id='or79Cob97KSh') + + for expense in expenses: + assert expense.accounting_export_summary['synced'] == True + assert expense.accounting_export_summary['state'] == 'SKIPPED' + assert expense.accounting_export_summary['error_type'] == None + assert expense.accounting_export_summary['url'] == '{}/workspaces/main/export_log'.format( + settings.XERO_INTEGRATION_APP_URL + ) + assert expense.accounting_export_summary['id'] == expense.expense_id diff --git a/tests/test_fyle/test_queue.py b/tests/test_fyle/test_queue.py new file mode 100644 index 00000000..81a2ea24 --- /dev/null +++ b/tests/test_fyle/test_queue.py @@ -0,0 +1,44 @@ +from apps.fyle.models import Expense +from apps.fyle.queue import async_import_and_export_expenses, async_post_accounting_export_summary +from apps.workspaces.models import FyleCredential, XeroCredentials +from apps.xero.queue import __create_chain_and_run +from apps.xero.utils import XeroConnector + + +# This test is just for cov :D +def test_async_post_accounting_export_summary(db): + async_post_accounting_export_summary(1, 1) + assert True + + +# This test is just for cov :D +def test_create_chain_and_run(db): + workspace_id = 1 + fyle_credentials = FyleCredential.objects.get(workspace_id=workspace_id) + in_progress_expenses = Expense.objects.filter(org_id='or79Cob97KSh') + xero_credentials = XeroCredentials.get_active_xero_credentials(workspace_id) + xero_connection = XeroConnector(xero_credentials, workspace_id) + chain_tasks = [ + { + 'target': 'apps.sage_intacct.tasks.create_bill', + 'expense_group_id': 1, + 'task_log_id': 1, + 'last_export': True + } + ] + + __create_chain_and_run(fyle_credentials, xero_connection, in_progress_expenses, workspace_id, chain_tasks, 'PERSONAL') + assert True + + +# This test is just for cov :D +def test_async_import_and_export_expenses(db): + body = { + 'action': 'ACCOUNTING_EXPORT_INITIATED', + 'data': { + 'id': 'rp1s1L3QtMpF', + 'org_id': 'or79Cob97KSh' + } + } + + async_import_and_export_expenses(body) diff --git a/tests/test_fyle/test_task.py b/tests/test_fyle/test_task.py index a936d901..ff35f1f0 100644 --- a/tests/test_fyle/test_task.py +++ b/tests/test_fyle/test_task.py @@ -2,8 +2,9 @@ from fyle.platform.exceptions import InvalidTokenError as FyleInvalidTokenError -from apps.fyle.models import ExpenseGroupSettings -from apps.fyle.tasks import create_expense_groups +from apps.fyle.actions import update_expenses_in_progress +from apps.fyle.models import Expense, ExpenseGroup, ExpenseGroupSettings +from apps.fyle.tasks import create_expense_groups, post_accounting_export_summary from apps.tasks.models import TaskLog from apps.workspaces.models import FyleCredential from tests.test_fyle.fixtures import data @@ -60,3 +61,25 @@ def test_create_expense_groups(mocker, db): task_log = TaskLog.objects.get(id=task_log.id) assert task_log.status == "FATAL" + + +def test_post_accounting_export_summary(db, mocker): + expense_group = ExpenseGroup.objects.filter(workspace_id=1).first() + expense = expense_group.expenses.first() + expense_group.expenses.remove(expense.id) + + expense = Expense.objects.filter(id=expense.id).first() + expense.workspace_id = 1 + expense.save() + + update_expenses_in_progress([expense]) + + assert Expense.objects.filter(id=expense.id).first().accounting_export_summary['synced'] == False + + mocker.patch( + 'fyle_integrations_platform_connector.apis.Expenses.post_bulk_accounting_export_summary', + return_value=[] + ) + post_accounting_export_summary('orPJvXuoLqvJ', 1) + + assert Expense.objects.filter(id=expense.id).first().accounting_export_summary['synced'] == True diff --git a/tests/test_workspaces/test_apis/test_advanced_settings/test_views.py b/tests/test_workspaces/test_apis/test_advanced_settings/test_views.py index a6b69b67..3f0391f0 100644 --- a/tests/test_workspaces/test_apis/test_advanced_settings/test_views.py +++ b/tests/test_workspaces/test_apis/test_advanced_settings/test_views.py @@ -1,11 +1,18 @@ import json -from apps.workspaces.models import Workspace, WorkspaceGeneralSettings +from apps.workspaces.models import FyleCredential, Workspace, WorkspaceGeneralSettings from tests.helper import dict_compare_keys from tests.test_workspaces.test_apis.test_advanced_settings.fixtures import data def test_advanced_settings(api_client, test_connection): + FyleCredential.objects.update_or_create( + workspace_id=1, + defaults={ + 'refresh_token': 'ey.ey.ey', + 'cluster_domain': 'cluster_domain' + } + ) workspace = Workspace.objects.get(id=1) workspace.onboarding_state = "ADVANCED_SETTINGS" workspace.save() diff --git a/tests/test_workspaces/test_tasks.py b/tests/test_workspaces/test_tasks.py index 4c0c6743..85773779 100644 --- a/tests/test_workspaces/test_tasks.py +++ b/tests/test_workspaces/test_tasks.py @@ -9,11 +9,12 @@ from apps.workspaces.queue import schedule_sync from apps.workspaces.tasks import ( async_add_admins_to_workspace, + async_create_admin_subcriptions, async_update_fyle_credentials, async_update_workspace_name, + post_to_integration_settings, run_email_notification, run_sync_schedule, - async_create_admin_subcriptions ) from tests.test_fyle.fixtures import data as fyle_data from tests.test_workspaces.fixtures import data @@ -178,3 +179,17 @@ def test_async_create_admin_subcriptions(db, mocker): return_value={} ) async_create_admin_subcriptions(1) + + +@pytest.mark.django_db(databases=['default']) +def test_post_to_integration_settings(mocker): + mocker.patch( + 'apps.fyle.helpers.post_request', + return_value='' + ) + + no_exception = True + post_to_integration_settings(1, True) + + # If exception is raised, this test will fail + assert no_exception diff --git a/tests/test_xero/test_tasks.py b/tests/test_xero/test_tasks.py index e818bd30..293b0864 100644 --- a/tests/test_xero/test_tasks.py +++ b/tests/test_xero/test_tasks.py @@ -13,22 +13,25 @@ from apps.workspaces.models import LastExportDetail, WorkspaceGeneralSettings, XeroCredentials from apps.xero.exceptions import update_last_export_details from apps.xero.models import BankTransaction, BankTransactionLineItem, Bill, BillLineItem -from apps.xero.queue import schedule_payment_creation, schedule_reimbursements_sync, schedule_xero_objects_status_sync +from apps.xero.queue import ( + schedule_bank_transaction_creation, + schedule_bills_creation, + schedule_payment_creation, + schedule_reimbursements_sync, + schedule_xero_objects_status_sync, +) from apps.xero.tasks import ( __validate_expense_group, attach_customer_to_export, check_xero_object_status, create_bank_transaction, create_bill, - create_chain_and_export, create_missing_currency, create_or_update_employee_mapping, create_payment, get_or_create_credit_card_contact, load_attachments, process_reimbursements, - schedule_bank_transaction_creation, - schedule_bills_creation, update_xero_short_code, ) from apps.xero.utils import XeroConnector @@ -412,10 +415,9 @@ def test_schedule_bills_creation(db): task_log.status = "READY" task_log.save() - chaining_attributes = schedule_bills_creation( - workspace_id=workspace_id, expense_group_ids=[4] + schedule_bills_creation( + workspace_id=workspace_id, expense_group_ids=[4], is_auto_export=False, fund_source="PERSONAL" ) - assert len(chaining_attributes) == 1 def test_post_create_bank_transaction_success(mocker, db): @@ -511,10 +513,9 @@ def test_schedule_bank_transaction_creation(db): task_log.status = "READY" task_log.save() - chaining_attributes = schedule_bank_transaction_creation( - workspace_id=workspace_id, expense_group_ids=[5] + schedule_bank_transaction_creation( + workspace_id=workspace_id, expense_group_ids=[5], is_auto_export=False, fund_source="CCC" ) - assert len(chaining_attributes) == 1 def test_create_bank_transactions_exceptions(db): @@ -932,20 +933,6 @@ def test_update_xero_short_code(db, mocker): update_xero_short_code(workspace_id) -def test_create_chain_and_export(db): - workspace_id = 1 - - chaining_attributes = [ - { - "expense_group_id": 4, - "export_type": "bill", - "task_log_id": 3, - "last_export": False, - } - ] - create_chain_and_export(chaining_attributes, workspace_id) - - def test_update_last_export_details(db): workspace_id = 1