Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/fylein/fyle-qbo-api into …
Browse files Browse the repository at this point in the history
…merchant-name-j3-fix
  • Loading branch information
NileshPant1999 committed Sep 27, 2023
2 parents 9effee3 + 39a14bd commit bb15465
Show file tree
Hide file tree
Showing 47 changed files with 1,353 additions and 465 deletions.
1 change: 1 addition & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ max-line-length = 99
max-complexity = 19
ban-relative-imports = true
select = B,C,E,F,N,W,I25
exclude=*env
2 changes: 1 addition & 1 deletion .github/workflows/production_deployment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,4 @@ jobs:
sentry-cli releases finalize $SENTRY_RELEASE
# Create new deploy for this Sentry release
sentry-cli releases deploys $SENTRY_RELEASE new -e $SENTRY_DEPLOY_ENVIRONMENT
sentry-cli releases deploys $SENTRY_RELEASE new -e $SENTRY_DEPLOY_ENVIRONMENT
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ share/python-wheels/
.installed.cfg
*.egg
MANIFEST
fyle_integrations_platform_connector/

# PyInstaller
# Usually these files are written by a python script from a template
Expand Down
152 changes: 151 additions & 1 deletion apps/fyle/actions.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
from datetime import datetime, timezone
from typing import List

from django.conf import settings
from django.db.models import Q

from fyle_integrations_platform_connector import PlatformConnector
from fyle_accounting_mappings.models import ExpenseAttribute

from apps.fyle.constants import DEFAULT_FYLE_CONDITIONS
from apps.fyle.models import ExpenseAttribute, ExpenseGroup
from apps.fyle.models import ExpenseGroup, Expense
from apps.workspaces.models import FyleCredential, Workspace, WorkspaceGeneralSettings

from .helpers import get_updated_accounting_export_summary


def get_expense_group_ids(workspace_id: int):
configuration = WorkspaceGeneralSettings.objects.get(workspace_id=workspace_id)
Expand Down Expand Up @@ -74,3 +80,147 @@ def get_custom_fields(workspace_id: int):
if custom_field['type'] in ('SELECT', 'NUMBER', 'TEXT'):
response.append({'field_name': custom_field['field_name'], 'type': custom_field['type'], 'is_custom': custom_field['is_custom']})
return response


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, ['is_skipped', '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.QBO_INTEGRATION_APP_URL),
False
)
)
)

__bulk_update_expenses(expense_to_be_updated)


def mark_expenses_as_skipped(final_query: Q, expenses_object_ids: List, workspace: Workspace) -> None:
"""
Mark expenses as skipped in bulk
:param final_query: final query
:param expenses_object_ids: expenses object ids
:param workspace: workspace object
:return: None
"""
# We'll iterate through the list of expenses to be skipped, construct accounting export summary and update expenses
expense_to_be_updated = []
expenses_to_be_skipped = Expense.objects.filter(
final_query,
id__in=expenses_object_ids,
expensegroup__isnull=True,
org_id=workspace.fyle_org_id
)

for expense in expenses_to_be_skipped:
expense_to_be_updated.append(
Expense(
id=expense.id,
is_skipped=True,
accounting_export_summary=get_updated_accounting_export_summary(
expense.expense_id,
'SKIPPED',
None,
'{}/workspaces/main/export_log'.format(settings.QBO_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.QBO_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)
24 changes: 22 additions & 2 deletions apps/fyle/helpers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import json
from typing import List
from typing import List, Union

import requests
from django.conf import settings
Expand All @@ -22,7 +22,7 @@ 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)
Expand Down Expand Up @@ -150,3 +150,23 @@ def construct_expense_filter(expense_filter):

# Return the constructed expense filter
return constructed_expense_filter


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
}
18 changes: 18 additions & 0 deletions apps/fyle/migrations/0032_expense_accounting_export_summary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.1.14 on 2023-09-01 13:36

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('fyle', '0031_auto_20230801_1215'),
]

operations = [
migrations.AddField(
model_name='expense',
name='accounting_export_summary',
field=models.JSONField(default=dict),
),
]
20 changes: 20 additions & 0 deletions apps/fyle/migrations/0033_expense_workspace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 3.1.14 on 2023-09-05 12:17

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('workspaces', '0042_workspacegeneralsettings_name_in_journal_entry'),
('fyle', '0032_expense_accounting_export_summary'),
]

operations = [
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'),
),
]
18 changes: 18 additions & 0 deletions apps/fyle/migrations/0034_expense_previous_export_state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.1.14 on 2023-09-07 10:17

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('fyle', '0033_expense_workspace'),
]

operations = [
migrations.AddField(
model_name='expense',
name='previous_export_state',
field=models.CharField(help_text='Previous export state', max_length=255, null=True),
),
]
31 changes: 26 additions & 5 deletions apps/fyle/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Fyle Models
"""
from collections import defaultdict
from datetime import datetime
from typing import Dict, List

Expand Down Expand Up @@ -99,9 +100,14 @@ class Expense(models.Model):
fund_source = models.CharField(max_length=255, help_text='Expense fund source')
verified_at = models.DateTimeField(help_text='Report verified at', null=True)
custom_properties = JSONField(null=True)
previous_export_state = models.CharField(max_length=255, help_text='Previous export state', null=True)
accounting_export_summary = JSONField(default=dict)
paid_on_qbo = models.BooleanField(help_text='Expense Payment status on QBO', default=False)
payment_number = models.CharField(max_length=55, help_text='Expense payment number', null=True)
is_skipped = models.BooleanField(null=True, default=False, help_text='Expense is skipped or not')
workspace = models.ForeignKey(
Workspace, on_delete=models.PROTECT, help_text='To which workspace this expense belongs to', null=True
)

class Meta:
db_table = 'expenses'
Expand Down Expand Up @@ -154,6 +160,7 @@ def create_expense_objects(expenses: List[Dict], workspace_id: int):
'verified_at': expense['verified_at'],
'custom_properties': expense['custom_properties'],
'payment_number': expense['payment_number'],
'workspace_id': workspace_id
},
)

Expand Down Expand Up @@ -324,12 +331,26 @@ def create_expense_groups_by_report_id_fund_source(expense_objects: List[Expense

if general_settings.reimbursable_expenses_object == 'EXPENSE' and 'expense_id' not in reimbursable_expense_group_fields:
total_amount = 0
for expense in reimbursable_expenses:
total_amount += expense.amount

if total_amount < 0:
reimbursable_expenses = list(filter(lambda expense: expense.amount > 0, reimbursable_expenses))
if 'spent_at' in reimbursable_expense_group_fields:
grouped_data = defaultdict(list)
for expense in reimbursable_expenses:
spent_at = expense.spent_at
grouped_data[spent_at].append(expense)
grouped_expenses = list(grouped_data.values())
reimbursable_expenses = []
for expense_group in grouped_expenses:
total_amount = 0
for expense in expense_group:
total_amount += expense.amount
if total_amount < 0:
expense_group = list(filter(lambda expense: expense.amount > 0, expense_group))
reimbursable_expenses.extend(expense_group)
else:
for expense in reimbursable_expenses:
total_amount += expense.amount

if total_amount < 0:
reimbursable_expenses = list(filter(lambda expense: expense.amount > 0, reimbursable_expenses))
elif general_settings.reimbursable_expenses_object != 'JOURNAL ENTRY':
reimbursable_expenses = list(filter(lambda expense: expense.amount > 0, reimbursable_expenses))

Expand Down
12 changes: 12 additions & 0 deletions apps/fyle/queue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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)
Loading

0 comments on commit bb15465

Please sign in to comment.