-
Notifications
You must be signed in to change notification settings - Fork 1
feat: Real-time exports, phase 2 - Intacct #695
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,7 @@ | |
InternalServerError, | ||
InvalidTokenError | ||
) | ||
from fyle_accounting_library.fyle_platform.branding import feature_configuration | ||
from fyle_accounting_library.fyle_platform.helpers import get_expense_import_states, filter_expenses_based_on_state | ||
from fyle_accounting_library.fyle_platform.enums import ExpenseImportSourceEnum | ||
|
||
|
@@ -22,7 +23,8 @@ | |
LastExportDetail, | ||
Workspace, | ||
FyleCredential, | ||
Configuration | ||
Configuration, | ||
WorkspaceSchedule | ||
) | ||
from apps.fyle.models import ( | ||
Expense, | ||
|
@@ -34,7 +36,8 @@ | |
get_fund_source, | ||
get_source_account_type, | ||
handle_import_exception, | ||
construct_expense_filter_query | ||
construct_expense_filter_query, | ||
update_task_log_post_import | ||
) | ||
from apps.fyle.actions import ( | ||
mark_expenses_as_skipped, | ||
|
@@ -86,7 +89,7 @@ def schedule_expense_group_creation(workspace_id: int) -> None: | |
async_task('apps.fyle.tasks.create_expense_groups', workspace_id, fund_source, task_log) | ||
|
||
|
||
def create_expense_groups(workspace_id: int, fund_source: list[str], task_log: TaskLog, imported_from: ExpenseImportSourceEnum) -> None: | ||
def create_expense_groups(workspace_id: int, fund_source: list[str], task_log: TaskLog | None, imported_from: ExpenseImportSourceEnum) -> None: | ||
""" | ||
Create expense groups | ||
:param task_log: Task log object | ||
|
@@ -97,8 +100,8 @@ def create_expense_groups(workspace_id: int, fund_source: list[str], task_log: T | |
with transaction.atomic(): | ||
workspace = Workspace.objects.get(pk=workspace_id) | ||
|
||
last_synced_at = workspace.last_synced_at | ||
ccc_last_synced_at = workspace.ccc_last_synced_at | ||
last_synced_at = workspace.last_synced_at if imported_from != ExpenseImportSourceEnum.CONFIGURATION_UPDATE else None | ||
ccc_last_synced_at = workspace.ccc_last_synced_at if imported_from != ExpenseImportSourceEnum.CONFIGURATION_UPDATE else None | ||
fyle_credentials = FyleCredential.objects.get(workspace_id=workspace_id) | ||
|
||
expense_group_settings = ExpenseGroupSettings.objects.get(workspace_id=workspace_id) | ||
|
@@ -140,61 +143,38 @@ def create_expense_groups(workspace_id: int, fund_source: list[str], task_log: T | |
if workspace.ccc_last_synced_at or len(expenses) != reimbursable_expense_count: | ||
workspace.ccc_last_synced_at = datetime.now() | ||
|
||
workspace.save() | ||
if imported_from != ExpenseImportSourceEnum.CONFIGURATION_UPDATE: | ||
workspace.save() | ||
|
||
group_expenses_and_save(expenses, task_log, workspace, imported_from=imported_from) | ||
|
||
except NoPrivilegeError: | ||
logger.info('Invalid Fyle Credentials / Admin is disabled') | ||
task_log.detail = { | ||
'message': 'Invalid Fyle Credentials / Admin is disabled' | ||
} | ||
task_log.status = 'FAILED' | ||
task_log.save() | ||
update_task_log_post_import(task_log, 'FAILED', message='Invalid Fyle Credentials / Admin is disabled') | ||
|
||
except FyleCredential.DoesNotExist: | ||
logger.info('Fyle credentials not found %s', workspace_id) | ||
task_log.detail = { | ||
'message': 'Fyle credentials do not exist in workspace' | ||
} | ||
task_log.status = 'FAILED' | ||
task_log.save() | ||
update_task_log_post_import(task_log, 'FAILED', message='Fyle credentials do not exist in workspace') | ||
|
||
except RetryException: | ||
logger.info('Fyle Retry Exception occured in workspace_id: %s', workspace_id) | ||
task_log.detail = { | ||
'message': 'Fyle Retry Exception occured' | ||
} | ||
task_log.status = 'FATAL' | ||
task_log.save() | ||
update_task_log_post_import(task_log, 'FATAL', message='Fyle Retry Exception occured') | ||
|
||
except InvalidTokenError: | ||
logger.info('Invalid Token for Fyle') | ||
task_log.detail = { | ||
'message': 'Invalid Token for Fyle' | ||
} | ||
task_log.status = 'FAILED' | ||
task_log.save() | ||
update_task_log_post_import(task_log, 'FAILED', message='Invalid Token for Fyle') | ||
|
||
except InternalServerError: | ||
logger.info('Fyle Internal Server Error occured in workspace_id: %s', workspace_id) | ||
task_log.detail = { | ||
'message': 'Fyle Internal Server Error occured' | ||
} | ||
task_log.status = 'FAILED' | ||
task_log.save() | ||
update_task_log_post_import(task_log, 'FAILED', message='Fyle Internal Server Error occured') | ||
|
||
except Exception: | ||
error = traceback.format_exc() | ||
task_log.detail = { | ||
'error': error | ||
} | ||
task_log.status = 'FATAL' | ||
task_log.save() | ||
update_task_log_post_import(task_log, 'FATAL', error=error) | ||
logger.exception('Something unexpected happened workspace_id: %s %s', task_log.workspace_id, task_log.detail) | ||
|
||
|
||
def group_expenses_and_save(expenses: list[dict], task_log: TaskLog, workspace: Workspace, imported_from: ExpenseImportSourceEnum = None) -> None: | ||
def group_expenses_and_save(expenses: list[dict], task_log: TaskLog | None, workspace: Workspace, imported_from: ExpenseImportSourceEnum = None) -> None: | ||
""" | ||
Group expenses and save | ||
:param expenses: Expenses | ||
|
@@ -237,8 +217,10 @@ def group_expenses_and_save(expenses: list[dict], task_log: TaskLog, workspace: | |
except Exception: | ||
logger.error('Error posting accounting export summary for workspace_id: %s', workspace.id) | ||
|
||
task_log.status = 'COMPLETE' | ||
task_log.save() | ||
if task_log: | ||
task_log.status = 'COMPLETE' | ||
task_log.updated_at = datetime.now() | ||
task_log.save(update_fields=['status', 'updated_at']) | ||
|
||
|
||
def import_and_export_expenses(report_id: str, org_id: str, is_state_change_event: bool, report_state: str = None, imported_from: ExpenseImportSourceEnum = None) -> None: | ||
|
@@ -282,8 +264,17 @@ def import_and_export_expenses(report_id: str, org_id: str, is_state_change_even | |
expense_groups = ExpenseGroup.objects.filter(expenses__id__in=[expense_ids], workspace_id=workspace.id, exported_at__isnull=True).distinct('id').values('id') | ||
expense_group_ids = [expense_group['id'] for expense_group in expense_groups] | ||
|
||
if len(expense_group_ids) and not is_state_change_event: | ||
export_to_intacct(workspace.id, None, expense_group_ids, triggered_by=imported_from) | ||
if len(expense_group_ids): | ||
if is_state_change_event: | ||
# Trigger export immediately for customers who have enabled real time export | ||
is_real_time_export_enabled = WorkspaceSchedule.objects.filter(workspace_id=workspace.id, is_real_time_export_enabled=True).exists() | ||
|
||
# Don't allow real time export if it's not supported for the branded app / setting not enabled | ||
if not is_real_time_export_enabled or not feature_configuration.feature.real_time_export_1hr_orgs: | ||
return | ||
|
||
logger.info(f'Exporting expenses for workspace {workspace.id} with expense group ids {expense_group_ids}, triggered by {imported_from}') | ||
Comment on lines
+267
to
+276
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Feature-flag guard bypasses export for unsupported apps but leaves task log COMPLETE When 🤖 Prompt for AI Agents
|
||
export_to_intacct(workspace_id=workspace.id, expense_group_ids=expense_group_ids, triggered_by=imported_from) | ||
|
||
except Configuration.DoesNotExist: | ||
logger.info('Configuration does not exist for workspace_id: %s', workspace.id) | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -19,7 +19,7 @@ | |||||||||||||||||||
logger.level = logging.INFO | ||||||||||||||||||||
|
||||||||||||||||||||
|
||||||||||||||||||||
def export_to_intacct(workspace_id: int, export_mode: bool = None, expense_group_ids: list = [], triggered_by: ExpenseImportSourceEnum = None) -> None: | ||||||||||||||||||||
def export_to_intacct(workspace_id: int, expense_group_ids: list = [], triggered_by: ExpenseImportSourceEnum = None) -> None: | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replace mutable default argument with None The function signature uses a mutable default argument ( -def export_to_intacct(workspace_id: int, expense_group_ids: list = [], triggered_by: ExpenseImportSourceEnum = None) -> None:
+def export_to_intacct(workspace_id: int, expense_group_ids: list = None, triggered_by: ExpenseImportSourceEnum = None) -> None: Then initialize the list inside the function: + if expense_group_ids is None:
+ expense_group_ids = [] 📝 Committable suggestion
Suggested change
🧰 Tools🪛 Ruff (0.11.9)22-22: Do not use mutable data structures for argument defaults Replace with (B006) 🤖 Prompt for AI Agents
|
||||||||||||||||||||
""" | ||||||||||||||||||||
Export expenses to Intacct | ||||||||||||||||||||
:param workspace_id: Workspace ID | ||||||||||||||||||||
|
@@ -33,7 +33,7 @@ def export_to_intacct(workspace_id: int, export_mode: bool = None, expense_group | |||||||||||||||||||
|
||||||||||||||||||||
last_exported_at = datetime.now() | ||||||||||||||||||||
is_expenses_exported = False | ||||||||||||||||||||
export_mode = export_mode or 'MANUAL' | ||||||||||||||||||||
export_mode = 'MANUAL' if triggered_by in (ExpenseImportSourceEnum.DASHBOARD_SYNC, ExpenseImportSourceEnum.DIRECT_EXPORT, ExpenseImportSourceEnum.CONFIGURATION_UPDATE) else 'AUTO' | ||||||||||||||||||||
expense_group_filters = { | ||||||||||||||||||||
'exported_at__isnull': True, | ||||||||||||||||||||
'workspace_id': workspace_id | ||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# Generated by Django 4.2.21 on 2025-05-21 17:04 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('workspaces', '0044_configuration_je_single_credit_line'), | ||
] | ||
|
||
operations = [ | ||
migrations.AddField( | ||
model_name='workspaceschedule', | ||
name='is_real_time_export_enabled', | ||
field=models.BooleanField(default=False), | ||
), | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Incorrect wrapper causes
id__in
lookup to receive a list of QuerySetsExpenseGroup.objects.filter(expenses__id__in=[expense_ids], …)
wraps the QuerySet in an additional list, producing[<QuerySet …>]
, which Django will attempt to coerce into ints and fail.📝 Committable suggestion
🤖 Prompt for AI Agents