Skip to content

Commit

Permalink
validate accounting export group before the export (#94)
Browse files Browse the repository at this point in the history
  • Loading branch information
NileshPant1999 authored Nov 29, 2023
1 parent 4d79ba2 commit 1e9469e
Show file tree
Hide file tree
Showing 18 changed files with 382 additions and 119 deletions.
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
9 changes: 9 additions & 0 deletions apps/sage300/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
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
Expand Down Expand Up @@ -54,6 +55,14 @@ def new_fn(*args):
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}
Expand Down
18 changes: 11 additions & 7 deletions apps/sage300/exports/accounting_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

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
Expand All @@ -19,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 @@ -47,17 +48,20 @@ def create_sage300_object(self, accounting_export: AccountingExport):
# If the status is already 'IN_PROGRESS' or 'COMPLETE', return without further processing
return

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)
body_model_object = self.body_model.create_or_update_object(accounting_export, advance_settings)

# Create or update line items for the accounting object
lineitems_model_objects = self.lineitem_model.create_or_update_object(
accounting_export, advance_settings
)
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)
created_object = self.post(accounting_export, body_model_object, lineitems_model_objects)

# Update the accounting export details
accounting_export.detail = created_object
Expand Down
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()
86 changes: 86 additions & 0 deletions apps/sage300/exports/direct_cost/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from typing import Dict

from apps.sage300.exports.accounting_export import AccountingDataExporter
from apps.accounting_exports.models import AccountingExport
from apps.workspaces.models import Sage300Credential
from apps.sage300.utils import SageDesktopConnector
from apps.sage300.exports.direct_cost.queues import check_accounting_export_and_start_import
from apps.sage300.exceptions import handle_sage300_exceptions
from apps.sage300.exports.direct_cost.models import DirectCost


class ExportDirectCost(AccountingDataExporter):
"""
Class for handling the export of Direct Cost to Sage 300.
Extends the base AccountingDataExporter class.
"""

def __init__(self):
super().__init__() # Call the constructor of the parent class
self.body_model = DirectCost

def trigger_export(self, workspace_id, accounting_export_ids):
"""
Trigger the import process for the Project module.
"""
check_accounting_export_and_start_import(workspace_id, accounting_export_ids)

def __construct_direct_cost(self, body: DirectCost) -> Dict:
"""
Construct the payload for the direct invoice.
:param expense_report: ExpenseReport object extracted from database
:param expense_report_lineitems: ExpenseReportLineitem objects extracted from database
:return: constructed expense_report
"""

transaction_date = '2023-08-17'
direct_cost_payload = {
"AccountingDate": transaction_date,
"Amount": body.amount,
"Code": 234234,
"CategoryId": body.category_id,
"CostCodeId": body.cost_code_id,
"CreditAccountId": body.credit_card_account_id,
"DebitAccountId": body.debit_card_account_id,
"Description": "Fyle - Line 1 Wow",
"JobId": body.job_id,
"TransactionDate": transaction_date,
"StandardCategoryId": body.standard_category_id,
"StandardCostCodeId": body.standard_cost_code_id,
"TransactionType": 1
}

return direct_cost_payload

def post(self, accounting_export, item, lineitem = None):
"""
Export the direct cost to Sage 300.
"""

direct_cost_payload = self.__construct_direct_cost(item)
sage300_credentials = Sage300Credential.objects.filter(workspace_id=accounting_export.workspace_id).first()
# Establish a connection to Sage 300
sage300_connection = SageDesktopConnector(sage300_credentials, accounting_export.workspace_id)

# Post the direct cost to Sage 300
created_direct_cost_export_id = sage300_connection.connection.direct_costs.post_direct_cost(direct_cost_payload)

accounting_export.export_id = created_direct_cost_export_id
accounting_export.save()

exported_direct_cost_id = sage300_connection.connection.direct_costs.export_direct_cost(created_direct_cost_export_id)

return exported_direct_cost_id


@handle_sage300_exceptions()
def create_direct_cost(accounting_export: AccountingExport):
"""
Helper function to create and export a direct cost.
"""
export_direct_cost_instance = ExportDirectCost()

# Create and export the direct cost using the base class method
exported_direct_cost = export_direct_cost_instance.create_sage300_object(accounting_export=accounting_export)

return exported_direct_cost
Loading

0 comments on commit 1e9469e

Please sign in to comment.