From b353f2f93f39b785ae76cf66d4444261687d7d16 Mon Sep 17 00:00:00 2001 From: Viswas Haridas <37623357+JustARatherRidiculouslyLongUsername@users.noreply.github.com> Date: Wed, 6 Nov 2024 16:18:08 +0530 Subject: [PATCH 1/9] fix: fix export url generation for sandbox accounts (#659) * fix: fix export url generation for sandbox accounts * fix: add script to update bad db data --- fyle_netsuite_api/utils.py | 3 +++ .../scripts/029-update-sandbox-export-urls.sql | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 scripts/sql/scripts/029-update-sandbox-export-urls.sql diff --git a/fyle_netsuite_api/utils.py b/fyle_netsuite_api/utils.py index f4cc03f5..c70de7a2 100644 --- a/fyle_netsuite_api/utils.py +++ b/fyle_netsuite_api/utils.py @@ -45,6 +45,9 @@ def generate_netsuite_export_url(response_logs : OrderedDict, netsuite_credentia if response_logs: try: ns_account_id = netsuite_credentials.ns_account_id.lower() + if '_sb' in ns_account_id: + ns_account_id = ns_account_id.replace('_sb', '-sb') + export_type = response_logs['type'] if 'type' in response_logs and response_logs['type'] else 'chargeCard' internal_id = response_logs['internalId'] redirection = EXPORT_TYPE_REDIRECTION[export_type] diff --git a/scripts/sql/scripts/029-update-sandbox-export-urls.sql b/scripts/sql/scripts/029-update-sandbox-export-urls.sql new file mode 100644 index 00000000..b42c67ad --- /dev/null +++ b/scripts/sql/scripts/029-update-sandbox-export-urls.sql @@ -0,0 +1,17 @@ +rollback; +begin; + +-- These values should be swapped after running the script +-- netsuite=> select count(export_url) from expense_groups where export_url like '%_sb%'; +-- count +-- ------- +-- 240 +-- (1 row) + +-- netsuite=> select count(export_url) from expense_groups where export_url like '%-sb%'; +-- count +-- ------- +-- 0 +-- (1 row) + +update expense_groups set export_url = replace(export_url, '_sb', '-sb'); From 96c13db068a2c1120033df919fde0c413983eba5 Mon Sep 17 00:00:00 2001 From: Hrishabh Tiwari <74908943+Hrishabh17@users.noreply.github.com> Date: Thu, 7 Nov 2024 11:54:33 +0530 Subject: [PATCH 2/9] fix: add transaction block (#660) --- apps/netsuite/connector.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/apps/netsuite/connector.py b/apps/netsuite/connector.py index 64d623be..4f273ecc 100644 --- a/apps/netsuite/connector.py +++ b/apps/netsuite/connector.py @@ -1,7 +1,10 @@ import re import json from datetime import datetime, timedelta + from django.utils import timezone +from django.db import transaction + from typing import List, Dict import logging @@ -431,14 +434,15 @@ def update_destination_attributes(self, attribute_type:str, custom_records: List temp_internal_ids[record_id] = (temp_id, new_id) destination_attributes_to_be_updated.append(DestinationAttribute(id=record_id, destination_id=temp_id, workspace_id=self.workspace_id)) - DestinationAttribute.objects.bulk_update(destination_attributes_to_be_updated, ['destination_id'], batch_size=100) + with transaction.atomic(): + DestinationAttribute.objects.bulk_update(destination_attributes_to_be_updated, ['destination_id'], batch_size=100) - updated_destination_attributes = [ - DestinationAttribute(id=record_id, destination_id=new_id, workspace_id=self.workspace_id) - for record_id, (temp_id, new_id) in temp_internal_ids.items() - ] - - DestinationAttribute.objects.bulk_update(updated_destination_attributes, ['destination_id'], batch_size=100) + updated_destination_attributes = [ + DestinationAttribute(id=record_id, destination_id=new_id, workspace_id=self.workspace_id) + for record_id, (temp_id, new_id) in temp_internal_ids.items() + ] + + DestinationAttribute.objects.bulk_update(updated_destination_attributes, ['destination_id'], batch_size=100) def get_custom_record_attributes(self, attribute_type: str, internal_id: str): custom_segment_attributes = [] From ebc243b70407d077872505819430148b402a3010 Mon Sep 17 00:00:00 2001 From: Viswas Haridas <37623357+JustARatherRidiculouslyLongUsername@users.noreply.github.com> Date: Tue, 12 Nov 2024 14:25:30 +0530 Subject: [PATCH 3/9] fix: remove sentry transaction sampling (#663) --- fyle_netsuite_api/sentry.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/fyle_netsuite_api/sentry.py b/fyle_netsuite_api/sentry.py index 5190adad..07754782 100644 --- a/fyle_netsuite_api/sentry.py +++ b/fyle_netsuite_api/sentry.py @@ -14,7 +14,6 @@ def init(): send_default_pii=True, integrations=[DjangoIntegration()], environment=os.environ.get('SENTRY_ENV'), - traces_sampler=Sentry.traces_sampler, attach_stacktrace=True, before_send=Sentry.before_send, request_bodies='small', @@ -29,14 +28,6 @@ def init(): ) @staticmethod - def traces_sampler(sampling_context): - # avoiding ready APIs in performance tracing - if sampling_context.get('wsgi_environ') is not None: - if 'ready/' in sampling_context['wsgi_environ']['PATH_INFO']: - return 0 - - return 1 - @staticmethod def before_send(event, hint): if 'exc_info' in hint: exc_type, exc_value, tb = hint['exc_info'] From 1d1ea730c423f92ba75b140f36a3e7a904ef7249 Mon Sep 17 00:00:00 2001 From: Hrishabh Tiwari <74908943+Hrishabh17@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:17:23 +0530 Subject: [PATCH 4/9] feat: update netsuite sdk/wsdl version (#664) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0998893e..7eacc432 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,7 +33,7 @@ isort==5.10.1 lazy-object-proxy==1.6.0 lxml==4.6.5 mccabe==0.6.1 -netsuitesdk==2.23.0 +netsuitesdk==3.0.0 oauthlib==3.2.1 packaging==21.3 platformdirs==2.4.0 From f3b599ba8e80af938ea3aa723363a9266867c25d Mon Sep 17 00:00:00 2001 From: Ashutosh singh <55102089+Ashutosh619-sudo@users.noreply.github.com> Date: Thu, 14 Nov 2024 12:28:22 +0530 Subject: [PATCH 5/9] Fix: Remove code for adding department in expense group settings (#665) --- .../apis/import_settings/triggers.py | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/apps/workspaces/apis/import_settings/triggers.py b/apps/workspaces/apis/import_settings/triggers.py index cbbbf569..0582da69 100644 --- a/apps/workspaces/apis/import_settings/triggers.py +++ b/apps/workspaces/apis/import_settings/triggers.py @@ -57,16 +57,6 @@ def add_department_grouping(self, source_field: str): expense_group_settings.save() - def __update_expense_group_settings_for_departments(self): - """ - Should group expenses by department source field in case the export is journal entries - """ - department_setting = list(filter(lambda setting: setting['destination_field'] == 'DEPARTMENT', self.__mapping_settings)) - - if department_setting: - department_setting = department_setting[0] - - self.add_department_grouping(department_setting['source_field']) def post_save_configurations(self, configurations_instance: Configuration): """ @@ -77,16 +67,6 @@ def post_save_configurations(self, configurations_instance: Configuration): mapping_settings=self.__mapping_settings ) - def __remove_old_department_source_field(self, current_mappings_settings: List[MappingSetting], new_mappings_settings: List[Dict]): - """ - Should remove Department Source field from Reimbursable settings in case of deletion and updation - """ - old_department_setting = current_mappings_settings.filter(destination_field='DEPARTMENT').first() - - new_department_setting = list(filter(lambda setting: setting['destination_field'] == 'DEPARTMENT', new_mappings_settings)) - - if old_department_setting and new_department_setting and old_department_setting.source_field != new_department_setting[0]['source_field']: - self.remove_department_grouping(old_department_setting.source_field.lower()) def __unset_auto_mapped_flag(self, current_mapping_settings: List[MappingSetting], new_mappings_settings: List[Dict]): """ @@ -119,8 +99,6 @@ def pre_save_mapping_settings(self): # Update department mapping to some other Fyle field current_mapping_settings = MappingSetting.objects.filter(workspace_id=self.__workspace_id).all() - - self.__remove_old_department_source_field(current_mappings_settings=current_mapping_settings, new_mappings_settings=mapping_settings) self.__unset_auto_mapped_flag(current_mapping_settings=current_mapping_settings, new_mappings_settings=mapping_settings) def post_save_mapping_settings(self, configurations_instance: Configuration): @@ -139,8 +117,6 @@ def post_save_mapping_settings(self, configurations_instance: Configuration): MappingSetting.objects.filter(~Q(destination_field__in=destination_fields), workspace_id=self.__workspace_id).delete() - self.__update_expense_group_settings_for_departments() - new_schedule_or_delete_fyle_import_tasks( configuration_instance=configurations_instance, mapping_settings=self.__mapping_settings From 8990c3a23a54a922c78f16e6d79a0ed22e0012f4 Mon Sep 17 00:00:00 2001 From: Hrishabh Tiwari <74908943+Hrishabh17@users.noreply.github.com> Date: Fri, 15 Nov 2024 10:01:08 +0530 Subject: [PATCH 6/9] fix: add unique constraint in task_logs for expense_group (#666) --- .../0012_alter_tasklog_expense_group.py | 20 +++++++++++++++++++ apps/tasks/models.py | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 apps/tasks/migrations/0012_alter_tasklog_expense_group.py diff --git a/apps/tasks/migrations/0012_alter_tasklog_expense_group.py b/apps/tasks/migrations/0012_alter_tasklog_expense_group.py new file mode 100644 index 00000000..c513daf0 --- /dev/null +++ b/apps/tasks/migrations/0012_alter_tasklog_expense_group.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.14 on 2024-11-14 10:35 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('fyle', '0033_expense_paid_on_fyle'), + ('tasks', '0011_error_repetition_count'), + ] + + operations = [ + migrations.AlterField( + model_name='tasklog', + name='expense_group', + field=models.ForeignKey(help_text='Reference to Expense group', null=True, on_delete=django.db.models.deletion.PROTECT, to='fyle.expensegroup', unique=True), + ), + ] diff --git a/apps/tasks/models.py b/apps/tasks/models.py index f713168f..fc3d07f8 100644 --- a/apps/tasks/models.py +++ b/apps/tasks/models.py @@ -43,7 +43,7 @@ class TaskLog(models.Model): type = models.CharField(max_length=50, choices=TASK_TYPE, help_text='Task type (FETCH_EXPENSES / CREATE_BILL)') task_id = models.CharField(max_length=255, null=True, help_text='Django Q task reference') expense_group = models.ForeignKey(ExpenseGroup, on_delete=models.PROTECT, - null=True, help_text='Reference to Expense group') + null=True, help_text='Reference to Expense group', unique=True) bill = models.ForeignKey(Bill, on_delete=models.PROTECT, help_text='Reference to Bill', null=True) expense_report = models.ForeignKey(ExpenseReport, on_delete=models.PROTECT, help_text='Reference to Expense Report', null=True) From f5b390a5f7c27d21ac99a1fb28325d09ab2d9e89 Mon Sep 17 00:00:00 2001 From: Anish Kr Singh <116036738+anishfyle@users.noreply.github.com> Date: Mon, 18 Nov 2024 14:36:10 +0530 Subject: [PATCH 7/9] feat: handle posted at (#661) * handle posted at * migrations, reset db, conftest * feat: auto enable accounting period (#668) * feat: auto enable accounting period * reset db * platform connector ver --- .../0034_expense_is_posted_at_null.py | 18 ++++++ apps/fyle/models.py | 1 + ..._configuration_change_accounting_period.py | 18 ++++++ apps/workspaces/models.py | 2 +- requirements.txt | 2 +- .../reset_db_fixtures/reset_db.sql | 61 ++++++++++--------- tests/test_netsuite/conftest.py | 4 +- 7 files changed, 75 insertions(+), 31 deletions(-) create mode 100644 apps/fyle/migrations/0034_expense_is_posted_at_null.py create mode 100644 apps/workspaces/migrations/0040_alter_configuration_change_accounting_period.py diff --git a/apps/fyle/migrations/0034_expense_is_posted_at_null.py b/apps/fyle/migrations/0034_expense_is_posted_at_null.py new file mode 100644 index 00000000..e19df8e9 --- /dev/null +++ b/apps/fyle/migrations/0034_expense_is_posted_at_null.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.14 on 2024-11-17 20:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('fyle', '0033_expense_paid_on_fyle'), + ] + + operations = [ + migrations.AddField( + model_name='expense', + name='is_posted_at_null', + field=models.BooleanField(default=False, help_text='Flag check if posted at is null or not'), + ), + ] diff --git a/apps/fyle/models.py b/apps/fyle/models.py index 9eb1ff6a..3227c774 100644 --- a/apps/fyle/models.py +++ b/apps/fyle/models.py @@ -107,6 +107,7 @@ class Expense(models.Model): spent_at = models.DateTimeField(null=True, help_text='Expense spent at') approved_at = models.DateTimeField(null=True, help_text='Expense approved at') posted_at = models.DateTimeField(null=True, 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') expense_created_at = models.DateTimeField(help_text='Expense created at') expense_updated_at = models.DateTimeField(help_text='Expense created at') created_at = models.DateTimeField(auto_now_add=True, help_text='Created at') diff --git a/apps/workspaces/migrations/0040_alter_configuration_change_accounting_period.py b/apps/workspaces/migrations/0040_alter_configuration_change_accounting_period.py new file mode 100644 index 00000000..5cb0cd39 --- /dev/null +++ b/apps/workspaces/migrations/0040_alter_configuration_change_accounting_period.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.14 on 2024-11-18 02:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('workspaces', '0039_configuration_je_single_credit_line'), + ] + + operations = [ + migrations.AlterField( + model_name='configuration', + name='change_accounting_period', + field=models.BooleanField(default=True, help_text='Change the accounting period'), + ), + ] diff --git a/apps/workspaces/models.py b/apps/workspaces/models.py index 606f4931..cef4474a 100644 --- a/apps/workspaces/models.py +++ b/apps/workspaces/models.py @@ -155,7 +155,7 @@ class Configuration(models.Model): import_projects = models.BooleanField(default=False, help_text='Auto import projects to Fyle') import_vendors_as_merchants = models.BooleanField(default=False, help_text='Auto import vendors from netsuite as merchants to Fyle') import_netsuite_employees = models.BooleanField(default=False, help_text='Auto import employees from netsuite as employees to Fyle') - change_accounting_period = models.BooleanField(default=False, help_text='Change the accounting period') + change_accounting_period = models.BooleanField(default=True, help_text='Change the accounting period') sync_fyle_to_netsuite_payments = models.BooleanField( default=False, help_text='Auto Sync Payments from Fyle to Netsuite' ) diff --git a/requirements.txt b/requirements.txt index 7eacc432..56b036ff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ enum34==1.1.10 future==0.18.2 fyle==0.37.0 fyle-accounting-mappings==1.34.8 -fyle-integrations-platform-connector==1.38.4 +fyle-integrations-platform-connector==1.39.3 fyle-rest-auth==1.7.2 gunicorn==20.1.0 gevent==23.9.1 diff --git a/tests/sql_fixtures/reset_db_fixtures/reset_db.sql b/tests/sql_fixtures/reset_db_fixtures/reset_db.sql index f899ec84..55889c0f 100644 --- a/tests/sql_fixtures/reset_db_fixtures/reset_db.sql +++ b/tests/sql_fixtures/reset_db_fixtures/reset_db.sql @@ -2,8 +2,8 @@ -- PostgreSQL database dump -- --- Dumped from database version 15.7 (Debian 15.7-1.pgdg120+1) --- Dumped by pg_dump version 15.8 (Debian 15.8-1.pgdg120+1) +-- Dumped from database version 15.9 (Debian 15.9-1.pgdg120+1) +-- Dumped by pg_dump version 15.9 (Debian 15.9-1.pgdg120+1) SET statement_timeout = 0; SET lock_timeout = 0; @@ -1148,7 +1148,8 @@ CREATE TABLE public.expenses ( accounting_export_summary jsonb NOT NULL, previous_export_state character varying(255), workspace_id integer, - paid_on_fyle boolean NOT NULL + paid_on_fyle boolean NOT NULL, + is_posted_at_null boolean NOT NULL ); @@ -7986,6 +7987,9 @@ COPY public.django_migrations (id, app, name, applied) FROM stdin; 198 netsuite 0027_auto_20240924_0820 2024-09-24 08:24:35.223017+00 199 fyle_accounting_mappings 0026_destinationattribute_code 2024-10-01 08:54:06.770864+00 200 workspaces 0039_configuration_je_single_credit_line 2024-10-11 13:43:49.169823+00 +201 fyle 0034_expense_is_posted_at_null 2024-11-17 20:37:53.17847+00 +202 tasks 0012_alter_tasklog_expense_group 2024-11-17 20:37:53.213044+00 +203 workspaces 0040_alter_configuration_change_accounting_period 2024-11-18 04:28:36.094429+00 \. @@ -11627,15 +11631,15 @@ COPY public.expense_reports (id, account_id, entity_id, currency, department_id, -- Data for Name: expenses; Type: TABLE DATA; Schema: public; Owner: postgres -- -COPY public.expenses (id, employee_email, category, sub_category, project, expense_id, expense_number, claim_number, amount, currency, foreign_amount, foreign_currency, settlement_id, reimbursable, state, vendor, cost_center, purpose, report_id, spent_at, approved_at, expense_created_at, expense_updated_at, created_at, updated_at, fund_source, custom_properties, verified_at, paid_on_netsuite, billable, org_id, tax_amount, tax_group_id, project_id, file_ids, corporate_card_id, is_skipped, report_title, employee_name, posted_at, accounting_export_summary, previous_export_state, workspace_id, paid_on_fyle) FROM stdin; -1 ashwin.t@fyle.in Accounts Payable Accounts Payable \N txjvDntD9ZXR E/2021/11/T/11 C/2021/11/R/5 50 USD \N \N set6GUp6tcEEp t PAYMENT_PROCESSING \N Treasury \N rpuN3bgphxbK 2021-11-15 00:00:00+00 2021-11-15 00:00:00+00 2021-11-15 10:27:53.649+00 2021-11-15 10:28:46.775+00 2021-11-15 10:29:07.597095+00 2021-11-15 10:29:07.597111+00 PERSONAL {"Team": "", "Class": "", "Klass": "", "Team 2": "", "Location": "", "Team Copy": "", "Tax Groups": "", "Departments": "", "User Dimension": "", "Location Entity": "", "Operating System": "", "System Operating": "", "User Dimension Copy": ""} \N f \N or79Cob97KSh \N \N \N \N \N f \N \N \N {} \N \N f -2 ashwin.t@fyle.in Accounts Payable Accounts Payable \N txy6folbrG2j E/2021/11/T/12 C/2021/11/R/6 100 USD \N \N setNVTcPkZ6on f PAYMENT_PROCESSING Ashwin Vendor \N \N rpHLA9Dfp9hN 2021-11-15 00:00:00+00 2021-11-15 00:00:00+00 2021-11-15 13:11:22.304+00 2021-11-15 13:11:58.032+00 2021-11-15 13:12:12.250613+00 2021-11-15 13:12:12.250638+00 CCC {"Team": "", "Class": "", "Klass": "Klass", "Team 2": "", "Location": "", "Team Copy": "", "Tax Groups": "", "Departments": "", "User Dimension": "", "Location Entity": "", "Operating System": "", "System Operating": "", "User Dimension Copy": ""} \N f \N or79Cob97KSh \N \N \N \N \N f \N \N \N {} \N \N f -3 ashwin.t@fyle.in Accounts Payable Accounts Payable \N txeLau9Rdu4X E/2021/11/T/1 C/2021/11/R/2 80 USD \N \N setqgvGQnsAya t PAYMENT_PROCESSING \N \N \N rpu5W0LYrk6e 2021-11-16 00:00:00+00 2021-11-16 00:00:00+00 2021-11-16 04:24:18.688+00 2021-11-16 04:25:21.996+00 2021-11-16 04:25:49.174565+00 2021-11-16 04:25:49.174584+00 PERSONAL {"Klass": "Klass", "Device Type": "", "Fyle Category": ""} \N f \N oraWFQlEpjbb 4.53 tg31j9m4PoEO \N \N \N f \N \N \N {} \N \N f -4 ashwin.t@fyle.in Accounts Payable Accounts Payable \N txMLGb6Xy8m8 E/2021/11/T/2 C/2021/11/R/1 100 USD \N \N setqgvGQnsAya f PAYMENT_PROCESSING \N \N \N rprqDvARHUnv 2021-11-16 00:00:00+00 2021-11-16 00:00:00+00 2021-11-16 04:24:38.141+00 2021-11-16 04:25:21.996+00 2021-11-16 04:25:49.192351+00 2021-11-16 04:25:49.192367+00 CCC {"Device Type": "", "Fyle Category": ""} \N f \N oraWFQlEpjbb 16.67 tgSYjXsBCviv \N \N \N f \N \N \N {} \N \N f -173 admin1@fyleforintacct.in Food Food Project 2 tx7A5QpesrV5 E/2021/12/T/1 C/2021/12/R/1 120 USD \N \N set15sMvtRIiS t PAYMENT_PROCESSING \N Sales and Cross \N rpXqCutQj85N 2021-12-03 00:00:00+00 2021-12-03 00:00:00+00 2021-12-03 10:58:30.076+00 2021-12-03 11:00:22.64+00 2021-12-03 11:26:58.685597+00 2021-12-03 11:26:58.685616+00 PERSONAL {} \N f \N orHe8CpW2hyN \N \N \N \N \N f \N \N \N {} \N \N f -174 admin1@fyleforintacct.in Food Food Project 2 txcKVVELn1Vl E/2021/12/T/2 C/2021/12/R/1 130 USD \N \N set15sMvtRIiS f PAYMENT_PROCESSING \N Sales and Cross \N rpXqCutQj85N 2021-12-03 00:00:00+00 2021-12-03 00:00:00+00 2021-12-03 10:58:49.51+00 2021-12-03 11:00:22.64+00 2021-12-03 11:26:58.702183+00 2021-12-03 11:26:58.702209+00 CCC {} \N f \N orHe8CpW2hyN \N \N \N \N \N f \N \N \N {} \N \N f -600 jhonsnoww@fyle.in Food Food Project 2 txcKVVELn1Vlkill E/2021/12/T/298 C/2021/12/R/198 130 USD \N \N set15sMvtRIiSkill f PAYMENT_PROCESSING \N Sales and Cross \N rpXqCutQj85Nkill 2021-12-03 00:00:00+00 2021-12-03 00:00:00+00 2021-12-03 10:58:49.51+00 2021-12-03 11:00:22.64+00 2021-12-03 11:26:58.702183+00 2021-12-03 11:26:58.702209+00 CCC {} \N f \N or79Cob97KSh \N \N \N \N \N t \N \N \N {} \N \N f -601 jhonsnoww@fyle.in Food Food Project 2 txcKVVELn1Vlgon E/2021/12/T/299 C/2021/12/R/199 130 USD \N \N set15sMvtRIiSgon f PAYMENT_PROCESSING \N Sales and Cross \N rpXqCutQj85Ngon 2021-12-03 00:00:00+00 2021-12-03 00:00:00+00 2021-12-03 10:58:49.51+00 2021-12-03 11:00:22.64+00 2021-12-03 11:26:58.702183+00 2021-12-03 11:26:58.702209+00 CCC {} \N f \N or79Cob97KSh \N \N \N \N \N t \N \N \N {} \N \N f +COPY public.expenses (id, employee_email, category, sub_category, project, expense_id, expense_number, claim_number, amount, currency, foreign_amount, foreign_currency, settlement_id, reimbursable, state, vendor, cost_center, purpose, report_id, spent_at, approved_at, expense_created_at, expense_updated_at, created_at, updated_at, fund_source, custom_properties, verified_at, paid_on_netsuite, billable, org_id, tax_amount, tax_group_id, project_id, file_ids, corporate_card_id, is_skipped, report_title, employee_name, posted_at, accounting_export_summary, previous_export_state, workspace_id, paid_on_fyle, is_posted_at_null) FROM stdin; +1 ashwin.t@fyle.in Accounts Payable Accounts Payable \N txjvDntD9ZXR E/2021/11/T/11 C/2021/11/R/5 50 USD \N \N set6GUp6tcEEp t PAYMENT_PROCESSING \N Treasury \N rpuN3bgphxbK 2021-11-15 00:00:00+00 2021-11-15 00:00:00+00 2021-11-15 10:27:53.649+00 2021-11-15 10:28:46.775+00 2021-11-15 10:29:07.597095+00 2021-11-15 10:29:07.597111+00 PERSONAL {"Team": "", "Class": "", "Klass": "", "Team 2": "", "Location": "", "Team Copy": "", "Tax Groups": "", "Departments": "", "User Dimension": "", "Location Entity": "", "Operating System": "", "System Operating": "", "User Dimension Copy": ""} \N f \N or79Cob97KSh \N \N \N \N \N f \N \N \N {} \N \N f f +2 ashwin.t@fyle.in Accounts Payable Accounts Payable \N txy6folbrG2j E/2021/11/T/12 C/2021/11/R/6 100 USD \N \N setNVTcPkZ6on f PAYMENT_PROCESSING Ashwin Vendor \N \N rpHLA9Dfp9hN 2021-11-15 00:00:00+00 2021-11-15 00:00:00+00 2021-11-15 13:11:22.304+00 2021-11-15 13:11:58.032+00 2021-11-15 13:12:12.250613+00 2021-11-15 13:12:12.250638+00 CCC {"Team": "", "Class": "", "Klass": "Klass", "Team 2": "", "Location": "", "Team Copy": "", "Tax Groups": "", "Departments": "", "User Dimension": "", "Location Entity": "", "Operating System": "", "System Operating": "", "User Dimension Copy": ""} \N f \N or79Cob97KSh \N \N \N \N \N f \N \N \N {} \N \N f f +3 ashwin.t@fyle.in Accounts Payable Accounts Payable \N txeLau9Rdu4X E/2021/11/T/1 C/2021/11/R/2 80 USD \N \N setqgvGQnsAya t PAYMENT_PROCESSING \N \N \N rpu5W0LYrk6e 2021-11-16 00:00:00+00 2021-11-16 00:00:00+00 2021-11-16 04:24:18.688+00 2021-11-16 04:25:21.996+00 2021-11-16 04:25:49.174565+00 2021-11-16 04:25:49.174584+00 PERSONAL {"Klass": "Klass", "Device Type": "", "Fyle Category": ""} \N f \N oraWFQlEpjbb 4.53 tg31j9m4PoEO \N \N \N f \N \N \N {} \N \N f f +4 ashwin.t@fyle.in Accounts Payable Accounts Payable \N txMLGb6Xy8m8 E/2021/11/T/2 C/2021/11/R/1 100 USD \N \N setqgvGQnsAya f PAYMENT_PROCESSING \N \N \N rprqDvARHUnv 2021-11-16 00:00:00+00 2021-11-16 00:00:00+00 2021-11-16 04:24:38.141+00 2021-11-16 04:25:21.996+00 2021-11-16 04:25:49.192351+00 2021-11-16 04:25:49.192367+00 CCC {"Device Type": "", "Fyle Category": ""} \N f \N oraWFQlEpjbb 16.67 tgSYjXsBCviv \N \N \N f \N \N \N {} \N \N f f +173 admin1@fyleforintacct.in Food Food Project 2 tx7A5QpesrV5 E/2021/12/T/1 C/2021/12/R/1 120 USD \N \N set15sMvtRIiS t PAYMENT_PROCESSING \N Sales and Cross \N rpXqCutQj85N 2021-12-03 00:00:00+00 2021-12-03 00:00:00+00 2021-12-03 10:58:30.076+00 2021-12-03 11:00:22.64+00 2021-12-03 11:26:58.685597+00 2021-12-03 11:26:58.685616+00 PERSONAL {} \N f \N orHe8CpW2hyN \N \N \N \N \N f \N \N \N {} \N \N f f +174 admin1@fyleforintacct.in Food Food Project 2 txcKVVELn1Vl E/2021/12/T/2 C/2021/12/R/1 130 USD \N \N set15sMvtRIiS f PAYMENT_PROCESSING \N Sales and Cross \N rpXqCutQj85N 2021-12-03 00:00:00+00 2021-12-03 00:00:00+00 2021-12-03 10:58:49.51+00 2021-12-03 11:00:22.64+00 2021-12-03 11:26:58.702183+00 2021-12-03 11:26:58.702209+00 CCC {} \N f \N orHe8CpW2hyN \N \N \N \N \N f \N \N \N {} \N \N f f +600 jhonsnoww@fyle.in Food Food Project 2 txcKVVELn1Vlkill E/2021/12/T/298 C/2021/12/R/198 130 USD \N \N set15sMvtRIiSkill f PAYMENT_PROCESSING \N Sales and Cross \N rpXqCutQj85Nkill 2021-12-03 00:00:00+00 2021-12-03 00:00:00+00 2021-12-03 10:58:49.51+00 2021-12-03 11:00:22.64+00 2021-12-03 11:26:58.702183+00 2021-12-03 11:26:58.702209+00 CCC {} \N f \N or79Cob97KSh \N \N \N \N \N t \N \N \N {} \N \N f f +601 jhonsnoww@fyle.in Food Food Project 2 txcKVVELn1Vlgon E/2021/12/T/299 C/2021/12/R/199 130 USD \N \N set15sMvtRIiSgon f PAYMENT_PROCESSING \N Sales and Cross \N rpXqCutQj85Ngon 2021-12-03 00:00:00+00 2021-12-03 00:00:00+00 2021-12-03 10:58:49.51+00 2021-12-03 11:00:22.64+00 2021-12-03 11:26:58.702183+00 2021-12-03 11:26:58.702209+00 CCC {} \N f \N or79Cob97KSh \N \N \N \N \N t \N \N \N {} \N \N f f \. @@ -11897,7 +11901,7 @@ SELECT pg_catalog.setval('public.django_content_type_id_seq', 47, true); -- Name: django_migrations_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres -- -SELECT pg_catalog.setval('public.django_migrations_id_seq', 200, true); +SELECT pg_catalog.setval('public.django_migrations_id_seq', 203, true); -- @@ -12721,6 +12725,14 @@ ALTER TABLE ONLY public.subsidiary_mappings ADD CONSTRAINT subsidiary_mappings_workspace_id_d2d83a94_uniq UNIQUE (workspace_id); +-- +-- Name: task_logs task_logs_expense_group_id_f19c75f9_uniq; Type: CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.task_logs + ADD CONSTRAINT task_logs_expense_group_id_f19c75f9_uniq UNIQUE (expense_group_id); + + -- -- Name: task_logs tasks_tasklog_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres -- @@ -13210,13 +13222,6 @@ CREATE INDEX reimbursements_workspace_id_084805e4 ON public.reimbursements USING CREATE INDEX task_log_bill_id_30283abe ON public.task_logs USING btree (bill_id); --- --- Name: task_log_expense_group_id_241da11b; Type: INDEX; Schema: public; Owner: postgres --- - -CREATE INDEX task_log_expense_group_id_241da11b ON public.task_logs USING btree (expense_group_id); - - -- -- Name: task_log_expense_report_id_74a8817f; Type: INDEX; Schema: public; Owner: postgres -- @@ -13726,14 +13731,6 @@ ALTER TABLE ONLY public.task_logs ADD CONSTRAINT task_log_bill_id_30283abe_fk_bills_id FOREIGN KEY (bill_id) REFERENCES public.bills(id) DEFERRABLE INITIALLY DEFERRED; --- --- Name: task_logs task_log_expense_group_id_241da11b_fk_expense_groups_id; Type: FK CONSTRAINT; Schema: public; Owner: postgres --- - -ALTER TABLE ONLY public.task_logs - ADD CONSTRAINT task_log_expense_group_id_241da11b_fk_expense_groups_id FOREIGN KEY (expense_group_id) REFERENCES public.expense_groups(id) DEFERRABLE INITIALLY DEFERRED; - - -- -- Name: task_logs task_log_expense_report_id_74a8817f_fk_expense_reports_id; Type: FK CONSTRAINT; Schema: public; Owner: postgres -- @@ -13774,6 +13771,14 @@ ALTER TABLE ONLY public.task_logs ADD CONSTRAINT task_logs_credit_card_charge_i_078401a1_fk_credit_ca FOREIGN KEY (credit_card_charge_id) REFERENCES public.credit_card_charges(id) DEFERRABLE INITIALLY DEFERRED; +-- +-- Name: task_logs task_logs_expense_group_id_f19c75f9_fk_expense_groups_id; Type: FK CONSTRAINT; Schema: public; Owner: postgres +-- + +ALTER TABLE ONLY public.task_logs + ADD CONSTRAINT task_logs_expense_group_id_f19c75f9_fk_expense_groups_id FOREIGN KEY (expense_group_id) REFERENCES public.expense_groups(id) DEFERRABLE INITIALLY DEFERRED; + + -- -- Name: vendor_payment_lineitems vendor_payment_linei_expense_group_id_abfd0a4c_fk_expense_g; Type: FK CONSTRAINT; Schema: public; Owner: postgres -- diff --git a/tests/test_netsuite/conftest.py b/tests/test_netsuite/conftest.py index 1a7cd222..9c0f4dbc 100644 --- a/tests/test_netsuite/conftest.py +++ b/tests/test_netsuite/conftest.py @@ -62,7 +62,9 @@ def create_task_logs(db): @pytest.fixture def create_expense_report(db, add_netsuite_credentials, add_fyle_credentials): - + # Clear existing TaskLogs for this expense group + TaskLog.objects.filter(expense_group_id=1).delete() + TaskLog.objects.update_or_create( workspace_id=2, expense_group_id=1, From 3e833586a4e31e4f42716adb432cf74f731b6085 Mon Sep 17 00:00:00 2001 From: ruuushhh <66899387+ruuushhh@users.noreply.github.com> Date: Tue, 19 Nov 2024 10:05:48 +0530 Subject: [PATCH 8/9] feat: Netsuite Tax Override (#662) * feat: Netsuite Tax Override * fix comments * Fix tests * feat: netsuite tax override tests (#667) * feat: netsuite tax override tests * increase cov --------- Co-authored-by: GitHub Actions * fix: get tax mapping (#669) * fix: Get Tax Mapping * fix tests --------- Co-authored-by: GitHub Actions --------- Co-authored-by: GitHub Actions --- ...generalmapping_is_tax_balancing_enabled.py | 18 + apps/mappings/models.py | 1 + apps/netsuite/connector.py | 458 +++++++++--------- apps/netsuite/models.py | 16 +- apps/netsuite/tasks.py | 18 +- .../reset_db_fixtures/reset_db.sql | 19 +- tests/test_netsuite/conftest.py | 30 ++ tests/test_netsuite/fixtures.py | 42 +- tests/test_netsuite/test_connector.py | 261 +++++++++- tests/test_netsuite/test_tasks.py | 5 +- 10 files changed, 598 insertions(+), 270 deletions(-) create mode 100644 apps/mappings/migrations/0015_generalmapping_is_tax_balancing_enabled.py diff --git a/apps/mappings/migrations/0015_generalmapping_is_tax_balancing_enabled.py b/apps/mappings/migrations/0015_generalmapping_is_tax_balancing_enabled.py new file mode 100644 index 00000000..242be4bb --- /dev/null +++ b/apps/mappings/migrations/0015_generalmapping_is_tax_balancing_enabled.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.14 on 2024-11-04 10:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mappings', '0014_auto_20240417_0807'), + ] + + operations = [ + migrations.AddField( + model_name='generalmapping', + name='is_tax_balancing_enabled', + field=models.BooleanField(default=False, help_text='Is tax balancing enabled'), + ), + ] diff --git a/apps/mappings/models.py b/apps/mappings/models.py index 6c9f4346..a8717d79 100644 --- a/apps/mappings/models.py +++ b/apps/mappings/models.py @@ -68,6 +68,7 @@ class GeneralMapping(models.Model): ) override_tax_details = models.BooleanField(default=False, help_text='Override tax details') + is_tax_balancing_enabled = models.BooleanField(default=False, help_text='Is tax balancing enabled') workspace = models.OneToOneField(Workspace, on_delete=models.PROTECT, help_text='Reference to Workspace model', related_name='general_mappings') created_at = models.DateTimeField(auto_now_add=True, help_text='Created at datetime') diff --git a/apps/netsuite/connector.py b/apps/netsuite/connector.py index 4f273ecc..d3baa9f5 100644 --- a/apps/netsuite/connector.py +++ b/apps/netsuite/connector.py @@ -1,5 +1,6 @@ import re import json +import copy from datetime import datetime, timedelta from django.utils import timezone @@ -1161,42 +1162,100 @@ def sync_customers(self): attributes, 'PROJECT', self.workspace_id, True) return [] - - def construct_bill_lineitems( - self, - bill_lineitems: List[BillLineitem], - attachment_links: Dict, - cluster_domain: str, org_id: str, - override_tax_details: bool - ) -> List[Dict]: + + def handle_taxed_line_items(self, base_line, line, workspace_id, export_module, general_mapping: GeneralMapping): """ - Create bill line items - :return: constructed line items + Handle line items where tax is applied or modified by the user. + :param base_line: The base line item template that will be modified. + :param line: The original line with tax and amount information. + :param is_credit_card_charge: Boolean flag to differentiate between credit card charges and other transactions. + :return: List of lines (taxed and/or untaxed). """ - expense_list = [] - item_list = [] + tax_item = DestinationAttribute.objects.filter( + workspace_id=workspace_id, + attribute_type='TAX_ITEM', + destination_id=str(line.tax_item_id) + ).first() + tax_item_rate = tax_item.detail['tax_rate'] - for line in bill_lineitems: - expense: Expense = Expense.objects.get(pk=line.expense_id) + lines = [] + original_amount = round(line.amount, 2) + expected_tax_amount = round((line.amount * (tax_item_rate / 100)) / (1 + (tax_item_rate / 100)), 2) + + if general_mapping.is_tax_balancing_enabled and round(line.tax_amount, 2) != expected_tax_amount: + # Recalculate the net amount based on the modified tax + recalculated_net_amount = round((line.tax_amount * 100) / tax_item_rate, 2) + untaxed_amount = round(original_amount - recalculated_net_amount - line.tax_amount, 2) + + # Create a taxable line item + taxable_line = copy.deepcopy(base_line) + taxable_line['amount'] = recalculated_net_amount + taxable_line['taxCode']['internalId'] = line.tax_item_id + + # Create an untaxed line item + untaxed_line = copy.deepcopy(base_line) + untaxed_line['amount'] = untaxed_amount + untaxed_line['taxCode']['internalId'] = general_mapping.default_tax_code_id # Use default for untaxed items + + if export_module == 'JOURNAL_ENTRY': + taxable_line['grossAmt'] = round(recalculated_net_amount + line.tax_amount, 2) + taxable_line['debit'] = recalculated_net_amount + taxable_line.pop('amount', None) + untaxed_line['grossAmt'] = round(untaxed_amount, 2) + untaxed_line['debit'] = untaxed_amount + untaxed_line.pop('amount', None) + + if export_module in ('EXPENSE_REPORT', 'JOURNAL_ENTRY'): + taxable_line['tax1Amt'] = round(line.tax_amount, 2) # Tax is applied to this line - netsuite_custom_segments = line.netsuite_custom_segments + if export_module == 'BILL' and taxable_line.get('rate'): + taxable_line['rate'] = str(round(line.amount - line.tax_amount, 2)) - if attachment_links and expense.expense_id in attachment_links: - netsuite_custom_segments.append( - { - 'scriptId': 'custcolfyle_receipt_link', - 'type': 'String', - 'value': attachment_links[expense.expense_id] - } - ) - netsuite_custom_segments.append( - { - 'scriptId': 'custcolfyle_receipt_link_2', - 'type': 'String', - 'value': attachment_links[expense.expense_id] - } - ) + lines.append(taxable_line) + lines.append(untaxed_line) + else: + # When the tax is not modified, just subtract the tax and apply it directly + base_line['amount'] = round(original_amount - line.tax_amount, 2) + base_line['taxCode']['internalId'] = line.tax_item_id + + if export_module in ('EXPENSE_REPORT', 'JOURNAL_ENTRY'): + base_line['tax1Amt'] = round(line.tax_amount, 2) # Tax is applied to this line + + if export_module == 'BILL' and base_line.get('rate'): + base_line['rate'] = str(round(line.amount - line.tax_amount, 2)) + if export_module == 'JOURNAL_ENTRY': + base_line['grossAmt'] = original_amount + base_line['debit'] = round(original_amount - line.tax_amount, 2) + base_line.pop('amount', None) + + lines.append(base_line) + + return lines + + def prepare_custom_segments(self, line_netsuite_custom_segments, attachment_links, expense, org_id, is_credit=False): + """ + Prepare custom segments for line items. + """ + netsuite_custom_segments = line_netsuite_custom_segments + + if attachment_links and expense.expense_id in attachment_links: + netsuite_custom_segments.append( + { + 'scriptId': 'custcolfyle_receipt_link', + 'type': 'String', + 'value': attachment_links[expense.expense_id] + } + ) + netsuite_custom_segments.append( + { + 'scriptId': 'custcolfyle_receipt_link_2', + 'type': 'String', + 'value': attachment_links[expense.expense_id] + } + ) + + if not is_credit: netsuite_custom_segments.append( { 'scriptId': 'custcolfyle_expense_url', @@ -1220,13 +1279,35 @@ def construct_bill_lineitems( } ) - lineitem = { + return netsuite_custom_segments + + def construct_bill_lineitems( + self, + bill_lineitems: List[BillLineitem], + attachment_links: Dict, + cluster_domain: str, org_id: str, + override_tax_details: bool, + general_mapping: GeneralMapping + ) -> List[Dict]: + """ + Create bill line items + :return: constructed line items + """ + expense_list = [] + item_list = [] + + for line in bill_lineitems: + expense: Expense = Expense.objects.get(pk=line.expense_id) + + netsuite_custom_segments = self.prepare_custom_segments(line.netsuite_custom_segments, attachment_links, expense, org_id) + + base_line = { 'orderDoc': None, 'orderLine': None, 'line': None, - 'amount': line.amount - line.tax_amount if (line.tax_item_id and line.tax_amount is not None) else line.amount, - 'grossAmt': None if override_tax_details else line.amount, - 'taxDetailsReference': expense.expense_number if override_tax_details else None, + 'amount': line.amount, + 'grossAmt': line.amount, + 'taxDetailsReference': None, 'department': { 'name': None, 'internalId': line.department_id, @@ -1254,10 +1335,10 @@ def construct_bill_lineitems( 'customFieldList': netsuite_custom_segments, 'isBillable': line.billable, 'tax1Amt': None, - 'taxAmount': line.tax_amount if (line.tax_item_id and line.tax_amount is not None and not override_tax_details) else None, + 'taxAmount': None, 'taxCode':{ 'name': None, - 'internalId': line.tax_item_id if (line.tax_item_id and line.tax_amount is not None and not override_tax_details) else None, + 'internalId': None, 'externalId': None, 'type': 'taxGroup' }, @@ -1270,41 +1351,59 @@ def construct_bill_lineitems( } if line.detail_type == 'AccountBasedExpenseLineDetail': - lineitem['account'] = { + base_line['account'] = { 'name': None, 'internalId': line.account_id, 'externalId': None, 'type': 'account' } - lineitem['category'] = None - lineitem['memo'] = line.memo - lineitem['projectTask'] = None + base_line['category'] = None + base_line['memo'] = line.memo + base_line['projectTask'] = None - expense_list.append(lineitem) + if line.tax_item_id is None or line.tax_amount is None: + expense_list.append(base_line) + else: + if override_tax_details: + base_line['grossAmt'] = None + base_line['taxDetailsReference'] = expense.expense_number + base_line['amount'] = line.amount - line.tax_amount + expense_list.append(base_line) + else: + expense_list += self.handle_taxed_line_items(base_line, line, expense.workspace_id, 'BILL', general_mapping) else: - lineitem['item'] = { + base_line['item'] = { 'name': None, 'internalId': line.item_id, 'externalId': None, 'type': None } - lineitem['vendorName'] = None - lineitem['quantity'] = 1.0 - lineitem['units'] = None - lineitem['inventoryDetail'] = None - lineitem['description'] = line.memo - lineitem['serialNumbers'] = None - lineitem['binNumbers'] = None - lineitem['expirationDate'] = None - lineitem['rate'] = str(line.amount - line.tax_amount if (line.tax_item_id and line.tax_amount is not None) else line.amount) - lineitem['options'] = None - lineitem['landedCostCategory'] = None - lineitem['billVarianceStatus'] = None - lineitem['billreceiptsList'] = None - lineitem['landedCost'] = None - - item_list.append(lineitem) + base_line['vendorName'] = None + base_line['quantity'] = 1.0 + base_line['units'] = None + base_line['inventoryDetail'] = None + base_line['description'] = line.memo + base_line['serialNumbers'] = None + base_line['binNumbers'] = None + base_line['expirationDate'] = None + base_line['rate'] = str(line.amount) + base_line['options'] = None + base_line['landedCostCategory'] = None + base_line['billVarianceStatus'] = None + base_line['billreceiptsList'] = None + base_line['landedCost'] = None + + if line.tax_item_id is None or line.tax_amount is None: + item_list.append(base_line) + else: + if override_tax_details: + base_line['grossAmt'] = None + base_line['taxDetailsReference'] = expense.expense_number + base_line['amount'] = line.amount - line.tax_amount + item_list.append(base_line) + else: + item_list += self.handle_taxed_line_items(base_line, line, expense.workspace_id, 'BILL', general_mapping) return expense_list, item_list @@ -1339,7 +1438,7 @@ def construct_tax_details_list(self, bill_lineitems: List[BillLineitem]): return tax_details_list - def __construct_bill(self, bill: Bill, bill_lineitems: List[BillLineitem]) -> Dict: + def __construct_bill(self, bill: Bill, bill_lineitems: List[BillLineitem], general_mappings: GeneralMapping) -> Dict: """ Create a bill :return: constructed bill @@ -1351,7 +1450,7 @@ def __construct_bill(self, bill: Bill, bill_lineitems: List[BillLineitem]) -> Di org_id = Workspace.objects.get(id=bill.expense_group.workspace_id).fyle_org_id tax_details_list = None - expense_list, item_list = self.construct_bill_lineitems(bill_lineitems, {}, cluster_domain, org_id, bill.override_tax_details) + expense_list, item_list = self.construct_bill_lineitems(bill_lineitems, {}, cluster_domain, org_id, bill.override_tax_details, general_mappings) if bill.override_tax_details: tax_details_list = self.construct_tax_details_list(bill_lineitems) @@ -1447,13 +1546,13 @@ def __construct_bill(self, bill: Bill, bill_lineitems: List[BillLineitem]) -> Di return bill_payload - def post_bill(self, bill: Bill, bill_lineitems: List[BillLineitem]): + def post_bill(self, bill: Bill, bill_lineitems: List[BillLineitem], general_mappings: GeneralMapping): """ Post vendor bills to NetSuite """ configuration = Configuration.objects.get(workspace_id=self.workspace_id) try: - bills_payload = self.__construct_bill(bill, bill_lineitems) + bills_payload = self.__construct_bill(bill, bill_lineitems, general_mappings) logger.info("| Payload for Bill creation | Content: {{WORKSPACE_ID: {} EXPENSE_GROUP_ID: {} BILL_PAYLOAD: {}}}".format(self.workspace_id, bill.expense_group.id, bills_payload)) created_bill = self.connection.vendor_bills.post(bills_payload) @@ -1465,7 +1564,7 @@ def post_bill(self, bill: Bill, bill_lineitems: List[BillLineitem]): message = 'An error occured in a upsert request: The transaction date you specified is not within the date range of your accounting period.' if configuration.change_accounting_period and detail['message'] == message: first_day_of_month = datetime.today().date().replace(day=1) - bills_payload = self.__construct_bill(bill, bill_lineitems) + bills_payload = self.__construct_bill(bill, bill_lineitems, general_mappings) bills_payload['tranDate'] = first_day_of_month created_bill = self.connection.vendor_bills.post(bills_payload) @@ -1482,7 +1581,7 @@ def get_bill(self, internal_id): return bill def construct_credit_card_charge_lineitems( - self, credit_card_charge_lineitem: CreditCardChargeLineItem, + self, credit_card_charge_lineitem: CreditCardChargeLineItem, general_mapping: GeneralMapping, attachment_links: Dict, cluster_domain: str, org_id: str) -> List[Dict]: """ Create credit_card_charge line items @@ -1494,80 +1593,39 @@ def construct_credit_card_charge_lineitems( expense = Expense.objects.get(pk=line.expense_id) - netsuite_custom_segments = line.netsuite_custom_segments + netsuite_custom_segments = self.prepare_custom_segments(line.netsuite_custom_segments, attachment_links, expense, org_id) - if attachment_links and expense.expense_id in attachment_links: - netsuite_custom_segments.append( - { - 'scriptId': 'custcolfyle_receipt_link', - 'value': attachment_links[expense.expense_id] - } - ) - netsuite_custom_segments.append( - { - 'scriptId': 'custcolfyle_receipt_link_2', - 'type': 'String', - 'value': attachment_links[expense.expense_id] - } - ) - - netsuite_custom_segments.append( - { - 'scriptId': 'custcolfyle_expense_url', - 'value': '{}/app/admin/#/enterprise/view_expense/{}?org_id={}'.format( - settings.FYLE_EXPENSE_URL, - expense.expense_id, - org_id - ) - } - ) - netsuite_custom_segments.append( - { - 'scriptId': 'custcolfyle_expense_url_2', - 'value': '{}/app/admin/#/enterprise/view_expense/{}?org_id={}'.format( - settings.FYLE_EXPENSE_URL, - expense.expense_id, - org_id - ) - } - ) - - line = { - 'account': { - 'internalId': line.account_id - }, - 'amount': line.amount - line.tax_amount if (line.tax_item_id and line.tax_amount is not None) else line.amount, + base_line = { + 'account': {'internalId': line.account_id}, + 'amount': line.amount, 'memo': line.memo, 'grossAmt': line.amount, - 'department': { - 'internalId': line.department_id - }, - 'class': { - 'internalId': line.class_id - }, - 'location': { - 'internalId': line.location_id - }, - 'customer': { - 'internalId': line.customer_id - }, + 'department': {'internalId': line.department_id}, + 'class': {'internalId': line.class_id}, + 'location': {'internalId': line.location_id}, + 'customer': {'internalId': line.customer_id}, 'customFieldList': netsuite_custom_segments, 'isBillable': line.billable, 'taxAmount': None, 'taxCode': { - 'name': None, - 'internalId': line.tax_item_id if (line.tax_item_id and line.tax_amount is not None) else None, 'externalId': None, + 'internalId': None, + 'name': None, 'type': 'taxGroup' }, } - lines.append(line) + + # Handle cases where no tax is applied first + if line.tax_item_id is None or line.tax_amount is None: + lines.append(base_line) + else: + lines += self.handle_taxed_line_items(base_line, line, expense.workspace_id, 'CREDIT_CARD_CHARGE', general_mapping) return lines def __construct_credit_card_charge( self, credit_card_charge: CreditCardCharge, - credit_card_charge_lineitem: CreditCardChargeLineItem, attachment_links: Dict) -> Dict: + credit_card_charge_lineitem: CreditCardChargeLineItem, general_mapping: GeneralMapping, attachment_links: Dict) -> Dict: """ Create a credit_card_charge :return: constructed credit_card_charge @@ -1606,7 +1664,7 @@ def __construct_credit_card_charge( 'memo': credit_card_charge.memo, 'tranid': credit_card_charge.reference_number, 'expenses': self.construct_credit_card_charge_lineitems( - credit_card_charge_lineitem, attachment_links, cluster_domain, org_id + credit_card_charge_lineitem, general_mapping, attachment_links, cluster_domain, org_id ), 'externalId': credit_card_charge.external_id } @@ -1614,7 +1672,7 @@ def __construct_credit_card_charge( return credit_card_charge_payload def post_credit_card_charge(self, credit_card_charge: CreditCardCharge, - credit_card_charge_lineitem: CreditCardChargeLineItem, attachment_links: Dict, + credit_card_charge_lineitem: CreditCardChargeLineItem, general_mapping: GeneralMapping, attachment_links: Dict, refund: bool): """ Post vendor credit_card_charges to NetSuite @@ -1641,7 +1699,7 @@ def post_credit_card_charge(self, credit_card_charge: CreditCardCharge, f"script=customscript_cc_refund_fyle&deploy=customdeploy_cc_refund_fyle" credit_card_charges_payload = self.__construct_credit_card_charge( - credit_card_charge, credit_card_charge_lineitem, attachment_links) + credit_card_charge, credit_card_charge_lineitem, general_mapping, attachment_links) logger.info("| Payload for Credit Card Charge creation | Content: {{WORKSPACE_ID: {} EXPENSE_GROUP_ID: {} CREDIT_CARD_CHARGE_PAYLOAD: {}}}".format(self.workspace_id, credit_card_charge.expense_group.id, credit_card_charges_payload)) @@ -1696,7 +1754,7 @@ def post_credit_card_charge(self, credit_card_charge: CreditCardCharge, raise NetSuiteRequestError(code=code, message=message) def construct_expense_report_lineitems( - self, expense_report_lineitems: List[ExpenseReportLineItem], attachment_links: Dict, cluster_domain: str, + self, expense_report_lineitems: List[ExpenseReportLineItem], general_mapping: GeneralMapping, attachment_links: Dict, cluster_domain: str, org_id: str ) -> List[Dict]: """ @@ -1707,7 +1765,7 @@ def construct_expense_report_lineitems( for line in expense_report_lineitems: expense: Expense = Expense.objects.get(pk=line.expense_id) - netsuite_custom_segments = line.netsuite_custom_segments + netsuite_custom_segments = self.prepare_custom_segments(line.netsuite_custom_segments, attachment_links, expense, org_id) if expense.foreign_amount: if expense.amount == 0: @@ -1717,47 +1775,8 @@ def construct_expense_report_lineitems( else: foreign_amount = None - if attachment_links and expense.expense_id in attachment_links: - netsuite_custom_segments.append( - { - 'scriptId': 'custcolfyle_receipt_link', - 'type': 'String', - 'value': attachment_links[expense.expense_id] - } - ) - netsuite_custom_segments.append( - { - 'scriptId': 'custcolfyle_receipt_link_2', - 'type': 'String', - 'value': attachment_links[expense.expense_id] - } - ) - - netsuite_custom_segments.append( - { - 'scriptId': 'custcolfyle_expense_url', - 'type': 'String', - 'value': '{}/app/admin/#/enterprise/view_expense/{}?org_id={}'.format( - settings.FYLE_EXPENSE_URL, - expense.expense_id, - org_id - ) - } - ) - netsuite_custom_segments.append( - { - 'scriptId': 'custcolfyle_expense_url_2', - 'type': 'String', - 'value': '{}/app/admin/#/enterprise/view_expense/{}?org_id={}'.format( - settings.FYLE_EXPENSE_URL, - expense.expense_id, - org_id - ) - } - ) - - lineitem = { - 'amount': line.amount - line.tax_amount if (line.tax_item_id and line.tax_amount is not None) else line.amount, + base_line = { + 'amount': line.amount, 'category': { 'name': None, 'internalId': line.category, @@ -1809,10 +1828,10 @@ def construct_expense_report_lineitems( 'rate': None, 'receipt': None, 'refNumber': None, - 'tax1Amt': line.tax_amount if (line.tax_item_id and line.tax_amount is not None) else None, + 'tax1Amt': None, 'taxCode': { 'name': None, - 'internalId': line.tax_item_id if (line.tax_item_id and line.tax_amount is not None) else None, + 'internalId': None, 'externalId': None, 'type': 'taxGroup' }, @@ -1820,12 +1839,16 @@ def construct_expense_report_lineitems( 'taxRate2': None } - lines.append(lineitem) + # Handle cases where no tax is applied first + if line.tax_item_id is None or line.tax_amount is None: + lines.append(base_line) + else: + lines += self.handle_taxed_line_items(base_line, line, expense.workspace_id, 'EXPENSE_REPORT', general_mapping) return lines def __construct_expense_report(self, expense_report: ExpenseReport, - expense_report_lineitems: List[ExpenseReportLineItem]) -> Dict: + expense_report_lineitems: List[ExpenseReportLineItem], general_mapping: GeneralMapping) -> Dict: """ Create a expense report :return: constructed expense report @@ -1909,7 +1932,7 @@ def __construct_expense_report(self, expense_report: ExpenseReport, 'type': 'location' }, 'expenseList': self.construct_expense_report_lineitems( - expense_report_lineitems, {}, cluster_domain, org_id + expense_report_lineitems, general_mapping, {}, cluster_domain, org_id ), 'accountingBookDetailList': None, 'customFieldList': None, @@ -1921,14 +1944,14 @@ def __construct_expense_report(self, expense_report: ExpenseReport, def post_expense_report( self, expense_report: ExpenseReport, - expense_report_lineitems: List[ExpenseReportLineItem]): + expense_report_lineitems: List[ExpenseReportLineItem], general_mapping: GeneralMapping): """ Post expense reports to NetSuite """ configuration = Configuration.objects.get(workspace_id=self.workspace_id) try: expense_report_payload = self.__construct_expense_report(expense_report, - expense_report_lineitems) + expense_report_lineitems, general_mapping) logger.info("| Payload for Expense Report creation | Content: {{WORKSPACE_ID: {} EXPENSE_GROUP_ID: {} EXPENSE_REPORT_PAYLOAD: {}}}".format(self.workspace_id, expense_report.expense_group.id, expense_report_payload)) @@ -1942,7 +1965,7 @@ def post_expense_report( if configuration.change_accounting_period and detail['message'] == message: expense_report_payload = self.__construct_expense_report(expense_report, - expense_report_lineitems) + expense_report_lineitems, general_mapping) first_day_of_month = datetime.today().date().replace(day=1) expense_report_payload['tranDate'] = first_day_of_month.strftime('%Y-%m-%dT%H:%M:%S') @@ -1962,7 +1985,7 @@ def get_expense_report(self, internal_id): return expense_report - def construct_journal_entry_lineitems(self, journal_entry_lineitems: List[JournalEntryLineItem], org_id: str, + def construct_journal_entry_lineitems(self, journal_entry_lineitems: List[JournalEntryLineItem], general_mapping: GeneralMapping, org_id: str, credit=None, debit=None, attachment_links: Dict = None, cluster_domain: str = None) -> List[Dict]: """ @@ -1980,50 +2003,9 @@ def construct_journal_entry_lineitems(self, journal_entry_lineitems: List[Journa if debit is None: account_ref = line.debit_account_id - netsuite_custom_segments = line.netsuite_custom_segments - - if attachment_links and expense.expense_id in attachment_links: - netsuite_custom_segments.append( - { - 'scriptId': 'custcolfyle_receipt_link', - 'type': 'String', - 'value': attachment_links[expense.expense_id] - } - ) - netsuite_custom_segments.append( - { - 'scriptId': 'custcolfyle_receipt_link_2', - 'type': 'String', - 'value': attachment_links[expense.expense_id] - } - ) - - if debit: - netsuite_custom_segments.append( - { - 'scriptId': 'custcolfyle_expense_url', - 'type': 'String', - 'value': '{}/app/admin/#/enterprise/view_expense/{}?org_id={}'.format( - settings.FYLE_EXPENSE_URL, - expense.expense_id, - org_id - ) - } - ) - netsuite_custom_segments.append( - { - 'scriptId': 'custcolfyle_expense_url_2', - 'type': 'String', - 'value': '{}/app/admin/#/enterprise/view_expense/{}?org_id={}'.format( - settings.FYLE_EXPENSE_URL, - expense.expense_id, - org_id - ) - } - ) + netsuite_custom_segments = self.prepare_custom_segments(line.netsuite_custom_segments, attachment_links, expense, org_id, credit) - tax_inclusive_amount = round((line.amount - line.tax_amount), 2) if (line.tax_amount is not None and line.tax_item_id ) else line.amount - lineitem = { + base_line = { 'account': { 'name': None, 'internalId': account_ref, @@ -2054,14 +2036,14 @@ def construct_journal_entry_lineitems(self, journal_entry_lineitems: List[Journa 'externalId': None, 'type': 'vendor' }, - 'credit': line.amount if credit is not None else None, + 'credit': None, 'creditTax': None, 'customFieldList': netsuite_custom_segments, - 'debit': tax_inclusive_amount if debit is not None else None, + 'debit': line.amount, 'debitTax': None, 'eliminate': None, 'endDate': None, - 'grossAmt': line.amount if (line.tax_amount is not None and line.tax_item_id and debit is not None) else None, + 'grossAmt': None, 'line': None, 'lineTaxCode': None, 'lineTaxRate': None, @@ -2074,18 +2056,28 @@ def construct_journal_entry_lineitems(self, journal_entry_lineitems: List[Journa 'tax1Acct': None, 'taxAccount': None, 'taxBasis': None, - 'tax1Amt': line.tax_amount if (line.tax_amount is not None and line.tax_item_id and debit is not None) else None, + 'tax1Amt': None, 'taxCode': { 'name': None, - 'internalId': line.tax_item_id if (line.tax_amount is not None and line.tax_item_id ) else None, + 'internalId': None, 'externalId': None, 'type': 'taxGroup' - } if debit is not None else None, + }, 'taxRate1': None, 'totalAmount': None, } - lines.append(lineitem) + + if debit: + if line.tax_item_id is None or line.tax_amount is None: + lines.append(base_line) + else: + lines += self.handle_taxed_line_items(base_line, line, expense.workspace_id, 'JOURNAL_ENTRY', general_mapping) + elif credit: + base_line['credit'] = line.amount + base_line['debit'] = None + lines.append(base_line) + return lines @@ -2172,7 +2164,7 @@ def __construct_single_itemized_credit_line(journal_entry_lineitems: List[Journa return lines def __construct_journal_entry(self, journal_entry: JournalEntry, - journal_entry_lineitems: List[JournalEntryLineItem], configuration: Configuration) -> Dict: + journal_entry_lineitems: List[JournalEntryLineItem], configuration: Configuration, general_mapping: GeneralMapping) -> Dict: """ Create a journal entry report :return: constructed journal entry @@ -2185,12 +2177,12 @@ def __construct_journal_entry(self, journal_entry: JournalEntry, if configuration.je_single_credit_line: credit_line = self.__construct_single_itemized_credit_line(journal_entry_lineitems) else: - credit_line = self.construct_journal_entry_lineitems(journal_entry_lineitems, credit='Credit', org_id=org_id) + credit_line = self.construct_journal_entry_lineitems(journal_entry_lineitems, credit='Credit', org_id=org_id, general_mapping=general_mapping) debit_line = self.construct_journal_entry_lineitems( journal_entry_lineitems, debit='Debit', attachment_links={}, - cluster_domain=cluster_domain, org_id=org_id + cluster_domain=cluster_domain, org_id=org_id, general_mapping=general_mapping ) lines = [] lines.extend(credit_line) @@ -2256,13 +2248,13 @@ def __construct_journal_entry(self, journal_entry: JournalEntry, return journal_entry_payload def post_journal_entry(self, journal_entry: JournalEntry, - journal_entry_lineitems: List[JournalEntryLineItem], configuration: Configuration): + journal_entry_lineitems: List[JournalEntryLineItem], configuration: Configuration, general_mapping: GeneralMapping): """ Post journal entries to NetSuite """ configuration = Configuration.objects.get(workspace_id=self.workspace_id) try: - journal_entry_payload = self.__construct_journal_entry(journal_entry, journal_entry_lineitems, configuration) + journal_entry_payload = self.__construct_journal_entry(journal_entry, journal_entry_lineitems, configuration, general_mapping) logger.info("| Payload for Journal Entry creation | Content: {{WORKSPACE_ID: {} EXPENSE_GROUP_ID: {} JOURNAL_ENTRY_PAYLOAD: {}}}".format(self.workspace_id, journal_entry.expense_group.id, journal_entry_payload)) @@ -2276,7 +2268,7 @@ def post_journal_entry(self, journal_entry: JournalEntry, if configuration.change_accounting_period and detail['message'] == message: first_day_of_month = datetime.today().date().replace(day=1) - journal_entry_payload = self.__construct_journal_entry(journal_entry, journal_entry_lineitems, configuration) + journal_entry_payload = self.__construct_journal_entry(journal_entry, journal_entry_lineitems, configuration, general_mapping) journal_entry_payload['tranDate'] = first_day_of_month created_journal_entry = self.connection.journal_entries.post(journal_entry_payload) diff --git a/apps/netsuite/models.py b/apps/netsuite/models.py index b5c104a4..d69657b4 100644 --- a/apps/netsuite/models.py +++ b/apps/netsuite/models.py @@ -115,12 +115,18 @@ def get_tax_group_mapping(lineitem: Expense = None, workspace_id: int = None): def get_tax_item_id_or_none(expense_group: ExpenseGroup, general_mapping: GeneralMapping, lineitem: Expense = None): tax_code = None - mapping = get_tax_group_mapping(lineitem, expense_group.workspace_id) + tax_setting: MappingSetting = MappingSetting.objects.filter( + workspace_id=expense_group.workspace_id, + destination_field='TAX_ITEM' + ).first() + + if tax_setting: + mapping = get_tax_group_mapping(lineitem, expense_group.workspace_id) - if mapping: - tax_code = mapping.destination.destination_id - else: - tax_code = general_mapping.default_tax_code_id + if mapping: + tax_code = mapping.destination.destination_id + else: + tax_code = general_mapping.default_tax_code_id return tax_code diff --git a/apps/netsuite/tasks.py b/apps/netsuite/tasks.py index 36f7dfd3..da56208a 100644 --- a/apps/netsuite/tasks.py +++ b/apps/netsuite/tasks.py @@ -326,9 +326,9 @@ def construct_payload_and_update_export(expense_id_receipt_url_map: dict, task_l construct_lines = getattr(netsuite_connection, func) # calling the target construct payload function with credit and debit - credit_line = construct_lines(export_line_items, credit='Credit', org_id=workspace.fyle_org_id) + credit_line = construct_lines(export_line_items, general_mappings, credit='Credit', org_id=workspace.fyle_org_id) debit_line = construct_lines( - export_line_items, debit='Debit', attachment_links=expense_id_receipt_url_map, + export_line_items, general_mappings, debit='Debit', attachment_links=expense_id_receipt_url_map, cluster_domain=cluster_domain, org_id=workspace.fyle_org_id ) lines.extend(credit_line) @@ -337,11 +337,11 @@ def construct_payload_and_update_export(expense_id_receipt_url_map: dict, task_l elif task_log.type == 'CREATING_BILL': construct_lines = getattr(netsuite_connection, func) # calling the target construct payload function - expense_list, item_list = construct_lines(export_line_items, expense_id_receipt_url_map, cluster_domain, workspace.fyle_org_id, general_mappings.override_tax_details) + expense_list, item_list = construct_lines(export_line_items, expense_id_receipt_url_map, cluster_domain, workspace.fyle_org_id, general_mappings.override_tax_details, general_mappings) else: construct_lines = getattr(netsuite_connection, func) # calling the target construct payload function - lines = construct_lines(export_line_items, expense_id_receipt_url_map, cluster_domain, workspace.fyle_org_id) + lines = construct_lines(export_line_items, general_mappings, expense_id_receipt_url_map, cluster_domain, workspace.fyle_org_id) # final payload to be sent to netsuite, since this is an update operation, we need to pass the external id if task_log.type == 'CREATING_BILL': @@ -487,7 +487,7 @@ def create_bill(expense_group: ExpenseGroup, task_log_id, last_export): bill_lineitems_objects = BillLineitem.create_bill_lineitems(expense_group, configuration) - created_bill = netsuite_connection.post_bill(bill_object, bill_lineitems_objects) + created_bill = netsuite_connection.post_bill(bill_object, bill_lineitems_objects, general_mappings) logger.info('Created Bill with Expense Group %s successfully', expense_group.id) task_log.detail = created_bill @@ -567,7 +567,7 @@ def create_credit_card_charge(expense_group, task_log_id, last_export): attachment_links[expense.expense_id] = attachment_link created_credit_card_charge = netsuite_connection.post_credit_card_charge( - credit_card_charge_object, credit_card_charge_lineitems_object, attachment_links, refund + credit_card_charge_object, credit_card_charge_lineitems_object, general_mappings, attachment_links, refund ) worker_logger.info('Created Credit Card Charge with Expense Group %s successfully', expense_group.id) @@ -612,6 +612,7 @@ def create_expense_report(expense_group, task_log_id, last_export): return configuration = Configuration.objects.get(workspace_id=expense_group.workspace_id) + general_mapping = GeneralMapping.objects.get(workspace_id=expense_group.workspace_id) fyle_credentials = FyleCredential.objects.get(workspace_id=expense_group.workspace_id) netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=expense_group.workspace_id) @@ -633,7 +634,7 @@ def create_expense_report(expense_group, task_log_id, last_export): ) created_expense_report = netsuite_connection.post_expense_report( - expense_report_object, expense_report_lineitems_objects + expense_report_object, expense_report_lineitems_objects, general_mapping ) worker_logger.info('Created Expense Report with Expense Group %s successfully', expense_group.id) @@ -676,6 +677,7 @@ def create_journal_entry(expense_group, task_log_id, last_export): return configuration = Configuration.objects.get(workspace_id=expense_group.workspace_id) + general_mapping = GeneralMapping.objects.get(workspace_id=expense_group.workspace_id) fyle_credentials = FyleCredential.objects.get(workspace_id=expense_group.workspace_id) @@ -698,7 +700,7 @@ def create_journal_entry(expense_group, task_log_id, last_export): ) created_journal_entry = netsuite_connection.post_journal_entry( - journal_entry_object, journal_entry_lineitems_objects, configuration + journal_entry_object, journal_entry_lineitems_objects, configuration, general_mapping ) worker_logger.info('Created Journal Entry with Expense Group %s successfully', expense_group.id) diff --git a/tests/sql_fixtures/reset_db_fixtures/reset_db.sql b/tests/sql_fixtures/reset_db_fixtures/reset_db.sql index 55889c0f..285dd66b 100644 --- a/tests/sql_fixtures/reset_db_fixtures/reset_db.sql +++ b/tests/sql_fixtures/reset_db_fixtures/reset_db.sql @@ -1440,7 +1440,8 @@ CREATE TABLE public.general_mappings ( class_level character varying(255), class_name character varying(255), default_tax_code_id character varying(255), - default_tax_code_name character varying(255) + default_tax_code_name character varying(255), + is_tax_balancing_enabled boolean NOT NULL ); @@ -7987,9 +7988,10 @@ COPY public.django_migrations (id, app, name, applied) FROM stdin; 198 netsuite 0027_auto_20240924_0820 2024-09-24 08:24:35.223017+00 199 fyle_accounting_mappings 0026_destinationattribute_code 2024-10-01 08:54:06.770864+00 200 workspaces 0039_configuration_je_single_credit_line 2024-10-11 13:43:49.169823+00 -201 fyle 0034_expense_is_posted_at_null 2024-11-17 20:37:53.17847+00 -202 tasks 0012_alter_tasklog_expense_group 2024-11-17 20:37:53.213044+00 -203 workspaces 0040_alter_configuration_change_accounting_period 2024-11-18 04:28:36.094429+00 +201 mappings 0015_generalmapping_is_tax_balancing_enabled 2024-11-11 18:30:21.068097+00 +202 fyle 0034_expense_is_posted_at_null 2024-11-17 20:37:53.17847+00 +203 tasks 0012_alter_tasklog_expense_group 2024-11-17 20:37:53.213044+00 +204 workspaces 0040_alter_configuration_change_accounting_period 2024-11-18 04:28:36.094429+00 \. @@ -11655,10 +11657,10 @@ COPY public.fyle_credentials (id, refresh_token, created_at, updated_at, workspa -- Data for Name: general_mappings; Type: TABLE DATA; Schema: public; Owner: postgres -- -COPY public.general_mappings (id, location_name, location_id, accounts_payable_name, accounts_payable_id, created_at, updated_at, workspace_id, default_ccc_account_id, default_ccc_account_name, reimbursable_account_id, reimbursable_account_name, default_ccc_vendor_id, default_ccc_vendor_name, vendor_payment_account_id, vendor_payment_account_name, location_level, department_level, use_employee_department, use_employee_class, use_employee_location, department_id, department_name, override_tax_details, class_id, class_level, class_name, default_tax_code_id, default_tax_code_name) FROM stdin; -1 hubajuba 8 Accounts Payable 25 2021-11-15 08:56:31.432106+00 2021-11-15 13:21:26.113427+00 1 \N \N 118 Unapproved Expense Reports 1674 Ashwin Vendor \N \N TRANSACTION_BODY \N f f f \N \N f \N \N \N \N \N -2 \N \N Accounts Payable 25 2021-11-16 04:18:39.195287+00 2021-11-16 04:18:39.195312+00 2 228 Aus Account 118 Unapproved Expense Reports 12104 Nilesh Aus Vendor \N \N \N \N f f f \N \N f \N \N \N \N \N -3 hukiju 10 \N \N 2021-12-03 11:24:17.962764+00 2021-12-03 11:24:17.962809+00 49 228 Aus Account 228 Aus Account 12104 Nilesh Aus Vendor \N \N TRANSACTION_BODY \N f f f \N \N f \N \N \N \N \N +COPY public.general_mappings (id, location_name, location_id, accounts_payable_name, accounts_payable_id, created_at, updated_at, workspace_id, default_ccc_account_id, default_ccc_account_name, reimbursable_account_id, reimbursable_account_name, default_ccc_vendor_id, default_ccc_vendor_name, vendor_payment_account_id, vendor_payment_account_name, location_level, department_level, use_employee_department, use_employee_class, use_employee_location, department_id, department_name, override_tax_details, class_id, class_level, class_name, default_tax_code_id, default_tax_code_name, is_tax_balancing_enabled) FROM stdin; +1 hubajuba 8 Accounts Payable 25 2021-11-15 08:56:31.432106+00 2021-11-15 13:21:26.113427+00 1 \N \N 118 Unapproved Expense Reports 1674 Ashwin Vendor \N \N TRANSACTION_BODY \N f f f \N \N f \N \N \N \N \N f +2 \N \N Accounts Payable 25 2021-11-16 04:18:39.195287+00 2021-11-16 04:18:39.195312+00 2 228 Aus Account 118 Unapproved Expense Reports 12104 Nilesh Aus Vendor \N \N \N \N f f f \N \N f \N \N \N \N \N f +3 hukiju 10 \N \N 2021-12-03 11:24:17.962764+00 2021-12-03 11:24:17.962809+00 49 228 Aus Account 228 Aus Account 12104 Nilesh Aus Vendor \N \N TRANSACTION_BODY \N f f f \N \N f \N \N \N \N \N f \. @@ -11901,6 +11903,7 @@ SELECT pg_catalog.setval('public.django_content_type_id_seq', 47, true); -- Name: django_migrations_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres -- +SELECT pg_catalog.setval('public.django_migrations_id_seq', 201, true); SELECT pg_catalog.setval('public.django_migrations_id_seq', 203, true); diff --git a/tests/test_netsuite/conftest.py b/tests/test_netsuite/conftest.py index 9c0f4dbc..6995201e 100644 --- a/tests/test_netsuite/conftest.py +++ b/tests/test_netsuite/conftest.py @@ -78,6 +78,9 @@ def create_expense_report(db, add_netsuite_credentials, add_fyle_credentials): ) expense_group = ExpenseGroup.objects.filter(workspace_id=1).first() + for expense in expense_group.expenses.all(): + expense.workspace_id = 1 + expense.save() configuration = Configuration.objects.get(workspace_id=1) expense_report = ExpenseReport.create_expense_report(expense_group) expense_report_lineitems = ExpenseReportLineItem.create_expense_report_lineitems(expense_group, configuration) @@ -89,6 +92,9 @@ def create_expense_report(db, add_netsuite_credentials, add_fyle_credentials): def create_bill_account_based(db, add_netsuite_credentials, add_fyle_credentials): expense_group = ExpenseGroup.objects.get(id=2) + for expense in expense_group.expenses.all(): + expense.workspace_id = 1 + expense.save() configuration = Configuration.objects.get(workspace_id=1) bill = Bill.create_bill(expense_group) bill_lineitem = BillLineitem.create_bill_lineitems(expense_group, configuration) @@ -243,6 +249,9 @@ def create_bill_task(db, add_netsuite_credentials, add_fyle_credentials): def create_journal_entry(db, add_netsuite_credentials, add_fyle_credentials): expense_group = ExpenseGroup.objects.filter(workspace_id=49).first() + for expense in expense_group.expenses.all(): + expense.workspace_id = 1 + expense.save() configuration = Configuration.objects.get(workspace_id=1) journal_entry = JournalEntry.create_journal_entry(expense_group) journal_entry_lineitem = JournalEntryLineItem.create_journal_entry_lineitems(expense_group, configuration) @@ -254,6 +263,9 @@ def create_journal_entry(db, add_netsuite_credentials, add_fyle_credentials): def create_credit_card_charge(db, add_netsuite_credentials, add_fyle_credentials): expense_group = ExpenseGroup.objects.filter(workspace_id=49).last() + for expense in expense_group.expenses.all(): + expense.workspace_id = 49 + expense.save() configuration = Configuration.objects.get(workspace_id=1) credit_card_charge_object = CreditCardCharge.create_credit_card_charge(expense_group) @@ -292,3 +304,21 @@ def add_custom_segment(db): custom_segments[i] = CustomSegment(**custom_segments[i]) CustomSegment.objects.bulk_create(custom_segments) + +@pytest.fixture +def add_tax_destination_attributes(db): + + for i in(1, 2, 49): + DestinationAttribute.objects.create( + id = 98765+i, + attribute_type='TAX_ITEM', + value='Rushikesh', + destination_id = '103578', + active = True, + detail = { + 'tax_rate': 5.0 + }, + workspace_id = i, + created_at = datetime.now(), + updated_at = datetime.now(), + ) \ No newline at end of file diff --git a/tests/test_netsuite/fixtures.py b/tests/test_netsuite/fixtures.py index 566486c6..0dbb4884 100644 --- a/tests/test_netsuite/fixtures.py +++ b/tests/test_netsuite/fixtures.py @@ -2,6 +2,33 @@ from datetime import datetime, timezone data = { + 'tax_list_detail' : { + 'taxDetails': [ + { + 'taxType': { + 'internalId': 'tax_type_1' + }, + 'taxCode': { + 'internalId': 'tax_code_1' + }, + 'taxRate': 'tax_type_1', + 'taxBasis': 90.0, + 'taxAmount': 10.0, + 'taxDetailsReference': 'EXP001' + }, + { + 'taxType': { + 'internalId': 'tax_type_1' + }, + 'taxCode': { + 'internalId': 'tax_code_1' + }, + 'taxRate': 'tax_type_1', 'taxBasis': 180.0, + 'taxAmount': 20.0, + 'taxDetailsReference': 'EXP002' + } + ] + }, 'expense':[{ "id": 1, "employee_email": "ashwin.t@fyle.in", @@ -964,7 +991,12 @@ 'taxAccount': None, 'taxBasis': None, 'tax1Amt': None, - 'taxCode': None, + 'taxCode': { + 'externalId': None, + 'internalId': None, + 'name': None, + 'type': 'taxGroup' + }, 'taxRate1': None, 'totalAmount': None, }, { @@ -1170,17 +1202,19 @@ 'location': {'internalId': None}, 'customer': {'internalId': None}, 'customFieldList': [{'scriptId': 'custcolfyle_expense_url', + 'type': 'String', 'value': 'None/app/admin/#/enterprise/view_expense/txcKVVELn1Vl?org_id=orHe8CpW2hyN' },{'scriptId': 'custcolfyle_expense_url_2', + 'type': 'String', 'value': 'None/app/admin/#/enterprise/view_expense/txcKVVELn1Vl?org_id=orHe8CpW2hyN' }], 'isBillable': False, 'taxAmount': None, 'taxCode': { - 'name': None, - 'internalId': None, 'externalId': None, - 'type': 'taxGroup', + 'internalId': None, + 'name': None, + 'type': 'taxGroup' }, }], 'externalId': 'cc-charge 48 - admin1@fyleforintacct.in', diff --git a/tests/test_netsuite/test_connector.py b/tests/test_netsuite/test_connector.py index 74726334..47d4217b 100644 --- a/tests/test_netsuite/test_connector.py +++ b/tests/test_netsuite/test_connector.py @@ -6,6 +6,7 @@ from fyle_accounting_mappings.models import DestinationAttribute, ExpenseAttribute, Mapping, CategoryMapping from apps.netsuite.connector import NetSuiteConnector, NetSuiteCredentials from apps.workspaces.models import Configuration, Workspace +from apps.mappings.models import GeneralMapping from netsuitesdk import NetSuiteRequestError from tests.helper import dict_compare_keys from .fixtures import data @@ -18,22 +19,70 @@ def test_construct_expense_report(create_expense_report): netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=1) netsuite_connection = NetSuiteConnector(netsuite_credentials=netsuite_credentials, workspace_id=1) + general_mapping = GeneralMapping.objects.get(workspace_id=1) expense_report, expense_report_lineitem = create_expense_report - expense_report = netsuite_connection._NetSuiteConnector__construct_expense_report(expense_report, expense_report_lineitem) + expense_report = netsuite_connection._NetSuiteConnector__construct_expense_report(expense_report, expense_report_lineitem, general_mapping) data['expense_report_payload'][0]['tranDate'] = expense_report['tranDate'] data['expense_report_payload'][0]['expenseList'][0]['expenseDate'] = expense_report['expenseList'][0]['expenseDate'] assert expense_report == data['expense_report_payload'][0] +def test_construct_expense_report_with_tax_balancing(create_expense_report, add_tax_destination_attributes): + netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=1) + netsuite_connection = NetSuiteConnector(netsuite_credentials=netsuite_credentials, workspace_id=1) + general_mapping = GeneralMapping.objects.get(workspace_id=1) + + # without tax balancing + expense_report, expense_report_lineitem = create_expense_report + expense_report_lineitem[0].amount = 100 + expense_report_lineitem[0].tax_amount = 3 + expense_report_lineitem[0].tax_item_id = '103578' + + expense_report_object = netsuite_connection._NetSuiteConnector__construct_expense_report(expense_report, expense_report_lineitem, general_mapping) + + assert len(expense_report_object['expenseList']) == 1 + assert expense_report_object['expenseList'][0]['amount'] == 97 + assert expense_report_object['expenseList'][0]['taxCode']['internalId'] == '103578' + assert expense_report_object['expenseList'][0]['tax1Amt'] == 3 + + # with tax balancing + general_mapping.is_tax_balancing_enabled = True + general_mapping.save() + + expense_report_object = netsuite_connection._NetSuiteConnector__construct_expense_report(expense_report, expense_report_lineitem, general_mapping) + + assert len(expense_report_object['expenseList']) == 2 + assert expense_report_object['expenseList'][0]['amount'] == 60 + assert expense_report_object['expenseList'][0]['taxCode']['internalId'] == '103578' + assert expense_report_object['expenseList'][0]['tax1Amt'] == 3 + assert expense_report_object['expenseList'][1]['amount'] == 37 + assert expense_report_object['expenseList'][1]['taxCode']['internalId'] == general_mapping.default_tax_code_id + + + # with tax balancing enabled and right tax amount + expense_report_lineitem[0].amount = 100 + expense_report_lineitem[0].tax_amount = 4.76 + expense_report_lineitem[0].tax_item_id = '103578' + + expense_report_object = netsuite_connection._NetSuiteConnector__construct_expense_report(expense_report, expense_report_lineitem, general_mapping) + + assert len(expense_report_object['expenseList']) == 1 + assert expense_report_object['expenseList'][0]['amount'] == 95.24 + assert expense_report_object['expenseList'][0]['taxCode']['internalId'] == '103578' + assert expense_report_object['expenseList'][0]['tax1Amt'] == 4.76 + + general_mapping.is_tax_balancing_enabled = False + general_mapping.save() def test_construct_bill_account_based(create_bill_account_based): netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=1) netsuite_connection = NetSuiteConnector(netsuite_credentials=netsuite_credentials, workspace_id=1) + general_mapping = GeneralMapping.objects.get(workspace_id=1) bill, bill_lineitem = create_bill_account_based - bill_object = netsuite_connection._NetSuiteConnector__construct_bill(bill, bill_lineitem) + bill_object = netsuite_connection._NetSuiteConnector__construct_bill(bill, bill_lineitem, general_mapping) data['bill_payload_account_based'][0]['tranDate'] = bill_object['tranDate'] data['bill_payload_account_based'][0]['tranId'] = bill_object['tranId'] @@ -44,9 +93,10 @@ def test_construct_bill_account_based(create_bill_account_based): def test_construct_bill_item_based(create_bill_item_based): netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=1) netsuite_connection = NetSuiteConnector(netsuite_credentials=netsuite_credentials, workspace_id=1) + general_mapping = GeneralMapping.objects.get(workspace_id=1) bill, bill_lineitem = create_bill_item_based - bill_object = netsuite_connection._NetSuiteConnector__construct_bill(bill, bill_lineitem) + bill_object = netsuite_connection._NetSuiteConnector__construct_bill(bill, bill_lineitem, general_mapping) assert data['bill_payload_item_based']['expenseList'] == None assert dict_compare_keys(bill_object, data['bill_payload_item_based']) == [], 'construct bill_payload entry api return diffs in keys' @@ -55,20 +105,64 @@ def test_construct_bill_item_based(create_bill_item_based): def test_construct_bill_item_and_account_based(create_bill_item_and_account_based): netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=1) netsuite_connection = NetSuiteConnector(netsuite_credentials=netsuite_credentials, workspace_id=1) + general_mapping = GeneralMapping.objects.get(workspace_id=1) bill, bill_lineitem = create_bill_item_and_account_based - bill_object = netsuite_connection._NetSuiteConnector__construct_bill(bill, bill_lineitem) + bill_object = netsuite_connection._NetSuiteConnector__construct_bill(bill, bill_lineitem, general_mapping) assert dict_compare_keys(bill_object, data['bill_payload_item_and_account_based']) == [], 'construct bill_payload entry api return diffs in keys' +def test_construct_bill_item_for_tax_balancing(create_bill_account_based, add_tax_destination_attributes): + netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=1) + netsuite_connection = NetSuiteConnector(netsuite_credentials=netsuite_credentials, workspace_id=1) + general_mapping = GeneralMapping.objects.get(workspace_id=1) + + # without tax balancing + bill, bill_lineitem = create_bill_account_based + bill_lineitem[0].amount = 100 + bill_lineitem[0].tax_amount = 3 + bill_lineitem[0].tax_item_id = '103578' + + bill_object = netsuite_connection._NetSuiteConnector__construct_bill(bill, bill_lineitem, general_mapping) + + assert len(bill_object['expenseList']) == 1 + assert bill_object['expenseList'][0]['amount'] == 97 + assert bill_object['expenseList'][0]['taxCode']['internalId'] == '103578' + assert dict_compare_keys(bill_object, data['bill_payload_account_based'][0]) == [], 'construct bill_payload entry api return diffs in keys' + + # with tax balancing + general_mapping.is_tax_balancing_enabled = True + general_mapping.save() + + bill_object = netsuite_connection._NetSuiteConnector__construct_bill(bill, bill_lineitem, general_mapping) + assert len(bill_object['expenseList']) == 2 + assert bill_object['expenseList'][0]['amount'] == 60 + assert bill_object['expenseList'][0]['taxCode']['internalId'] == '103578' + assert bill_object['expenseList'][1]['amount'] == 37 + assert bill_object['expenseList'][1]['taxCode']['internalId'] == general_mapping.default_tax_code_id + + # with tax balancing enabled and right tax amount + bill_lineitem[0].amount = 100 + bill_lineitem[0].tax_amount = 4.76 + bill_lineitem[0].tax_item_id = '103578' + + bill_object = netsuite_connection._NetSuiteConnector__construct_bill(bill, bill_lineitem, general_mapping) + assert len(bill_object['expenseList']) == 1 + assert bill_object['expenseList'][0]['amount'] == 95.24 + assert bill_object['expenseList'][0]['taxCode']['internalId'] == '103578' + + general_mapping.is_tax_balancing_enabled = False + general_mapping.save() + def test_construct_journal_entry(create_journal_entry): netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=1) netsuite_connection = NetSuiteConnector(netsuite_credentials=netsuite_credentials, workspace_id=1) configuration = Configuration.objects.get(workspace_id=1) + general_mapping = GeneralMapping.objects.get(workspace_id=1) journal_entry, journal_entry_lineitem = create_journal_entry - journal_entry_object = netsuite_connection._NetSuiteConnector__construct_journal_entry(journal_entry, journal_entry_lineitem, configuration) + journal_entry_object = netsuite_connection._NetSuiteConnector__construct_journal_entry(journal_entry, journal_entry_lineitem, configuration, general_mapping) journal_entry_object['tranDate'] = data['journal_entry_without_single_line'][0]['tranDate'] @@ -77,7 +171,7 @@ def test_construct_journal_entry(create_journal_entry): configuration.je_single_credit_line = True configuration.save() - journal_entry_object = netsuite_connection._NetSuiteConnector__construct_journal_entry(journal_entry, journal_entry_lineitem, configuration) + journal_entry_object = netsuite_connection._NetSuiteConnector__construct_journal_entry(journal_entry, journal_entry_lineitem, configuration, general_mapping) # With flag being different, the output should be different assert journal_entry_object != data['journal_entry_without_single_line'][0] @@ -137,13 +231,66 @@ def test_construct_single_itemized_credit_line(create_journal_entry): assert constructed_lines == expected_lines +def test_construct_journal_entry_with_tax_balancing(create_journal_entry, add_tax_destination_attributes): + netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=1) + netsuite_connection = NetSuiteConnector(netsuite_credentials=netsuite_credentials, workspace_id=1) + configuration = Configuration.objects.get(workspace_id=1) + general_mapping = GeneralMapping.objects.get(workspace_id=1) + + # without tax balancing + journal_entry, journal_entry_lineitem = create_journal_entry + journal_entry_lineitem[0].amount = 100 + journal_entry_lineitem[0].tax_amount = 3 + journal_entry_lineitem[0].tax_item_id = '103578' + + journal_entry_object = netsuite_connection._NetSuiteConnector__construct_journal_entry(journal_entry, journal_entry_lineitem, configuration, general_mapping) + + assert len(journal_entry_object['lineList']) == 2 + assert journal_entry_object['lineList'][1]['debit'] == 97 + assert journal_entry_object['lineList'][1]['taxCode']['internalId'] == '103578' + assert journal_entry_object['lineList'][1]['grossAmt'] == 100 + assert journal_entry_object['lineList'][1]['tax1Amt'] == 3 + + # with tax balancing + general_mapping.is_tax_balancing_enabled = True + general_mapping.save() + + journal_entry_object = netsuite_connection._NetSuiteConnector__construct_journal_entry(journal_entry, journal_entry_lineitem, configuration, general_mapping) + + assert len(journal_entry_object['lineList']) == 3 + assert journal_entry_object['lineList'][1]['debit'] == 60 + assert journal_entry_object['lineList'][1]['taxCode']['internalId'] == '103578' + assert journal_entry_object['lineList'][2]['debit'] == 37 + assert journal_entry_object['lineList'][2]['taxCode']['internalId'] == general_mapping.default_tax_code_id + assert journal_entry_object['lineList'][1]['grossAmt'] == 63 + assert journal_entry_object['lineList'][2]['grossAmt'] == 37 + assert journal_entry_object['lineList'][1]['tax1Amt'] == 3 + + # with tax balancing enabled and right tax amount + journal_entry_lineitem[0].amount = 100 + journal_entry_lineitem[0].tax_amount = 4.76 + journal_entry_lineitem[0].tax_item_id = '103578' + + journal_entry_object = netsuite_connection._NetSuiteConnector__construct_journal_entry(journal_entry, journal_entry_lineitem, configuration, general_mapping) + + assert len(journal_entry_object['lineList']) == 2 + assert journal_entry_object['lineList'][1]['debit'] == 95.24 + assert journal_entry_object['lineList'][1]['taxCode']['internalId'] == '103578' + assert journal_entry_object['lineList'][1]['tax1Amt'] == 4.76 + assert journal_entry_object['lineList'][1]['grossAmt'] == 100 + + general_mapping.is_tax_balancing_enabled = False + general_mapping.save() + + def test_contruct_credit_card_charge(create_credit_card_charge): netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=49) netsuite_connection = NetSuiteConnector(netsuite_credentials=netsuite_credentials, workspace_id=49) + general_mapping = GeneralMapping.objects.get(workspace_id=49) credit_card_charge, credit_card_charge_lineitem = create_credit_card_charge - credit_card_charge_object = netsuite_connection._NetSuiteConnector__construct_credit_card_charge(credit_card_charge, credit_card_charge_lineitem, []) + credit_card_charge_object = netsuite_connection._NetSuiteConnector__construct_credit_card_charge(credit_card_charge, credit_card_charge_lineitem, general_mapping, []) credit_card_charge_object['tranDate'] = data['credit_card_charge'][0]['tranDate'] credit_card_charge_object['tranid'] = data['credit_card_charge'][0]['tranid'] @@ -151,6 +298,47 @@ def test_contruct_credit_card_charge(create_credit_card_charge): assert credit_card_charge_object == data['credit_card_charge'][0] +def test_contruct_credit_card_charge_with_tax_balancing(create_credit_card_charge, add_tax_destination_attributes): + netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=49) + netsuite_connection = NetSuiteConnector(netsuite_credentials=netsuite_credentials, workspace_id=49) + general_mapping = GeneralMapping.objects.get(workspace_id=49) + + # without tax balancing + credit_card_charge, credit_card_charge_lineitem = create_credit_card_charge + credit_card_charge_lineitem.amount = 100 + credit_card_charge_lineitem.tax_amount = 3 + credit_card_charge_lineitem.tax_item_id = '103578' + + credit_card_charge_object = netsuite_connection._NetSuiteConnector__construct_credit_card_charge(credit_card_charge, credit_card_charge_lineitem, general_mapping, []) + + assert len(credit_card_charge_object['expenses']) == 1 + assert credit_card_charge_object['expenses'][0]['amount'] == 97 + assert credit_card_charge_object['expenses'][0]['taxCode']['internalId'] == '103578' + + # with tax balancing + general_mapping.is_tax_balancing_enabled = True + general_mapping.save() + + credit_card_charge_object = netsuite_connection._NetSuiteConnector__construct_credit_card_charge(credit_card_charge, credit_card_charge_lineitem, general_mapping, []) + + assert len(credit_card_charge_object['expenses']) == 2 + assert credit_card_charge_object['expenses'][0]['amount'] == 60 + assert credit_card_charge_object['expenses'][0]['taxCode']['internalId'] == '103578' + assert credit_card_charge_object['expenses'][1]['amount'] == 37 + assert credit_card_charge_object['expenses'][1]['taxCode']['internalId'] == general_mapping.default_tax_code_id + + # with tax balancing enabled and right tax amount + credit_card_charge_lineitem.amount = 100 + credit_card_charge_lineitem.tax_amount = 4.76 + credit_card_charge_lineitem.tax_item_id = '103578' + + credit_card_charge_object = netsuite_connection._NetSuiteConnector__construct_credit_card_charge(credit_card_charge, credit_card_charge_lineitem, general_mapping, []) + + assert len(credit_card_charge_object['expenses']) == 1 + assert credit_card_charge_object['expenses'][0]['amount'] == 95.24 + assert credit_card_charge_object['expenses'][0]['taxCode']['internalId'] == '103578' + + def test_post_vendor(mocker, db): mocker.patch( 'netsuitesdk.api.vendors.Vendors.post', @@ -662,6 +850,7 @@ def test_post_bill_exception(db, mocker, create_bill_account_based): netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=workspace_id) netsuite_connection = NetSuiteConnector(netsuite_credentials=netsuite_credentials, workspace_id=workspace_id) + general_mapping = GeneralMapping.objects.get(workspace_id=workspace_id) bill_transaction, bill_transaction_lineitems = create_bill_account_based @@ -671,7 +860,7 @@ def test_post_bill_exception(db, mocker, create_bill_account_based): with mock.patch('netsuitesdk.api.vendor_bills.VendorBills.post') as mock_call: mock_call.side_effect = [NetSuiteRequestError('An error occured in a upsert request: The transaction date you specified is not within the date range of your accounting period.'), None] - netsuite_connection.post_bill(bill_transaction, bill_transaction_lineitems) + netsuite_connection.post_bill(bill_transaction, bill_transaction_lineitems, general_mapping) def test_post_expense_report_exception(db, mocker, create_expense_report): @@ -679,6 +868,7 @@ def test_post_expense_report_exception(db, mocker, create_expense_report): netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=workspace_id) netsuite_connection = NetSuiteConnector(netsuite_credentials=netsuite_credentials, workspace_id=workspace_id) + general_mapping = GeneralMapping.objects.get(workspace_id=workspace_id) expense_report_transaction, expense_report_transaction_lineitems = create_expense_report @@ -688,7 +878,7 @@ def test_post_expense_report_exception(db, mocker, create_expense_report): with mock.patch('netsuitesdk.api.expense_reports.ExpenseReports.post') as mock_call: mock_call.side_effect = [NetSuiteRequestError('An error occured in a upsert request: The transaction date you specified is not within the date range of your accounting period.'), None] - netsuite_connection.post_expense_report(expense_report_transaction, expense_report_transaction_lineitems) + netsuite_connection.post_expense_report(expense_report_transaction, expense_report_transaction_lineitems, general_mapping) def test_post_journal_entry_exception(db, mocker, create_journal_entry): @@ -696,6 +886,7 @@ def test_post_journal_entry_exception(db, mocker, create_journal_entry): netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=workspace_id) netsuite_connection = NetSuiteConnector(netsuite_credentials=netsuite_credentials, workspace_id=workspace_id) + general_mapping = GeneralMapping.objects.get(workspace_id=workspace_id) journal_entry_transaction, journal_entry_transaction_lineitems = create_journal_entry @@ -707,7 +898,7 @@ def test_post_journal_entry_exception(db, mocker, create_journal_entry): with mock.patch('netsuitesdk.api.journal_entries.JournalEntries.post') as mock_call: mock_call.side_effect = [NetSuiteRequestError('An error occured in a upsert request: The transaction date you specified is not within the date range of your accounting period.'), None] - netsuite_connection.post_journal_entry(journal_entry_transaction, journal_entry_transaction_lineitems, configuration) + netsuite_connection.post_journal_entry(journal_entry_transaction, journal_entry_transaction_lineitems, configuration, general_mapping) def test_update_destination_attributes(db, mocker): mocker.patch( @@ -828,4 +1019,52 @@ def test_skip_sync_attributes(mocker, db): new_project_count = DestinationAttribute.objects.filter(workspace_id=1, attribute_type='CUSTOMER').count() assert new_project_count == 0 - \ No newline at end of file + +def test_constructs_tax_details_list_for_multiple_items(mocker, db): + netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=1) + netsuite_connection = NetSuiteConnector(netsuite_credentials=netsuite_credentials, workspace_id=1) + + # Create a more complete mock Mapping object + mock_mapping = mocker.Mock() + mock_mapping.destination.destination_id = 'tax_code_1' + mock_mapping.destination.detail.get.return_value = 'tax_type_1' + mock_mapping.destination.detail.all.return_value = [mocker.Mock(value=10.0)] + + # Mock get_tax_group_mapping to return our complete mock mapping + mocker.patch( + 'apps.netsuite.models.get_tax_group_mapping', + return_value=mock_mapping + ) + + # Creating mock expense objects with workspace_id and tax_group_id + expense1 = mocker.Mock( + amount=100.0, + tax_amount=10.0, + expense_number='EXP001', + workspace_id=1, + tax_group_id=1 + ) + + expense2 = mocker.Mock( + amount=200.0, + tax_amount=20.0, + expense_number='EXP002', + workspace_id=1, + tax_group_id=1 + ) + + # Creating mock bill line items with expense attribute and workspace_id + bill_lineitem1 = mocker.Mock( + expense=expense1, + workspace_id=1 + ) + bill_lineitem2 = mocker.Mock( + expense=expense2, + workspace_id=1 + ) + + bill_lineitems = [bill_lineitem1, bill_lineitem2] + + result = netsuite_connection.construct_tax_details_list(bill_lineitems) + + assert result == data['tax_list_detail'] diff --git a/tests/test_netsuite/test_tasks.py b/tests/test_netsuite/test_tasks.py index b5183f89..e3f7ab4f 100644 --- a/tests/test_netsuite/test_tasks.py +++ b/tests/test_netsuite/test_tasks.py @@ -175,7 +175,7 @@ def test_get_or_create_credit_card_vendor_create_false(mocker, db): assert created_vendor == None @pytest.mark.django_db() -def test_post_bill_success(mocker, db): +def test_post_bill_success(add_tax_destination_attributes, mocker, db): mocker.patch( 'netsuitesdk.api.vendor_bills.VendorBills.post', return_value=data['creation_response'] @@ -207,6 +207,9 @@ def test_post_bill_success(mocker, db): created_at='2023-07-07 11:57:53.184441+00', updated_at='2023-07-07 11:57:53.184441+00') expense_group = ExpenseGroup.objects.filter(workspace_id=workspace_id, fund_source='PERSONAL').first() + for expenses in expense_group.expenses.all(): + expenses.workspace_id = 2 + expenses.save() create_bill(expense_group, task_log.id, True) task_log = TaskLog.objects.get(pk=task_log.id) From a2bb92ac7e3752372461c754d3d0c2166889b355 Mon Sep 17 00:00:00 2001 From: Anish Kr Singh <116036738+anishfyle@users.noreply.github.com> Date: Wed, 20 Nov 2024 11:46:33 +0530 Subject: [PATCH 9/9] fix: add default value for is_posted_at_null (#674) * fix: add default value for is_posted_at_null * fixtures --- apps/fyle/models.py | 1 + .../sql_fixtures/reset_db_fixtures/reset_db.sql | 1 - tests/test_fyle/fixtures.py | 16 +++++++++++++--- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/apps/fyle/models.py b/apps/fyle/models.py index 3227c774..087ab4dd 100644 --- a/apps/fyle/models.py +++ b/apps/fyle/models.py @@ -176,6 +176,7 @@ def create_expense_objects(expenses: List[Dict], workspace_id, skip_update: bool 'file_ids': expense['file_ids'], 'spent_at': expense['spent_at'], 'posted_at': expense['posted_at'], + 'is_posted_at_null': expense['is_posted_at_null'], 'fund_source': SOURCE_ACCOUNT_MAP[expense['source_account_type']], 'verified_at': expense['verified_at'], 'custom_properties': expense['custom_properties'], diff --git a/tests/sql_fixtures/reset_db_fixtures/reset_db.sql b/tests/sql_fixtures/reset_db_fixtures/reset_db.sql index 285dd66b..9f4b573e 100644 --- a/tests/sql_fixtures/reset_db_fixtures/reset_db.sql +++ b/tests/sql_fixtures/reset_db_fixtures/reset_db.sql @@ -11903,7 +11903,6 @@ SELECT pg_catalog.setval('public.django_content_type_id_seq', 47, true); -- Name: django_migrations_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres -- -SELECT pg_catalog.setval('public.django_migrations_id_seq', 201, true); SELECT pg_catalog.setval('public.django_migrations_id_seq', 203, true); diff --git a/tests/test_fyle/fixtures.py b/tests/test_fyle/fixtures.py index 2fd00f08..d6bef867 100644 --- a/tests/test_fyle/fixtures.py +++ b/tests/test_fyle/fixtures.py @@ -280,6 +280,7 @@ 'Vehicle Type': '', 'Fyle Categories': '', }, + "is_posted_at_null": True }, { 'id': '1235', @@ -320,6 +321,7 @@ 'Vehicle Type': '', 'Fyle Categories': '', }, + "is_posted_at_null": True }, { 'id': '1236', @@ -360,6 +362,7 @@ 'Vehicle Type': '', 'Fyle Categories': '', }, + "is_posted_at_null": True }, ], "skipped_expenses":{ @@ -379,14 +382,16 @@ 'amount': 101.0, 'report_id': 'dummy_report_id', 'settlement_id': 'dummy_settlement_id', - 'expense_id': 'dummy_expense_id' + 'expense_id': 'dummy_expense_id', + "is_posted_at_null": True }, { 'updated_at': '2021-12-03T11:26:58.702209Z', 'claim_number': ' C/2021/12/R/199', 'employee_email': 'jhonsnow@fyle.in', 'employee_name': None, - 'fund_source': 'CCC' + 'fund_source': 'CCC', + "is_posted_at_null": True } ] }, @@ -608,6 +613,7 @@ 'Vehicle Type': '', 'Fyle Categories': '', }, + 'is_posted_at_null': True }, { 'id': 'tx6wOnBVaumklol', @@ -649,6 +655,7 @@ 'Vehicle Type': '', 'Fyle Categories': '', }, + 'is_posted_at_null': True }, ], "expenses": [ @@ -692,6 +699,7 @@ 'Vehicle Type': '', 'Fyle Categories': '', }, + "is_posted_at_null": True }, { 'id': 'tx6wOnBVaumk', @@ -733,6 +741,7 @@ 'Vehicle Type': '', 'Fyle Categories': '', }, + "is_posted_at_null": False }, ], "expense_group_expenses": [ @@ -807,7 +816,8 @@ "amount": 6377.0, "report_id": "rp0kaXoqkJle", "settlement_id": "setPzkM7eyQFd", - "expense_id": "txyZ1zJDQfiK" + "expense_id": "txyZ1zJDQfiK", + "is_posted_at_null": True }], }, "expense_group_setting_response": {