From bd9ad4a2423d267dc32140165fe63b65add94ec3 Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Sat, 18 Nov 2023 01:39:53 +0530 Subject: [PATCH 1/3] update methods of base model --- apps/mappings/imports/modules/base.py | 2 +- apps/sage300/exports/base_model.py | 247 +++++++++++++++++- apps/sage300/exports/helpers.py | 17 +- .../exports/purchase_invoice/models.py | 3 - .../exports/purchase_invoice/queues.py | 4 - .../sage300/exports/purchase_invoice/tasks.py | 4 +- apps/sage300/helpers.py | 6 +- apps/workspaces/tasks.py | 4 +- 8 files changed, 260 insertions(+), 27 deletions(-) diff --git a/apps/mappings/imports/modules/base.py b/apps/mappings/imports/modules/base.py index 533926bc..99406cf5 100644 --- a/apps/mappings/imports/modules/base.py +++ b/apps/mappings/imports/modules/base.py @@ -172,7 +172,7 @@ def sync_destination_attributes(self, sage300_attribute_type: str): 'JOB': sage300_connection.sync_jobs, 'COMMITMENT': sage300_connection.sync_commitments, 'VENDOR': sage300_connection.sync_vendors, - 'STANDARD_COST_CODES': sage300_connection.sync_standard_cost_codes, + 'STANDARD_COST_CODE': sage300_connection.sync_standard_cost_codes, 'ACCOUNT': sage300_connection.sync_accounts, } diff --git a/apps/sage300/exports/base_model.py b/apps/sage300/exports/base_model.py index 4597901e..9f0c19d3 100644 --- a/apps/sage300/exports/base_model.py +++ b/apps/sage300/exports/base_model.py @@ -1,10 +1,16 @@ +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 +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): """ @@ -49,22 +55,243 @@ def get_expense_purpose(workspace_id, lineitem: Expense, category: str, advance_ return purpose def get_vendor_id(accounting_export: AccountingExport): - return '124' + return '3a3485d9-5cc7-4668-9557-b06100a3e8c9' def get_total_amount(accounting_export: AccountingExport): - return '123123' + """ + 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): - return datetime.now() + 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): - return '2312' + """ + 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): - return '12312' + """ + 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: + 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_standard_category_id(accounting_export: AccountingExport, expense: Expense): - return '123123' + 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_type_id_or_none(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_type = 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_type + ).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='CLASS' + ).first() + + # 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): - return '123123' + """ + 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='CLASS' + ).first() + + # 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_CATEGORY', + 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 8f902c0b..929f3c76 100644 --- a/apps/sage300/exports/helpers.py +++ b/apps/sage300/exports/helpers.py @@ -1,4 +1,17 @@ +from fyle_accounting_mappings.models import Mapping -def get_sage300_connection_class(): - pass +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 88778e9c..56f5afc6 100644 --- a/apps/sage300/exports/purchase_invoice/models.py +++ b/apps/sage300/exports/purchase_invoice/models.py @@ -40,11 +40,9 @@ def create_purchase_invoice(self, accounting_export: AccountingExport): :param accounting_export: expense group :return: purchase invoices object """ - print('virat kolhi') description = accounting_export.description vendor_id = self.get_vendor_id(accounting_export=accounting_export) - print('vendor_id', vendor_id) amount = self.get_total_amount(accounting_export=accounting_export) invoice_date = self.get_invoice_date(accounting_export=accounting_export) @@ -91,7 +89,6 @@ def create_purchase_invoice_lineitems(self, accounting_export: AccountingExport, :return: purchase invoices object """ - print('guitar') 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() diff --git a/apps/sage300/exports/purchase_invoice/queues.py b/apps/sage300/exports/purchase_invoice/queues.py index ce046c15..3e171469 100644 --- a/apps/sage300/exports/purchase_invoice/queues.py +++ b/apps/sage300/exports/purchase_invoice/queues.py @@ -38,7 +38,6 @@ def check_accounting_export_and_start_import(workspace_id: int, accounting_expo chain.append('apps.sage300.exports.purchase_invoice.queues.import_fyle_dimensions', fyle_credentials) - print('accounting exports', accounting_exports) for index, accounting_export_group in enumerate(accounting_exports): accounting_export, _ = AccountingExport.objects.update_or_create( workspace_id=accounting_export_group.workspace_id, @@ -53,10 +52,7 @@ def check_accounting_export_and_start_import(workspace_id: int, accounting_expo accounting_export.status = 'ENQUEUED' accounting_export.save() - print('i am here wow') chain.append('apps.sage300.exports.purchase_invoice.queues.create_purchase_invoice', workspace_id, accounting_export) - print('chain', chain.length()) if chain.length() > 1: - print('i am here t00') chain.run() diff --git a/apps/sage300/exports/purchase_invoice/tasks.py b/apps/sage300/exports/purchase_invoice/tasks.py index 2374fbb1..cda7f3c3 100644 --- a/apps/sage300/exports/purchase_invoice/tasks.py +++ b/apps/sage300/exports/purchase_invoice/tasks.py @@ -4,7 +4,7 @@ from apps.accounting_exports.models import AccountingExport from apps.sage300.exports.purchase_invoice.models import PurchaseInvoice, PurchaseInvoiceLineitems from apps.workspaces.models import AdvancedSetting -from apps.sage300.utils import SageDesktopConnector +from apps.sage300.utils import SageDesktopConnector # noqa class ExportPurchaseInvoice: @@ -29,7 +29,7 @@ def post_purchase_invoice(self, item, lineitem): """ try: - purchase_invoice_payload = self.__construct_purchase_invoice(item, lineitem) + # purchase_invoice_payload = self.__construct_purchase_invoice(item, lineitem) # sage300_connection = SageDesktopConnector() # created_purchase_invoice_ = sage300_connection.connection.documents.post_document(purchase_invoice_payload) diff --git a/apps/sage300/helpers.py b/apps/sage300/helpers.py index b0b74ae3..c98ab4b2 100644 --- a/apps/sage300/helpers.py +++ b/apps/sage300/helpers.py @@ -5,7 +5,7 @@ from django.utils.module_loading import import_string from apps.workspaces.models import Workspace, Sage300Credential - +from apps.mappings.models import Version logger = logging.getLogger(__name__) logger.level = logging.INFO @@ -45,11 +45,13 @@ def sync_dimensions(sage300_credential: Sage300Credential, workspace_id: int) -> This function syncs dimensions like accounts, vendors, commitments, jobs, categories, and cost codes. """ + Version.objects.update_or_create(workspace_id=workspace_id) + # Initialize the Sage 300 connection using the provided credentials and workspace ID 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/workspaces/tasks.py b/apps/workspaces/tasks.py index 943bd6dc..e855be33 100644 --- a/apps/workspaces/tasks.py +++ b/apps/workspaces/tasks.py @@ -35,12 +35,10 @@ def run_import_export(workspace_id: int, export_mode = None): type='FETCHING_REIMBURSABLE_EXPENSES' ) - print('i am here ronadldo', accounting_export.status) if accounting_export.status == 'COMPLETE': accounting_export_ids = AccountingExport.objects.filter( fund_source='PERSONAL', exported_at__isnull=True).values_list('id', flat=True) - - print('accounting expdts', accounting_export_ids) + if len(accounting_export_ids): is_expenses_exported = True From 904dd8deffec9d35b17470f4c5e5764070cfb8aa Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Tue, 21 Nov 2023 00:04:35 +0530 Subject: [PATCH 2/3] add support for methinds --- apps/sage300/exports/accounting_export.py | 13 +++++++------ apps/sage300/exports/base_model.py | 3 +++ apps/sage300/exports/purchase_invoice/models.py | 4 ++-- apps/sage300/exports/purchase_invoice/queues.py | 2 +- apps/sage300/exports/purchase_invoice/tasks.py | 14 +++++++++++--- apps/sage300/serializers.py | 1 - .../migrations/0004_merge_20231117_1715.py | 14 -------------- apps/workspaces/models.py | 1 + 8 files changed, 25 insertions(+), 27 deletions(-) delete mode 100644 apps/workspaces/migrations/0004_merge_20231117_1715.py diff --git a/apps/sage300/exports/accounting_export.py b/apps/sage300/exports/accounting_export.py index 33b03a00..64beade0 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,8 +11,9 @@ 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): """ @@ -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,7 +50,7 @@ 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 diff --git a/apps/sage300/exports/base_model.py b/apps/sage300/exports/base_model.py index 9f0c19d3..8e68867b 100644 --- a/apps/sage300/exports/base_model.py +++ b/apps/sage300/exports/base_model.py @@ -19,6 +19,9 @@ class BaseExportModel(models.Model): created_at = models.DateTimeField(auto_now_add=True, help_text='Created at') updated_at = models.DateTimeField(auto_now=True, help_text='Updated at') + 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 diff --git a/apps/sage300/exports/purchase_invoice/models.py b/apps/sage300/exports/purchase_invoice/models.py index 1b577c54..f7fd1f37 100644 --- a/apps/sage300/exports/purchase_invoice/models.py +++ b/apps/sage300/exports/purchase_invoice/models.py @@ -34,7 +34,7 @@ class Meta: db_table = 'purchase_invoices' @classmethod - def create_purchase_invoice(self, accounting_export: AccountingExport): + def create_or_update_object(self, accounting_export: AccountingExport): """ Create Purchase Invoice :param accounting_export: expense group @@ -82,7 +82,7 @@ class Meta: db_table = 'purchase_invoice_lineitems' @classmethod - def create_purchase_invoice_lineitems(self, accounting_export: AccountingExport, advance_setting: AdvancedSetting): + def create_or_update_object(self, accounting_export: AccountingExport, advance_setting: AdvancedSetting): """ Create Purchase Invoice :param accounting_export: expense group diff --git a/apps/sage300/exports/purchase_invoice/queues.py b/apps/sage300/exports/purchase_invoice/queues.py index ff25a0b9..fd989f81 100644 --- a/apps/sage300/exports/purchase_invoice/queues.py +++ b/apps/sage300/exports/purchase_invoice/queues.py @@ -20,7 +20,7 @@ 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', + status='IN_PROGRESS', workspace_id=workspace_id, id__in=accounting_export_ids, purchaseinvoice__id__isnull=True, exported_at__isnull=True ).all() diff --git a/apps/sage300/exports/purchase_invoice/tasks.py b/apps/sage300/exports/purchase_invoice/tasks.py index b63f7ea2..40abb017 100644 --- a/apps/sage300/exports/purchase_invoice/tasks.py +++ b/apps/sage300/exports/purchase_invoice/tasks.py @@ -1,7 +1,9 @@ from apps.sage300.exports.accounting_export import AccountingDataExporter from apps.accounting_exports.models import AccountingExport -from apps.sage300.utils import SageDesktopConnector # noqa +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,6 +12,11 @@ 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. @@ -23,15 +30,16 @@ def __construct_purchase_invoice(self, item, lineitem): # Implementation for constructing the purchase invoice payload goes here pass - def post(self, item, lineitem): + 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.get(workspace_id=workspace_id) # 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) diff --git a/apps/sage300/serializers.py b/apps/sage300/serializers.py index fbebfdc9..0b9a54f3 100644 --- a/apps/sage300/serializers.py +++ b/apps/sage300/serializers.py @@ -87,7 +87,6 @@ def format_sage300_fields(self, workspace_id): "COST_CODE", "PAYMENT", ] - attributes = ( DestinationAttribute.objects.filter( ~Q(attribute_type__in=attribute_types), diff --git a/apps/workspaces/migrations/0004_merge_20231117_1715.py b/apps/workspaces/migrations/0004_merge_20231117_1715.py deleted file mode 100644 index 2af703a1..00000000 --- a/apps/workspaces/migrations/0004_merge_20231117_1715.py +++ /dev/null @@ -1,14 +0,0 @@ -# Generated by Django 4.1.2 on 2023-11-17 17:15 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('workspaces', '0003_alter_importsetting_workspace'), - ('workspaces', '0003_rename_ccc_last_synced_at_workspace_credit_card_last_synced_at_and_more'), - ] - - operations = [ - ] 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'), From f745e0e41d1b9e65c86e48c174ef1c029fb04202 Mon Sep 17 00:00:00 2001 From: Nilesh Pant <58652823+NileshPant1999@users.noreply.github.com> Date: Mon, 27 Nov 2023 12:19:18 +0530 Subject: [PATCH 3/3] Construct payload function for purchase invoice (#91) --- apps/sage300/exports/accounting_export.py | 4 +- apps/sage300/exports/base_model.py | 71 ++++++++++--------- .../exports/purchase_invoice/models.py | 18 ++--- .../exports/purchase_invoice/queues.py | 2 +- .../sage300/exports/purchase_invoice/tasks.py | 52 ++++++++++++-- ...ace_credit_card_last_synced_at_and_more.py | 1 - sage_desktop_sdk/apis/documents.py | 2 +- 7 files changed, 96 insertions(+), 54 deletions(-) diff --git a/apps/sage300/exports/accounting_export.py b/apps/sage300/exports/accounting_export.py index 64beade0..9f30de3d 100644 --- a/apps/sage300/exports/accounting_export.py +++ b/apps/sage300/exports/accounting_export.py @@ -15,7 +15,7 @@ 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. """ @@ -54,7 +54,7 @@ def create_sage300_object(self, accounting_export: AccountingExport): ) # 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 8e68867b..effe3c77 100644 --- a/apps/sage300/exports/base_model.py +++ b/apps/sage300/exports/base_model.py @@ -18,6 +18,7 @@ class BaseExportModel(models.Model): """ 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 @@ -167,7 +168,7 @@ def get_commitment_id(accounting_export: AccountingExport, expense: Expense): commitment_id = None source_id = None - if accounting_export: + if accounting_export and commitment_setting: if expense: if commitment_setting.source_field == 'PROJECT': source_id = expense.project_id @@ -204,16 +205,16 @@ def get_cost_code_id(accounting_export: AccountingExport, lineitem: Expense, dep return cost_code_id - def get_cost_type_id_or_none(expense_group: AccountingExport, lineitem: Expense, dependent_field_setting: DependentFieldSetting, project_id: str, cost_code_id: str): + 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_type = lineitem.custom_properties.get(dependent_field_setting.cost_type_field_name, 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_type + name=selected_cost_category ).first() if cost_category: @@ -237,26 +238,27 @@ def get_standard_category_id(accounting_export: AccountingExport, expense: Expen # Retrieve mapping settings for standard category standard_category_setting: MappingSetting = MappingSetting.objects.filter( workspace_id=accounting_export.workspace_id, - destination_field='CLASS' + destination_field='STANDARD_CATEGORY' ).first() - # Retrieve the attribute corresponding to the source field - attribute = ExpenseAttribute.objects.filter(attribute_type=standard_category_setting.source_field).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) + # 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() + # 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 + # If a mapping is found, retrieve the destination standard category ID + if mapping: + standard_category_id = mapping.destination.destination_id return standard_category_id @@ -276,25 +278,26 @@ def get_standard_cost_code_id(accounting_export: AccountingExport, expense: Expe # Retrieve mapping settings for standard cost code standard_cost_code_setting: MappingSetting = MappingSetting.objects.filter( workspace_id=accounting_export.workspace_id, - destination_field='CLASS' + destination_field='STANDARD_COST_CODE' ).first() - # Retrieve the attribute corresponding to the source field - attribute = ExpenseAttribute.objects.filter(attribute_type=standard_cost_code_setting.source_field).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) + # 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_CATEGORY', - source__value=source_value, - workspace_id=accounting_export.workspace_id - ).first() + # 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 + # 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/purchase_invoice/models.py b/apps/sage300/exports/purchase_invoice/models.py index f7fd1f37..af56c0b8 100644 --- a/apps/sage300/exports/purchase_invoice/models.py +++ b/apps/sage300/exports/purchase_invoice/models.py @@ -105,20 +105,21 @@ def create_or_update_object(self, accounting_export: AccountingExport, advance_s ).first() job_id = self.get_job_id(accounting_export, lineitem) - commitment_id = self.get_job_id(accounting_export, lineitem) - standard_category_id = self.get_job_id(accounting_export, lineitem) - standard_cost_code_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_or_none(accounting_export, lineitem, dependent_field_setting, job_id) - cost_code_id = self.get_cost_type_id_or_none(accounting_export, lineitem, dependent_field_setting, job_id, cost_category_id) + 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_objects, _ = PurchaseInvoiceLineitems.objects.update_or_create( + purchase_invoice_lineitem_object, _ = PurchaseInvoiceLineitems.objects.update_or_create( purchase_invoice_id=purchase_invoice.id, expense_id=lineitem.id, defaults={ - 'accounts_payable_account_id': account, + '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, @@ -128,5 +129,6 @@ def create_or_update_object(self, accounting_export: AccountingExport, advance_s 'description': description } ) + purchase_invoice_lineitem_objects.append(purchase_invoice_lineitem_object) - return purchase_invoice_lineitem_objects + return purchase_invoice_lineitem_objects diff --git a/apps/sage300/exports/purchase_invoice/queues.py b/apps/sage300/exports/purchase_invoice/queues.py index fd989f81..c3a6ff7e 100644 --- a/apps/sage300/exports/purchase_invoice/queues.py +++ b/apps/sage300/exports/purchase_invoice/queues.py @@ -20,7 +20,7 @@ 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='IN_PROGRESS', + status__in=['IN_PROGRESS', 'ENQUEUED'], workspace_id=workspace_id, id__in=accounting_export_ids, purchaseinvoice__id__isnull=True, exported_at__isnull=True ).all() diff --git a/apps/sage300/exports/purchase_invoice/tasks.py b/apps/sage300/exports/purchase_invoice/tasks.py index 40abb017..8efcf694 100644 --- a/apps/sage300/exports/purchase_invoice/tasks.py +++ b/apps/sage300/exports/purchase_invoice/tasks.py @@ -1,3 +1,5 @@ +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 @@ -23,12 +25,47 @@ def trigger_export(self, workspace_id, accounting_export_ids): """ 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 + + 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): """ @@ -36,14 +73,15 @@ def post(self, workspace_id, item, lineitem): """ try: purchase_invoice_payload = self.__construct_purchase_invoice(item, lineitem) - sage300_credentials = Sage300Credential.objects.get(workspace_id=workspace_id) - + sage300_credentials = Sage300Credential.objects.filter(workspace_id=workspace_id).first() # Establish a connection to Sage 300 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/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 2e66e962..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 @@ -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/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): """