From 2cba6e8648938de6b32492da19fcc31b767b7ef0 Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Fri, 15 Nov 2024 14:52:34 +0530 Subject: [PATCH 01/11] Feat: Add sync method for bank accounts, dimensions and dimension_values --- apps/business_central/utils.py | 46 +++- requirements.txt | 2 +- tests/test_business_central/fixtures.py | 247 ++++++++++++++-------- tests/test_business_central/test_utils.py | 56 ++++- 4 files changed, 260 insertions(+), 91 deletions(-) diff --git a/apps/business_central/utils.py b/apps/business_central/utils.py index 5c4a94a..501bbb4 100644 --- a/apps/business_central/utils.py +++ b/apps/business_central/utils.py @@ -114,13 +114,57 @@ def _sync_data(self, data, attribute_type, display_name, workspace_id, field_nam attribute_type, display_name, value, - item['number'] if item.get('number') else item['id'], + item['number'] if (item.get('number') and attribute_type != 'BANK_ACCOUNT') else item['id'], active, detail )) DestinationAttribute.bulk_create_or_update_destination_attributes( destination_attributes, attribute_type, workspace_id, True) + def sync_bank_accounts(self): + """ + sync business central bank accounts + """ + + bank_accounts = self.connection.bank_accounts.get_all() + field_names = ['currencyCode', 'intercompanyEnabled', 'number'] + + self._sync_data(bank_accounts, 'BANK_ACCOUNT', 'bank_account', self.workspace_id, field_names) + return [] + + def sync_dimensions(self): + """ + sync business central dimensions + """ + + dimensions = self.connection.dimensions.get_all_dimensions() + for dimension in dimensions: + dimension_attributes = [] + dimension_id = dimension["id"] + dimension_name = dimension["code"] + dimension_values = self.connection.dimensions.get_all_dimension_values( + dimension_id + ) + + for value in dimension_values: + detail = {"dimension_id": dimension_id} + dimension_attributes.append( + { + "attribute_type": dimension_name, + "display_name": dimension["displayName"], + "value": value["displayName"], + "destination_id": value["id"], + "detail": detail, + "active": True, + } + ) + + DestinationAttribute.bulk_create_or_update_destination_attributes( + dimension_attributes, dimension_name, self.workspace_id + ) + + return [] + def sync_companies(self): """ sync business central companies diff --git a/requirements.txt b/requirements.txt index a7189f9..1190e1e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,7 +28,7 @@ gunicorn==20.1.0 fyle==0.37.2 # Business central sdk -ms-dynamics-business-central-sdk==1.5.2 +ms-dynamics-business-central-sdk==1.6.0 # Reusable Fyle Packages fyle-rest-auth==1.7.2 diff --git a/tests/test_business_central/fixtures.py b/tests/test_business_central/fixtures.py index 070f0f2..150c217 100644 --- a/tests/test_business_central/fixtures.py +++ b/tests/test_business_central/fixtures.py @@ -10,12 +10,12 @@ "expense_id": "ABCDEF123", "fund_source": "PERSONAL", "employee_email": "ashwin.t@fyle.in", - "expense_number": "E/2023/04/T/31" + "expense_number": "E/2023/04/T/31", }, "status": "Exported", "detail": {"key": "value"}, "business_central_errors": {"error": "Something went wrong"}, - "exported_at": "2024-02-15T12:00:00Z" + "exported_at": "2024-02-15T12:00:00Z", }, "export_settings": { "workspace_id": 1, @@ -34,7 +34,7 @@ "journal_entry_folder_id": "folder123", "employee_field_mapping": "EMPLOYEE", "name_in_journal_entry": "EMPLOYEE", - "auto_map_employees": "NAME" + "auto_map_employees": "NAME", }, "advanced_setting": { "workspace_id": 1, @@ -44,7 +44,7 @@ "interval_hours": 24, "emails_selected": ["email1@example.com", "email2@example.com"], "emails_added": ["newemail@example.com"], - "auto_create_vendor": False + "auto_create_vendor": False, }, "employee_expense_attributes": { "attribute_type": "EMPLOYEE", @@ -81,105 +81,176 @@ "active": True, "destination_id": "dest_category123", }, - "expenses":[ + "expenses": [ { - 'id':91, - 'employee_email':'ashwin.t@fyle.in', - 'employee_name':'Joanna', - 'category':'Food', - 'sub_category':None, - 'project':'Aaron Abbott', - 'org_id':'or79Cob97KSh', - 'expense_id':'txxTi9ZfdepC', - 'expense_number':'E/2022/05/T/16', - 'claim_number':'C/2022/05/R/4', - 'report_title':'R/2022/05/R/4', - 'amount':50.0, - 'currency':'USD', - 'foreign_amount':None, - 'foreign_currency':None, - 'tax_amount':None, - 'tax_group_id':None, - 'reimbursable':True, - 'billable':False, - 'exported':False, - 'state':'PAYMENT_PROCESSING', - 'vendor':'Ashwin', - 'cost_center':'Marketing', - 'purpose':None, - 'report_id':'rpViBmuYmAgw', - 'corporate_card_id':None, - 'file_ids':[ - - ], - 'spent_at':'2022-05-13T17:00:00Z', - 'approved_at':'2022-05-13T09:30:13.484000Z', - 'posted_at': '2021-12-22T07:30:26.289842+00:00', - 'expense_created_at':'2022-05-13T09:29:43.535468Z', - 'expense_updated_at':'2022-05-13T09:32:06.643941Z', - 'created_at':'2022-05-23T11:11:28.241406Z', - 'updated_at':'2022-05-23T11:11:28.241440Z', - 'fund_source':'PERSONAL', - 'source_account_type': 'PERSONAL_CASH_ACCOUNT', - 'verified_at':'2022-05-23T11:11:28.241440Z', - 'custom_properties':{ - 'Team':'', - 'Class':'', - 'Klass':'', - 'Location':'', - 'Team Copy':'', - 'Tax Groups':'', - 'Departments':'', - 'Team 2 Postman':'', - 'User Dimension':'', - 'Location Entity':'', - 'Operating System':'', - 'System Operating':'', - 'User Dimension Copy':'', - 'Custom Expense Field':'None' + "id": 91, + "employee_email": "ashwin.t@fyle.in", + "employee_name": "Joanna", + "category": "Food", + "sub_category": None, + "project": "Aaron Abbott", + "org_id": "or79Cob97KSh", + "expense_id": "txxTi9ZfdepC", + "expense_number": "E/2022/05/T/16", + "claim_number": "C/2022/05/R/4", + "report_title": "R/2022/05/R/4", + "amount": 50.0, + "currency": "USD", + "foreign_amount": None, + "foreign_currency": None, + "tax_amount": None, + "tax_group_id": None, + "reimbursable": True, + "billable": False, + "exported": False, + "state": "PAYMENT_PROCESSING", + "vendor": "Ashwin", + "cost_center": "Marketing", + "purpose": None, + "report_id": "rpViBmuYmAgw", + "corporate_card_id": None, + "file_ids": [], + "spent_at": "2022-05-13T17:00:00Z", + "approved_at": "2022-05-13T09:30:13.484000Z", + "posted_at": "2021-12-22T07:30:26.289842+00:00", + "expense_created_at": "2022-05-13T09:29:43.535468Z", + "expense_updated_at": "2022-05-13T09:32:06.643941Z", + "created_at": "2022-05-23T11:11:28.241406Z", + "updated_at": "2022-05-23T11:11:28.241440Z", + "fund_source": "PERSONAL", + "source_account_type": "PERSONAL_CASH_ACCOUNT", + "verified_at": "2022-05-23T11:11:28.241440Z", + "custom_properties": { + "Team": "", + "Class": "", + "Klass": "", + "Location": "", + "Team Copy": "", + "Tax Groups": "", + "Departments": "", + "Team 2 Postman": "", + "User Dimension": "", + "Location Entity": "", + "Operating System": "", + "System Operating": "", + "User Dimension Copy": "", + "Custom Expense Field": "None", }, - 'paid_on_qbo':False, - 'payment_number':'P/2022/05/R/7' + "paid_on_qbo": False, + "payment_number": "P/2022/05/R/7", } ], "expense_fields": [ + {"field_name": "EMPLOYEE", "id": 1, "is_enabled": True}, + {"field_name": "CATEGORY", "id": 2, "is_enabled": True}, + {"field_name": "VENDOR", "id": 3, "is_enabled": True}, + {"field_name": "PROJECT", "id": 4, "is_enabled": True}, + {"field_name": "COST_CENTER", "id": 5, "is_enabled": True}, + {"field_name": "LOCATION", "id": 6, "is_enabled": True}, + ], + "included_fields": [ + "EMPLOYEE", + "CATEGORY", + "VENDOR", + "PROJECT", + "COST_CENTER", + "LOCATION", + ], + "bank_accounts": [ { - 'field_name': 'EMPLOYEE', - 'id': 1, - 'is_enabled': True + "id": "3dc7df61-85bb-ee11-9078-6045bda5d17e", + "number": "B010", + "displayName": "", + "lastModifiedDateTime": "2024-01-25T13:26:51.467Z", + "bankAccountNumber": "", + "blocked": False, + "currencyCode": "INR", + "currencyId": "00000000-0000-0000-0000-000000000000", + "iban": "", + "intercompanyEnabled": False, }, { - 'field_name': 'CATEGORY', - 'id': 2, - 'is_enabled': True + "id": "bbbb303f-4319-ee11-9cc4-6045bdc8dcac", + "number": "CHECKING", + "displayName": "World Wide Bank", + "lastModifiedDateTime": "2023-07-03T01:45:45.537Z", + "bankAccountNumber": "99-99-888", + "blocked": False, + "currencyCode": "INR", + "currencyId": "00000000-0000-0000-0000-000000000000", + "iban": "", + "intercompanyEnabled": False, }, { - 'field_name': 'VENDOR', - 'id': 3, - 'is_enabled': True + "id": "bcbb303f-4319-ee11-9cc4-6045bdc8dcac", + "number": "GIRO", + "displayName": "Giro Bank", + "lastModifiedDateTime": "2023-07-03T01:45:45.573Z", + "bankAccountNumber": "14-55-678", + "blocked": False, + "currencyCode": "INR", + "currencyId": "00000000-0000-0000-0000-000000000000", + "iban": "GB 80 RBOS 161732 41116737", + "intercompanyEnabled": False, }, { - 'field_name': 'PROJECT', - 'id': 4, - 'is_enabled': True + "id": "bdbb303f-4319-ee11-9cc4-6045bdc8dcac", + "number": "NBL", + "displayName": "New Bank of London", + "lastModifiedDateTime": "2023-07-03T01:45:45.597Z", + "bankAccountNumber": "78-66-345", + "blocked": False, + "currencyCode": "INR", + "currencyId": "00000000-0000-0000-0000-000000000000", + "iban": "", + "intercompanyEnabled": False, }, { - 'field_name': 'COST_CENTER', - 'id': 5, - 'is_enabled': True + "id": "bebb303f-4319-ee11-9cc4-6045bdc8dcac", + "number": "SAVINGS", + "displayName": "World Wide Bank", + "lastModifiedDateTime": "2023-07-03T01:45:45.62Z", + "bankAccountNumber": "99-44-567", + "blocked": False, + "currencyCode": "INR", + "currencyId": "00000000-0000-0000-0000-000000000000", + "iban": "", + "intercompanyEnabled": False, }, + ], + "dimensions": [ { - 'field_name': 'LOCATION', - 'id': 6, - 'is_enabled': True - } + "id": "5bbc303f-4319-ee11-9cc4-6045bdc8dcac", + "code": "AREA", + "displayName": "Area", + "consolidationCode": "", + "lastModifiedDateTime": "2023-07-03T01:45:46.687Z", + }, + ], + "dimension_values": [ + { + "id": "1b4a2945-4319-ee11-9cc4-6045bdc8dcac", + "code": "LARGE", + "dimensionId": "5dbc303f-4319-ee11-9cc4-6045bdc8dcac", + "displayName": "Large Business", + "consolidationCode": "", + "lastModifiedDateTime": "2023-07-03T01:46:02.817Z", + }, + { + "id": "1c4a2945-4319-ee11-9cc4-6045bdc8dcac", + "code": "MEDIUM", + "dimensionId": "5dbc303f-4319-ee11-9cc4-6045bdc8dcac", + "displayName": "Medium Business", + "consolidationCode": "", + "lastModifiedDateTime": "2023-07-03T01:46:02.82Z", + }, + { + "id": "1d4a2945-4319-ee11-9cc4-6045bdc8dcac", + "code": "SMALL", + "dimensionId": "5dbc303f-4319-ee11-9cc4-6045bdc8dcac", + "displayName": "Small Business", + "consolidationCode": "", + "lastModifiedDateTime": "2023-07-03T01:46:02.823Z", + }, ], - "included_fields": [ - 'EMPLOYEE', - 'CATEGORY', - 'VENDOR', - 'PROJECT', - 'COST_CENTER', - 'LOCATION' - ] } diff --git a/tests/test_business_central/test_utils.py b/tests/test_business_central/test_utils.py index 6f6cc5f..916b16c 100644 --- a/tests/test_business_central/test_utils.py +++ b/tests/test_business_central/test_utils.py @@ -1,7 +1,7 @@ from apps.workspaces.models import Workspace from datetime import datetime from fyle_accounting_mappings.models import DestinationAttribute, Mapping, CategoryMapping - +from tests.test_business_central.fixtures import data def test_post_attachments( db, @@ -195,3 +195,57 @@ def test_skip_sync_attributes(mocker, db, create_business_central_connection): accounts = DestinationAttribute.objects.filter(attribute_type='ACCOUNT', workspace_id=1).count() assert accounts == 0 + + +def test_sync_bank_accounts(mocker, db, create_business_central_connection): + workspace_id = 1 + business_central_connection = create_business_central_connection + + mocker.patch.object( + business_central_connection.connection.bank_accounts, + 'get_all', + return_value=data['bank_accounts'] + ) + + bank_account_count = DestinationAttribute.objects.filter( + workspace_id=workspace_id, attribute_type="BANK_ACCOUNT" + ).count() + + assert bank_account_count == 0 + business_central_connection.sync_bank_accounts() + + bank_accounts = DestinationAttribute.objects.filter( + workspace_id=workspace_id, attribute_type="BANK_ACCOUNT" + ) + + assert bank_accounts.count() == 4 + + +def test_sync_dimension(mocker, db, create_business_central_connection): + + workspace_id = 1 + business_central_connection = create_business_central_connection + + mocker.patch.object( + business_central_connection.connection.dimensions, + 'get_all_dimensions', + return_value=data['dimensions'] + ) + + mocker.patch.object( + business_central_connection.connection.dimensions, + 'get_all_dimension_values', + return_value=data['dimension_values'] + ) + + area_count = DestinationAttribute.objects.filter( + workspace_id=workspace_id, attribute_type="AREA" + ).count() + + assert area_count == 0 + business_central_connection.sync_dimensions() + + areas = DestinationAttribute.objects.filter( + workspace_id=workspace_id, attribute_type="AREA" + ) + assert areas.count() == 3 From 44c18a3153424008e2d6bb9443bfba1e63da072c Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Fri, 15 Nov 2024 14:56:42 +0530 Subject: [PATCH 02/11] pylint solve --- tests/test_business_central/test_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_business_central/test_utils.py b/tests/test_business_central/test_utils.py index 916b16c..c6ea104 100644 --- a/tests/test_business_central/test_utils.py +++ b/tests/test_business_central/test_utils.py @@ -3,6 +3,7 @@ from fyle_accounting_mappings.models import DestinationAttribute, Mapping, CategoryMapping from tests.test_business_central.fixtures import data + def test_post_attachments( db, mocker, From 83ede99460f9a9fc6f8a03655c826d194d395394 Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Fri, 15 Nov 2024 16:21:51 +0530 Subject: [PATCH 03/11] minor changes --- apps/business_central/helpers.py | 2 +- apps/business_central/utils.py | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/business_central/helpers.py b/apps/business_central/helpers.py index 077f2b7..838d0e0 100644 --- a/apps/business_central/helpers.py +++ b/apps/business_central/helpers.py @@ -48,7 +48,7 @@ def sync_dimensions(business_central_credential: BusinessCentralCredentials, wor business_central_connection = import_string('apps.business_central.utils.BusinessCentralConnector')(business_central_credential, workspace_id) # List of dimensions to sync - dimensions = ['companies', 'accounts', 'vendors', 'employees', 'locations'] + dimensions = ['companies', 'accounts', 'vendors', 'employees', 'locations', 'bank_accounts', 'dimensions'] for dimension in dimensions: try: diff --git a/apps/business_central/utils.py b/apps/business_central/utils.py index 501bbb4..a846e0e 100644 --- a/apps/business_central/utils.py +++ b/apps/business_central/utils.py @@ -140,22 +140,22 @@ def sync_dimensions(self): dimensions = self.connection.dimensions.get_all_dimensions() for dimension in dimensions: dimension_attributes = [] - dimension_id = dimension["id"] - dimension_name = dimension["code"] + dimension_id = dimension['id'] + dimension_name = dimension['code'] dimension_values = self.connection.dimensions.get_all_dimension_values( dimension_id ) for value in dimension_values: - detail = {"dimension_id": dimension_id} + detail = {'dimension_id': dimension_id, 'code': value['code']} dimension_attributes.append( { - "attribute_type": dimension_name, - "display_name": dimension["displayName"], - "value": value["displayName"], - "destination_id": value["id"], - "detail": detail, - "active": True, + 'attribute_type': dimension_name, + 'display_name': dimension['displayName'], + 'value': value['displayName'], + 'destination_id': value['id'], + 'detail': detail, + 'active': True, } ) From 085d86c13ace69bf6d9b345b29202e8b52b79d92 Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Fri, 15 Nov 2024 18:03:42 +0530 Subject: [PATCH 04/11] added limit for bank account and dimensions --- apps/business_central/utils.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/apps/business_central/utils.py b/apps/business_central/utils.py index a846e0e..058426a 100644 --- a/apps/business_central/utils.py +++ b/apps/business_central/utils.py @@ -16,7 +16,10 @@ SYNC_UPPER_LIMIT = { 'accounts': 2000, 'vendors': 10000, - 'locations': 1000 + 'locations': 1000, + 'bank_accounts':2000, + 'dimension_values': 1000, + 'dimensions': 1000 } @@ -125,7 +128,10 @@ def sync_bank_accounts(self): """ sync business central bank accounts """ - + attribute_count = self.connection.bank_accounts.count() + if not self.is_sync_allowed(attribute_type = 'accounts', attribute_count=attribute_count): + logger.info('Skipping sync of bank accounts for workspace %s as it has %s counts which is over the limit', self.workspace_id, attribute_count) + return bank_accounts = self.connection.bank_accounts.get_all() field_names = ['currencyCode', 'intercompanyEnabled', 'number'] @@ -137,15 +143,24 @@ def sync_dimensions(self): sync business central dimensions """ + attribute_count = self.connection.dimensions.count() + if not self.is_sync_allowed(attribute_type = 'dimensions', attribute_count=attribute_count): + logger.info('Skipping sync of dimensions for workspace %s as it has %s counts which is over the limit', self.workspace_id, attribute_count) + return dimensions = self.connection.dimensions.get_all_dimensions() for dimension in dimensions: dimension_attributes = [] dimension_id = dimension['id'] dimension_name = dimension['code'] + + attribute_count = self.connection.dimensions.count_dimension_values(dimension_id) + if not self.is_sync_allowed(attribute_type = 'dimension_values', attribute_count=attribute_count): + logger.info('Skipping sync of dimension_values %s for workspace %s as it has %s counts which is over the limit', dimension_name, self.workspace_id, attribute_count) + continue + dimension_values = self.connection.dimensions.get_all_dimension_values( dimension_id ) - for value in dimension_values: detail = {'dimension_id': dimension_id, 'code': value['code']} dimension_attributes.append( @@ -180,7 +195,7 @@ def sync_accounts(self): Synchronize accounts from MS Dynamics SDK to your application """ attribute_count = self.connection.accounts.count() - if not self.is_sync_allowed(attribute_type = 'accounts', attribute_count=attribute_count): + if not self.is_sync_allowed(attribute_type = 'bank_accounts', attribute_count=attribute_count): logger.info('Skipping sync of accounts for workspace %s as it has %s counts which is over the limit', self.workspace_id, attribute_count) return field_names = ['category', 'subCategory', 'accountType', 'directPosting', 'lastModifiedDateTime'] From c638418df6cdfa064a6efc36b11da4e255999a3a Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Fri, 15 Nov 2024 18:10:27 +0530 Subject: [PATCH 05/11] fix test --- tests/test_business_central/test_utils.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_business_central/test_utils.py b/tests/test_business_central/test_utils.py index c6ea104..616f52e 100644 --- a/tests/test_business_central/test_utils.py +++ b/tests/test_business_central/test_utils.py @@ -208,6 +208,12 @@ def test_sync_bank_accounts(mocker, db, create_business_central_connection): return_value=data['bank_accounts'] ) + mocker.patch.object( + business_central_connection.connection.bank_accounts, + 'count', + return_value=5 + ) + bank_account_count = DestinationAttribute.objects.filter( workspace_id=workspace_id, attribute_type="BANK_ACCOUNT" ).count() @@ -233,12 +239,24 @@ def test_sync_dimension(mocker, db, create_business_central_connection): return_value=data['dimensions'] ) + mocker.patch.object( + business_central_connection.connection.dimensions, + 'count', + return_value=5 + ) + mocker.patch.object( business_central_connection.connection.dimensions, 'get_all_dimension_values', return_value=data['dimension_values'] ) + mocker.patch.object( + business_central_connection.connection.dimensions, + 'count_dimension_values', + return_value=5 + ) + area_count = DestinationAttribute.objects.filter( workspace_id=workspace_id, attribute_type="AREA" ).count() From 382a3a4fcb63e0cbdaa528c9db9a1d2c3d411de2 Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Fri, 22 Nov 2024 14:43:49 +0530 Subject: [PATCH 06/11] comment resolved --- apps/business_central/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/business_central/utils.py b/apps/business_central/utils.py index 058426a..fd048d4 100644 --- a/apps/business_central/utils.py +++ b/apps/business_central/utils.py @@ -129,7 +129,7 @@ def sync_bank_accounts(self): sync business central bank accounts """ attribute_count = self.connection.bank_accounts.count() - if not self.is_sync_allowed(attribute_type = 'accounts', attribute_count=attribute_count): + if not self.is_sync_allowed(attribute_type = 'bank_accounts', attribute_count=attribute_count): logger.info('Skipping sync of bank accounts for workspace %s as it has %s counts which is over the limit', self.workspace_id, attribute_count) return bank_accounts = self.connection.bank_accounts.get_all() @@ -195,7 +195,7 @@ def sync_accounts(self): Synchronize accounts from MS Dynamics SDK to your application """ attribute_count = self.connection.accounts.count() - if not self.is_sync_allowed(attribute_type = 'bank_accounts', attribute_count=attribute_count): + if not self.is_sync_allowed(attribute_type = 'accounts', attribute_count=attribute_count): logger.info('Skipping sync of accounts for workspace %s as it has %s counts which is over the limit', self.workspace_id, attribute_count) return field_names = ['category', 'subCategory', 'accountType', 'directPosting', 'lastModifiedDateTime'] From 3e3e4fe849f632f10e6ded7326f6cf6609a30b7d Mon Sep 17 00:00:00 2001 From: Ashutosh singh <55102089+Ashutosh619-sudo@users.noreply.github.com> Date: Fri, 22 Nov 2024 14:46:48 +0530 Subject: [PATCH 07/11] Feat: Support for dimension during export Journal Entry and purchase Invoice (#174) * Feat: Support for dimension during export Journal Entry and purchase Invoice * comment resolved * added dimension support for JE and bug fix * implementation of exporting dimensions for JE and PI * bug fixed * comment resolved * flake fixed * Feat: Adding new Journal Entry changes (#175) * Feat: Adding new Journal Entry changes * comment resolved --- apps/business_central/exports/base_model.py | 40 +++++++++ .../exports/journal_entry/models.py | 87 +++++++++++-------- .../exports/journal_entry/tasks.py | 51 +++++++++-- .../exports/purchase_invoice/models.py | 9 +- .../exports/purchase_invoice/tasks.py | 53 ++++++++++- ...urnalentrylineitems_dimensions_and_more.py | 23 +++++ ...ylineitems_dimension_error_log_and_more.py | 23 +++++ ...ineitems_dimension_success_log_and_more.py | 33 +++++++ apps/business_central/utils.py | 46 ++++++++++ apps/mappings/imports/modules/base.py | 2 +- 10 files changed, 317 insertions(+), 50 deletions(-) create mode 100644 apps/business_central/migrations/0002_journalentrylineitems_dimensions_and_more.py create mode 100644 apps/business_central/migrations/0003_journalentrylineitems_dimension_error_log_and_more.py create mode 100644 apps/business_central/migrations/0004_journalentrylineitems_dimension_success_log_and_more.py diff --git a/apps/business_central/exports/base_model.py b/apps/business_central/exports/base_model.py index 0b9bebd..a51ac26 100644 --- a/apps/business_central/exports/base_model.py +++ b/apps/business_central/exports/base_model.py @@ -182,3 +182,43 @@ def get_location_id(accounting_export: AccountingExport, lineitem: Expense): if mapping: location_id = mapping.destination.destination_id return location_id + + def get_dimension_object(accounting_export: AccountingExport, lineitem: Expense): + mapping_settings = MappingSetting.objects.filter(workspace_id=accounting_export.workspace_id).all() + + dimensions = [] + default_expense_attributes = ['CATEGORY', 'EMPLOYEE'] + default_destination_attributes = ['COMPANIES', 'ACCOUNTS', 'VENDORS', 'EMPLOYEES', 'LOCATIONS', 'BANK_ACCOUNTS'] + + for setting in mapping_settings: + if setting.source_field not in default_expense_attributes and \ + setting.destination_field not in default_destination_attributes: + if setting.source_field == 'PROJECT': + source_value = lineitem.project + elif setting.source_field == 'COST_CENTER': + source_value = lineitem.cost_center + else: + attribute = ExpenseAttribute.objects.filter( + attribute_type=setting.source_field, + workspace_id=accounting_export.workspace_id + ).first() + source_value = lineitem.custom_properties.get(attribute.display_name, None) + + mapping: Mapping = Mapping.objects.filter( + source_type=setting.source_field, + destination_type=setting.destination_field, + source__value=source_value, + workspace_id=accounting_export.workspace_id + ).first() + + if mapping: + dimension_data = { + 'id': mapping.destination.detail['dimension_id'], + 'code': mapping.destination.attribute_type, + 'valueId': mapping.destination.destination_id, + 'valueCode': mapping.destination.detail['code'], + 'expense_number': lineitem.expense_number + } + dimensions.append(dimension_data) + + return dimensions diff --git a/apps/business_central/exports/journal_entry/models.py b/apps/business_central/exports/journal_entry/models.py index 3b8971a..bbb89e6 100644 --- a/apps/business_central/exports/journal_entry/models.py +++ b/apps/business_central/exports/journal_entry/models.py @@ -1,4 +1,6 @@ from django.db import models +from django.db.models import JSONField + from fyle_accounting_mappings.models import CategoryMapping from apps.accounting_exports.models import AccountingExport @@ -39,22 +41,26 @@ def create_or_update_object(self, accounting_export: AccountingExport, _: Advanc :param accounting_export: expense group :return: purchase invoices object """ - expenses = accounting_export.expenses.all() + expense = accounting_export.expenses.first() accounts_payable_account_id = export_settings.default_bank_account_id - document_number = accounting_export.description['claim_number'] if accounting_export.description and accounting_export.description.get('claim_number') else accounting_export.description['expense_number'] + advance_setting = AdvancedSetting.objects.get(workspace_id=accounting_export.workspace_id) + + print(accounting_export.__dict__) + + document_number = expense.expense_number - comment = "Consolidated Credit Entry For Report/Expense {}".format(document_number) + comment = self.get_expense_comment(accounting_export.workspace_id, expense, expense.category, advance_setting) invoice_date = self.get_invoice_date(accounting_export=accounting_export) - account_type, account_id = self.get_account_id_type(accounting_export=accounting_export, export_settings=export_settings) + account_type, account_id = self.get_account_id_type(accounting_export=accounting_export, export_settings=export_settings, merchant=expense.vendor) journal_entry_object, _ = JournalEntry.objects.update_or_create( accounting_export= accounting_export, defaults={ - 'amount': sum([expense.amount for expense in expenses]), + 'amount': expense.amount, 'document_number': document_number, 'accounts_payable_account_id': accounts_payable_account_id, 'account_id': account_id, @@ -83,6 +89,9 @@ class JournalEntryLineItems(BaseExportModel): invoice_date = CustomDateTimeField(help_text='date of invoice') document_number = TextNotNullField(help_text='document number of the invoice') journal_entry = models.ForeignKey(JournalEntry, on_delete=models.PROTECT, help_text='Journal Entry reference', related_name='journal_entry_lineitems') + dimensions = JSONField(default=list, help_text='Business Central dimensions') + dimension_error_log = JSONField(null=True, help_text='dimension set response log') + dimension_success_log = JSONField(null=True, help_text='dimension set success response log') class Meta: db_table = 'journal_entries_lineitems' @@ -94,42 +103,44 @@ def create_or_update_object(self, accounting_export: AccountingExport, advance_s :param accounting_export: expense group :return: purchase invoices object """ - expenses = accounting_export.expenses.all() + lineitem = accounting_export.expenses.first() journal_entry = JournalEntry.objects.get(accounting_export=accounting_export) journal_entry_lineitems = [] - for lineitem in expenses: - category = lineitem.category if (lineitem.category == lineitem.sub_category or lineitem.sub_category == None) else '{0} / {1}'.format(lineitem.category, lineitem.sub_category) - - account = CategoryMapping.objects.filter( - source_category__value=category, - workspace_id=accounting_export.workspace_id - ).first() - - comment = self.get_expense_comment(accounting_export.workspace_id, lineitem, lineitem.category, advance_setting) - - invoice_date = self.get_invoice_date(accounting_export=accounting_export) - - account_type, account_id = self.get_account_id_type(accounting_export=accounting_export, export_settings=export_settings, merchant=lineitem.vendor) - - document_number = accounting_export.description['claim_number'] if accounting_export.description and accounting_export.description.get('claim_number') else accounting_export.description['expense_number'] - - journal_entry_lineitems_object, _ = JournalEntryLineItems.objects.update_or_create( - journal_entry_id = journal_entry.id, - expense_id=lineitem.id, - defaults={ - 'amount': lineitem.amount * -1, - 'account_id': account_id, - 'account_type': account_type, - 'document_number': document_number, - 'accounts_payable_account_id': account.destination_account.destination_id, - 'comment': comment, - 'workspace_id': accounting_export.workspace_id, - 'invoice_date': invoice_date, - 'description': lineitem.purpose if lineitem.purpose else None - } - ) - journal_entry_lineitems.append(journal_entry_lineitems_object) + category = lineitem.category if (lineitem.category == lineitem.sub_category or lineitem.sub_category == None) else '{0} / {1}'.format(lineitem.category, lineitem.sub_category) + + account = CategoryMapping.objects.filter( + source_category__value=category, + workspace_id=accounting_export.workspace_id + ).first() + + comment = self.get_expense_comment(accounting_export.workspace_id, lineitem, lineitem.category, advance_setting) + + invoice_date = self.get_invoice_date(accounting_export=accounting_export) + + account_type, account_id = self.get_account_id_type(accounting_export=accounting_export, export_settings=export_settings, merchant=lineitem.vendor) + + document_number = lineitem.expense_number + + dimensions = self.get_dimension_object(accounting_export, lineitem) + + journal_entry_lineitems_object, _ = JournalEntryLineItems.objects.update_or_create( + journal_entry_id = journal_entry.id, + expense_id=lineitem.id, + defaults={ + 'amount': lineitem.amount * -1, + 'account_id': account_id, + 'account_type': account_type, + 'document_number': document_number, + 'accounts_payable_account_id': account.destination_account.destination_id, + 'comment': comment, + 'workspace_id': accounting_export.workspace_id, + 'invoice_date': invoice_date, + 'description': lineitem.purpose if lineitem.purpose else None, + 'dimensions': dimensions + } + ) + journal_entry_lineitems.append(journal_entry_lineitems_object) return journal_entry_lineitems diff --git a/apps/business_central/exports/journal_entry/tasks.py b/apps/business_central/exports/journal_entry/tasks.py index 243ecc6..8a3262a 100644 --- a/apps/business_central/exports/journal_entry/tasks.py +++ b/apps/business_central/exports/journal_entry/tasks.py @@ -11,6 +11,8 @@ from apps.business_central.utils import BusinessCentralConnector from apps.workspaces.models import BusinessCentralCredentials +from fyle_accounting_mappings.models import DestinationAttribute + logger = logging.getLogger(__name__) logger.level = logging.INFO @@ -40,6 +42,13 @@ def __construct_journal_entry(self, body: JournalEntry, lineitems: List[JournalE :return: constructed expense_report ''' batch_journal_entry_payload = [] + dimensions = [] + + account_attribute_type = DestinationAttribute.objects.filter(workspace_id=body.workspace_id, destination_id=body.accounts_payable_account_id).first() + + balance_account_type = 'G/L Account' + if account_attribute_type and account_attribute_type.attribute_type == 'BANK_ACCOUNT': + balance_account_type = 'Bank Account' journal_entry_payload = { 'accountType': body.account_type, @@ -49,7 +58,7 @@ def __construct_journal_entry(self, body: JournalEntry, lineitems: List[JournalE 'amount': body.amount, 'comment': body.comment, 'description': body.description, - 'balanceAccountType': 'G/L Account', + 'balanceAccountType': balance_account_type, 'balancingAccountNumber': body.accounts_payable_account_id } @@ -57,6 +66,10 @@ def __construct_journal_entry(self, body: JournalEntry, lineitems: List[JournalE batch_journal_entry_payload.append(journal_entry_payload) for lineitem in lineitems: + for dimension in lineitem.dimensions: + dimension['exported_module_id'] = lineitem.id + + dimensions.extend(lineitem.dimensions) journal_entry_lineitem_payload = { 'accountType': lineitem.account_type, 'accountNumber': lineitem.account_id, @@ -65,21 +78,21 @@ def __construct_journal_entry(self, body: JournalEntry, lineitems: List[JournalE 'amount': lineitem.amount, 'comment': lineitem.comment, 'description': lineitem.description if lineitem.description else '', - 'balanceAccountType': 'G/L Account', + 'balanceAccountType': balance_account_type, 'balancingAccountNumber': lineitem.accounts_payable_account_id } batch_journal_entry_payload.append(journal_entry_lineitem_payload) - return batch_journal_entry_payload + return batch_journal_entry_payload, dimensions def post(self, accounting_export, item, lineitem): ''' Export the Journal Entry to Business Central. ''' - batch_journal_entry_payload = self.__construct_journal_entry(item, lineitem) - logger.info('WORKSPACE_ID: {0}, ACCOUNTING_EXPORT_ID: {1}, BATCH_PURCHASE_INVOICE_PAYLOAD: {2}'.format(accounting_export.workspace_id, accounting_export.id, batch_journal_entry_payload)) + batch_journal_entry_payload, dimensions = self.__construct_journal_entry(item, lineitem) + logger.info('WORKSPACE_ID: {0}, ACCOUNTING_EXPORT_ID: {1}, JOURNAL_ENTRY_PAYLOAD: {2}'.format(accounting_export.workspace_id, accounting_export.id, batch_journal_entry_payload)) business_central_credentials = BusinessCentralCredentials.get_active_business_central_credentials(accounting_export.workspace_id) # Establish a connection to Business Central business_central_connection = BusinessCentralConnector(business_central_credentials, accounting_export.workspace_id) @@ -87,8 +100,17 @@ def post(self, accounting_export, item, lineitem): # Post the journal entry to Business Central response = business_central_connection.bulk_post_journal_lineitems(batch_journal_entry_payload, accounting_export) - expenses = accounting_export.expenses.all() + if dimensions: + dimension_set_line_payloads = self.construct_dimension_set_line_payload(dimensions, response['responses']) + logger.info('WORKSPACE_ID: {0}, ACCOUNTING_EXPORT_ID: {1}, DIMENSION_SET_LINE_PAYLOADS: {2}'.format(accounting_export.workspace_id, accounting_export.id, dimension_set_line_payloads)) + dimension_line_responses = ( + business_central_connection.post_dimension_lines( + dimension_set_line_payloads, "JOURNAL_ENTRY", item.id + ) + ) + response["dimension_line_responses"] = dimension_line_responses + expenses = accounting_export.expenses.all() # Load attachments to Business Central for i in range(1, len(response["responses"])): load_attachments( @@ -100,6 +122,23 @@ def post(self, accounting_export, item, lineitem): return response + def construct_dimension_set_line_payload(self, dimensions: list, exported_response: dict): + """ + construct payload for setting dimension for Journal Entry + """ + + dimension_payload = [] + + for response in exported_response: + parent_id = response['body']['id'] + for dimension in dimensions: + dimension_copy = dimension.copy() + dimension_copy.pop('expense_number') + dimension_copy['parentId'] = parent_id + dimension_payload.append(dimension_copy) + + return dimension_payload + @handle_business_central_exceptions() def create_journal_entry(accounting_export: AccountingExport, last_export: bool): diff --git a/apps/business_central/exports/purchase_invoice/models.py b/apps/business_central/exports/purchase_invoice/models.py index b52d027..0379955 100644 --- a/apps/business_central/exports/purchase_invoice/models.py +++ b/apps/business_central/exports/purchase_invoice/models.py @@ -1,6 +1,8 @@ from typing import List from django.db import models +from django.db.models import JSONField + from fyle_accounting_mappings.models import CategoryMapping from apps.accounting_exports.models import AccountingExport @@ -61,6 +63,9 @@ class PurchaseInvoiceLineitems(BaseExportModel): amount = FloatNullField(help_text='Amount of the invoice') description = TextNotNullField(help_text='description for the invoice') location_id = StringNullField(help_text='location id of the invoice') + dimensions = JSONField(default=list, help_text='Business Central dimensions') + dimension_error_log = JSONField(null=True, help_text='dimension set response log') + dimension_success_log = JSONField(null=True, help_text='dimension set success response log') class Meta: db_table = 'purchase_invoice_lineitems' @@ -88,6 +93,7 @@ def create_or_update_object(self, accounting_export: AccountingExport, advance_s description = self.get_expense_purpose(lineitem, lineitem.category, advance_setting) location_id = self.get_location_id(accounting_export, lineitem) + dimensions = self.get_dimension_object(accounting_export, lineitem) purchase_invoice_lineitem_object, _ = PurchaseInvoiceLineitems.objects.update_or_create( purchase_invoice_id=purchase_invoice.id, @@ -97,7 +103,8 @@ def create_or_update_object(self, accounting_export: AccountingExport, advance_s 'accounts_payable_account_id': account.destination_account.destination_id if account else None, 'description': description, 'workspace_id': accounting_export.workspace_id, - 'location_id': location_id + 'location_id': location_id, + 'dimensions': dimensions } ) purchase_invoice_lineitem_objects.append(purchase_invoice_lineitem_object) diff --git a/apps/business_central/exports/purchase_invoice/tasks.py b/apps/business_central/exports/purchase_invoice/tasks.py index 896576b..30f7cd6 100644 --- a/apps/business_central/exports/purchase_invoice/tasks.py +++ b/apps/business_central/exports/purchase_invoice/tasks.py @@ -41,6 +41,7 @@ def __construct_purchase_invoice(self, body: PurchaseInvoice, lineitems: List[Pu :return: constructed expense_report ''' batch_purchase_invoice_lineitem_payload = [] + dimensions = [] purchase_invoice_payload = { 'vendorNumber': body.vendor_number, @@ -49,26 +50,32 @@ def __construct_purchase_invoice(self, body: PurchaseInvoice, lineitems: List[Pu } for lineitem in lineitems: + for dimension in lineitem.dimensions: + dimension['exported_module_id'] = lineitem.id + + print('lineitem.dimensions', lineitem.dimensions) + + dimensions.extend(lineitem.dimensions) purchase_invoice_lineitem_payload = { "lineType": "Account", 'lineObjectNumber': lineitem.accounts_payable_account_id, 'unitCost': lineitem.amount, 'quantity': 1, - 'description': lineitem.description if lineitem.description else '' + 'description': lineitem.description if lineitem.description else '', + 'description2': lineitem.expense.expense_number } if lineitem.location_id: purchase_invoice_lineitem_payload['locationId'] = lineitem.location_id batch_purchase_invoice_lineitem_payload.append(purchase_invoice_lineitem_payload) - return purchase_invoice_payload, batch_purchase_invoice_lineitem_payload + return purchase_invoice_payload, batch_purchase_invoice_lineitem_payload, dimensions def post(self, accounting_export, item, lineitem): ''' Export the Journal Entry to Business Central. ''' - - purchase_invoice_payload, batch_purchase_invoice_payload = self.__construct_purchase_invoice(item, lineitem) + purchase_invoice_payload, batch_purchase_invoice_payload, dimensions = self.__construct_purchase_invoice(item, lineitem) logger.info('WORKSPACE_ID: {0}, ACCOUNTING_EXPORT_ID: {1}, PURCHASE_INVOICE_PAYLOAD: {2}, BATCH_PURCHASE_INVOICE_PAYLOAD: {3}'.format(accounting_export.workspace_id, accounting_export.id, purchase_invoice_payload, batch_purchase_invoice_payload)) business_central_credentials = BusinessCentralCredentials.get_active_business_central_credentials(accounting_export.workspace_id) # Establish a connection to Business Central @@ -76,6 +83,14 @@ def post(self, accounting_export, item, lineitem): response = business_central_connection.post_purchase_invoice(purchase_invoice_payload, batch_purchase_invoice_payload) + if dimensions: + dimension_set_line_payloads = self.construct_dimension_set_line_payload(dimensions, response['bulk_post_response']['responses']) + logger.info('WORKSPACE_ID: {0}, ACCOUNTING_EXPORT_ID: {1}, DIMENSION_SET_LINE_PAYLOADS: {2}'.format(accounting_export.workspace_id, accounting_export.id, dimension_set_line_payloads)) + dimension_line_responses = business_central_connection.post_dimension_lines( + dimension_set_line_payloads, 'PURCHASE_INVOICE', item.id + ) + response['dimension_line_responses'] = dimension_line_responses + expenses = accounting_export.expenses.all() # Load attachments to Business Central @@ -89,6 +104,36 @@ def post(self, accounting_export, item, lineitem): return response + def construct_dimension_set_line_payload(self, dimensions: list, exported_response: dict): + + """ + construct payload for setting dimension for Purchase Invoice + """ + + dimension_payload = [] + + document_mapping = { + response['body']['description2']: response['body']['id'] + for response in exported_response + if response.get('body') and 'description2' in response['body'] and 'id' in response['body'] + } + + for dimension in dimensions: + expense_number = dimension.get('expense_number') + parent_id = document_mapping.get(expense_number) + + if parent_id: + dimension_payload.append({ + "id": dimension['id'], + "code": dimension['code'], + "parentId": parent_id, + "valueId": dimension['valueId'], + "valueCode": dimension['valueCode'], + "exported_module_id": dimension['exported_module_id'] + }) + + return dimension_payload + @handle_business_central_exceptions() def create_purchase_invoice(accounting_export: AccountingExport, last_export: bool): diff --git a/apps/business_central/migrations/0002_journalentrylineitems_dimensions_and_more.py b/apps/business_central/migrations/0002_journalentrylineitems_dimensions_and_more.py new file mode 100644 index 0000000..04bdc58 --- /dev/null +++ b/apps/business_central/migrations/0002_journalentrylineitems_dimensions_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.2 on 2024-11-15 12:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('business_central', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='journalentrylineitems', + name='dimensions', + field=models.JSONField(help_text='Business Central dimensions', null=True), + ), + migrations.AddField( + model_name='purchaseinvoicelineitems', + name='dimensions', + field=models.JSONField(help_text='Business Central dimensions', null=True), + ), + ] diff --git a/apps/business_central/migrations/0003_journalentrylineitems_dimension_error_log_and_more.py b/apps/business_central/migrations/0003_journalentrylineitems_dimension_error_log_and_more.py new file mode 100644 index 0000000..6760033 --- /dev/null +++ b/apps/business_central/migrations/0003_journalentrylineitems_dimension_error_log_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.2 on 2024-11-18 08:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('business_central', '0002_journalentrylineitems_dimensions_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='journalentrylineitems', + name='dimension_error_log', + field=models.JSONField(help_text='dimension set response log', null=True), + ), + migrations.AddField( + model_name='purchaseinvoicelineitems', + name='dimension_error_log', + field=models.JSONField(help_text='dimension set response log', null=True), + ), + ] diff --git a/apps/business_central/migrations/0004_journalentrylineitems_dimension_success_log_and_more.py b/apps/business_central/migrations/0004_journalentrylineitems_dimension_success_log_and_more.py new file mode 100644 index 0000000..027eaeb --- /dev/null +++ b/apps/business_central/migrations/0004_journalentrylineitems_dimension_success_log_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 4.1.2 on 2024-11-21 09:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('business_central', '0003_journalentrylineitems_dimension_error_log_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='journalentrylineitems', + name='dimension_success_log', + field=models.JSONField(help_text='dimension set success response log', null=True), + ), + migrations.AddField( + model_name='purchaseinvoicelineitems', + name='dimension_success_log', + field=models.JSONField(help_text='dimension set success response log', null=True), + ), + migrations.AlterField( + model_name='journalentrylineitems', + name='dimensions', + field=models.JSONField(default=list, help_text='Business Central dimensions'), + ), + migrations.AlterField( + model_name='purchaseinvoicelineitems', + name='dimensions', + field=models.JSONField(default=list, help_text='Business Central dimensions'), + ), + ] diff --git a/apps/business_central/utils.py b/apps/business_central/utils.py index fd048d4..5ef94ce 100644 --- a/apps/business_central/utils.py +++ b/apps/business_central/utils.py @@ -1,12 +1,15 @@ import base64 import logging from typing import Dict, List + from datetime import datetime from django.utils import timezone from dynamics.core.client import Dynamics from fyle_accounting_mappings.models import DestinationAttribute +from apps.business_central.exports.journal_entry.models import JournalEntryLineItems +from apps.business_central.exports.purchase_invoice.models import PurchaseInvoiceLineitems from apps.workspaces.models import BusinessCentralCredentials, ExportSetting, Workspace from ms_business_central_api import settings @@ -291,6 +294,49 @@ def post_purchase_invoice(self, purchase_invoice_payload, purchase_invoice_linei } return response + def post_dimension_lines(self, dimension_line_payloads: List[Dict], export_module_type: str, top_level_id: int) -> List[Dict]: + """ + Post dimension lines for purchase invoice line and journal line items. + + :param dimension_line_payloads: List of payload dictionaries for dimension lines. + :param export_module_type: Type of export module ('JOURNAL_ENTRY' and 'PURCHASE_INVOICE'). + :return: List of exception responses, if any. + """ + exception_response = [] + + def get_lineitem_by_id(module_type: str, item_id: int): + if module_type == 'JOURNAL_ENTRY': + return JournalEntryLineItems.objects.get(id=item_id, journal_entry_id=top_level_id) + else: + return PurchaseInvoiceLineitems.objects.get(id=item_id, purchase_invoice_id=top_level_id) + + for dimension_line_payload in dimension_line_payloads: + exported_module_id = dimension_line_payload.pop('exported_module_id') + + try: + if export_module_type == 'JOURNAL_ENTRY': + response = self.connection.journal_line_items.post_journal_entry_dimensions( + journal_line_item_id=dimension_line_payload['parentId'], + data=dimension_line_payload + ) + else: + response = self.connection.purchase_invoice_line_items.post_purchase_invoice_dimensions( + purchase_invoice_item_id=dimension_line_payload['parentId'], + data=dimension_line_payload + ) + + lineitem = get_lineitem_by_id(export_module_type, exported_module_id) + lineitem.dimension_success_log = str(response) + lineitem.save() + + except Exception as exception: + lineitem = get_lineitem_by_id(export_module_type, exported_module_id) + error_message = str(getattr(exception, 'response', exception)) + lineitem.dimension_error_log = error_message + lineitem.save() + + return exception_response + def post_attachments( self, ref_type: str, ref_id: str, attachments: List[Dict] ) -> List: diff --git a/apps/mappings/imports/modules/base.py b/apps/mappings/imports/modules/base.py index 553195a..a40e9bb 100644 --- a/apps/mappings/imports/modules/base.py +++ b/apps/mappings/imports/modules/base.py @@ -191,7 +191,7 @@ def sync_destination_attributes(self, business_central_attribute_type: str): 'VENDOR': business_central_connection.sync_vendors, } - sync_method = sync_methods.get(business_central_attribute_type) + sync_method = sync_methods.get(business_central_attribute_type, business_central_connection.sync_dimensions) sync_method() def construct_payload_and_import_to_fyle( From 70ba7b80d8b4a8f0c387b51368a7998c70cc8ebb Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Fri, 22 Nov 2024 14:54:41 +0530 Subject: [PATCH 08/11] pylint --- apps/business_central/exports/journal_entry/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/business_central/exports/journal_entry/tasks.py b/apps/business_central/exports/journal_entry/tasks.py index 8a3262a..152fcf7 100644 --- a/apps/business_central/exports/journal_entry/tasks.py +++ b/apps/business_central/exports/journal_entry/tasks.py @@ -47,7 +47,7 @@ def __construct_journal_entry(self, body: JournalEntry, lineitems: List[JournalE account_attribute_type = DestinationAttribute.objects.filter(workspace_id=body.workspace_id, destination_id=body.accounts_payable_account_id).first() balance_account_type = 'G/L Account' - if account_attribute_type and account_attribute_type.attribute_type == 'BANK_ACCOUNT': + if account_attribute_type and account_attribute_type.attribute_type == 'BANK_ACCOUNT': balance_account_type = 'Bank Account' journal_entry_payload = { From c2ecbed32a1e7dddef0c97350a9e2919bd84fb7b Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Fri, 22 Nov 2024 15:48:01 +0530 Subject: [PATCH 09/11] comment line --- apps/fyle/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/fyle/models.py b/apps/fyle/models.py index 1f3d700..c2bb059 100644 --- a/apps/fyle/models.py +++ b/apps/fyle/models.py @@ -79,7 +79,7 @@ class Expense(BaseForeignWorkspaceModel): spent_at = CustomDateTimeField(help_text='Expense spent at') approved_at = CustomDateTimeField(help_text='Expense approved at') posted_at = CustomDateTimeField(help_text='Date when the money is taken from the bank') - is_posted_at_null = models.BooleanField(default=False, help_text='Flag check if posted at is null or not') + # is_posted_at_null = models.BooleanField(default=False, help_text='Flag check if posted at is null or not') is_skipped = models.BooleanField(null=True, default=False, help_text='Expense is skipped or not') expense_created_at = CustomDateTimeField(help_text='Expense created at') expense_updated_at = CustomDateTimeField(help_text='Expense created at') From a0286527b5e98d8fa86eddf293f57e5be86f53e0 Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Fri, 22 Nov 2024 16:27:57 +0530 Subject: [PATCH 10/11] test fixed --- .../business_central/exports/journal_entry/models.py | 2 -- .../exports/purchase_invoice/tasks.py | 2 -- tests/test_business_central/conftest.py | 3 ++- tests/test_business_central/test_models.py | 12 ++++++++---- tests/test_business_central/test_tasks.py | 10 ++++++++-- 5 files changed, 18 insertions(+), 11 deletions(-) diff --git a/apps/business_central/exports/journal_entry/models.py b/apps/business_central/exports/journal_entry/models.py index bbb89e6..f23cfab 100644 --- a/apps/business_central/exports/journal_entry/models.py +++ b/apps/business_central/exports/journal_entry/models.py @@ -47,8 +47,6 @@ def create_or_update_object(self, accounting_export: AccountingExport, _: Advanc advance_setting = AdvancedSetting.objects.get(workspace_id=accounting_export.workspace_id) - print(accounting_export.__dict__) - document_number = expense.expense_number comment = self.get_expense_comment(accounting_export.workspace_id, expense, expense.category, advance_setting) diff --git a/apps/business_central/exports/purchase_invoice/tasks.py b/apps/business_central/exports/purchase_invoice/tasks.py index 30f7cd6..2070c21 100644 --- a/apps/business_central/exports/purchase_invoice/tasks.py +++ b/apps/business_central/exports/purchase_invoice/tasks.py @@ -53,8 +53,6 @@ def __construct_purchase_invoice(self, body: PurchaseInvoice, lineitems: List[Pu for dimension in lineitem.dimensions: dimension['exported_module_id'] = lineitem.id - print('lineitem.dimensions', lineitem.dimensions) - dimensions.extend(lineitem.dimensions) purchase_invoice_lineitem_payload = { "lineType": "Account", diff --git a/tests/test_business_central/conftest.py b/tests/test_business_central/conftest.py index d050dc6..cc5367d 100644 --- a/tests/test_business_central/conftest.py +++ b/tests/test_business_central/conftest.py @@ -161,7 +161,8 @@ def create_journal_entry( create_export_settings, add_advanced_settings, create_accounting_export_expenses, - create_employee_mapping_with_employee + create_employee_mapping_with_employee, + add_fyle_credentials ): workspace_id = 1 export_settings = ExportSetting.objects.get(workspace_id=workspace_id) diff --git a/tests/test_business_central/test_models.py b/tests/test_business_central/test_models.py index d913970..55aa588 100644 --- a/tests/test_business_central/test_models.py +++ b/tests/test_business_central/test_models.py @@ -13,7 +13,8 @@ def test_create_or_update_journal_entry_1( create_export_settings, add_advanced_settings, create_accounting_export_expenses, - create_employee_mapping_with_employee + create_employee_mapping_with_employee, + add_fyle_credentials ): workspace_id = 1 export_settings = ExportSetting.objects.get(workspace_id=workspace_id) @@ -39,7 +40,8 @@ def test_create_or_update_journal_entry_2( create_export_settings, add_advanced_settings, create_accounting_export_expenses, - create_employee_mapping_with_vendor + create_employee_mapping_with_vendor, + add_fyle_credentials ): workspace_id = 1 export_settings = ExportSetting.objects.get(workspace_id=workspace_id) @@ -75,7 +77,8 @@ def test_create_or_update_journal_entry_3( create_export_settings, add_advanced_settings, create_accounting_export_expenses, - create_employee_mapping_with_vendor + create_employee_mapping_with_vendor, + add_fyle_credentials ): workspace_id = 1 export_settings = ExportSetting.objects.get(workspace_id=workspace_id) @@ -324,7 +327,8 @@ def test_accounting_data_exporter_4( def test_base_model_get_invoice_date( db, create_temp_workspace, - create_journal_entry + create_journal_entry, + add_fyle_credentials ): workspace_id = 1 diff --git a/tests/test_business_central/test_tasks.py b/tests/test_business_central/test_tasks.py index 6fdc75c..8b2f73d 100644 --- a/tests/test_business_central/test_tasks.py +++ b/tests/test_business_central/test_tasks.py @@ -6,6 +6,11 @@ from apps.business_central.exports.journal_entry.tasks import create_journal_entry from apps.business_central.exports.purchase_invoice.tasks import create_purchase_invoice +import logging + +logger = logging.getLogger(__name__) +logger.level = logging.INFO + def test_trigger_export_journal_entry(db, mocker): ''' @@ -36,8 +41,9 @@ def test_construct_journal_entry( lineitems = JournalEntryLineItems.objects.filter(workspace_id=workspace_id) export_journal_entry = ExportJournalEntry() - payload_list = export_journal_entry._ExportJournalEntry__construct_journal_entry(journal_entry, lineitems) + payload_list, _ = export_journal_entry._ExportJournalEntry__construct_journal_entry(journal_entry, lineitems) + logger.info(payload_list) assert len(payload_list) > 0 payload = payload_list[0] @@ -159,7 +165,7 @@ def test_construct_purchase_invoice( lineitems = PurchaseInvoiceLineitems.objects.filter(workspace_id=workspace_id) export_purchase_invoice = ExportPurchaseInvoice() - payload, batch_payload = export_purchase_invoice.\ + payload, batch_payload, _ = export_purchase_invoice.\ _ExportPurchaseInvoice__construct_purchase_invoice( purchase_invoice, lineitems From 271ef8122ec36db87185ddfdd192763c9aeaa388 Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Fri, 22 Nov 2024 17:03:12 +0530 Subject: [PATCH 11/11] remove print --- apps/fyle/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/fyle/models.py b/apps/fyle/models.py index a950f65..9af8a6c 100644 --- a/apps/fyle/models.py +++ b/apps/fyle/models.py @@ -79,7 +79,7 @@ class Expense(BaseForeignWorkspaceModel): spent_at = CustomDateTimeField(help_text='Expense spent at') approved_at = CustomDateTimeField(help_text='Expense approved at') posted_at = CustomDateTimeField(help_text='Date when the money is taken from the bank') - # is_posted_at_null = models.BooleanField(default=False, help_text='Flag check if posted at is null or not') + is_posted_at_null = models.BooleanField(default=False, help_text='Flag check if posted at is null or not') is_skipped = models.BooleanField(null=True, default=False, help_text='Expense is skipped or not') expense_created_at = CustomDateTimeField(help_text='Expense created at') expense_updated_at = CustomDateTimeField(help_text='Expense created at')