Skip to content
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

Add loggers and use decorators for exceptions #93

Merged
merged 20 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 4.1.2 on 2023-11-28 09:49

from django.db import migrations
import sage_desktop_api.models.fields


class Migration(migrations.Migration):

dependencies = [
('accounting_exports', '0001_initial'),
]

operations = [
migrations.RenameField(
model_name='accountingexport',
old_name='sage_300_errors',
new_name='sage300_errors',
),
migrations.AddField(
model_name='accountingexport',
name='export_id',
field=sage_desktop_api.models.fields.StringNullField(help_text='id of the exported expense', max_length=255, null=True),
),
]
6 changes: 4 additions & 2 deletions apps/accounting_exports/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ class AccountingExport(BaseForeignWorkspaceModel):
description = CustomJsonField(help_text='Description')
status = StringNotNullField(help_text='Task Status')
detail = CustomJsonField(help_text='Task Response')
sage_300_errors = CustomJsonField(help_text='Sage 300 Errors')
sage300_errors = CustomJsonField(help_text='Sage 300 Errors')
export_id = StringNullField(help_text='id of the exported expense')
exported_at = CustomDateTimeField(help_text='time of export')

class Meta:
Expand All @@ -113,6 +114,7 @@ def create_accounting_export(expense_objects: List[Expense], fund_source: str, w

# Group expenses based on specified fields and fund_source
accounting_exports = _group_expenses(expense_objects, export_setting, fund_source)

fund_source_map = {
'PERSONAL': 'reimbursable',
'CCC': 'credit_card'
Expand All @@ -137,7 +139,7 @@ def create_accounting_export(expense_objects: List[Expense], fund_source: str, w
workspace_id=workspace_id,
fund_source=accounting_export['fund_source'],
description=accounting_export,
status='ENQUEUED'
status='EXPORT_READY'
)

# Add related expenses to the AccountingExport object
Expand Down
22 changes: 22 additions & 0 deletions apps/sage300/actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from django.db.models import Q

from apps.workspaces.models import LastExportDetail
from apps.accounting_exports.models import AccountingExport


def update_last_export_details(workspace_id):
last_export_detail = LastExportDetail.objects.get(workspace_id=workspace_id)

failed_exports = AccountingExport.objects.filter(~Q(type__in=['FETCHING_REIMBURSABLE_EXPENSES', 'FETCHING_CREDIT_CARD_EXPENSES']), workspace_id=workspace_id, status__in=['FAILED', 'FATAL']).count()

successful_exports = AccountingExport.objects.filter(
~Q(type__in=['FETCHING_REIMBURSABLE_EXPENSES', 'FETCHING_CREDIT_CARD_EXPENSES']),
workspace_id=workspace_id, status='COMPLETE',
).count()

last_export_detail.failed_expense_groups_count = failed_exports
last_export_detail.successful_expense_groups_count = successful_exports
last_export_detail.total_expense_groups_count = failed_exports + successful_exports
last_export_detail.save()

return last_export_detail
78 changes: 78 additions & 0 deletions apps/sage300/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@

import logging
import traceback

from sage_desktop_api.exceptions import BulkError
from apps.workspaces.models import FyleCredential, Sage300Credential
from sage_desktop_sdk.exceptions.hh2_exceptions import WrongParamsError
from apps.accounting_exports.models import AccountingExport
from apps.accounting_exports.models import Error
from apps.sage300.actions import update_last_export_details


logger = logging.getLogger(__name__)
logger.level = logging.INFO


def handle_sage300_error(exception, accounting_export: AccountingExport, export_type: str):
logger.info(exception.response)

sage300_error = exception.response['Errors']

error_msg = 'Failed to create {0}'.format(export_type)

Error.objects.update_or_create(workspace_id=accounting_export.workspace_id, accounting_export=accounting_export, defaults={'error_title': error_msg, 'type': 'SAGE300_ERROR', 'error_detail': sage300_error, 'is_resolved': False})

accounting_export.status = 'FAILED'
accounting_export.detail = None
accounting_export.sage_300_errors = sage300_error
accounting_export.save()


def handle_sage300_exceptions():
def decorator(func):
def new_fn(*args):

accounting_export = args[0]

try:
return func(*args)
except (FyleCredential.DoesNotExist):
logger.info('Fyle credentials not found %s', accounting_export.workspace_id)
accounting_export.detail = {'message': 'Fyle credentials do not exist in workspace'}
accounting_export.status = 'FAILED'

accounting_export.save()

except Sage300Credential.DoesNotExist:
logger.info('Sage300 Account not connected / token expired for workspace_id %s / accounting export %s', accounting_export.workspace_id, accounting_export.id)
detail = {'accounting_export_id': accounting_export.id, 'message': 'Sage300 Account not connected / token expired'}
accounting_export.status = 'FAILED'
accounting_export.detail = detail

accounting_export.save()

except WrongParamsError as exception:
handle_sage300_error(exception, accounting_export, 'Purchase Invoice')

except BulkError as exception:
logger.info(exception.response)
detail = exception.response
accounting_export.status = 'FAILED'
accounting_export.detail = detail

accounting_export.save()

except Exception as error:
error = traceback.format_exc()
accounting_export.detail = {'error': error}
accounting_export.status = 'FATAL'

accounting_export.save()
logger.error('Something unexpected happened workspace_id: %s %s', accounting_export.workspace_id, accounting_export.detail)

update_last_export_details(accounting_export.workspace_id)

return new_fn

return decorator
39 changes: 21 additions & 18 deletions apps/sage300/exports/accounting_export.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import logging
from datetime import datetime
from django.db import transaction

from apps.accounting_exports.models import AccountingExport
from apps.workspaces.models import AdvancedSetting
from apps.sage300.exports.helpers import validate_accounting_export

logger = logging.getLogger(__name__)
logger.level = logging.INFO


class AccountingDataExporter:
Expand All @@ -15,15 +20,15 @@ def __init__(self):
self.body_model = None
self.lineitem_model = None

def post(self, workspace_id, body, lineitems):
def post(self, workspace_id, body, lineitems = None):
"""
Implement this method to post data to the external accounting system.
"""
raise NotImplementedError("Please implement this method")

def create_sage300_object(self, accounting_export: AccountingExport):
"""
Create a purchase invoice in the external accounting system.
Create a accounting expense in the external accounting system.

Args:
accounting_export (AccountingExport): The accounting export object.
Expand All @@ -43,26 +48,24 @@ def create_sage300_object(self, accounting_export: AccountingExport):
# If the status is already 'IN_PROGRESS' or 'COMPLETE', return without further processing
return

try:
with transaction.atomic():
# Create or update the main body of the accounting object
body_model_object = self.body_model.create_or_update_object(accounting_export)
validate_accounting_export(accounting_export)
with transaction.atomic():
# Create or update the main body of the accounting object
body_model_object = self.body_model.create_or_update_object(accounting_export, advance_settings)

# Create or update line items for the accounting object
# Create or update line items for the accounting object
lineitems_model_objects = None
if self.lineitem_model:
lineitems_model_objects = self.lineitem_model.create_or_update_object(
accounting_export, advance_settings
)

# Post the data to the external accounting system
created_object = self.post(accounting_export.workspace_id, body_model_object, lineitems_model_objects)

# Update the accounting export details
accounting_export.detail = created_object
accounting_export.status = 'COMPLETE'
accounting_export.exported_at = datetime.now()
# Post the data to the external accounting system
created_object = self.post(accounting_export, body_model_object, lineitems_model_objects)

accounting_export.save()
# Update the accounting export details
accounting_export.detail = created_object
accounting_export.status = 'COMPLETE'
accounting_export.exported_at = datetime.now()

except Exception as e:
print(e)
# Handle exceptions specific to the export process here
accounting_export.save()
90 changes: 90 additions & 0 deletions apps/sage300/exports/direct_cost/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from django.db import models

from fyle_accounting_mappings.models import CategoryMapping

from apps.sage300.exports.base_model import BaseExportModel
from apps.accounting_exports.models import AccountingExport
from apps.fyle.models import Expense, DependentFieldSetting
from apps.workspaces.models import AdvancedSetting

from sage_desktop_api.models.fields import (
CustomDateTimeField,
FloatNullField,
StringNullField,
TextNotNullField
)


class DirectCost(BaseExportModel):
"""
Direct Cost Model
"""

accounting_export = models.OneToOneField(AccountingExport, on_delete=models.PROTECT, help_text='Accounting Export reference')
accounting_date = CustomDateTimeField(help_text='accounting date of direct cost')
code = StringNullField(max_length=10, help_text='unique code for invoice')
expense = models.OneToOneField(Expense, on_delete=models.PROTECT, help_text='Reference to Expense')
amount = FloatNullField(help_text='Amount of the invoice')
category_id = StringNullField(help_text='destination id of category')
commitment_id = StringNullField(help_text='destination id of commitment')
cost_code_id = StringNullField(help_text='destination id of cost code')
credit_card_account_id = StringNullField(help_text='destination id of credit card account')
debit_card_account_id = StringNullField(help_text='destination id of debit card account')
description = TextNotNullField(help_text='description for the invoice')
job_id = StringNullField(help_text='destination id of job')
standard_category_id = StringNullField(help_text='destination id of standard category')
standard_cost_code_id = StringNullField(help_text='destination id of standard cost code')

class Meta:
db_table = 'direct_costs'

@classmethod
def create_or_update_object(self, accounting_export: AccountingExport, advance_setting: AdvancedSetting):
"""
Create Direct Cost
:param accounting_export: expense group
:return: Direct cost object
"""
expenses = accounting_export.expenses.all()
dependent_field_setting = DependentFieldSetting.objects.filter(workspace_id=accounting_export.workspace_id).first()

cost_category_id = None
cost_code_id = None

direct_cost_objects = []

for lineitem in expenses:
account = CategoryMapping.objects.filter(
source_category__value=lineitem.category,
workspace_id=accounting_export.workspace_id
).first()

job_id = self.get_job_id(accounting_export, lineitem)
commitment_id = self.get_commitment_id(accounting_export, lineitem)
standard_category_id = self.get_standard_category_id(accounting_export, lineitem)
standard_cost_code_id = self.get_standard_cost_code_id(accounting_export, lineitem)
description = self.get_expense_purpose(accounting_export.workspace_id, lineitem, lineitem.category, advance_setting)

if dependent_field_setting:
cost_category_id = self.get_cost_category_id(accounting_export, lineitem, dependent_field_setting, job_id)
cost_code_id = self.get_cost_code_id(accounting_export, lineitem, dependent_field_setting, job_id, cost_category_id)

direct_cost_object, _ = DirectCost.objects.update_or_create(
expense_id=lineitem.id,
accounting_export=accounting_export,
defaults={
'amount': lineitem.amount,
'credit_card_account_id': account.destination_account.destination_id,
'job_id': job_id,
'commitment_id': commitment_id,
'standard_category_id': standard_category_id,
'standard_cost_code_id': standard_cost_code_id,
'category_id': cost_category_id,
'cost_code_id': cost_code_id,
'description': description,
'workspace_id': accounting_export.workspace_id
}
)
direct_cost_objects.append(direct_cost_object)

return direct_cost_objects
46 changes: 46 additions & 0 deletions apps/sage300/exports/direct_cost/queues.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from typing import List
from django.db.models import Q
from django_q.tasks import Chain

from apps.accounting_exports.models import AccountingExport
from apps.workspaces.models import FyleCredential


def check_accounting_export_and_start_import(workspace_id: int, accounting_export_ids: List[str]):
"""
Check accounting export group and start export
"""

fyle_credentials = FyleCredential.objects.filter(workspace_id=workspace_id).first()

accounting_exports = AccountingExport.objects.filter(
~Q(status__in=['IN_PROGRESS', 'COMPLETE']),
workspace_id=workspace_id, id__in=accounting_export_ids, directcost__id__isnull=True,
exported_at__isnull=True
).all()

chain = Chain()
chain.append('apps.fyle.helpers.sync_dimensions', fyle_credentials)

for index, accounting_export_group in enumerate(accounting_exports):
accounting_export, _ = AccountingExport.objects.update_or_create(
workspace_id=accounting_export_group.workspace_id,
id=accounting_export_group.id,
defaults={
'status': 'ENQUEUED',
'type': 'PURCHASE_INVOICE'
}
)

if accounting_export.status not in ['IN_PROGRESS', 'ENQUEUED']:
accounting_export.status = 'ENQUEUED'
accounting_export.save()

"""
Todo: Add last export details
"""

chain.append('apps.sage300.exports.direct_cost.tasks.create_direct_cost', accounting_export)

if chain.length() > 1:
chain.run()
Loading
Loading