From e4fd9aa8c5e82b5cdecef4e9648b52df58ec6c61 Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Thu, 16 Nov 2023 14:35:39 +0530 Subject: [PATCH 01/11] add exports module base structure --- apps/sage300/exports/base_model.py | 0 apps/sage300/exports/helpers.py | 4 + .../exports/purchase_invoice/models.py | 6 ++ .../exports/purchase_invoice/queues.py | 37 +++++++++ .../sage300/exports/purchase_invoice/tasks.py | 79 +++++++++++++++++++ apps/sage300/exports/tasks.py | 21 +++++ 6 files changed, 147 insertions(+) create mode 100644 apps/sage300/exports/base_model.py create mode 100644 apps/sage300/exports/helpers.py create mode 100644 apps/sage300/exports/purchase_invoice/models.py create mode 100644 apps/sage300/exports/purchase_invoice/queues.py create mode 100644 apps/sage300/exports/purchase_invoice/tasks.py create mode 100644 apps/sage300/exports/tasks.py diff --git a/apps/sage300/exports/base_model.py b/apps/sage300/exports/base_model.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/sage300/exports/helpers.py b/apps/sage300/exports/helpers.py new file mode 100644 index 00000000..8f902c0b --- /dev/null +++ b/apps/sage300/exports/helpers.py @@ -0,0 +1,4 @@ + + +def get_sage300_connection_class(): + pass diff --git a/apps/sage300/exports/purchase_invoice/models.py b/apps/sage300/exports/purchase_invoice/models.py new file mode 100644 index 00000000..e6927382 --- /dev/null +++ b/apps/sage300/exports/purchase_invoice/models.py @@ -0,0 +1,6 @@ +class PurchaceInvoice(): + pass + + +class PurchaceInvoiceLineitems(): + pass diff --git a/apps/sage300/exports/purchase_invoice/queues.py b/apps/sage300/exports/purchase_invoice/queues.py new file mode 100644 index 00000000..19662760 --- /dev/null +++ b/apps/sage300/exports/purchase_invoice/queues.py @@ -0,0 +1,37 @@ +from typing import List +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(self, export_type: str, accounting_exports: List[AccountingExport]): + """ + Check accounting export group and start export + """ + + fyle_credentials = FyleCredential.objects.filter(workspace_id=self.workspace_id) + + chain = Chain() + chain.append('apps.fyle.helpers.sync_dimensions', fyle_credentials, self.workspace_id) + for index, accounting_export_group in enumerate(accounting_exports): + accounting_export, _ = AccountingExport.objects.get_or_create( + workspace_id=accounting_export_group.workspace_id, + id=accounting_export_group.id, + defaults={ + 'status': 'ENQUEUED', + 'type': export_type + } + ) + if accounting_export.status not in ['IN_PROGRESS', 'ENQUEUED']: + accounting_export.status = 'ENQUEUED' + accounting_export.save() + + last_export = False + if accounting_export.count() == index + 1: + last_export = True + + chain.append('apps.sage300.purchace_invoice.queues.create_purchace_invoice', accounting_export, last_export) + + if chain.length() > 1: + chain.run() diff --git a/apps/sage300/exports/purchase_invoice/tasks.py b/apps/sage300/exports/purchase_invoice/tasks.py new file mode 100644 index 00000000..bb8fa00d --- /dev/null +++ b/apps/sage300/exports/purchase_invoice/tasks.py @@ -0,0 +1,79 @@ +from datetime import datetime +from django.db import transaction + +from apps.accounting_exports.models import AccountingExport +from apps.sage300.exports.purchase_invoice.models import PurchaceInvoice, PurchaceInvoiceLineitems +from apps.workspaces.models import ExportSetting +from apps.sage300.utils import SageDesktopConnector + + +class ExportPurchaceInvoice: + + """ + Class for purchace invoice module + """ + + def __init__( + self, + workspace_id: int, + ): + self.workspace_id = workspace_id + + def trigger_import(self): + """ + Trigger import for Project module + """ + self.check_accounting_export_and_start_import() + + def __construct_purchace_invoice(item, lineitem): + pass + + def post_purchace_invoice(self, item, lineitem): + """ + Export Purchace Invoice + """ + + try: + purchace_invoice_payload = self.__construct_purchace_invoice(item, lineitem) + + sage300_connection = SageDesktopConnector() + created_purchace_invoice_ = sage300_connection.connection.documents.post_document(purchace_invoice_payload) + return created_purchace_invoice_ + + except Exception as e: + print(e) + + def create_purchace_invoice(self, accounting_export: AccountingExport): + """ + function to create purchace invoice + """ + + export_settings = ExportSetting.objects.filter(workspace_id=accounting_export.workspace_id) + + if accounting_export.status not in ['IN_PROGRESS', 'COMPLETE']: + accounting_export.status = 'IN_PROGRESS' + accounting_export.save() + else: + return + + try: + with transaction.atomic(): + purchace_invoice_object = PurchaceInvoice.create_expense_report(accounting_export) + + purchace_invoice_lineitems_objects = PurchaceInvoiceLineitems.create_expense_report_lineitems( + accounting_export, export_settings + ) + + created_purchace_invoice = self.post_purchace_invoice( + purchace_invoice_object, purchace_invoice_lineitems_objects + ) + + accounting_export.detail = created_purchace_invoice + accounting_export.status = 'COMPLETE' + accounting_export.exported_at = datetime.now() + + accounting_export.save() + + except Exception as e: + print(e) + # will add execptions here diff --git a/apps/sage300/exports/tasks.py b/apps/sage300/exports/tasks.py new file mode 100644 index 00000000..5010ee8b --- /dev/null +++ b/apps/sage300/exports/tasks.py @@ -0,0 +1,21 @@ +from typing import List + +from sage300.exports.purchase_invoice.tasks import PurchaceInvoice +from accounting_exports.models import AccountingExport + +EXPORT_CLASS_MAP = { + 'PURCHACE_INVOICE': PurchaceInvoice, +} + + +def trigger_export_via_schedule(workspace_id: int, export_type: str, accounting_exports: List[AccountingExport]): + """ + Trigger import via schedule + :param workspace_id: Workspace id + :param destination_field: Destination field + :param source_field: Type of attribute (e.g., 'PROJECT', 'CATEGORY', 'COST_CENTER') + """ + + module_class = EXPORT_CLASS_MAP[export_type] + item = module_class(workspace_id, accounting_exports) + item.trigger_export() From e0ef404b9f4b2de181df52207e082d1df788dfd8 Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Thu, 16 Nov 2023 14:36:38 +0530 Subject: [PATCH 02/11] minor bug fixes and all --- apps/accounting_exports/models.py | 4 +--- apps/fyle/exceptions.py | 22 +++++++++++----------- apps/fyle/models.py | 3 +-- apps/fyle/tasks.py | 10 +++++----- apps/workspaces/models.py | 4 ++-- 5 files changed, 20 insertions(+), 23 deletions(-) diff --git a/apps/accounting_exports/models.py b/apps/accounting_exports/models.py index eaae2791..6a376c09 100644 --- a/apps/accounting_exports/models.py +++ b/apps/accounting_exports/models.py @@ -66,14 +66,13 @@ def _group_expenses(expenses: List[Expense], export_setting: ExportSetting, fund group_by_field = fund_source_data.get('group_by') date_field = fund_source_data.get('date_field') - default_fields.extend([group_by_field, fund_source]) + default_fields.extend(group_by_field) if date_field: default_fields.append(date_field) # Extract expense IDs from the provided expenses expense_ids = [expense.id for expense in expenses] - # Retrieve expenses from the database expenses = Expense.objects.filter(id__in=expense_ids).all() @@ -109,7 +108,6 @@ def create_accounting_export(expense_objects: List[Expense], fund_source: str, w """ Group expenses by report_id and fund_source, format date fields, and create AccountingExport objects. """ - # Retrieve the ExportSetting for the workspace export_setting = ExportSetting.objects.get(workspace_id=workspace_id) diff --git a/apps/fyle/exceptions.py b/apps/fyle/exceptions.py index 4d14ae44..469b0b80 100644 --- a/apps/fyle/exceptions.py +++ b/apps/fyle/exceptions.py @@ -16,22 +16,22 @@ def wrapper(*args, **kwargs): try: return func(*args, **kwargs) except FyleCredential.DoesNotExist: - logger.info('Fyle credentials not found %s', args[1]) # args[1] is workspace_id - args[2].detail = {'message': 'Fyle credentials do not exist in workspace'} - args[2].status = 'FAILED' - args[2].save() + logger.info('Fyle credentials not found %s', args[0]) # args[1] is workspace_id + args[1].detail = {'message': 'Fyle credentials do not exist in workspace'} + args[1].status = 'FAILED' + args[1].save() except NoPrivilegeError: logger.info('Invalid Fyle Credentials / Admin is disabled') - args[2].detail = {'message': 'Invalid Fyle Credentials / Admin is disabled'} - args[2].status = 'FAILED' - args[2].save() + args[1].detail = {'message': 'Invalid Fyle Credentials / Admin is disabled'} + args[1].status = 'FAILED' + args[1].save() except Exception: error = traceback.format_exc() - args[2].detail = {'error': error} - args[2].status = 'FATAL' - args[2].save() - logger.exception('Something unexpected happened workspace_id: %s %s', args[1], args[2].detail) + args[1].detail = {'error': error} + args[1].status = 'FATAL' + args[1].save() + logger.exception('Something unexpected happened workspace_id: %s %s', args[0], args[1].detail) return wrapper diff --git a/apps/fyle/models.py b/apps/fyle/models.py index 293967fd..fb8aefc4 100644 --- a/apps/fyle/models.py +++ b/apps/fyle/models.py @@ -121,7 +121,6 @@ def create_expense_objects(expenses: List[Dict], workspace_id: int): # Create an empty list to store expense objects expense_objects = [] - for expense in expenses: # Iterate through custom property fields and handle empty values for custom_property_field in expense['custom_properties']: @@ -171,7 +170,7 @@ def create_expense_objects(expenses: List[Dict], workspace_id: int): ) # Check if an AccountingExport related to the expense object already exists - if not Expense.objects.filter(accountingexport__isnull=False).distinct(): + if not expense_object.accountingexport_set.exists(): expense_objects.append(expense_object) return expense_objects diff --git a/apps/fyle/tasks.py b/apps/fyle/tasks.py index 33d4c041..c9549436 100644 --- a/apps/fyle/tasks.py +++ b/apps/fyle/tasks.py @@ -39,18 +39,18 @@ def import_expenses(workspace_id, accounting_export: AccountingExport, source_ac } export_settings = ExportSetting.objects.get(workspace_id=workspace_id) workspace = Workspace.objects.get(pk=workspace_id) - last_synced_at = getattr(workspace, f"{fund_source_key.lower()}_last_synced_at", None) + last_synced_at = getattr(workspace, f"{fund_source_map.get(fund_source_key)}_last_synced_at", None) fyle_credentials = FyleCredential.objects.get(workspace_id=workspace_id) platform = PlatformConnector(fyle_credentials) expenses = platform.expenses.get( source_account_type=[source_account_type], - state=getattr(export_settings, f"{fund_source_key.lower()}_expense_state"), + state=getattr(export_settings, f"{fund_source_map.get(fund_source_key)}_expense_state"), settled_at=last_synced_at if getattr(export_settings, f"{fund_source_map.get(fund_source_key)}_expense_state") == 'PAYMENT_PROCESSING' else None, - approved_at=last_synced_at if getattr(export_settings, f"{fund_source_map.get(fund_source_key)}_expense_state") == 'APPROVED' else None, + # approved_at=last_synced_at if getattr(export_settings, f"{fund_source_map.get(fund_source_key)}_expense_state") == 'APPROVED' else None, filter_credit_expenses=(fund_source_key == 'CCC'), - last_paid_at=last_synced_at if getattr(export_settings, f"{fund_source_key.lower()}_expense_state") == 'PAID' else None + last_paid_at=last_synced_at if getattr(export_settings, f"{fund_source_map.get(fund_source_key)}_expense_state") == 'PAID' else None ) if expenses: @@ -59,7 +59,7 @@ def import_expenses(workspace_id, accounting_export: AccountingExport, source_ac with transaction.atomic(): expenses_object = Expense.create_expense_objects(expenses, workspace_id) - AccountingExport.create_accounting_export_report_id( + AccountingExport.create_accounting_export( expenses_object, fund_source=fund_source_key, workspace_id=workspace_id diff --git a/apps/workspaces/models.py b/apps/workspaces/models.py index 653b9bb1..42a27b81 100644 --- a/apps/workspaces/models.py +++ b/apps/workspaces/models.py @@ -38,8 +38,8 @@ class Workspace(models.Model): name = StringNotNullField(help_text='Name of the workspace') user = models.ManyToManyField(User, help_text='Reference to users table') org_id = models.CharField(max_length=255, help_text='org id', unique=True) - last_synced_at = CustomDateTimeField(help_text='Datetime when expenses were pulled last') - ccc_last_synced_at = CustomDateTimeField(help_text='Datetime when ccc expenses were pulled last') + reimbursable_last_synced_at = CustomDateTimeField(help_text='Datetime when expenses were pulled last') + credit_card_last_synced_at = CustomDateTimeField(help_text='Datetime when ccc expenses were pulled last') source_synced_at = CustomDateTimeField(help_text='Datetime when source dimensions were pulled') destination_synced_at = CustomDateTimeField(help_text='Datetime when destination dimensions were pulled') onboarding_state = StringOptionsField( From ebd6717e3fac07dadd4c13da4999330915fba474 Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Thu, 16 Nov 2023 18:48:52 +0530 Subject: [PATCH 03/11] test fix --- apps/fyle/tasks.py | 2 +- tests/conftest.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/fyle/tasks.py b/apps/fyle/tasks.py index c9549436..369bbf5b 100644 --- a/apps/fyle/tasks.py +++ b/apps/fyle/tasks.py @@ -48,7 +48,7 @@ def import_expenses(workspace_id, accounting_export: AccountingExport, source_ac source_account_type=[source_account_type], state=getattr(export_settings, f"{fund_source_map.get(fund_source_key)}_expense_state"), settled_at=last_synced_at if getattr(export_settings, f"{fund_source_map.get(fund_source_key)}_expense_state") == 'PAYMENT_PROCESSING' else None, - # approved_at=last_synced_at if getattr(export_settings, f"{fund_source_map.get(fund_source_key)}_expense_state") == 'APPROVED' else None, + approved_at=last_synced_at if getattr(export_settings, f"{fund_source_map.get(fund_source_key)}_expense_state") == 'APPROVED' else None, filter_credit_expenses=(fund_source_key == 'CCC'), last_paid_at=last_synced_at if getattr(export_settings, f"{fund_source_map.get(fund_source_key)}_expense_state") == 'PAID' else None ) diff --git a/tests/conftest.py b/tests/conftest.py index 854bf6e7..ce399325 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -122,8 +122,8 @@ def create_temp_workspace(): id=workspace_id, name='Fyle For Testing {}'.format(workspace_id), org_id='riseabovehate{}'.format(workspace_id), - last_synced_at=None, - ccc_last_synced_at=None, + reimbursable_last_synced_at=None, + credit_card_last_synced_at=None, created_at=datetime.now(tz=timezone.utc), updated_at=datetime.now(tz=timezone.utc) ) From d50834bb3186fab2bb335f7c5b0284d5bc07d94a Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Mon, 20 Nov 2023 12:37:09 +0530 Subject: [PATCH 04/11] minor fixes --- apps/fyle/helpers.py | 6 ++ .../exports/purchase_invoice/helpers.py | 13 ++++ .../exports/purchase_invoice/models.py | 4 +- .../exports/purchase_invoice/queues.py | 23 ++++--- .../sage300/exports/purchase_invoice/tasks.py | 69 ++++--------------- apps/sage300/exports/tasks.py | 69 +++++++++++++++---- apps/sage300/serializers.py | 2 + apps/workspaces/tasks.py | 20 +++--- 8 files changed, 116 insertions(+), 90 deletions(-) create mode 100644 apps/sage300/exports/purchase_invoice/helpers.py diff --git a/apps/fyle/helpers.py b/apps/fyle/helpers.py index 276f29a5..1310ac42 100644 --- a/apps/fyle/helpers.py +++ b/apps/fyle/helpers.py @@ -125,6 +125,12 @@ def get_fyle_orgs(refresh_token: str, cluster_domain: str): return get_request(api_url, {}, refresh_token) +def sync_dimensions(fyle_credentials: FyleCredential) -> None: + platform = PlatformConnector(fyle_credentials) + + platform.import_fyle_dimensions() + + def connect_to_platform(workspace_id: int) -> PlatformConnector: fyle_credentials: FyleCredential = FyleCredential.objects.get(workspace_id=workspace_id) diff --git a/apps/sage300/exports/purchase_invoice/helpers.py b/apps/sage300/exports/purchase_invoice/helpers.py new file mode 100644 index 00000000..024287cb --- /dev/null +++ b/apps/sage300/exports/purchase_invoice/helpers.py @@ -0,0 +1,13 @@ +from apps.accounting_exports.models import AccountingExport +from apps.sage300.exports.purchase_invoice.tasks import ExportPurchaseInvoice + + +def create_purchase_invoice(accounting_export: AccountingExport): + """ + Helper function to create purchase invoice + """ + + export_purchase_invoice_instance = ExportPurchaseInvoice() + exported_purchase_invoice = export_purchase_invoice_instance.create_sage300_object(accounting_export=accounting_export) + + return exported_purchase_invoice diff --git a/apps/sage300/exports/purchase_invoice/models.py b/apps/sage300/exports/purchase_invoice/models.py index e6927382..6a1e06ad 100644 --- a/apps/sage300/exports/purchase_invoice/models.py +++ b/apps/sage300/exports/purchase_invoice/models.py @@ -1,6 +1,6 @@ -class PurchaceInvoice(): +class PurchaseInvoice(): pass -class PurchaceInvoiceLineitems(): +class PurchaseInvoiceLineitems(): pass diff --git a/apps/sage300/exports/purchase_invoice/queues.py b/apps/sage300/exports/purchase_invoice/queues.py index 19662760..69e3d616 100644 --- a/apps/sage300/exports/purchase_invoice/queues.py +++ b/apps/sage300/exports/purchase_invoice/queues.py @@ -5,33 +5,40 @@ from apps.workspaces.models import FyleCredential -def check_accounting_export_and_start_import(self, export_type: str, accounting_exports: List[AccountingExport]): +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=self.workspace_id) + fyle_credentials = FyleCredential.objects.filter(workspace_id=workspace_id).first() + + accounting_exports = AccountingExport.objects.filter( + status='ENQUEUED', + workspace_id=workspace_id, id__in=accounting_export_ids, + exported_at__isnull=True + ).all() chain = Chain() - chain.append('apps.fyle.helpers.sync_dimensions', fyle_credentials, self.workspace_id) + chain.append('apps.fyle.helpers.sync_dimensions', fyle_credentials) for index, accounting_export_group in enumerate(accounting_exports): accounting_export, _ = AccountingExport.objects.get_or_create( workspace_id=accounting_export_group.workspace_id, id=accounting_export_group.id, defaults={ 'status': 'ENQUEUED', - 'type': export_type + 'type': 'PURCHASE_INVOICE' } ) + if accounting_export.status not in ['IN_PROGRESS', 'ENQUEUED']: accounting_export.status = 'ENQUEUED' accounting_export.save() - last_export = False - if accounting_export.count() == index + 1: - last_export = True + """ + Todo: Add last export details + """ - chain.append('apps.sage300.purchace_invoice.queues.create_purchace_invoice', accounting_export, last_export) + chain.append('apps.sage300.exports.purchase_invoice.helpers.create_purchase_invoice', accounting_export) if chain.length() > 1: chain.run() diff --git a/apps/sage300/exports/purchase_invoice/tasks.py b/apps/sage300/exports/purchase_invoice/tasks.py index bb8fa00d..2daed9ce 100644 --- a/apps/sage300/exports/purchase_invoice/tasks.py +++ b/apps/sage300/exports/purchase_invoice/tasks.py @@ -1,79 +1,34 @@ -from datetime import datetime -from django.db import transaction - -from apps.accounting_exports.models import AccountingExport -from apps.sage300.exports.purchase_invoice.models import PurchaceInvoice, PurchaceInvoiceLineitems -from apps.workspaces.models import ExportSetting +from apps.sage300.exports.tasks import AccountingDataExporter from apps.sage300.utils import SageDesktopConnector -class ExportPurchaceInvoice: +class ExportPurchaseInvoice(AccountingDataExporter): """ - Class for purchace invoice module + Class for purchase invoice module """ - def __init__( - self, - workspace_id: int, - ): - self.workspace_id = workspace_id - - def trigger_import(self): + def trigger_export(self, workspace_id, accounting_export_ids): """ Trigger import for Project module """ - self.check_accounting_export_and_start_import() + from apps.sage300.exports.purchase_invoice.queues import check_accounting_export_and_start_import + check_accounting_export_and_start_import(workspace_id, accounting_export_ids) - def __construct_purchace_invoice(item, lineitem): + def __construct_purchase_invoice(item, lineitem): pass - def post_purchace_invoice(self, item, lineitem): + def post(self, item, lineitem): """ - Export Purchace Invoice + Export Purchase Invoice """ try: - purchace_invoice_payload = self.__construct_purchace_invoice(item, lineitem) + purchase_invoice_payload = self.__construct_purchase_invoice(item, lineitem) sage300_connection = SageDesktopConnector() - created_purchace_invoice_ = sage300_connection.connection.documents.post_document(purchace_invoice_payload) - return created_purchace_invoice_ - - except Exception as e: - print(e) - - def create_purchace_invoice(self, accounting_export: AccountingExport): - """ - function to create purchace invoice - """ - - export_settings = ExportSetting.objects.filter(workspace_id=accounting_export.workspace_id) - - if accounting_export.status not in ['IN_PROGRESS', 'COMPLETE']: - accounting_export.status = 'IN_PROGRESS' - accounting_export.save() - else: - return - - try: - with transaction.atomic(): - purchace_invoice_object = PurchaceInvoice.create_expense_report(accounting_export) - - purchace_invoice_lineitems_objects = PurchaceInvoiceLineitems.create_expense_report_lineitems( - accounting_export, export_settings - ) - - created_purchace_invoice = self.post_purchace_invoice( - purchace_invoice_object, purchace_invoice_lineitems_objects - ) - - accounting_export.detail = created_purchace_invoice - accounting_export.status = 'COMPLETE' - accounting_export.exported_at = datetime.now() - - accounting_export.save() + created_purchase_invoice = sage300_connection.connection.documents.post_document(purchase_invoice_payload) + return created_purchase_invoice except Exception as e: print(e) - # will add execptions here diff --git a/apps/sage300/exports/tasks.py b/apps/sage300/exports/tasks.py index 5010ee8b..fde2cb85 100644 --- a/apps/sage300/exports/tasks.py +++ b/apps/sage300/exports/tasks.py @@ -1,21 +1,60 @@ -from typing import List +from datetime import datetime +from django.db import transaction -from sage300.exports.purchase_invoice.tasks import PurchaceInvoice -from accounting_exports.models import AccountingExport +from apps.accounting_exports.models import AccountingExport +from apps.workspaces.models import ExportSetting -EXPORT_CLASS_MAP = { - 'PURCHACE_INVOICE': PurchaceInvoice, -} - -def trigger_export_via_schedule(workspace_id: int, export_type: str, accounting_exports: List[AccountingExport]): +class AccountingDataExporter: """ - Trigger import via schedule - :param workspace_id: Workspace id - :param destination_field: Destination field - :param source_field: Type of attribute (e.g., 'PROJECT', 'CATEGORY', 'COST_CENTER') + Base class for exporting accounting data to an external accounting system. + Subclasses should implement the 'post' method for posting data. """ - module_class = EXPORT_CLASS_MAP[export_type] - item = module_class(workspace_id, accounting_exports) - item.trigger_export() + body_model = None + lineitem_model = None + + def post(self, body, lineitems): + """ + 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. + """ + + export_settings = ExportSetting.objects.filter(workspace_id=accounting_export.workspace_id) + + if accounting_export.status not in ['IN_PROGRESS', 'COMPLETE']: + accounting_export.status = 'IN_PROGRESS' + accounting_export.save() + else: + return + + try: + with transaction.atomic(): + # Create the main body of the expense report + body_model_object = self.body_model.create_expense_report(accounting_export) + + # Create line items for the expense report + lineitems_model_objects = self.lineitem_model.create_expense_report_lineitems( + accounting_export, export_settings + ) + + # Post the data to the external accounting system + created_object = self.post( + 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() + + accounting_export.save() + + except Exception as e: + print(e) + # Handle exceptions specific to the export process here diff --git a/apps/sage300/serializers.py b/apps/sage300/serializers.py index 0b9a54f3..ebe89840 100644 --- a/apps/sage300/serializers.py +++ b/apps/sage300/serializers.py @@ -8,6 +8,7 @@ from fyle_accounting_mappings.models import DestinationAttribute from apps.workspaces.models import Workspace, Sage300Credential +from apps.workspaces.tasks import run_import_export from apps.sage300.helpers import sync_dimensions, check_interval_and_sync_dimension @@ -79,6 +80,7 @@ class Sage300FieldSerializer(serializers.Serializer): display_name = serializers.CharField() def format_sage300_fields(self, workspace_id): + run_import_export(workspace_id=1) attribute_types = [ "VENDOR", "ACCOUNT", diff --git a/apps/workspaces/tasks.py b/apps/workspaces/tasks.py index 27cd7937..c9935ddf 100644 --- a/apps/workspaces/tasks.py +++ b/apps/workspaces/tasks.py @@ -5,6 +5,8 @@ from apps.workspaces.models import ExportSetting, AdvancedSetting from apps.accounting_exports.models import AccountingExport, AccountingExportSummary +from apps.sage300.exports.purchase_invoice.tasks import ExportPurchaseInvoice +from apps.fyle.queue import queue_import_reimbursable_expenses, queue_import_credit_card_expenses logger = logging.getLogger(__name__) @@ -19,13 +21,16 @@ def run_import_export(workspace_id: int, export_mode = None): export_settings = ExportSetting.objects.get(workspace_id=workspace_id) advance_settings = AdvancedSetting.objects.get(workspace_id=workspace_id) - accounting_summary = AccountingExportSummary.objects.get(workspace_id=workspace_id) + accounting_summary, _ = AccountingExportSummary.objects.update_or_create( + workspace_id=workspace_id + ) last_exported_at = datetime.now() is_expenses_exported = False # For Reimbursable Expenses if export_settings.reimbursable_expenses_export_type: + queue_import_reimbursable_expenses(workspace_id=workspace_id, synchronous=True) accounting_export = AccountingExport.objects.get( workspace_id=workspace_id, type='FETCHING_REIMBURSABLE_EXPENSES' @@ -38,15 +43,15 @@ def run_import_export(workspace_id: int, export_mode = None): if len(accounting_export_ids): is_expenses_exported = True - """ - Export Logic goes here - """ + purchase_invoice = ExportPurchaseInvoice() + purchase_invoice.trigger_export(workspace_id=workspace_id, accounting_export_ids=accounting_export_ids) # For Credit Card Expenses if export_settings.credit_card_expense_export_type: + queue_import_credit_card_expenses(workspace_id=workspace_id, synchronous=True) accounting_export = AccountingExport.objects.get( workspace_id=workspace_id, - type='FETCHING_CREDIT_CARD_EXPENENSES' + type='FETCHING_CREDIT_CARD_EXPENSES' ) if accounting_export.status == 'COMPLETE': @@ -56,9 +61,8 @@ def run_import_export(workspace_id: int, export_mode = None): if len(accounting_export_ids): is_expenses_exported = True - """ - Export Logic goes here - """ + purchase_invoice = ExportPurchaseInvoice() + purchase_invoice.trigger_export(workspace_id=workspace_id, accounting_export_ids=accounting_export_ids) if is_expenses_exported: accounting_summary.last_exported_at = last_exported_at From 074b2478fd8c05acc9aee3cb805d4136b51b2344 Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Mon, 20 Nov 2023 12:38:36 +0530 Subject: [PATCH 05/11] comments resolved --- apps/sage300/exports/helpers.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/sage300/exports/helpers.py b/apps/sage300/exports/helpers.py index 8f902c0b..e69de29b 100644 --- a/apps/sage300/exports/helpers.py +++ b/apps/sage300/exports/helpers.py @@ -1,4 +0,0 @@ - - -def get_sage300_connection_class(): - pass From d64df79793d3833fdb6bd918b0056aa00aa53d77 Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Mon, 20 Nov 2023 12:44:54 +0530 Subject: [PATCH 06/11] lint fix --- apps/sage300/exports/purchase_invoice/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sage300/exports/purchase_invoice/helpers.py b/apps/sage300/exports/purchase_invoice/helpers.py index 024287cb..e43a87eb 100644 --- a/apps/sage300/exports/purchase_invoice/helpers.py +++ b/apps/sage300/exports/purchase_invoice/helpers.py @@ -1,5 +1,5 @@ from apps.accounting_exports.models import AccountingExport -from apps.sage300.exports.purchase_invoice.tasks import ExportPurchaseInvoice +from apps.sage300.exports.purchase_invoice.tasks import ExportPurchaseInvoice def create_purchase_invoice(accounting_export: AccountingExport): From 408a75dfb02bdb34ef69ce5e9009e862a577fa95 Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Mon, 20 Nov 2023 12:50:58 +0530 Subject: [PATCH 07/11] minor change --- apps/sage300/serializers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/sage300/serializers.py b/apps/sage300/serializers.py index ebe89840..88342f0e 100644 --- a/apps/sage300/serializers.py +++ b/apps/sage300/serializers.py @@ -80,7 +80,6 @@ class Sage300FieldSerializer(serializers.Serializer): display_name = serializers.CharField() def format_sage300_fields(self, workspace_id): - run_import_export(workspace_id=1) attribute_types = [ "VENDOR", "ACCOUNT", From 7d60258a652433b4d7df8b10ed7121a9a9d38306 Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Mon, 20 Nov 2023 12:56:20 +0530 Subject: [PATCH 08/11] resolve master --- apps/sage300/serializers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/sage300/serializers.py b/apps/sage300/serializers.py index 88342f0e..0b9a54f3 100644 --- a/apps/sage300/serializers.py +++ b/apps/sage300/serializers.py @@ -8,7 +8,6 @@ from fyle_accounting_mappings.models import DestinationAttribute from apps.workspaces.models import Workspace, Sage300Credential -from apps.workspaces.tasks import run_import_export from apps.sage300.helpers import sync_dimensions, check_interval_and_sync_dimension From f1e2023dccc3a3d3678f133e841efd79f1c3aa78 Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Mon, 20 Nov 2023 14:10:24 +0530 Subject: [PATCH 09/11] naming fixed --- .../{tasks.py => accounting_export.py} | 21 +++++++---- .../exports/purchase_invoice/helpers.py | 13 ------- .../exports/purchase_invoice/queues.py | 2 +- .../sage300/exports/purchase_invoice/tasks.py | 35 ++++++++++++++----- apps/sage300/views.py | 1 + 5 files changed, 43 insertions(+), 29 deletions(-) rename apps/sage300/exports/{tasks.py => accounting_export.py} (69%) delete mode 100644 apps/sage300/exports/purchase_invoice/helpers.py diff --git a/apps/sage300/exports/tasks.py b/apps/sage300/exports/accounting_export.py similarity index 69% rename from apps/sage300/exports/tasks.py rename to apps/sage300/exports/accounting_export.py index fde2cb85..33b03a00 100644 --- a/apps/sage300/exports/tasks.py +++ b/apps/sage300/exports/accounting_export.py @@ -23,30 +23,37 @@ def post(self, body, lineitems): def create_sage300_object(self, accounting_export: AccountingExport): """ Create a purchase invoice in the external accounting system. + + Args: + accounting_export (AccountingExport): The accounting export object. + + Raises: + NotImplementedError: If the method is not implemented in the subclass. """ + # Retrieve export settings for the current workspace export_settings = ExportSetting.objects.filter(workspace_id=accounting_export.workspace_id) + # Check and update the status of the accounting export if accounting_export.status not in ['IN_PROGRESS', 'COMPLETE']: accounting_export.status = 'IN_PROGRESS' accounting_export.save() else: + # If the status is already 'IN_PROGRESS' or 'COMPLETE', return without further processing return try: with transaction.atomic(): - # Create the main body of the expense report - body_model_object = self.body_model.create_expense_report(accounting_export) + # Create or update the main body of the accounting object + body_model_object = self.body_model.create_or_update_object(accounting_export) - # Create line items for the expense report - lineitems_model_objects = self.lineitem_model.create_expense_report_lineitems( + # Create or update line items for the accounting object + lineitems_model_objects = self.lineitem_model.create_or_update_object( accounting_export, export_settings ) # Post the data to the external accounting system - created_object = self.post( - body_model_object, lineitems_model_objects - ) + created_object = self.post(body_model_object, lineitems_model_objects) # Update the accounting export details accounting_export.detail = created_object diff --git a/apps/sage300/exports/purchase_invoice/helpers.py b/apps/sage300/exports/purchase_invoice/helpers.py deleted file mode 100644 index e43a87eb..00000000 --- a/apps/sage300/exports/purchase_invoice/helpers.py +++ /dev/null @@ -1,13 +0,0 @@ -from apps.accounting_exports.models import AccountingExport -from apps.sage300.exports.purchase_invoice.tasks import ExportPurchaseInvoice - - -def create_purchase_invoice(accounting_export: AccountingExport): - """ - Helper function to create purchase invoice - """ - - export_purchase_invoice_instance = ExportPurchaseInvoice() - exported_purchase_invoice = export_purchase_invoice_instance.create_sage300_object(accounting_export=accounting_export) - - return exported_purchase_invoice diff --git a/apps/sage300/exports/purchase_invoice/queues.py b/apps/sage300/exports/purchase_invoice/queues.py index 69e3d616..8e08c67d 100644 --- a/apps/sage300/exports/purchase_invoice/queues.py +++ b/apps/sage300/exports/purchase_invoice/queues.py @@ -38,7 +38,7 @@ def check_accounting_export_and_start_import(workspace_id: int, accounting_expor Todo: Add last export details """ - chain.append('apps.sage300.exports.purchase_invoice.helpers.create_purchase_invoice', accounting_export) + chain.append('apps.sage300.exports.purchase_invoice.tasks.create_purchase_invoice', accounting_export) if chain.length() > 1: chain.run() diff --git a/apps/sage300/exports/purchase_invoice/tasks.py b/apps/sage300/exports/purchase_invoice/tasks.py index 2daed9ce..9622aad6 100644 --- a/apps/sage300/exports/purchase_invoice/tasks.py +++ b/apps/sage300/exports/purchase_invoice/tasks.py @@ -1,34 +1,53 @@ -from apps.sage300.exports.tasks import AccountingDataExporter +from apps.sage300.exports.accounting_export import AccountingDataExporter +from apps.accounting_exports.models import AccountingExport from apps.sage300.utils import SageDesktopConnector +from apps.sage300.exports.purchase_invoice.queues import check_accounting_export_and_start_import class ExportPurchaseInvoice(AccountingDataExporter): - """ - Class for purchase invoice module + Class for handling the export of purchase invoices to Sage 300. + Extends the base AccountingDataExporter class. """ def trigger_export(self, workspace_id, accounting_export_ids): """ - Trigger import for Project module + Trigger the import process for the Project module. """ - from apps.sage300.exports.purchase_invoice.queues import check_accounting_export_and_start_import check_accounting_export_and_start_import(workspace_id, accounting_export_ids) - def __construct_purchase_invoice(item, lineitem): + def __construct_purchase_invoice(self, item, lineitem): + """ + Construct the payload for the purchase invoice. + """ + # Implementation for constructing the purchase invoice payload goes here pass def post(self, item, lineitem): """ - Export Purchase Invoice + Export the purchase invoice to Sage 300. """ - try: purchase_invoice_payload = self.__construct_purchase_invoice(item, lineitem) + # Establish a connection to Sage 300 sage300_connection = SageDesktopConnector() + + # Post the purchase invoice to Sage 300 created_purchase_invoice = sage300_connection.connection.documents.post_document(purchase_invoice_payload) return created_purchase_invoice except Exception as e: print(e) + + +def create_purchase_invoice(accounting_export: AccountingExport): + """ + Helper function to create and export a purchase invoice. + """ + export_purchase_invoice_instance = ExportPurchaseInvoice() + + # Create and export the purchase invoice using the base class method + exported_purchase_invoice = export_purchase_invoice_instance.create_sage300_object(accounting_export=accounting_export) + + return exported_purchase_invoice diff --git a/apps/sage300/views.py b/apps/sage300/views.py index e9dd506a..847aa6c9 100644 --- a/apps/sage300/views.py +++ b/apps/sage300/views.py @@ -22,6 +22,7 @@ class Sage300FieldsView(generics.ListAPIView): Sage300 Expense Fields View """ serializer_class = Sage300FieldSerializer + pagination_class = None def get_queryset(self): return Sage300FieldSerializer().format_sage300_fields(self.kwargs["workspace_id"]) From ae67de2bb040328fc1a7e7041da0ae584045c6f1 Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Mon, 20 Nov 2023 15:08:54 +0530 Subject: [PATCH 10/11] add fix --- tests/test_fyle/test_views.py | 2 +- tests/test_sage300/test_views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_fyle/test_views.py b/tests/test_fyle/test_views.py index b783b77f..aa3598cc 100644 --- a/tests/test_fyle/test_views.py +++ b/tests/test_fyle/test_views.py @@ -99,7 +99,7 @@ def test_fyle_fields(api_client, test_connection, create_temp_workspace, add_fyl assert response.status_code == 200 response = json.loads(response.content) - assert response['results'] == data['fyle_fields_response'] + assert response == data['fyle_fields_response'] def test_exportable_expense_group_view(api_client, test_connection, create_temp_workspace, add_export_settings): diff --git a/tests/test_sage300/test_views.py b/tests/test_sage300/test_views.py index b976d9c2..2c4a6144 100644 --- a/tests/test_sage300/test_views.py +++ b/tests/test_sage300/test_views.py @@ -38,4 +38,4 @@ def test_sage300_fields(api_client, test_connection): response = api_client.get(url) assert response.status_code == 200 - assert response.data['results'] == [{'attribute_type': 'JOB', 'display_name': 'Job'}] + assert response.data == [{'attribute_type': 'JOB', 'display_name': 'Job'}] From 73a612f0ba971b239b6652bf7335cb37a9458d8c Mon Sep 17 00:00:00 2001 From: Nilesh Pant <58652823+NileshPant1999@users.noreply.github.com> Date: Mon, 27 Nov 2023 12:29:30 +0530 Subject: [PATCH 11/11] Purchace invoice and Purchase invoice linitems model (#84) --- apps/accounting_exports/models.py | 2 +- apps/fyle/views.py | 1 + apps/sage300/exports/accounting_export.py | 17 +- apps/sage300/exports/base_model.py | 303 ++++++++++++++++++ apps/sage300/exports/helpers.py | 17 + .../exports/purchase_invoice/models.py | 136 +++++++- .../exports/purchase_invoice/queues.py | 18 +- .../sage300/exports/purchase_invoice/tasks.py | 62 +++- apps/sage300/helpers.py | 2 +- ...urchaseinvoice_purchaseinvoicelineitems.py | 60 ++++ apps/sage300/models.py | 2 + ...ace_credit_card_last_synced_at_and_more.py | 3 +- apps/workspaces/models.py | 1 + apps/workspaces/tasks.py | 1 - requirements.txt | 2 +- sage_desktop_sdk/apis/documents.py | 2 +- 16 files changed, 597 insertions(+), 32 deletions(-) create mode 100644 apps/sage300/migrations/0003_purchaseinvoice_purchaseinvoicelineitems.py diff --git a/apps/accounting_exports/models.py b/apps/accounting_exports/models.py index 6a376c09..064a7e1a 100644 --- a/apps/accounting_exports/models.py +++ b/apps/accounting_exports/models.py @@ -113,7 +113,6 @@ 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' @@ -138,6 +137,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' ) # Add related expenses to the AccountingExport object diff --git a/apps/fyle/views.py b/apps/fyle/views.py index 40280d0b..ce2385da 100644 --- a/apps/fyle/views.py +++ b/apps/fyle/views.py @@ -61,6 +61,7 @@ class FyleFieldsView(generics.ListAPIView): """ serializer_class = FyleFieldsSerializer + pagination_class = None def get_queryset(self): return FyleFieldsSerializer().format_fyle_fields(self.kwargs["workspace_id"]) diff --git a/apps/sage300/exports/accounting_export.py b/apps/sage300/exports/accounting_export.py index 33b03a00..9f30de3d 100644 --- a/apps/sage300/exports/accounting_export.py +++ b/apps/sage300/exports/accounting_export.py @@ -2,7 +2,7 @@ from django.db import transaction from apps.accounting_exports.models import AccountingExport -from apps.workspaces.models import ExportSetting +from apps.workspaces.models import AdvancedSetting class AccountingDataExporter: @@ -11,10 +11,11 @@ class AccountingDataExporter: Subclasses should implement the 'post' method for posting data. """ - body_model = None - lineitem_model = None + def __init__(self): + self.body_model = None + self.lineitem_model = None - def post(self, body, lineitems): + def post(self, workspace_id, body, lineitems): """ Implement this method to post data to the external accounting system. """ @@ -31,8 +32,8 @@ def create_sage300_object(self, accounting_export: AccountingExport): NotImplementedError: If the method is not implemented in the subclass. """ - # Retrieve export settings for the current workspace - export_settings = ExportSetting.objects.filter(workspace_id=accounting_export.workspace_id) + # Retrieve advance settings for the current workspace + advance_settings = AdvancedSetting.objects.filter(workspace_id=accounting_export.workspace_id).first() # Check and update the status of the accounting export if accounting_export.status not in ['IN_PROGRESS', 'COMPLETE']: @@ -49,11 +50,11 @@ def create_sage300_object(self, accounting_export: AccountingExport): # Create or update line items for the accounting object lineitems_model_objects = self.lineitem_model.create_or_update_object( - accounting_export, export_settings + accounting_export, advance_settings ) # Post the data to the external accounting system - created_object = self.post(body_model_object, lineitems_model_objects) + created_object = self.post(accounting_export.workspace_id, body_model_object, lineitems_model_objects) # Update the accounting export details accounting_export.detail = created_object diff --git a/apps/sage300/exports/base_model.py b/apps/sage300/exports/base_model.py index e69de29b..effe3c77 100644 --- a/apps/sage300/exports/base_model.py +++ b/apps/sage300/exports/base_model.py @@ -0,0 +1,303 @@ +from typing import Optional +from datetime import datetime +from django.db import models +from django.db.models import Sum + +from fyle_accounting_mappings.models import MappingSetting, ExpenseAttribute, Mapping + +from apps.accounting_exports.models import AccountingExport +from apps.fyle.models import Expense, DependentFieldSetting +from apps.workspaces.models import Workspace, FyleCredential, AdvancedSetting + +from apps.sage300.exports.helpers import get_filtered_mapping + + +class BaseExportModel(models.Model): + """ + Base Model for Sage300 Export + """ + created_at = models.DateTimeField(auto_now_add=True, help_text='Created at') + updated_at = models.DateTimeField(auto_now=True, help_text='Updated at') + workspace = models.ForeignKey(Workspace, on_delete=models.PROTECT, help_text='Reference to Workspace model') + + class Meta: + abstract = True + + def get_expense_purpose(workspace_id, lineitem: Expense, category: str, advance_setting: AdvancedSetting) -> str: + workspace = Workspace.objects.get(id=workspace_id) + org_id = workspace.org_id + + fyle_credentials = FyleCredential.objects.get(workspace_id=workspace_id) + cluster_domain = fyle_credentials.cluster_domain + workspace.cluster_domain = cluster_domain + workspace.save() + + expense_link = '{0}/app/main/#/enterprise/view_expense/{1}?org_id={2}'.format( + cluster_domain, lineitem.expense_id, org_id + ) + + memo_structure = advance_setting.expense_memo_structure + + details = { + 'employee_email': lineitem.employee_email, + 'merchant': '{0}'.format(lineitem.vendor) if lineitem.vendor else '', + 'category': '{0}'.format(category) if lineitem.category else '', + 'purpose': '{0}'.format(lineitem.purpose) if lineitem.purpose else '', + 'report_number': '{0}'.format(lineitem.claim_number), + 'spent_on': '{0}'.format(lineitem.spent_at.date()) if lineitem.spent_at else '', + 'expense_link': expense_link + } + + purpose = '' + + for id, field in enumerate(memo_structure): + if field in details: + purpose += details[field] + if id + 1 != len(memo_structure): + purpose = '{0} - '.format(purpose) + + return purpose + + def get_vendor_id(accounting_export: AccountingExport): + return '3a3485d9-5cc7-4668-9557-b06100a3e8c9' + + def get_total_amount(accounting_export: AccountingExport): + """ + Calculate the total amount of expenses associated with a given AccountingExport + + Parameters: + - accounting_export (AccountingExport): The AccountingExport instance for which to calculate the total amount. + + Returns: + - float: The total amount of expenses associated with the provided AccountingExport. + """ + + # Using the related name 'expenses' to access the expenses associated with the given AccountingExport + total_amount = accounting_export.expenses.aggregate(Sum('amount'))['amount__sum'] + + # If there are no expenses for the given AccountingExport, 'total_amount' will be None + # Handle this case by returning 0 or handling it as appropriate for your application + return total_amount or 0.0 + + def get_invoice_date(accounting_export: AccountingExport) -> str: + """ + Get the invoice date from the provided AccountingExport. + + Parameters: + - accounting_export (AccountingExport): The AccountingExport instance containing the description field. + + Returns: + - str: The invoice date as a string in the format '%Y-%m-%dT%H:%M:%S'. + """ + # Check for specific keys in the 'description' field and return the corresponding value + if 'spent_at' in accounting_export.description and accounting_export.description['spent_at']: + return accounting_export.description['spent_at'] + elif 'approved_at' in accounting_export.description and accounting_export.description['approved_at']: + return accounting_export.description['approved_at'] + elif 'verified_at' in accounting_export.description and accounting_export.description['verified_at']: + return accounting_export.description['verified_at'] + elif 'last_spent_at' in accounting_export.description and accounting_export.description['last_spent_at']: + return accounting_export.description['last_spent_at'] + elif 'posted_at' in accounting_export.description and accounting_export.description['posted_at']: + return accounting_export.description['posted_at'] + + # If none of the expected keys are present or if the values are empty, return the current date and time + return datetime.now().strftime('%Y-%m-%dT%H:%M:%S') + + def get_job_id(accounting_export: AccountingExport, expense: Expense): + """ + Get the job ID based on the provided AccountingExport and Expense. + + Parameters: + - accounting_export (AccountingExport): The AccountingExport instance containing workspace information. + - expense (Expense): The Expense instance containing information for job ID retrieval. + + Returns: + - Optional[str]: The job ID as a string if found, otherwise None. + """ + + job_id = None + + # Retrieve mapping settings for job + job_settings: MappingSetting = MappingSetting.objects.filter( + workspace_id=accounting_export.workspace_id, + destination_field='JOB' + ).first() + + if job_settings: + # Determine the source value based on the configured source field + if job_settings.source_field == 'PROJECT': + source_value = expense.project + elif job_settings.source_field == 'COST_CENTER': + source_value = expense.cost_center + else: + attribute = ExpenseAttribute.objects.filter(attribute_type=job_settings.source_field).first() + source_value = expense.custom_properties.get(attribute.display_name, None) + + # Check for a mapping based on the source value + mapping: Mapping = Mapping.objects.filter( + source_type=job_settings.source_field, + destination_type='JOB', + source__value=source_value, + workspace_id=accounting_export.workspace_id + ).first() + + # If a mapping is found, retrieve the destination job ID + if mapping: + job_id = mapping.destination.destination_id + + return job_id + + def get_commitment_id(accounting_export: AccountingExport, expense: Expense): + """ + Get the commitment ID based on the provided AccountingExport and Expense. + + Parameters: + - accounting_export (AccountingExport): The AccountingExport instance containing workspace information. + - expense (Expense): The Expense instance containing information for job ID retrieval. + + Returns: + - Optional[str]: The commitment ID as a string if found, otherwise None. + """ + + commitment_setting: MappingSetting = MappingSetting.objects.filter( + workspace_id=accounting_export.workspace_id, + destination_field='COMMITMENT' + ).first() + + commitment_id = None + source_id = None + + if accounting_export and commitment_setting: + if expense: + if commitment_setting.source_field == 'PROJECT': + source_id = expense.project_id + source_value = expense.project + elif commitment_setting.source_field == 'COST_CENTER': + source_value = expense.cost_center + else: + attribute = ExpenseAttribute.objects.filter(attribute_type=expense.source_field).first() + source_value = expense.custom_properties.get(attribute.display_name, None) + else: + source_value = accounting_export.description[accounting_export.source_field.lower()] + + mapping: Mapping = get_filtered_mapping( + commitment_setting.source_field, 'COMMITMENT', accounting_export.workspace_id, source_value, source_id + ) + + if mapping: + commitment_id = mapping.destination.destination_id + return commitment_id + + def get_cost_code_id(accounting_export: AccountingExport, lineitem: Expense, dependent_field_setting: DependentFieldSetting, job_id: str): + from apps.sage300.models import CostCategory + cost_code_id = None + + selected_cost_code = lineitem.custom_properties.get(dependent_field_setting.cost_code_field_name, None) + cost_code = CostCategory.objects.filter( + workspace_id=accounting_export.workspace_id, + cost_code_name=selected_cost_code, + project_id=job_id + ).first() + + if cost_code: + cost_code_id = cost_code.cost_code_id + + return cost_code_id + + def get_cost_category_id(expense_group: AccountingExport, lineitem: Expense, dependent_field_setting: DependentFieldSetting, project_id: str, cost_code_id: str): + from apps.sage300.models import CostCategory + cost_category_id = None + + selected_cost_category = lineitem.custom_properties.get(dependent_field_setting.cost_type_field_name, None) + cost_category = CostCategory.objects.filter( + workspace_id=expense_group.workspace_id, + cost_code_id=cost_code_id, + project_id=project_id, + name=selected_cost_category + ).first() + + if cost_category: + cost_category_id = cost_category.cost_category_id + + return cost_category_id + + def get_standard_category_id(accounting_export: AccountingExport, expense: Expense) -> Optional[str]: + """ + Get the standard category ID based on the provided AccountingExport and Expense. + + Parameters: + - accounting_export (AccountingExport): The AccountingExport instance containing workspace information. + - expense (Expense): The Expense instance containing information for standard category ID retrieval. + + Returns: + - Optional[str]: The standard category ID as a string if found, otherwise None. + """ + standard_category_id = None + + # Retrieve mapping settings for standard category + standard_category_setting: MappingSetting = MappingSetting.objects.filter( + workspace_id=accounting_export.workspace_id, + destination_field='STANDARD_CATEGORY' + ).first() + + if standard_category_setting: + # Retrieve the attribute corresponding to the source field + attribute = ExpenseAttribute.objects.filter(attribute_type=standard_category_setting.source_field).first() + + # Determine the source value based on the configured source field + source_value = expense.custom_properties.get(attribute.display_name, None) + + # Check for a mapping based on the source value + mapping: Mapping = Mapping.objects.filter( + source_type=standard_category_setting.source_field, + destination_type='STANDARD_CATEGORY', + source__value=source_value, + workspace_id=accounting_export.workspace_id + ).first() + + # If a mapping is found, retrieve the destination standard category ID + if mapping: + standard_category_id = mapping.destination.destination_id + + return standard_category_id + + def get_standard_cost_code_id(accounting_export: AccountingExport, expense: Expense): + """ + Get the standard cost code ID based on the provided AccountingExport and Expense. + + Parameters: + - accounting_export (AccountingExport): The AccountingExport instance containing workspace information. + - expense (Expense): The Expense instance containing information for standard category ID retrieval. + + Returns: + - Optional[str]: The standard cost code ID as a string if found, otherwise None. + """ + standard_cost_code_id = None + + # Retrieve mapping settings for standard cost code + standard_cost_code_setting: MappingSetting = MappingSetting.objects.filter( + workspace_id=accounting_export.workspace_id, + destination_field='STANDARD_COST_CODE' + ).first() + + if standard_cost_code_setting: + # Retrieve the attribute corresponding to the source field + attribute = ExpenseAttribute.objects.filter(attribute_type=standard_cost_code_setting.source_field).first() + + # Determine the source value based on the configured source field + source_value = expense.custom_properties.get(attribute.display_name, None) + + # Check for a mapping based on the source value + mapping: Mapping = Mapping.objects.filter( + source_type=standard_cost_code_setting.source_field, + destination_type='STANDARD_COST_CODE', + source__value=source_value, + workspace_id=accounting_export.workspace_id + ).first() + + # If a mapping is found, retrieve the destination standard cost code ID + if mapping: + standard_cost_code_id = mapping.destination.destination_id + + return standard_cost_code_id diff --git a/apps/sage300/exports/helpers.py b/apps/sage300/exports/helpers.py index e69de29b..929f3c76 100644 --- a/apps/sage300/exports/helpers.py +++ b/apps/sage300/exports/helpers.py @@ -0,0 +1,17 @@ +from fyle_accounting_mappings.models import Mapping + + +def get_filtered_mapping( + source_field: str, destination_type: str, workspace_id: int, source_value: str, source_id: str) -> Mapping: + filters = { + 'source_type': source_field, + 'destination_type': destination_type, + 'workspace_id': workspace_id + } + + if source_id: + filters['source__source_id'] = source_id + else: + filters['source__value'] = source_value + + return Mapping.objects.filter(**filters).first() diff --git a/apps/sage300/exports/purchase_invoice/models.py b/apps/sage300/exports/purchase_invoice/models.py index 6a1e06ad..af56c0b8 100644 --- a/apps/sage300/exports/purchase_invoice/models.py +++ b/apps/sage300/exports/purchase_invoice/models.py @@ -1,6 +1,134 @@ -class PurchaseInvoice(): - pass +from django.db import models +from fyle_accounting_mappings.models import CategoryMapping -class PurchaseInvoiceLineitems(): - pass +from apps.sage300.exports.base_model import BaseExportModel +from apps.accounting_exports.models import AccountingExport +from apps.workspaces.models import AdvancedSetting +from apps.fyle.models import Expense, DependentFieldSetting + + +from sage_desktop_api.models.fields import ( + CustomDateTimeField, + FloatNullField, + StringNullField, + TextNotNullField +) + + +class PurchaseInvoice(BaseExportModel): + """ + Purchase Invoice Model + """ + + accounting_export = models.OneToOneField(AccountingExport, on_delete=models.PROTECT, help_text='Expense group reference') + accounting_date = CustomDateTimeField(help_text='accounting date of purchase invoice') + amount = FloatNullField(help_text='Total Amount of the invoice') + code = StringNullField(max_length=10, help_text='unique code for invoice') + description = TextNotNullField(help_text='description for the invoice') + invoice_date = CustomDateTimeField(help_text='date of invoice') + tax_amount = FloatNullField(help_text='total tax amount of the invoice') + vendor_id = StringNullField(help_text='id of vendor') + + class Meta: + db_table = 'purchase_invoices' + + @classmethod + def create_or_update_object(self, accounting_export: AccountingExport): + """ + Create Purchase Invoice + :param accounting_export: expense group + :return: purchase invoices object + """ + description = accounting_export.description + + vendor_id = self.get_vendor_id(accounting_export=accounting_export) + amount = self.get_total_amount(accounting_export=accounting_export) + invoice_date = self.get_invoice_date(accounting_export=accounting_export) + + purchase_invoice, _ = PurchaseInvoice.objects.update_or_create( + accounting_export=accounting_export, + defaults={ + 'amount': amount, + 'vendor_id': vendor_id, + 'description': description, + 'invoice_date': invoice_date + } + ) + + return purchase_invoice + + +class PurchaseInvoiceLineitems(BaseExportModel): + """ + Purchase Invoice Lineitem Model + """ + + accounts_payable_account_id = StringNullField(help_text='destination id of accounts payable account') + purchase_invoice = models.ForeignKey(PurchaseInvoice, on_delete=models.PROTECT, help_text='Reference to PurchaseInvoice') + 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') + description = TextNotNullField(help_text='description for the invoice') + job_id = StringNullField(help_text='destination id of job') + tax_amount = FloatNullField(help_text='tax amount of the invoice') + tax_group_id = StringNullField(help_text='destination id of tax group') + 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 = 'purchase_invoice_lineitems' + + @classmethod + def create_or_update_object(self, accounting_export: AccountingExport, advance_setting: AdvancedSetting): + """ + Create Purchase Invoice + :param accounting_export: expense group + :return: purchase invoices object + """ + + expenses = accounting_export.expenses.all() + purchase_invoice = PurchaseInvoice.objects.get(accounting_export=accounting_export) + dependent_field_setting = DependentFieldSetting.objects.filter(workspace_id=accounting_export.workspace_id).first() + + cost_category_id = None + cost_code_id = None + + purchase_invoice_lineitem_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) + + purchase_invoice_lineitem_object, _ = PurchaseInvoiceLineitems.objects.update_or_create( + purchase_invoice_id=purchase_invoice.id, + expense_id=lineitem.id, + defaults={ + 'amount': lineitem.amount, + 'accounts_payable_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 + } + ) + purchase_invoice_lineitem_objects.append(purchase_invoice_lineitem_object) + + return purchase_invoice_lineitem_objects diff --git a/apps/sage300/exports/purchase_invoice/queues.py b/apps/sage300/exports/purchase_invoice/queues.py index 8e08c67d..c3a6ff7e 100644 --- a/apps/sage300/exports/purchase_invoice/queues.py +++ b/apps/sage300/exports/purchase_invoice/queues.py @@ -1,10 +1,17 @@ from typing import List from django_q.tasks import Chain +from fyle_integrations_platform_connector import PlatformConnector from apps.accounting_exports.models import AccountingExport from apps.workspaces.models import FyleCredential +def import_fyle_dimensions(fyle_credentials: FyleCredential): + + platform = PlatformConnector(fyle_credentials) + platform.import_fyle_dimensions() + + def check_accounting_export_and_start_import(workspace_id: int, accounting_export_ids: List[str]): """ Check accounting export group and start export @@ -13,15 +20,16 @@ def check_accounting_export_and_start_import(workspace_id: int, accounting_expor fyle_credentials = FyleCredential.objects.filter(workspace_id=workspace_id).first() accounting_exports = AccountingExport.objects.filter( - status='ENQUEUED', - workspace_id=workspace_id, id__in=accounting_export_ids, + status__in=['IN_PROGRESS', 'ENQUEUED'], + workspace_id=workspace_id, id__in=accounting_export_ids, purchaseinvoice__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.get_or_create( + accounting_export, _ = AccountingExport.objects.update_or_create( workspace_id=accounting_export_group.workspace_id, id=accounting_export_group.id, defaults={ @@ -40,5 +48,5 @@ def check_accounting_export_and_start_import(workspace_id: int, accounting_expor chain.append('apps.sage300.exports.purchase_invoice.tasks.create_purchase_invoice', accounting_export) - if chain.length() > 1: - chain.run() + if chain.length() > 1: + chain.run() diff --git a/apps/sage300/exports/purchase_invoice/tasks.py b/apps/sage300/exports/purchase_invoice/tasks.py index 9622aad6..8efcf694 100644 --- a/apps/sage300/exports/purchase_invoice/tasks.py +++ b/apps/sage300/exports/purchase_invoice/tasks.py @@ -1,7 +1,11 @@ +from typing import Dict, List + 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.purchase_invoice.queues import check_accounting_export_and_start_import +from apps.sage300.exports.purchase_invoice.models import PurchaseInvoice, PurchaseInvoiceLineitems class ExportPurchaseInvoice(AccountingDataExporter): @@ -10,32 +14,74 @@ class ExportPurchaseInvoice(AccountingDataExporter): Extends the base AccountingDataExporter class. """ + def __init__(self): + super().__init__() # Call the constructor of the parent class + self.body_model = PurchaseInvoice + self.lineitem_model = PurchaseInvoiceLineitems + 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_purchase_invoice(self, item, lineitem): + def __construct_purchase_invoice(self, body: PurchaseInvoice, lineitems: List[PurchaseInvoiceLineitems]) -> Dict: """ Construct the payload for the purchase invoice. + :param expense_report: ExpenseReport object extracted from database + :param expense_report_lineitems: ExpenseReportLineitem objects extracted from database + :return: constructed expense_report """ - # Implementation for constructing the purchase invoice payload goes here - pass - def post(self, item, lineitem): + purchase_invoice_lineitem_payload = [] + for lineitem in lineitems: + expense = { + "AccountsPayableAccountId": lineitem.accounts_payable_account_id, + "Amount": lineitem.amount, + "CategoryId": lineitem.category_id, + "CostCodeId": lineitem.cost_code_id, + "Description": 'sample description', + "ExpenseAccountId": lineitem.accounts_payable_account_id, + "JobId": lineitem.job_id, + "StandardCategoryId": lineitem.standard_category_id, + "StandardCostCodeId": lineitem.standard_cost_code_id + } + + purchase_invoice_lineitem_payload.append(expense) + + transaction_date = '2023-08-17' + purchase_invoice_payload = { + 'DocumentTypeId': '76744AB9-4697-430A-ADB5-701E633472A9', + 'Snapshot': { + 'Distributions': purchase_invoice_lineitem_payload, + 'Header': { + 'AccountingDate': transaction_date, + 'Amount': body.amount, + "Code": 'difgdofjig', + "Description": 'sample description', + "InvoiceDate": transaction_date, + "VendorId": body.vendor_id + } + } + } + + return purchase_invoice_payload + + def post(self, workspace_id, item, lineitem): """ Export the purchase invoice to Sage 300. """ try: purchase_invoice_payload = self.__construct_purchase_invoice(item, lineitem) - + sage300_credentials = Sage300Credential.objects.filter(workspace_id=workspace_id).first() # Establish a connection to Sage 300 - sage300_connection = SageDesktopConnector() + sage300_connection = SageDesktopConnector(sage300_credentials, workspace_id) # Post the purchase invoice to Sage 300 - created_purchase_invoice = sage300_connection.connection.documents.post_document(purchase_invoice_payload) - return created_purchase_invoice + created_purchase_invoice_id = sage300_connection.connection.documents.post_document(purchase_invoice_payload) + + exported_purchase_invoice_id = sage300_connection.connection.documents.export_document(created_purchase_invoice_id) + return exported_purchase_invoice_id except Exception as e: print(e) diff --git a/apps/sage300/helpers.py b/apps/sage300/helpers.py index 52551a8f..a6e2dd4c 100644 --- a/apps/sage300/helpers.py +++ b/apps/sage300/helpers.py @@ -52,7 +52,7 @@ def sync_dimensions(sage300_credential: Sage300Credential, workspace_id: int) -> sage300_connection = import_string('apps.sage300.utils.SageDesktopConnector')(sage300_credential, workspace_id) # List of dimensions to sync - dimensions = ['accounts', 'vendors', 'commitments', 'jobs', 'standard_categories', 'standard_cost_codes', 'cost_codes', 'cost_categories'] + dimensions = ['accounts', 'vendors', 'commitments', 'jobs', 'standard_categories', 'standard_cost_codes', 'cost_codes'] for dimension in dimensions: try: diff --git a/apps/sage300/migrations/0003_purchaseinvoice_purchaseinvoicelineitems.py b/apps/sage300/migrations/0003_purchaseinvoice_purchaseinvoicelineitems.py new file mode 100644 index 00000000..5469c59a --- /dev/null +++ b/apps/sage300/migrations/0003_purchaseinvoice_purchaseinvoicelineitems.py @@ -0,0 +1,60 @@ +# Generated by Django 4.1.2 on 2023-11-20 10:42 + +from django.db import migrations, models +import django.db.models.deletion +import sage_desktop_api.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('fyle', '0002_alter_dependentfieldsetting_workspace'), + ('accounting_exports', '0001_initial'), + ('sage300', '0002_costcategories'), + ] + + operations = [ + migrations.CreateModel( + name='PurchaseInvoice', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, help_text='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, help_text='Updated at')), + ('accounting_date', sage_desktop_api.models.fields.CustomDateTimeField(help_text='accounting date of purchase invoice', null=True)), + ('amount', sage_desktop_api.models.fields.FloatNullField(help_text='Total Amount of the invoice', null=True)), + ('code', sage_desktop_api.models.fields.StringNullField(help_text='unique code for invoice', max_length=10, null=True)), + ('description', sage_desktop_api.models.fields.TextNotNullField(help_text='description for the invoice')), + ('invoice_date', sage_desktop_api.models.fields.CustomDateTimeField(help_text='date of invoice', null=True)), + ('tax_amount', sage_desktop_api.models.fields.FloatNullField(help_text='total tax amount of the invoice', null=True)), + ('vendor_id', sage_desktop_api.models.fields.StringNullField(help_text='id of vendor', max_length=255, null=True)), + ('accounting_export', models.OneToOneField(help_text='Expense group reference', on_delete=django.db.models.deletion.PROTECT, to='accounting_exports.accountingexport')), + ], + options={ + 'db_table': 'purchase_invoices', + }, + ), + migrations.CreateModel( + name='PurchaseInvoiceLineitems', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True, help_text='Created at')), + ('updated_at', models.DateTimeField(auto_now=True, help_text='Updated at')), + ('accounts_payable_account_id', sage_desktop_api.models.fields.StringNullField(help_text='destination id of accounts payable account', max_length=255, null=True)), + ('amount', sage_desktop_api.models.fields.FloatNullField(help_text='Amount of the invoice', null=True)), + ('category_id', sage_desktop_api.models.fields.StringNullField(help_text='destination id of category', max_length=255, null=True)), + ('commitment_id', sage_desktop_api.models.fields.StringNullField(help_text='destination id of commitment', max_length=255, null=True)), + ('cost_code_id', sage_desktop_api.models.fields.StringNullField(help_text='destination id of cost code', max_length=255, null=True)), + ('description', sage_desktop_api.models.fields.TextNotNullField(help_text='description for the invoice')), + ('job_id', sage_desktop_api.models.fields.StringNullField(help_text='destination id of job', max_length=255, null=True)), + ('tax_amount', sage_desktop_api.models.fields.FloatNullField(help_text='tax amount of the invoice', null=True)), + ('tax_group_id', sage_desktop_api.models.fields.StringNullField(help_text='destination id of tax group', max_length=255, null=True)), + ('standard_category_id', sage_desktop_api.models.fields.StringNullField(help_text='destination id of standard category', max_length=255, null=True)), + ('standard_cost_code_id', sage_desktop_api.models.fields.StringNullField(help_text='destination id of standard cost code', max_length=255, null=True)), + ('expense', models.OneToOneField(help_text='Reference to Expense', on_delete=django.db.models.deletion.PROTECT, to='fyle.expense')), + ('purchase_invoice', models.ForeignKey(help_text='Reference to PurchaseInvoice', on_delete=django.db.models.deletion.PROTECT, to='sage300.purchaseinvoice')), + ], + options={ + 'db_table': 'purchase_invoice_lineitems', + }, + ), + ] diff --git a/apps/sage300/models.py b/apps/sage300/models.py index f463994d..4e024e5f 100644 --- a/apps/sage300/models.py +++ b/apps/sage300/models.py @@ -16,6 +16,8 @@ ) from apps.accounting_exports.models import AccountingExport +from apps.sage300.exports.purchase_invoice.models import PurchaseInvoice, PurchaseInvoiceLineitems # noqa + class Invoice(BaseModel): """ diff --git a/apps/workspaces/migrations/0003_rename_ccc_last_synced_at_workspace_credit_card_last_synced_at_and_more.py b/apps/workspaces/migrations/0003_rename_ccc_last_synced_at_workspace_credit_card_last_synced_at_and_more.py index f8832f98..4e4472bd 100644 --- a/apps/workspaces/migrations/0003_rename_ccc_last_synced_at_workspace_credit_card_last_synced_at_and_more.py +++ b/apps/workspaces/migrations/0003_rename_ccc_last_synced_at_workspace_credit_card_last_synced_at_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.2 on 2023-11-17 21:06 +# Generated by Django 4.1.2 on 2023-11-20 10:58 from django.db import migrations, models import django.db.models.deletion @@ -8,7 +8,6 @@ class Migration(migrations.Migration): dependencies = [ - ('django_q', '0014_alter_ormq_id_alter_schedule_id'), ('workspaces', '0002_sage300credential_importsetting_fylecredential_and_more'), ] diff --git a/apps/workspaces/models.py b/apps/workspaces/models.py index de73b226..3ea8de3b 100644 --- a/apps/workspaces/models.py +++ b/apps/workspaces/models.py @@ -126,6 +126,7 @@ class Meta: ('JOURNAL_ENTRY', 'JOURNAL_ENTRY'), ) + CREDIT_CARD_EXPENSE_STATE_CHOICES = ( ('APPROVED', 'APPROVED'), ('PAYMENT_PROCESSING', 'PAYMENT_PROCESSING'), diff --git a/apps/workspaces/tasks.py b/apps/workspaces/tasks.py index c9935ddf..cf6bbd55 100644 --- a/apps/workspaces/tasks.py +++ b/apps/workspaces/tasks.py @@ -53,7 +53,6 @@ def run_import_export(workspace_id: int, export_mode = None): workspace_id=workspace_id, type='FETCHING_CREDIT_CARD_EXPENSES' ) - if accounting_export.status == 'COMPLETE': accounting_export_ids = AccountingExport.objects.filter( fund_source='CCC', exported_at__isnull=True).values_list('id', flat=True) diff --git a/requirements.txt b/requirements.txt index 40c5ed75..ac8dd636 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ django-request-logging==0.7.5 django-filter==21.1 # DjangoQ for running async tasks -django-q==1.3.9 +django-q==1.3.4 # Read Database Credentials as URL dj-database-url==0.5.0 diff --git a/sage_desktop_sdk/apis/documents.py b/sage_desktop_sdk/apis/documents.py index 7a7b7bef..067e932c 100644 --- a/sage_desktop_sdk/apis/documents.py +++ b/sage_desktop_sdk/apis/documents.py @@ -24,7 +24,7 @@ def post_document(self, data: dict): Get Vendor Types :return: List of Dicts in Vendor Types Schema """ - return self._post_request(Documents.POST_DOCUMENT, data=json.dumps(data.__dict__)) + return self._post_request(Documents.POST_DOCUMENT, data=json.dumps(data)) def export_document(self, document_id: str): """