From 5c31b3f4b93e219b4b76ac5fe62e3b8cd9560391 Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Wed, 11 Oct 2023 13:26:46 +0530 Subject: [PATCH 01/36] onboarding state implementation --- .../0033_workspace_onboarding_state.py | 19 +++ apps/workspaces/models.py | 14 +++ .../sql/scripts/019-add-onboarding-state.sql | 115 ++++++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 apps/workspaces/migrations/0033_workspace_onboarding_state.py create mode 100644 scripts/sql/scripts/019-add-onboarding-state.sql diff --git a/apps/workspaces/migrations/0033_workspace_onboarding_state.py b/apps/workspaces/migrations/0033_workspace_onboarding_state.py new file mode 100644 index 00000000..e4fdbf14 --- /dev/null +++ b/apps/workspaces/migrations/0033_workspace_onboarding_state.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.14 on 2023-10-10 11:39 + +import apps.workspaces.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('workspaces', '0032_configuration_name_in_journal_entry'), + ] + + operations = [ + migrations.AddField( + model_name='workspace', + name='onboarding_state', + field=models.CharField(choices=[('CONNECTION', 'CONNECTION'), ('MAP_EMPLOYEES', 'MAP_EMPLOYEES'), ('EXPORT_SETTINGS', 'EXPORT_SETTINGS'), ('IMPORT_SETTINGS', 'IMPORT_SETTINGS'), ('ADVANCED_CONFIGURATION', 'ADVANCED_CONFIGURATION'), ('COMPLETE', 'COMPLETE')], default=apps.workspaces.models.get_default_onboarding_state, help_text='Onboarding status of the workspace', max_length=50, null=True), + ), + ] diff --git a/apps/workspaces/models.py b/apps/workspaces/models.py index eb7a503c..c501b73f 100644 --- a/apps/workspaces/models.py +++ b/apps/workspaces/models.py @@ -12,6 +12,19 @@ User = get_user_model() +ONBOARDING_STATE_CHOICES = ( + ('CONNECTION', 'CONNECTION'), + ('MAP_EMPLOYEES', 'MAP_EMPLOYEES'), + ('EXPORT_SETTINGS', 'EXPORT_SETTINGS'), + ('IMPORT_SETTINGS', 'IMPORT_SETTINGS'), + ('ADVANCED_CONFIGURATION', 'ADVANCED_CONFIGURATION'), + ('COMPLETE', 'COMPLETE'), +) + + +def get_default_onboarding_state(): + return 'CONNECTION' + class Workspace(models.Model): """ Workspace model @@ -29,6 +42,7 @@ class Workspace(models.Model): created_at = models.DateTimeField(auto_now_add=True, help_text='Created at datetime') updated_at = models.DateTimeField(auto_now=True, help_text='Updated at datetime') employee_exported_at = models.DateTimeField(auto_now_add=True, help_text='Employee exported to Fyle at datetime') + onboarding_state = models.CharField(max_length=50, choices=ONBOARDING_STATE_CHOICES, default=get_default_onboarding_state, help_text='Onboarding status of the workspace', null=True) class Meta: db_table = 'workspaces' diff --git a/scripts/sql/scripts/019-add-onboarding-state.sql b/scripts/sql/scripts/019-add-onboarding-state.sql new file mode 100644 index 00000000..c70bdd6f --- /dev/null +++ b/scripts/sql/scripts/019-add-onboarding-state.sql @@ -0,0 +1,115 @@ +-- Create a view for joined on all settings tables to figure out onboarding progress +create or replace view all_settings_view as +select + w.id as workspace_id, + wgs.id as configuration_id, + gm.id as general_mappings_id, + qc.id as netsuite_creds_id +from workspaces w +left join + configurations wgs on w.id = wgs.workspace_id +left join + netsuite_credentials qc on qc.workspace_id = w.id +left join + general_mappings gm on gm.workspace_id = w.id +where w.onboarding_state = 'CONNECTION'; + +begin; -- Start Transaction Block + +-- Count of all workspaces where qbo creds are present, configuration is present and general mappings are present +select + 'QC=TRUE, C=TRUE, GM=TRUE' as setting, count(*) +from all_settings_view +where + configuration_id is not null and general_mappings_id is not null and netsuite_creds_id is not null; + +--- Update all of the above to have onboarding state set to 'COMPLETE' +update workspaces +set + onboarding_state = 'COMPLETE' +where id in ( + select + workspace_id + from all_settings_view + where + configuration_id is not null and general_mappings_id is not null and netsuite_creds_id is not null +); + +-- Count of all workspaces where qbo creds are present, configuration is present and general mappings are not present +select + 'QC=TRUE, C=TRUE, GM=FALSE' as settings, count(*) +from all_settings_view +where + configuration_id is not null and general_mappings_id is null and netsuite_creds_id is not null; + +--- Update all of the above to have onboarding state set to 'EXPORT_SETTINGS' +update workspaces +set + onboarding_state = 'EXPORT_SETTINGS' +where id in ( + select + workspace_id + from all_settings_view + where + configuration_id is not null and general_mappings_id is not null and netsuite_creds_id is not null +); + + +-- Count of all workspaces where qbo creds are present, configuration is not present and general mappings are not present +select + 'QC=TRUE, C=FALSE, GM=FALSE' as settings, count(*) +from all_settings_view +where + configuration_id is null and general_mappings_id is null and netsuite_creds_id is not null; + +--- Update all of the above to have onboarding state set to 'MAP_EMPLOYEES' +update workspaces +set + onboarding_state = 'MAP_EMPLOYEES' +where id in ( + select + workspace_id + from all_settings_view + where + configuration_id is null and general_mappings_id is not null and netsuite_creds_id is not null +); + + +-- Count of all workspaces where qbo creds is not present, configuration is present and general mappings is present +select + 'QC=FALSE, C=TRUE, GM=TRUE' as settings, count(*) +from all_settings_view +where + configuration_id is not null and general_mappings_id is not null and netsuite_creds_id is null; + +--- Update all of the above to have onboarding state set to 'COMPLETE' +update workspaces +set + onboarding_state = 'COMPLETE' +where id in ( + select + workspace_id + from all_settings_view + where + configuration_id is not null and general_mappings_id is not null and netsuite_creds_id is null +); + + +-- Count of all workspaces where qbo creds are not present, configuration is present and general mappings are not present +select + 'QC=FALSE, C=TRUE, GM=FALSE' as settings, count(*) +from all_settings_view +where + configuration_id is not null and general_mappings_id is not null and netsuite_creds_id is null; + +--- Update all of the above to have onboarding state set to 'EXPORT_SETTINGS' +update workspaces +set + onboarding_state = 'EXPORT_SETTINGS' +where id in ( + select + workspace_id + from all_settings_view + where + configuration_id is not null and general_mappings_id is null and netsuite_creds_id is null +); From 798e2bc15a8a6f84bd648717317aedc1e3e46bd2 Mon Sep 17 00:00:00 2001 From: Nilesh Pant Date: Wed, 11 Oct 2023 15:32:19 +0530 Subject: [PATCH 02/36] tests migrations --- .../reset_db_fixtures/reset_db.sql | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/sql_fixtures/reset_db_fixtures/reset_db.sql b/tests/sql_fixtures/reset_db_fixtures/reset_db.sql index 102f9570..9cdb94e4 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.4 (Debian 15.4-1.pgdg120+1) --- Dumped by pg_dump version 15.4 (Debian 15.4-1.pgdg100+1) +-- Dumped from database version 15.2 (Debian 15.2-1.pgdg110+1) +-- Dumped by pg_dump version 15.4 (Debian 15.4-2.pgdg100+1) SET statement_timeout = 0; SET lock_timeout = 0; @@ -1753,7 +1753,8 @@ CREATE TABLE public.workspaces ( source_synced_at timestamp with time zone, cluster_domain character varying(255), employee_exported_at timestamp with time zone NOT NULL, - ccc_last_synced_at timestamp with time zone + ccc_last_synced_at timestamp with time zone, + onboarding_state character varying(50) ); @@ -7706,6 +7707,7 @@ COPY public.django_migrations (id, app, name, applied) FROM stdin; 162 fyle 0025_auto_20230622_0516 2023-06-22 10:17:05.677561+00 163 django_q 0014_schedule_cluster 2023-07-17 14:43:54.845689+00 164 netsuite 0022_creditcardcharge_department_id 2023-07-26 10:31:38.187076+00 +165 workspaces 0033_workspace_onboarding_state 2023-10-11 09:59:47.359324+00 \. @@ -11486,10 +11488,10 @@ COPY public.workspace_schedules (id, enabled, start_datetime, interval_hours, wo -- Data for Name: workspaces; Type: TABLE DATA; Schema: public; Owner: postgres -- -COPY public.workspaces (id, name, fyle_org_id, ns_account_id, last_synced_at, created_at, updated_at, destination_synced_at, source_synced_at, cluster_domain, employee_exported_at, ccc_last_synced_at) FROM stdin; -1 Fyle For Arkham Asylum or79Cob97KSh TSTDRV2089588 2021-11-15 13:12:12.210053+00 2021-11-15 08:46:16.062858+00 2021-11-15 13:12:12.210769+00 2021-11-15 08:56:43.737724+00 2021-11-15 08:55:57.620811+00 https://staging.fyle.tech 2021-09-17 14:32:05.585557+00 \N -2 Fyle For IntacctNew Technologies oraWFQlEpjbb TSTDRV2089588 2021-11-16 04:25:49.067507+00 2021-11-16 04:16:57.840307+00 2021-11-16 04:25:49.067805+00 2021-11-16 04:18:28.233322+00 2021-11-16 04:17:43.950915+00 https://staging.fyle.tech 2021-11-17 14:32:05.585557+00 \N -49 Fyle For intacct-test orHe8CpW2hyN TSTDRV2089588 2021-12-03 11:26:58.663241+00 2021-12-03 11:00:33.634494+00 2021-12-03 11:26:58.664557+00 2021-12-03 11:04:27.847159+00 2021-12-03 11:03:52.560696+00 https://staging.fyle.tech 2021-11-17 14:32:05.585557+00 \N +COPY public.workspaces (id, name, fyle_org_id, ns_account_id, last_synced_at, created_at, updated_at, destination_synced_at, source_synced_at, cluster_domain, employee_exported_at, ccc_last_synced_at, onboarding_state) FROM stdin; +1 Fyle For Arkham Asylum or79Cob97KSh TSTDRV2089588 2021-11-15 13:12:12.210053+00 2021-11-15 08:46:16.062858+00 2021-11-15 13:12:12.210769+00 2021-11-15 08:56:43.737724+00 2021-11-15 08:55:57.620811+00 https://staging.fyle.tech 2021-09-17 14:32:05.585557+00 \N CONNECTION +2 Fyle For IntacctNew Technologies oraWFQlEpjbb TSTDRV2089588 2021-11-16 04:25:49.067507+00 2021-11-16 04:16:57.840307+00 2021-11-16 04:25:49.067805+00 2021-11-16 04:18:28.233322+00 2021-11-16 04:17:43.950915+00 https://staging.fyle.tech 2021-11-17 14:32:05.585557+00 \N CONNECTION +49 Fyle For intacct-test orHe8CpW2hyN TSTDRV2089588 2021-12-03 11:26:58.663241+00 2021-12-03 11:00:33.634494+00 2021-12-03 11:26:58.664557+00 2021-12-03 11:04:27.847159+00 2021-12-03 11:03:52.560696+00 https://staging.fyle.tech 2021-11-17 14:32:05.585557+00 \N CONNECTION \. @@ -11585,7 +11587,7 @@ SELECT pg_catalog.setval('public.django_content_type_id_seq', 43, true); -- Name: django_migrations_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres -- -SELECT pg_catalog.setval('public.django_migrations_id_seq', 164, true); +SELECT pg_catalog.setval('public.django_migrations_id_seq', 165, true); -- From fdd7a9e20d2e7dfe79bc897b6585e5474b9feac3 Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Wed, 11 Oct 2023 15:52:55 +0530 Subject: [PATCH 03/36] added onboarding state --- tests/test_workspaces/data.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_workspaces/data.json b/tests/test_workspaces/data.json index 367c79c8..27a52c14 100644 --- a/tests/test_workspaces/data.json +++ b/tests/test_workspaces/data.json @@ -38,6 +38,7 @@ "updated_at":"2021-11-10T20:41:37.151696Z", "employee_exported_at":"2021-11-10T20:41:37.151696Z", "ccc_last_synced_at": "2021-11-10T20:41:37.151680Z", + "onboarding_state": "COMPLETE", "user":[ 1 ] From 609594dd577750302534d29cd613fb26d504b466 Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Thu, 12 Oct 2023 12:49:48 +0530 Subject: [PATCH 04/36] changed comment --- scripts/sql/scripts/019-add-onboarding-state.sql | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/sql/scripts/019-add-onboarding-state.sql b/scripts/sql/scripts/019-add-onboarding-state.sql index c70bdd6f..5047856c 100644 --- a/scripts/sql/scripts/019-add-onboarding-state.sql +++ b/scripts/sql/scripts/019-add-onboarding-state.sql @@ -16,7 +16,7 @@ where w.onboarding_state = 'CONNECTION'; begin; -- Start Transaction Block --- Count of all workspaces where qbo creds are present, configuration is present and general mappings are present +-- Count of all workspaces where netsuite are present, configuration is present and general mappings are present select 'QC=TRUE, C=TRUE, GM=TRUE' as setting, count(*) from all_settings_view @@ -35,7 +35,7 @@ where id in ( configuration_id is not null and general_mappings_id is not null and netsuite_creds_id is not null ); --- Count of all workspaces where qbo creds are present, configuration is present and general mappings are not present +-- Count of all workspaces where netsuite are present, configuration is present and general mappings are not present select 'QC=TRUE, C=TRUE, GM=FALSE' as settings, count(*) from all_settings_view @@ -55,7 +55,7 @@ where id in ( ); --- Count of all workspaces where qbo creds are present, configuration is not present and general mappings are not present +-- Count of all workspaces where netsuite are present, configuration is not present and general mappings are not present select 'QC=TRUE, C=FALSE, GM=FALSE' as settings, count(*) from all_settings_view @@ -75,7 +75,7 @@ where id in ( ); --- Count of all workspaces where qbo creds is not present, configuration is present and general mappings is present +-- Count of all workspaces where netsuite is not present, configuration is present and general mappings is present select 'QC=FALSE, C=TRUE, GM=TRUE' as settings, count(*) from all_settings_view @@ -95,7 +95,7 @@ where id in ( ); --- Count of all workspaces where qbo creds are not present, configuration is present and general mappings are not present +-- Count of all workspaces where netsuite are not present, configuration is present and general mappings are not present select 'QC=FALSE, C=TRUE, GM=FALSE' as settings, count(*) from all_settings_view From d15a8f5ae54d8d9d2ed724db70bd75fe50ddd63f Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Thu, 12 Oct 2023 13:23:16 +0530 Subject: [PATCH 05/36] added subsidiary state to onboarding state --- .../migrations/0034_auto_20231012_0750.py | 19 +++++++++++++++++++ apps/workspaces/models.py | 1 + 2 files changed, 20 insertions(+) create mode 100644 apps/workspaces/migrations/0034_auto_20231012_0750.py diff --git a/apps/workspaces/migrations/0034_auto_20231012_0750.py b/apps/workspaces/migrations/0034_auto_20231012_0750.py new file mode 100644 index 00000000..226e7c23 --- /dev/null +++ b/apps/workspaces/migrations/0034_auto_20231012_0750.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.14 on 2023-10-12 07:50 + +import apps.workspaces.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('workspaces', '0033_workspace_onboarding_state'), + ] + + operations = [ + migrations.AlterField( + model_name='workspace', + name='onboarding_state', + field=models.CharField(choices=[('CONNECTION', 'CONNECTION'), ('SUBSIDIARY', 'SUBSIDIARY'), ('MAP_EMPLOYEES', 'MAP_EMPLOYEES'), ('EXPORT_SETTINGS', 'EXPORT_SETTINGS'), ('IMPORT_SETTINGS', 'IMPORT_SETTINGS'), ('ADVANCED_CONFIGURATION', 'ADVANCED_CONFIGURATION'), ('COMPLETE', 'COMPLETE')], default=apps.workspaces.models.get_default_onboarding_state, help_text='Onboarding status of the workspace', max_length=50, null=True), + ), + ] diff --git a/apps/workspaces/models.py b/apps/workspaces/models.py index c501b73f..92e71072 100644 --- a/apps/workspaces/models.py +++ b/apps/workspaces/models.py @@ -14,6 +14,7 @@ ONBOARDING_STATE_CHOICES = ( ('CONNECTION', 'CONNECTION'), + ('SUBSIDIARY', 'SUBSIDIARY'), ('MAP_EMPLOYEES', 'MAP_EMPLOYEES'), ('EXPORT_SETTINGS', 'EXPORT_SETTINGS'), ('IMPORT_SETTINGS', 'IMPORT_SETTINGS'), From ec4f4d16de5aaad2bad4e14ae5986313b1453325 Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Fri, 13 Oct 2023 13:19:19 +0530 Subject: [PATCH 06/36] changed script to add subsidiary state and fixed some bug --- .../sql/scripts/019-add-onboarding-state.sql | 78 ++++++++----------- 1 file changed, 31 insertions(+), 47 deletions(-) diff --git a/scripts/sql/scripts/019-add-onboarding-state.sql b/scripts/sql/scripts/019-add-onboarding-state.sql index 5047856c..6f485fa7 100644 --- a/scripts/sql/scripts/019-add-onboarding-state.sql +++ b/scripts/sql/scripts/019-add-onboarding-state.sql @@ -4,24 +4,27 @@ select w.id as workspace_id, wgs.id as configuration_id, gm.id as general_mappings_id, - qc.id as netsuite_creds_id + qc.id as netsuite_creds_id, + sm.id as subsidiary_id from workspaces w left join configurations wgs on w.id = wgs.workspace_id left join - netsuite_credentials qc on qc.workspace_id = w.id + netsuite_credentials nc on qc.workspace_id = w.id left join general_mappings gm on gm.workspace_id = w.id +left join + subsidiary_mappings sm on sm.workspace_id = w.id where w.onboarding_state = 'CONNECTION'; begin; -- Start Transaction Block -- Count of all workspaces where netsuite are present, configuration is present and general mappings are present select - 'QC=TRUE, C=TRUE, GM=TRUE' as setting, count(*) + 'NC=TRUE, C=TRUE, GM=TRUE, SM=True' as setting, count(*) from all_settings_view where - configuration_id is not null and general_mappings_id is not null and netsuite_creds_id is not null; + configuration_id is not null and general_mappings_id is not null and netsuite_creds_id is not null and subsidiary_id is not null; --- Update all of the above to have onboarding state set to 'COMPLETE' update workspaces @@ -32,35 +35,34 @@ where id in ( workspace_id from all_settings_view where - configuration_id is not null and general_mappings_id is not null and netsuite_creds_id is not null + configuration_id is not null and general_mappings_id is not null and netsuite_creds_id is not null and subsidiary_id is not null ); --- Count of all workspaces where netsuite are present, configuration is present and general mappings are not present +-- Count of all workspaces where netsuite cred is present and general mapping and credentials and subsidiary are not present. select - 'QC=TRUE, C=TRUE, GM=FALSE' as settings, count(*) -from all_settings_view -where - configuration_id is not null and general_mappings_id is null and netsuite_creds_id is not null; + 'NC=TRUE, C=FALSE, GM=FALSE, SM=FALSE' as setting, count(*) +from all_settings_view +where + configuration_id is null and general_mappings_id is null and netsuite_creds_id is not null and subsidiary_id is null; ---- Update all of the above to have onboarding state set to 'EXPORT_SETTINGS' -update workspaces -set - onboarding_state = 'EXPORT_SETTINGS' +-- Update all of the above to have onboarding state set to 'SUBSIDIARY' +update workspaces +set + onboarding_state = 'SUBSIDIARY' where id in ( - select - workspace_id - from all_settings_view + select + workspace_id + from all_settings_view where - configuration_id is not null and general_mappings_id is not null and netsuite_creds_id is not null + configuration_id is null and general_mappings_id is null and netsuite_creds_id is not null and subsidiary_id is null ); - --- Count of all workspaces where netsuite are present, configuration is not present and general mappings are not present +-- Count of all workspaces where netsuite cred and subsidiary are present, configuration is not present and general mappings are not present select - 'QC=TRUE, C=FALSE, GM=FALSE' as settings, count(*) + 'NC=TRUE, C=FALSE, GM=FALSE, SM=TRUE' as settings, count(*) from all_settings_view where - configuration_id is null and general_mappings_id is null and netsuite_creds_id is not null; + configuration_id is null and general_mappings_id is null and netsuite_creds_id is not null and subsidiary_id is not null; --- Update all of the above to have onboarding state set to 'MAP_EMPLOYEES' update workspaces @@ -71,36 +73,16 @@ where id in ( workspace_id from all_settings_view where - configuration_id is null and general_mappings_id is not null and netsuite_creds_id is not null + configuration_id is null and general_mappings_id is null and netsuite_creds_id is not null and subsidiary_id is not null ); --- Count of all workspaces where netsuite is not present, configuration is present and general mappings is present +-- Count of all workspaces where netsuite are present, configuration is present, subsidiary is present and general mappings are not present select - 'QC=FALSE, C=TRUE, GM=TRUE' as settings, count(*) + 'NC=TRUE, C=TRUE, GM=FALSE, SM=TRUE' as settings, count(*) from all_settings_view where - configuration_id is not null and general_mappings_id is not null and netsuite_creds_id is null; - ---- Update all of the above to have onboarding state set to 'COMPLETE' -update workspaces -set - onboarding_state = 'COMPLETE' -where id in ( - select - workspace_id - from all_settings_view - where - configuration_id is not null and general_mappings_id is not null and netsuite_creds_id is null -); - - --- Count of all workspaces where netsuite are not present, configuration is present and general mappings are not present -select - 'QC=FALSE, C=TRUE, GM=FALSE' as settings, count(*) -from all_settings_view -where - configuration_id is not null and general_mappings_id is not null and netsuite_creds_id is null; + configuration_id is not null and general_mappings_id is null and netsuite_creds_id is not null and subsidiary_id is not null; --- Update all of the above to have onboarding state set to 'EXPORT_SETTINGS' update workspaces @@ -111,5 +93,7 @@ where id in ( workspace_id from all_settings_view where - configuration_id is not null and general_mappings_id is null and netsuite_creds_id is null + configuration_id is not null and general_mappings_id is null and netsuite_creds_id is not null and subsidiary_id is not null ); + + From 67ddf5997d4064591f4b69e325c7ebffcee5b253 Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Fri, 13 Oct 2023 17:15:42 +0530 Subject: [PATCH 07/36] bug fix --- scripts/sql/scripts/019-add-onboarding-state.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/sql/scripts/019-add-onboarding-state.sql b/scripts/sql/scripts/019-add-onboarding-state.sql index 6f485fa7..a57f04e6 100644 --- a/scripts/sql/scripts/019-add-onboarding-state.sql +++ b/scripts/sql/scripts/019-add-onboarding-state.sql @@ -4,13 +4,13 @@ select w.id as workspace_id, wgs.id as configuration_id, gm.id as general_mappings_id, - qc.id as netsuite_creds_id, + nc.id as netsuite_creds_id, sm.id as subsidiary_id from workspaces w left join configurations wgs on w.id = wgs.workspace_id left join - netsuite_credentials nc on qc.workspace_id = w.id + netsuite_credentials nc on nc.workspace_id = w.id left join general_mappings gm on gm.workspace_id = w.id left join From 3bce53863f5f941d1735d4422f4b8c0f1926c5a2 Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Mon, 16 Oct 2023 13:13:09 +0530 Subject: [PATCH 08/36] state change on connection and subsidiary change --- apps/mappings/signals.py | 10 +++++++++- apps/workspaces/views.py | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/mappings/signals.py b/apps/mappings/signals.py index 58b45385..220049f8 100644 --- a/apps/mappings/signals.py +++ b/apps/mappings/signals.py @@ -14,9 +14,17 @@ from apps.workspaces.models import Configuration from apps.workspaces.tasks import delete_cards_mapping_settings -from .models import GeneralMapping +from .models import GeneralMapping, SubsidiaryMapping from .tasks import schedule_auto_map_ccc_employees + +@receiver(post_save, sender=SubsidiaryMapping) +def run_post_subsidiary_mappings(sender, instance: SubsidiaryMapping, **kwargs): + + workspace = instance.workspace + workspace.onboarding_state = 'MAP_EMPLOYEES' + workspace.save() + @receiver(post_save, sender=MappingSetting) def run_post_mapping_settings_triggers(sender, instance: MappingSetting, **kwargs): """ diff --git a/apps/workspaces/views.py b/apps/workspaces/views.py index af40dab8..dc67116b 100644 --- a/apps/workspaces/views.py +++ b/apps/workspaces/views.py @@ -175,6 +175,7 @@ def post(self, request, **kwargs): workspace=workspace ) workspace.ns_account_id = ns_account_id + workspace.onboarding_state = 'SUBSIDIARY' workspace.save() else: From 4d291b33e321c079040e1c92ee2ae21921c5dd99 Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Thu, 19 Oct 2023 16:13:49 +0530 Subject: [PATCH 09/36] map employees v2 api --- .../workspaces/apis/map_employees/__init__.py | 0 .../apis/map_employees/serializers.py | 55 +++++++++++++++++++ .../workspaces/apis/map_employees/triggers.py | 10 ++++ apps/workspaces/apis/map_employees/views.py | 11 ++++ apps/workspaces/apis/urls.py | 8 +++ .../migrations/0035_auto_20231019_1025.py | 18 ++++++ apps/workspaces/models.py | 2 +- fyle_netsuite_api/urls.py | 3 +- 8 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 apps/workspaces/apis/map_employees/__init__.py create mode 100644 apps/workspaces/apis/map_employees/serializers.py create mode 100644 apps/workspaces/apis/map_employees/triggers.py create mode 100644 apps/workspaces/apis/map_employees/views.py create mode 100644 apps/workspaces/apis/urls.py create mode 100644 apps/workspaces/migrations/0035_auto_20231019_1025.py diff --git a/apps/workspaces/apis/map_employees/__init__.py b/apps/workspaces/apis/map_employees/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/workspaces/apis/map_employees/serializers.py b/apps/workspaces/apis/map_employees/serializers.py new file mode 100644 index 00000000..dc98fbd8 --- /dev/null +++ b/apps/workspaces/apis/map_employees/serializers.py @@ -0,0 +1,55 @@ +from apps.workspaces.apis.map_employees.triggers import MapEmplyeesTriggers +from apps.workspaces.models import Configuration, Workspace +from rest_framework import serializers + + +class ConfigurationSerializer(serializers.ModelSerializer): + class Meta: + model = Configuration + fields = ['employee_field_mapping', 'auto_map_employees'] + + +class MapEmployeesSerializer(serializers.ModelSerializer): + + configuration = ConfigurationSerializer() + workspace_id = serializers.SerializerMethodField() + + class Meta: + model = Workspace + fields = ['configuration', 'workspace_id'] + read_only_fields = ['workspace_id'] + + def get_workspace_id(self, instance): + return instance.id + + def update(self, instance, validated_data): + + workspace_id = instance.id + configuration = validated_data.pop('configuration') + + configuration_instance = Configuration.objects.filter(workspace_id=workspace_id).first() + + if configuration and (configuration_instance.employee_field_mapping != configuration['employee_field_mapping']): + configuration_instance.reimbursable_expenses_object = None + configuration_instance.save() + + configuration_instance, _ = Configuration.objects.update_or_create( + workspace_id=workspace_id, defaults={'employee_field_mapping': configuration['employee_field_mapping'], 'auto_map_employees': configuration['auto_map_employees']} + ) + + MapEmplyeesTriggers.run_workspace_general_settings_triggers(configuration=configuration_instance) + + if instance.onboarding_state == 'MAP_EMPLOYEES': + instance.onboarding_state = 'EXPORT_SETTINGS' + instance.save() + + return instance + + def validate(self,data): + if not data.get('configuration').get('employee_field_mapping'): + raise serializers.ValidationError('employee_field_mapping field is required') + + if data.get('configuration').get('auto_map_employees') and data.get('configuration').get('auto_map_employees') not in ['EMAIL', 'NAME', 'EMPLOYEE_CODE']: + raise serializers.ValidationError('auto_map_employees can have only EMAIL / NAME / EMPLOYEE_CODE') + + return data diff --git a/apps/workspaces/apis/map_employees/triggers.py b/apps/workspaces/apis/map_employees/triggers.py new file mode 100644 index 00000000..5e36f2a5 --- /dev/null +++ b/apps/workspaces/apis/map_employees/triggers.py @@ -0,0 +1,10 @@ +from apps.mappings.tasks import schedule_auto_map_employees +from apps.workspaces.models import Configuration + + +class MapEmplyeesTriggers: + + @staticmethod + def run_workspace_general_settings_triggers(configuration: Configuration): + + schedule_auto_map_employees(configuration.auto_map_employees, configuration.workspace.id) diff --git a/apps/workspaces/apis/map_employees/views.py b/apps/workspaces/apis/map_employees/views.py new file mode 100644 index 00000000..d749196e --- /dev/null +++ b/apps/workspaces/apis/map_employees/views.py @@ -0,0 +1,11 @@ +from rest_framework import generics +from apps.workspaces.apis.map_employees.serializers import MapEmployeesSerializer +from apps.workspaces.models import Workspace + + +class MapEmployeesView(generics.RetrieveUpdateAPIView): + + serializer_class = MapEmployeesSerializer + + def get_object(self): + return Workspace.objects.filter(id=self.kwargs['workspace_id']).first() diff --git a/apps/workspaces/apis/urls.py b/apps/workspaces/apis/urls.py new file mode 100644 index 00000000..80934c77 --- /dev/null +++ b/apps/workspaces/apis/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from apps.workspaces.apis.map_employees.views import MapEmployeesView + + +urlpatterns = [ + path('/map_employees/', MapEmployeesView.as_view()), +] diff --git a/apps/workspaces/migrations/0035_auto_20231019_1025.py b/apps/workspaces/migrations/0035_auto_20231019_1025.py new file mode 100644 index 00000000..7366e722 --- /dev/null +++ b/apps/workspaces/migrations/0035_auto_20231019_1025.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.14 on 2023-10-19 10:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('workspaces', '0034_auto_20231012_0750'), + ] + + operations = [ + migrations.AlterField( + model_name='configuration', + name='reimbursable_expenses_object', + field=models.CharField(choices=[('EXPENSE REPORT', 'EXPENSE REPORT'), ('JOURNAL ENTRY', 'JOURNAL ENTRY'), ('BILL', 'BILL')], help_text='Reimbursable Expenses type', max_length=50, null=True), + ), + ] diff --git a/apps/workspaces/models.py b/apps/workspaces/models.py index 92e71072..1409e6f9 100644 --- a/apps/workspaces/models.py +++ b/apps/workspaces/models.py @@ -144,7 +144,7 @@ class Configuration(models.Model): max_length=50, choices=EMPLOYEE_FIELD_MAPPING_CHOICES, help_text='Employee field mapping', null=True ) reimbursable_expenses_object = models.CharField( - max_length=50, choices=REIMBURSABLE_EXPENSES_OBJECT_CHOICES, help_text='Reimbursable Expenses type' + max_length=50, choices=REIMBURSABLE_EXPENSES_OBJECT_CHOICES, help_text='Reimbursable Expenses type', null=True ) corporate_credit_card_expenses_object = models.CharField( max_length=50, choices=COPORATE_CARD_EXPENSES_OBJECT_CHOICES, diff --git a/fyle_netsuite_api/urls.py b/fyle_netsuite_api/urls.py index b94fbc56..526b49ea 100644 --- a/fyle_netsuite_api/urls.py +++ b/fyle_netsuite_api/urls.py @@ -20,5 +20,6 @@ path('admin/', admin.site.urls), path('api/auth/', include('fyle_rest_auth.urls')), path('api/workspaces/', include('apps.workspaces.urls')), - path('api/user/', include('apps.users.urls')) + path('api/user/', include('apps.users.urls')), + path('api/v2/workspaces/', include('apps.workspaces.apis.urls')), ] From 173c5bb60a6bf00a56b010a042260082c1340770 Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Fri, 20 Oct 2023 16:55:42 +0530 Subject: [PATCH 10/36] map_employees typos --- apps/workspaces/apis/map_employees/serializers.py | 4 ++-- apps/workspaces/apis/map_employees/triggers.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/workspaces/apis/map_employees/serializers.py b/apps/workspaces/apis/map_employees/serializers.py index dc98fbd8..51cacf96 100644 --- a/apps/workspaces/apis/map_employees/serializers.py +++ b/apps/workspaces/apis/map_employees/serializers.py @@ -1,4 +1,4 @@ -from apps.workspaces.apis.map_employees.triggers import MapEmplyeesTriggers +from apps.workspaces.apis.map_employees.triggers import MapEmployeesTriggers from apps.workspaces.models import Configuration, Workspace from rest_framework import serializers @@ -37,7 +37,7 @@ def update(self, instance, validated_data): workspace_id=workspace_id, defaults={'employee_field_mapping': configuration['employee_field_mapping'], 'auto_map_employees': configuration['auto_map_employees']} ) - MapEmplyeesTriggers.run_workspace_general_settings_triggers(configuration=configuration_instance) + MapEmployeesTriggers.run_configurations_triggers(configuration=configuration_instance) if instance.onboarding_state == 'MAP_EMPLOYEES': instance.onboarding_state = 'EXPORT_SETTINGS' diff --git a/apps/workspaces/apis/map_employees/triggers.py b/apps/workspaces/apis/map_employees/triggers.py index 5e36f2a5..6d930cba 100644 --- a/apps/workspaces/apis/map_employees/triggers.py +++ b/apps/workspaces/apis/map_employees/triggers.py @@ -2,9 +2,9 @@ from apps.workspaces.models import Configuration -class MapEmplyeesTriggers: +class MapEmployeesTriggers: @staticmethod - def run_workspace_general_settings_triggers(configuration: Configuration): + def run_configurations_triggers(configuration: Configuration): schedule_auto_map_employees(configuration.auto_map_employees, configuration.workspace.id) From 932cf3f66c91be5081a92bcd3da50b97595ba0b8 Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Fri, 20 Oct 2023 16:59:24 +0530 Subject: [PATCH 11/36] bug fix --- apps/workspaces/apis/map_employees/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/workspaces/apis/map_employees/serializers.py b/apps/workspaces/apis/map_employees/serializers.py index 51cacf96..14f36ab6 100644 --- a/apps/workspaces/apis/map_employees/serializers.py +++ b/apps/workspaces/apis/map_employees/serializers.py @@ -29,7 +29,7 @@ def update(self, instance, validated_data): configuration_instance = Configuration.objects.filter(workspace_id=workspace_id).first() - if configuration and (configuration_instance.employee_field_mapping != configuration['employee_field_mapping']): + if configuration_instance and (configuration_instance.employee_field_mapping != configuration['employee_field_mapping']): configuration_instance.reimbursable_expenses_object = None configuration_instance.save() From 5972c5c7289ae2e5b83d6bdf04791c0bd5e1c4b0 Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Wed, 25 Oct 2023 14:25:34 +0530 Subject: [PATCH 12/36] export setting changes --- .../apis/export_settings/__init__.py | 0 .../apis/export_settings/serializers.py | 174 ++++++++++++++++++ apps/workspaces/apis/export_settings/views.py | 12 ++ apps/workspaces/apis/urls.py | 2 + 4 files changed, 188 insertions(+) create mode 100644 apps/workspaces/apis/export_settings/__init__.py create mode 100644 apps/workspaces/apis/export_settings/serializers.py create mode 100644 apps/workspaces/apis/export_settings/views.py diff --git a/apps/workspaces/apis/export_settings/__init__.py b/apps/workspaces/apis/export_settings/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/workspaces/apis/export_settings/serializers.py b/apps/workspaces/apis/export_settings/serializers.py new file mode 100644 index 00000000..a21d5eb6 --- /dev/null +++ b/apps/workspaces/apis/export_settings/serializers.py @@ -0,0 +1,174 @@ +from apps.fyle.models import ExpenseGroupSettings +from apps.mappings.models import GeneralMapping +from apps.workspaces.models import Configuration, Workspace +from rest_framework import serializers + +class ReadWriteSerializerMethodField(serializers.SerializerMethodField): + """ + Serializer Method Field to Read and Write from values + Inherits serializers.SerializerMethodField + """ + + def __init__(self, method_name=None, **kwargs): + self.method_name = method_name + kwargs['source'] = '*' + super(serializers.SerializerMethodField, self).__init__(**kwargs) + + def to_internal_value(self, data): + return { + self.field_name: data + } + +class ConfigurationSerializer(serializers.ModelSerializer): + + class Meta: + model = Configuration + fields = [ + 'reimbursable_expenses_object', + 'corporate_credit_card_expenses_object', + 'is_simplify_report_closure_enabled', + 'name_in_journal_entry', + 'auto_map_employees', + 'employee_field_mapping' + ] + + read_only_fields = ['is_simplify_report_closure_enabled'] + +class GeneralMappingsSerializer(serializers.ModelSerializer): + reimbursable_account = ReadWriteSerializerMethodField() + default_ccc_account = ReadWriteSerializerMethodField() + accounts_payable = ReadWriteSerializerMethodField() + default_ccc_vendor = ReadWriteSerializerMethodField() + + class Meta: + model = GeneralMapping + fields = [ + 'reimbursable_account', + 'default_ccc_account', + 'accounts_payable', + 'default_ccc_vendor', + ] + + def get_reimbursable_account(self, instance: GeneralMapping): + return { + 'id': instance.reimbursable_account_id, + 'name': instance.reimbursable_account_name + } + def get_default_ccc_account(self, instance: GeneralMapping): + return { + 'id': instance.default_ccc_account_id, + 'name': instance.default_ccc_account_name + } + def get_accounts_payable(self, instance: GeneralMapping): + return { + 'id': instance.accounts_payable_id, + 'name': instance.accounts_payable_name + } + def get_default_ccc_vendor(self, instance: GeneralMapping): + return { + 'id': instance.default_ccc_vendor_id, + 'name': instance.default_ccc_vendor_name + } + +class ExpenseGroupSettingsSerializer(serializers.ModelSerializer): + reimbursable_expense_group_fields = serializers.ListField(allow_null=True, required=False) + reimbursable_export_date_type = serializers.CharField(allow_null=True, allow_blank=True, required=False) + expense_state = serializers.CharField(allow_null=True, allow_blank=True, required=False) + corporate_credit_card_expense_group_fields = serializers.ListField(allow_null=True, required=False) + ccc_export_date_type = serializers.CharField(allow_null=True, allow_blank=True, required=False) + ccc_expense_state = serializers.CharField(allow_null=True, allow_blank=True, required=False) + + class Meta: + model = ExpenseGroupSettings + fields = [ + 'reimbursable_expense_group_fields', + 'reimbursable_export_date_type', + 'expense_state', + 'corporate_credit_card_expense_group_fields', + 'ccc_export_date_type', + 'ccc_expense_state' + ] + + +class ExportSettingsSerializer(serializers.ModelSerializer): + expense_group_settings = ExpenseGroupSettingsSerializer() + configuration = ConfigurationSerializer() + general_mappings = GeneralMappingsSerializer() + workspace_id = serializers.SerializerMethodField() + + class Meta: + model = Workspace + fields = [ + 'expense_group_settings', + 'configuration', + 'general_mappings', + 'workspace_id' + ] + read_only_fields = ['workspace_id'] + + + def get_workspace_id(self, instance): + return instance.id + + def update(self, instance: Workspace, validated_data): + + configurations = validated_data.pop('configuration') + expense_group_settings = validated_data.pop('expense_group_settings') + general_mappings = validated_data.pop('general_mappings') + workspace_id = instance.id + + Configuration.objects.update_or_create( + workspace_id=workspace_id, + defaults={ + 'reimbursable_expenses_object': configurations['reimbursable_expenses_object'], + 'corporate_credit_card_expenses_object': configurations['corporate_credit_card_expenses_object'], + 'employee_field_mapping': configurations['employee_field_mapping'] + } + ) + + if not expense_group_settings['reimbursable_expense_group_fields']: + expense_group_settings['reimbursable_expense_group_fields'] = ['employee_email', 'report_id', 'fund_source', 'claim_number'] + + if not expense_group_settings['corporate_credit_card_expense_group_fields']: + expense_group_settings['corporate_credit_card_expense_group_fields'] = ['employee_email', 'report_id', 'fund_source', 'claim_number'] + + if not expense_group_settings['reimbursable_export_date_type']: + expense_group_settings['reimbursable_export_date_type'] = 'current_date' + + if not expense_group_settings['ccc_export_date_type']: + expense_group_settings['ccc_export_date_type'] = 'current_date' + + ExpenseGroupSettings.update_expense_group_settings(expense_group_settings, workspace_id=workspace_id) + + GeneralMapping.objects.update_or_create( + workspace = instance, + defaults={ + 'reimbursable_account_id': general_mappings['reimbursable_account']['id'], + 'reimbursable_account_name': general_mappings['reimbursable_account']['name'], + 'default_ccc_account_id': general_mappings['default_ccc_account']['id'], + 'default_ccc_account_name': general_mappings['default_ccc_account']['name'], + 'accounts_payable_id': general_mappings['accounts_payable']['id'], + 'accounts_payable_name': general_mappings['accounts_payable']['name'], + 'default_ccc_vendor_id': general_mappings['default_ccc_vendor']['id'], + 'default_ccc_vendor_name': general_mappings['default_ccc_vendor']['name'] + } + ) + + if instance.onboarding_state == 'EXPORT_SETTINGS': + instance.onboarding_state = 'IMPORT_SETTINGS' + instance.save() + + return instance + + def validate(self, data): + + if not data.get('expense_group_settings'): + raise serializers.ValidationError('Expense group settings are required') + + if not data.get('configuration'): + raise serializers.ValidationError('Configurations settings are required') + + if not data.get('general_mappings'): + raise serializers.ValidationError('General mappings are required') + + return data diff --git a/apps/workspaces/apis/export_settings/views.py b/apps/workspaces/apis/export_settings/views.py new file mode 100644 index 00000000..43e0ab7e --- /dev/null +++ b/apps/workspaces/apis/export_settings/views.py @@ -0,0 +1,12 @@ +from rest_framework import generics + +from apps.workspaces.models import Workspace + +from apps.workspaces.apis.export_settings.serializers import ExportSettingsSerializer + + +class ExportSettingsView(generics.RetrieveUpdateAPIView): + serializer_class = ExportSettingsSerializer + + def get_object(self): + return Workspace.objects.filter(id=self.kwargs['workspace_id']).first() diff --git a/apps/workspaces/apis/urls.py b/apps/workspaces/apis/urls.py index 80934c77..0bbdc06e 100644 --- a/apps/workspaces/apis/urls.py +++ b/apps/workspaces/apis/urls.py @@ -1,8 +1,10 @@ from django.urls import path from apps.workspaces.apis.map_employees.views import MapEmployeesView +from apps.workspaces.apis.export_settings.views import ExportSettingsView urlpatterns = [ path('/map_employees/', MapEmployeesView.as_view()), + path('/export_settings/', ExportSettingsView.as_view()), ] From cfb791bfbdf650378d4366141b6427d7c8583c3d Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Wed, 25 Oct 2023 15:09:36 +0530 Subject: [PATCH 13/36] export settings V2 api --- .gitignore | 2 +- .../migrations/0026_auto_20231025_0913.py | 20 +++++++++++++++++++ apps/fyle/models.py | 3 ++- .../migrations/0010_auto_20231025_0915.py | 20 +++++++++++++++++++ apps/mappings/models.py | 2 +- .../apis/export_settings/serializers.py | 4 ++-- 6 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 apps/fyle/migrations/0026_auto_20231025_0913.py create mode 100644 apps/mappings/migrations/0010_auto_20231025_0915.py diff --git a/.gitignore b/.gitignore index 301a2fa6..b0917142 100644 --- a/.gitignore +++ b/.gitignore @@ -159,4 +159,4 @@ cache.db-journal test_scripts/ fyle_accounting_mappings/ fyle_integrations_platform_connector/ -fyle/ +./fyle diff --git a/apps/fyle/migrations/0026_auto_20231025_0913.py b/apps/fyle/migrations/0026_auto_20231025_0913.py new file mode 100644 index 00000000..61e85482 --- /dev/null +++ b/apps/fyle/migrations/0026_auto_20231025_0913.py @@ -0,0 +1,20 @@ +# Generated by Django 3.1.14 on 2023-10-25 09:13 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('workspaces', '0035_auto_20231019_1025'), + ('fyle', '0025_auto_20230622_0516'), + ] + + operations = [ + migrations.AlterField( + model_name='expensegroupsettings', + name='workspace', + field=models.OneToOneField(help_text='To which workspace this expense group setting belongs to', on_delete=django.db.models.deletion.PROTECT, related_name='expense_group_settings', to='workspaces.workspace'), + ), + ] diff --git a/apps/fyle/models.py b/apps/fyle/models.py index af59a097..866a54db 100644 --- a/apps/fyle/models.py +++ b/apps/fyle/models.py @@ -214,7 +214,8 @@ class ExpenseGroupSettings(models.Model): ccc_export_date_type = models.CharField(max_length=100, default='current_date', help_text='CCC Export Date') import_card_credits = models.BooleanField(help_text='Import Card Credits', default=False) workspace = models.OneToOneField( - Workspace, on_delete=models.PROTECT, help_text='To which workspace this expense group setting belongs to' + Workspace, on_delete=models.PROTECT, help_text='To which workspace this expense group setting belongs to', + related_name = 'expense_group_settings' ) created_at = models.DateTimeField(auto_now_add=True, help_text='Created at') updated_at = models.DateTimeField(auto_now=True, help_text='Updated at') diff --git a/apps/mappings/migrations/0010_auto_20231025_0915.py b/apps/mappings/migrations/0010_auto_20231025_0915.py new file mode 100644 index 00000000..1330418e --- /dev/null +++ b/apps/mappings/migrations/0010_auto_20231025_0915.py @@ -0,0 +1,20 @@ +# Generated by Django 3.1.14 on 2023-10-25 09:15 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('workspaces', '0035_auto_20231019_1025'), + ('mappings', '0009_auto_20211022_0702'), + ] + + operations = [ + migrations.AlterField( + model_name='generalmapping', + name='workspace', + field=models.OneToOneField(help_text='Reference to Workspace model', on_delete=django.db.models.deletion.PROTECT, related_name='general_mappings', to='workspaces.workspace'), + ), + ] diff --git a/apps/mappings/models.py b/apps/mappings/models.py index 7ac93821..9f1131d6 100644 --- a/apps/mappings/models.py +++ b/apps/mappings/models.py @@ -47,7 +47,7 @@ class GeneralMapping(models.Model): vendor_payment_account_name = models.CharField(max_length=255, help_text='VendorPayment Account name', null=True) default_ccc_vendor_id = models.CharField(max_length=255, help_text='Default CCC Vendor ID', null=True) default_ccc_vendor_name = models.CharField(max_length=255, help_text='Default CCC Vendor Name', null=True) - workspace = models.OneToOneField(Workspace, on_delete=models.PROTECT, help_text='Reference to Workspace model') + 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') updated_at = models.DateTimeField(auto_now=True, help_text='Updated at datetime') diff --git a/apps/workspaces/apis/export_settings/serializers.py b/apps/workspaces/apis/export_settings/serializers.py index a21d5eb6..372d8e0d 100644 --- a/apps/workspaces/apis/export_settings/serializers.py +++ b/apps/workspaces/apis/export_settings/serializers.py @@ -27,7 +27,6 @@ class Meta: 'reimbursable_expenses_object', 'corporate_credit_card_expenses_object', 'is_simplify_report_closure_enabled', - 'name_in_journal_entry', 'auto_map_employees', 'employee_field_mapping' ] @@ -122,7 +121,8 @@ def update(self, instance: Workspace, validated_data): defaults={ 'reimbursable_expenses_object': configurations['reimbursable_expenses_object'], 'corporate_credit_card_expenses_object': configurations['corporate_credit_card_expenses_object'], - 'employee_field_mapping': configurations['employee_field_mapping'] + 'employee_field_mapping': configurations['employee_field_mapping'], + 'auto_map_employees': configurations['auto_map_employees'] } ) From e9a0b1791c68d63a016f2269cf01e370c659d7c4 Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Wed, 25 Oct 2023 17:01:09 +0530 Subject: [PATCH 14/36] added test for export settings api --- apps/workspaces/apis/urls.py | 4 +- tests/test_workspaces/test_apis/__init__.py | 0 .../test_export_settings/__init__.py | 0 .../test_export_settings/fixtures.py | 196 ++++++++++++++++++ .../test_export_settings/test_views.py | 83 ++++++++ 5 files changed, 281 insertions(+), 2 deletions(-) create mode 100644 tests/test_workspaces/test_apis/__init__.py create mode 100644 tests/test_workspaces/test_apis/test_export_settings/__init__.py create mode 100644 tests/test_workspaces/test_apis/test_export_settings/fixtures.py create mode 100644 tests/test_workspaces/test_apis/test_export_settings/test_views.py diff --git a/apps/workspaces/apis/urls.py b/apps/workspaces/apis/urls.py index 0bbdc06e..fc9b312a 100644 --- a/apps/workspaces/apis/urls.py +++ b/apps/workspaces/apis/urls.py @@ -5,6 +5,6 @@ urlpatterns = [ - path('/map_employees/', MapEmployeesView.as_view()), - path('/export_settings/', ExportSettingsView.as_view()), + path('/map_employees/', MapEmployeesView.as_view(), name='map-employees'), + path('/export_settings/', ExportSettingsView.as_view(), name='export-settings'), ] diff --git a/tests/test_workspaces/test_apis/__init__.py b/tests/test_workspaces/test_apis/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_workspaces/test_apis/test_export_settings/__init__.py b/tests/test_workspaces/test_apis/test_export_settings/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_workspaces/test_apis/test_export_settings/fixtures.py b/tests/test_workspaces/test_apis/test_export_settings/fixtures.py new file mode 100644 index 00000000..18a01fec --- /dev/null +++ b/tests/test_workspaces/test_apis/test_export_settings/fixtures.py @@ -0,0 +1,196 @@ +data = { + 'export_settings': { + 'configuration': { + 'reimbursable_expenses_object': 'EXPENSE REPORT', + 'corporate_credit_card_expenses_object': 'EXPENSE REPORT', + 'auto_map_employees': 'NAME', + 'is_simplify_report_closure_enabled': False, + 'employee_field_mapping': 'EMPLOYEE' + }, + 'expense_group_settings': { + 'reimbursable_expense_group_fields': [ + 'fund_source', + 'employee_email', + 'claim_number', + 'report_id' + ], + 'reimbursable_export_date_type': 'current_date', + 'expense_state': 'PAYMENT_PROCESSING', + 'corporate_credit_card_expense_group_fields': [ + 'fund_source', + 'claim_number', + 'employee_email', + 'report_id', + 'spent_at', + 'expense_id' + ], + 'ccc_export_date_type': 'spent_at', + 'ccc_expense_state': 'PAID' + }, + 'general_mappings': { + 'reimbursable_account': { + 'id': '', + 'name': '' + }, + 'accounts_payable': { + 'id': '', + 'name': '' + }, + 'default_ccc_account': { + 'id': '1', + 'name': 'Elon musk' + }, + 'default_ccc_vendor': { + 'id': '', + 'name': '' + } + } + }, + 'response': { + 'configuration': { + 'reimbursable_expenses_object': 'EXPENSE_REPORT', + 'corporate_credit_card_expenses_object': 'CHARGE_CARD_TRANSACTION', + 'auto_map_employees': 'NAME', + 'is_simplify_report_closure_enabled': False, + 'employee_field_mapping': 'EMPLOYEE' + }, + 'expense_group_settings': { + 'reimbursable_expense_group_fields': [ + 'fund_source', + 'employee_email', + 'claim_number', + 'report_id' + ], + 'reimbursable_export_date_type': 'current_date', + 'expense_state': 'PAYMENT_PROCESSING', + 'corporate_credit_card_expense_group_fields': [ + 'fund_source', + 'claim_number', + 'employee_email', + 'report_id', + 'spent_at', + 'expense_id' + ], + 'ccc_export_date_type': 'spent_at', + 'ccc_expense_state': 'PAID' + }, + 'general_mappings': { + 'reimbursable_account': { + 'id': '', + 'name': '' + }, + 'accounts_payable': { + 'id': '', + 'name': '' + }, + 'default_ccc_account': { + 'id': '1', + 'name': 'Elon musk' + }, + 'default_ccc_vendor': { + 'id': '', + 'name': '' + } + }, + 'workspace_id': 1 + }, + 'export_settings_missing_values_configurations': { + 'configurations': {}, + 'expense_group_settings': { + 'reimbursable_expense_group_fields': [ + 'fund_source', + 'employee_email', + 'claim_number', + 'report_id' + ], + 'reimbursable_export_date_type': 'current_date', + 'expense_state': 'PAYMENT_PROCESSING', + 'corporate_credit_card_expense_group_fields': [ + 'fund_source', + 'claim_number', + 'employee_email', + 'report_id', + 'spent_at', + 'expense_id' + ], + 'ccc_export_date_type': 'spent_at', + 'ccc_expense_state': 'PAID' + }, + 'general_mappings': { + 'reimbursable_account': { + 'id': '', + 'name': '' + }, + 'accounts_payable': { + 'id': '', + 'name': '' + }, + 'default_ccc_account': { + 'id': '1', + 'name': 'Elon musk' + }, + 'default_ccc_vendor': { + 'id': '', + 'name': '' + } + } + }, + 'export_settings_missing_values_expense_group_settings': { + 'configurations': { + 'reimbursable_expenses_object': 'EXPENSE_REPORT', + 'corporate_credit_card_expenses_object': 'CHARGE_CARD_TRANSACTION', + 'auto_map_employees': 'NAME', + 'is_simplify_report_closure_enabled': False, + 'employee_field_mapping': 'EMPLOYEE' + }, + 'expense_group_settings': {}, + 'general_mappings': { + 'reimbursable_account': { + 'id': '', + 'name': '' + }, + 'accounts_payable': { + 'id': '', + 'name': '' + }, + 'default_ccc_account': { + 'id': '1', + 'name': 'Elon musk' + }, + 'default_ccc_vendor': { + 'id': '', + 'name': '' + } + } + }, + 'export_settings_missing_values_general_mappings': { + 'configurations': { + 'reimbursable_expenses_object': 'EXPENSE_REPORT', + 'corporate_credit_card_expenses_object': 'CHARGE_CARD_TRANSACTION', + 'auto_map_employees': 'NAME', + 'is_simplify_report_closure_enabled': False, + 'employee_field_mapping': 'EMPLOYEE' + }, + 'expense_group_settings': { + 'reimbursable_expense_group_fields': [ + 'fund_source', + 'employee_email', + 'claim_number', + 'report_id' + ], + 'reimbursable_export_date_type': 'current_date', + 'expense_state': 'PAYMENT_PROCESSING', + 'corporate_credit_card_expense_group_fields': [ + 'fund_source', + 'claim_number', + 'employee_email', + 'report_id', + 'spent_at', + 'expense_id' + ], + 'ccc_export_date_type': 'spent_at', + 'ccc_expense_state': 'PAID' + }, + 'general_mappings': {} + } +} diff --git a/tests/test_workspaces/test_apis/test_export_settings/test_views.py b/tests/test_workspaces/test_apis/test_export_settings/test_views.py new file mode 100644 index 00000000..203c154e --- /dev/null +++ b/tests/test_workspaces/test_apis/test_export_settings/test_views.py @@ -0,0 +1,83 @@ +import json +from tests.helper import dict_compare_keys +from apps.workspaces.models import Workspace, Configuration +from .fixtures import data +import pytest +from django.urls import reverse + +@pytest.mark.django_db(databases=['default']) +def test_export_settings(api_client, access_token): + + workspace = Workspace.objects.get(id=1) + workspace.onboarding_state = 'EXPORT_SETTINGS' + workspace.save() + + url = reverse( + 'export-settings', kwargs={ + 'workspace_id': 1 + } + ) + + api_client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(access_token)) + response = api_client.put( + url, + data=data['export_settings'], + format='json' + ) + + assert response.status_code == 200 + + response = json.loads(response.content) + workspace = Workspace.objects.get(id=1) + + assert dict_compare_keys(response, data['response']) == [], 'workspaces api returns a diff in the keys' + assert workspace.onboarding_state == 'IMPORT_SETTINGS' + + url = '/api/v2/workspaces/1/export_settings/' + api_client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(access_token)) + + payload = data['export_settings'] + payload['expense_group_settings']['reimbursable_expense_group_fields'] = [] + payload['expense_group_settings']['corporate_credit_card_expense_group_fields'] = [] + payload['expense_group_settings']['reimbursable_export_date_type'] = '' + payload['expense_group_settings']['ccc_export_date_type'] = '' + + response = api_client.put( + url, + data=payload, + format='json' + ) + + assert response.status_code == 200 + + response = json.loads(response.content) + workspace = Workspace.objects.get(id=1) + + assert dict_compare_keys(response, data['response']) == [], 'workspaces api returns a diff in the keys' + + invalid_configurations = data['export_settings_missing_values_configurations'] + response = api_client.put( + url, + data=invalid_configurations, + format='json' + ) + + assert response.status_code == 400 + + invalid_expense_group_settings = data['export_settings_missing_values_expense_group_settings'] + response = api_client.put( + url, + data=invalid_expense_group_settings, + format='json' + ) + + assert response.status_code == 400 + + invalid_general_mappings = data['export_settings_missing_values_general_mappings'] + response = api_client.put( + url, + data=invalid_general_mappings, + format='json' + ) + + assert response.status_code == 400 From aa56ca4a92a5669161f6a41d94aac3d6c7d4aad6 Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Thu, 26 Oct 2023 11:24:23 +0530 Subject: [PATCH 15/36] resolved comments --- apps/workspaces/apis/export_settings/serializers.py | 7 +++---- .../test_apis/test_export_settings/fixtures.py | 10 +--------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/apps/workspaces/apis/export_settings/serializers.py b/apps/workspaces/apis/export_settings/serializers.py index 372d8e0d..caef879a 100644 --- a/apps/workspaces/apis/export_settings/serializers.py +++ b/apps/workspaces/apis/export_settings/serializers.py @@ -27,8 +27,6 @@ class Meta: 'reimbursable_expenses_object', 'corporate_credit_card_expenses_object', 'is_simplify_report_closure_enabled', - 'auto_map_employees', - 'employee_field_mapping' ] read_only_fields = ['is_simplify_report_closure_enabled'] @@ -53,16 +51,19 @@ def get_reimbursable_account(self, instance: GeneralMapping): 'id': instance.reimbursable_account_id, 'name': instance.reimbursable_account_name } + def get_default_ccc_account(self, instance: GeneralMapping): return { 'id': instance.default_ccc_account_id, 'name': instance.default_ccc_account_name } + def get_accounts_payable(self, instance: GeneralMapping): return { 'id': instance.accounts_payable_id, 'name': instance.accounts_payable_name } + def get_default_ccc_vendor(self, instance: GeneralMapping): return { 'id': instance.default_ccc_vendor_id, @@ -121,8 +122,6 @@ def update(self, instance: Workspace, validated_data): defaults={ 'reimbursable_expenses_object': configurations['reimbursable_expenses_object'], 'corporate_credit_card_expenses_object': configurations['corporate_credit_card_expenses_object'], - 'employee_field_mapping': configurations['employee_field_mapping'], - 'auto_map_employees': configurations['auto_map_employees'] } ) diff --git a/tests/test_workspaces/test_apis/test_export_settings/fixtures.py b/tests/test_workspaces/test_apis/test_export_settings/fixtures.py index 18a01fec..003f082f 100644 --- a/tests/test_workspaces/test_apis/test_export_settings/fixtures.py +++ b/tests/test_workspaces/test_apis/test_export_settings/fixtures.py @@ -3,9 +3,7 @@ 'configuration': { 'reimbursable_expenses_object': 'EXPENSE REPORT', 'corporate_credit_card_expenses_object': 'EXPENSE REPORT', - 'auto_map_employees': 'NAME', 'is_simplify_report_closure_enabled': False, - 'employee_field_mapping': 'EMPLOYEE' }, 'expense_group_settings': { 'reimbursable_expense_group_fields': [ @@ -49,10 +47,8 @@ 'response': { 'configuration': { 'reimbursable_expenses_object': 'EXPENSE_REPORT', - 'corporate_credit_card_expenses_object': 'CHARGE_CARD_TRANSACTION', - 'auto_map_employees': 'NAME', + 'corporate_credit_card_expenses_object': 'CHARGE_CARD_TRANSACTION', 'is_simplify_report_closure_enabled': False, - 'employee_field_mapping': 'EMPLOYEE' }, 'expense_group_settings': { 'reimbursable_expense_group_fields': [ @@ -139,9 +135,7 @@ 'configurations': { 'reimbursable_expenses_object': 'EXPENSE_REPORT', 'corporate_credit_card_expenses_object': 'CHARGE_CARD_TRANSACTION', - 'auto_map_employees': 'NAME', 'is_simplify_report_closure_enabled': False, - 'employee_field_mapping': 'EMPLOYEE' }, 'expense_group_settings': {}, 'general_mappings': { @@ -167,9 +161,7 @@ 'configurations': { 'reimbursable_expenses_object': 'EXPENSE_REPORT', 'corporate_credit_card_expenses_object': 'CHARGE_CARD_TRANSACTION', - 'auto_map_employees': 'NAME', 'is_simplify_report_closure_enabled': False, - 'employee_field_mapping': 'EMPLOYEE' }, 'expense_group_settings': { 'reimbursable_expense_group_fields': [ From 62697bbfd76d2d696c5816b0a955a1d865aac3cb Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Thu, 26 Oct 2023 17:29:28 +0530 Subject: [PATCH 16/36] import settings v2 api --- .../apis/import_settings/__init__.py | 0 .../apis/import_settings/serializers.py | 145 ++++++++++++++++++ .../apis/import_settings/triggers.py | 134 ++++++++++++++++ apps/workspaces/apis/import_settings/views.py | 11 ++ apps/workspaces/apis/urls.py | 2 + 5 files changed, 292 insertions(+) create mode 100644 apps/workspaces/apis/import_settings/__init__.py create mode 100644 apps/workspaces/apis/import_settings/serializers.py create mode 100644 apps/workspaces/apis/import_settings/triggers.py create mode 100644 apps/workspaces/apis/import_settings/views.py diff --git a/apps/workspaces/apis/import_settings/__init__.py b/apps/workspaces/apis/import_settings/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/workspaces/apis/import_settings/serializers.py b/apps/workspaces/apis/import_settings/serializers.py new file mode 100644 index 00000000..12d26a90 --- /dev/null +++ b/apps/workspaces/apis/import_settings/serializers.py @@ -0,0 +1,145 @@ +from apps.workspaces.apis.import_settings.triggers import ImportSettingsTrigger +from rest_framework import serializers +from fyle_accounting_mappings.models import MappingSetting +from django.db import transaction +from django.db.models import Q + +from apps.workspaces.models import Workspace, Configuration +from apps.mappings.models import GeneralMapping + + +class MappingSettingFilteredListSerializer(serializers.ListSerializer): + """ + Serializer to filter the active system, which is a boolen field in + System Model. The value argument to to_representation() method is + the model instance + """ + def to_representation(self, data): + data = data.filter(~Q( + destination_field__in=[ + 'ACCOUNT', + 'CCC_ACCOUNT', + 'CHARGE_CARD_NUMBER', + 'EMPLOYEE', + 'EXPENSE_TYPE', + 'TAX_DETAIL', + 'VENDOR' + ]) + ) + return super(MappingSettingFilteredListSerializer, self).to_representation(data) + + +class ReadWriteSerializerMethodField(serializers.SerializerMethodField): + """ + Serializer Method Field to Read and Write from values + Inherits serializers.SerializerMethodField + """ + + def __init__(self, method_name=None, **kwargs): + self.method_name = method_name + kwargs['source'] = '*' + super(serializers.SerializerMethodField, self).__init__(**kwargs) + + def to_internal_value(self, data): + return { + self.field_name: data + } + + +class MappingSettingSerializer(serializers.ModelSerializer): + class Meta: + model = MappingSetting + list_serializer_class = MappingSettingFilteredListSerializer + fields = [ + 'source_field', + 'destination_field', + 'import_to_fyle', + 'is_custom', + 'source_placeholder' + ] + + +class ConfigurationsSerializer(serializers.ModelSerializer): + class Meta: + model = Configuration + fields = [ + 'import_categories', + 'import_vendors_as_merchants', + 'import_items', + 'import_tax_items', + 'auto_create_merchants' + ] + + +class ImportSettingsSerializer(serializers.ModelSerializer): + """ + Serializer for the ImportSettings Form/API + """ + + configuration = ConfigurationsSerializer() + mapping_settings = MappingSettingSerializer(many=True) + workspace_id = serializers.SerializerMethodField() + + class Meta: + model = Workspace + fields = ['configuration', 'mapping_settings', 'workspace_id'] + read_only_fields = ['workspace_id'] + + def get_workspace_id(self, instance): + return instance.id + + def update(self, instance, validated_data): + + configurations = validated_data.pop('configuration') + mapping_settings = validated_data.pop('mapping_settings') + + configurations_instance, _ = Configuration.objects.update_or_create( + workspace=instance, + defaults={ + 'import_categories': configurations.get('import_categories'), + 'import_items': configurations.get('import_items'), + 'charts_of_accounts': configurations.get('charts_of_accounts'), + 'import_tax_items': configurations.get('import_tax_items'), + 'import_vendors_as_merchants': configurations.get('import_vendors_as_merchants'), + }, + ) + + trigger: ImportSettingsTrigger = ImportSettingsTrigger(configurations=configurations, mapping_settings=mapping_settings, workspace_id=instance.id) + + trigger.post_save_configurations(configurations_instance) + trigger.pre_save_mapping_settings() + + if configurations['import_tax_items']: + mapping_settings.append({'source_field': 'TAX_GROUP', 'destination_field': 'TAX_CODE', 'import_to_fyle': True, 'is_custom': False}) + + mapping_settings.append({'source_field': 'CATEGORY', 'destination_field': 'ACCOUNT', 'import_to_fyle': False, 'is_custom': False}) + + with transaction.atomic(): + for setting in mapping_settings: + MappingSetting.objects.update_or_create( + destination_field=setting['destination_field'], + workspace_id=instance.id, + defaults={ + 'source_field': setting['source_field'], + 'import_to_fyle': setting['import_to_fyle'] if 'import_to_fyle' in setting else False, + 'is_custom': setting['is_custom'] if 'is_custom' in setting else False, + 'source_placeholder': setting['source_placeholder'] if 'source_placeholder' in setting else None, + }, + ) + + trigger.post_save_mapping_settings(configurations_instance) + + if instance.onboarding_state == 'IMPORT_SETTINGS': + instance.onboarding_state = 'ADVANCED_CONFIGURATION' + instance.save() + + return instance + + def validate(self, data): + if not data.get('configuration'): + raise serializers.ValidationError('Configurations are required') + + if data.get('mapping_settings') is None: + raise serializers.ValidationError('Mapping settings are required') + + return data diff --git a/apps/workspaces/apis/import_settings/triggers.py b/apps/workspaces/apis/import_settings/triggers.py new file mode 100644 index 00000000..6d9289dc --- /dev/null +++ b/apps/workspaces/apis/import_settings/triggers.py @@ -0,0 +1,134 @@ +from typing import Dict, List + +from django.db.models import Q +from fyle_accounting_mappings.models import MappingSetting + +from apps.fyle.models import ExpenseGroupSettings +from apps.mappings.helpers import schedule_or_delete_fyle_import_tasks +from apps.mappings.tasks import ( + schedule_cost_centers_creation, + schedule_fyle_attributes_creation, + schedule_tax_groups_creation, +) +from apps.workspaces.models import Configuration +from django_q.tasks import async_task + +class ImportSettingsTrigger: + """ + All the post save actions of Import Settings API + """ + + def __init__(self, configurations: Dict, mapping_settings: List[Dict], workspace_id): + self.__configurations = configurations + self.__mapping_settings = mapping_settings + self.__workspace_id = workspace_id + + def remove_department_grouping(self, source_field: str): + configurations: Configuration = Configuration.objects.filter(workspace_id=self.__workspace_id).first() + expense_group_settings = ExpenseGroupSettings.objects.get(workspace_id=self.__workspace_id) + + # Removing Department Source field from Reimbursable settings + if configurations.reimbursable_expenses_object: + reimbursable_settings = expense_group_settings.reimbursable_expense_group_fields + reimbursable_settings.remove(source_field.lower()) + expense_group_settings.reimbursable_expense_group_fields = list(set(reimbursable_settings)) + + # Removing Department Source field from Non reimbursable settings + if configurations.corporate_credit_card_expenses_object: + corporate_credit_card_settings = list(expense_group_settings.corporate_credit_card_expense_group_fields) + corporate_credit_card_settings.remove(source_field.lower()) + expense_group_settings.corporate_credit_card_expense_group_fields = list(set(corporate_credit_card_settings)) + + expense_group_settings.save() + + def add_department_grouping(self, source_field: str): + configurations: Configuration = Configuration.objects.filter(workspace_id=self.__workspace_id).first() + + expense_group_settings: ExpenseGroupSettings = ExpenseGroupSettings.objects.get(workspace_id=self.__workspace_id) + + # Adding Department Source field to Reimbursable settings + reimbursable_settings = expense_group_settings.reimbursable_expense_group_fields + + if configurations.reimbursable_expenses_object != 'JOURNAL_ENTRY': + reimbursable_settings.append(source_field.lower()) + expense_group_settings.reimbursable_expense_group_fields = list(set(reimbursable_settings)) + + # Adding Department Source field to Non reimbursable settings + corporate_credit_card_settings = list(expense_group_settings.corporate_credit_card_expense_group_fields) + + if configurations.corporate_credit_card_expenses_object != 'JOURNAL_ENTRY': + corporate_credit_card_settings.append(source_field.lower()) + expense_group_settings.corporate_credit_card_expense_group_fields = list(set(corporate_credit_card_settings)) + + 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): + """ + Post save action for workspace general settings + """ + schedule_tax_groups_creation(import_tax_items=self.__configurations.get('import_tax_items'), workspace_id=self.__workspace_id) + + schedule_or_delete_fyle_import_tasks(configurations_instance) + + if not configurations_instance.import_items: + async_task('apps.mappings.tasks.disable_category_for_items_mapping') + + 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 pre_save_mapping_settings(self): + """ + Post save action for mapping settings + """ + mapping_settings = self.__mapping_settings + + cost_center_mapping_available = False + + for setting in mapping_settings: + if setting['source_field'] == 'COST_CENTER': + cost_center_mapping_available = True + + if not cost_center_mapping_available: + schedule_cost_centers_creation(False, self.__workspace_id) + + schedule_fyle_attributes_creation(self.__workspace_id) + + # Removal of department grouping will be taken care from post_delete() signal + + # 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) + + def post_save_mapping_settings(self, configurations_instance: Configuration): + """ + Post save actions for mapping settings + """ + destination_fields = [] + for setting in self.__mapping_settings: + destination_fields.append(setting['destination_field']) + + MappingSetting.objects.filter(~Q(destination_field__in=destination_fields), destination_field__in=['CLASS', 'CUSTOMER', 'DEPARTMENT'], workspace_id=self.__workspace_id).delete() + + self.__update_expense_group_settings_for_departments() + + schedule_or_delete_fyle_import_tasks(configurations_instance) diff --git a/apps/workspaces/apis/import_settings/views.py b/apps/workspaces/apis/import_settings/views.py new file mode 100644 index 00000000..f80e0641 --- /dev/null +++ b/apps/workspaces/apis/import_settings/views.py @@ -0,0 +1,11 @@ +from rest_framework import generics + +from apps.workspaces.apis.import_settings.serializers import ImportSettingsSerializer +from apps.workspaces.models import Workspace + + +class ImportSettingsView(generics.RetrieveUpdateAPIView): + serializer_class = ImportSettingsSerializer + + def get_object(self): + return Workspace.objects.filter(id=self.kwargs['workspace_id']).first() diff --git a/apps/workspaces/apis/urls.py b/apps/workspaces/apis/urls.py index fc9b312a..548d642b 100644 --- a/apps/workspaces/apis/urls.py +++ b/apps/workspaces/apis/urls.py @@ -2,9 +2,11 @@ from apps.workspaces.apis.map_employees.views import MapEmployeesView from apps.workspaces.apis.export_settings.views import ExportSettingsView +from apps.workspaces.apis.import_settings.views import ImportSettingsView urlpatterns = [ path('/map_employees/', MapEmployeesView.as_view(), name='map-employees'), path('/export_settings/', ExportSettingsView.as_view(), name='export-settings'), + path('/import_settings/', ImportSettingsView.as_view(), name='import-settings'), ] From 3357a2ed7f87aceb7b02855d9cb4384e076a9a91 Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Thu, 26 Oct 2023 21:12:06 +0530 Subject: [PATCH 17/36] test added for import settings v2 api --- .../apis/import_settings/serializers.py | 2 +- .../test_import_settings/__init__.py | 0 .../test_import_settings/fixtures.py | 95 +++++++++++++++++++ .../test_import_settings/test_views.py | 41 ++++++++ 4 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 tests/test_workspaces/test_apis/test_import_settings/__init__.py create mode 100644 tests/test_workspaces/test_apis/test_import_settings/fixtures.py create mode 100644 tests/test_workspaces/test_apis/test_import_settings/test_views.py diff --git a/apps/workspaces/apis/import_settings/serializers.py b/apps/workspaces/apis/import_settings/serializers.py index 12d26a90..6f71d192 100644 --- a/apps/workspaces/apis/import_settings/serializers.py +++ b/apps/workspaces/apis/import_settings/serializers.py @@ -98,7 +98,7 @@ def update(self, instance, validated_data): defaults={ 'import_categories': configurations.get('import_categories'), 'import_items': configurations.get('import_items'), - 'charts_of_accounts': configurations.get('charts_of_accounts'), + 'auto_create_merchants': configurations.get('auto_create_merchants'), 'import_tax_items': configurations.get('import_tax_items'), 'import_vendors_as_merchants': configurations.get('import_vendors_as_merchants'), }, diff --git a/tests/test_workspaces/test_apis/test_import_settings/__init__.py b/tests/test_workspaces/test_apis/test_import_settings/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_workspaces/test_apis/test_import_settings/fixtures.py b/tests/test_workspaces/test_apis/test_import_settings/fixtures.py new file mode 100644 index 00000000..9ab621c9 --- /dev/null +++ b/tests/test_workspaces/test_apis/test_import_settings/fixtures.py @@ -0,0 +1,95 @@ +data = { + "import_settings": { + "configuration": { + "import_categories": True, + "import_items": False, + "import_tax_items": True, + "import_vendors_as_merchants": True, + "auto_create_merchants": False + }, + "mapping_settings": [ + { + "source_field": "COST_CENTER", + "destination_field": "DEPARTMENT", + "import_to_fyle": True, + "is_custom": False, + "source_placeholder": "cost center", + }, + { + "source_field": "PROJECT", + "destination_field": "CLASS", + "import_to_fyle": True, + "is_custom": False, + "source_placeholder": "project", + }, + { + "source_field": "Test Dependent", + "destination_field": "CUSTOMER", + "import_to_fyle": True, + "is_custom": True, + "source_placeholder": "class", + }, + ], + }, + "import_settings_without_mapping": { + "configuration": { + "import_categories": True, + "import_items": True, + "auto_create_merchants": False, + "import_tax_items": True, + "import_vendors_as_merchants": True, + }, + "mapping_settings": [ + { + "source_field": "Test Dependent", + "destination_field": "CUSTOMER", + "import_to_fyle": True, + "is_custom": True, + "source_placeholder": "class", + } + ], + }, + "response": { + "configuration": { + "import_categories": True, + "import_tax_items": True, + "import_items": False, + "auto_create_merchants": False, + "import_vendors_as_merchants": True, + }, + "mapping_settings": [ + { + "source_field": "COST_CENTER", + "destination_field": "CLASS", + "import_to_fyle": True, + "is_custom": False, + "source_placeholder": "", + }, + { + "source_field": "PROJECT", + "destination_field": "DEPARTMENT", + "import_to_fyle": True, + "is_custom": False, + "source_placeholder": "", + }, + { + "source_field": "Test Dependent", + "destination_field": "CUSTOMER", + "import_to_fyle": True, + "is_custom": True, + "source_placeholder": "", + }, + ], + "workspace_id": 9, + }, + "invalid_mapping_settings": { + "configuration": { + "import_categories": True, + "import_tax_items": True, + "import_items": False, + "auto_create_merchants": False, + "import_vendors_as_merchants": True, + }, + "mapping_settings": None, + }, +} diff --git a/tests/test_workspaces/test_apis/test_import_settings/test_views.py b/tests/test_workspaces/test_apis/test_import_settings/test_views.py new file mode 100644 index 00000000..7fe0797a --- /dev/null +++ b/tests/test_workspaces/test_apis/test_import_settings/test_views.py @@ -0,0 +1,41 @@ +import json +import pytest +from apps.workspaces.models import Workspace +from tests.helper import dict_compare_keys +from tests.test_workspaces.test_apis.test_import_settings.fixtures import data + +from django.urls import reverse + +@pytest.mark.django_db(databases=['default']) +def test_import_settings(mocker, api_client, access_token): + mocker.patch('fyle_integrations_platform_connector.apis.ExpenseCustomFields.get_by_id', return_value={'options': ['samp'], 'updated_at': '2020-06-11T13:14:55.201598+00:00'}) + mocker.patch('fyle_integrations_platform_connector.apis.ExpenseCustomFields.post', return_value=None) + mocker.patch('fyle_integrations_platform_connector.apis.ExpenseCustomFields.sync', return_value=None) + workspace = Workspace.objects.get(id=1) + workspace.onboarding_state = 'IMPORT_SETTINGS' + workspace.save() + + url = reverse( + 'import-settings', kwargs={ + 'workspace_id': 1 + } + ) + + api_client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(access_token)) + response = api_client.put(url, data=data['import_settings'], format='json') + + assert response.status_code == 200 + + response = json.loads(response.content) + assert dict_compare_keys(response, data['response']) == [], 'workspaces api returns a diff in the keys' + + response = api_client.put(url, data=data['import_settings_without_mapping'], format='json') + assert response.status_code == 200 + + invalid_configurations_settings = data['import_settings'] + invalid_configurations_settings['configuration'] = {} + response = api_client.put(url, data=invalid_configurations_settings, format='json') + assert response.status_code == 400 + + response = api_client.put(url, data=data['invalid_mapping_settings'], format='json') + assert response.status_code == 400 From 3558f3047bd916d0016401c85314abe3f941ba66 Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Tue, 31 Oct 2023 12:28:36 +0530 Subject: [PATCH 18/36] advanced settings v2 api --- .../apis/advanced_settings/__init__.py | 0 .../apis/advanced_settings/serializers.py | 150 ++++++++++++++++++ .../apis/advanced_settings/triggers.py | 0 .../apis/advanced_settings/views.py | 0 .../migrations/0036_auto_20231027_0709.py | 19 +++ apps/workspaces/models.py | 2 +- 6 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 apps/workspaces/apis/advanced_settings/__init__.py create mode 100644 apps/workspaces/apis/advanced_settings/serializers.py create mode 100644 apps/workspaces/apis/advanced_settings/triggers.py create mode 100644 apps/workspaces/apis/advanced_settings/views.py create mode 100644 apps/workspaces/migrations/0036_auto_20231027_0709.py diff --git a/apps/workspaces/apis/advanced_settings/__init__.py b/apps/workspaces/apis/advanced_settings/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/workspaces/apis/advanced_settings/serializers.py b/apps/workspaces/apis/advanced_settings/serializers.py new file mode 100644 index 00000000..33999571 --- /dev/null +++ b/apps/workspaces/apis/advanced_settings/serializers.py @@ -0,0 +1,150 @@ +from rest_framework import serializers + +from apps.workspaces.models import Configuration, Workspace, WorkspaceSchedule +from apps.mappings.models import GeneralMapping + + + +class ReadWriteSerializerMethodField(serializers.SerializerMethodField): + """ + Serializer Method Field to Read and Write from values + Inherits serializers.SerializerMethodField + """ + + def __init__(self, method_name=None, **kwargs): + self.method_name = method_name + kwargs['source'] = '*' + super(serializers.SerializerMethodField, self).__init__(**kwargs) + + def to_internal_value(self, data): + return {self.field_name: data} + + +class ConfigurationSerializer(serializers.ModelSerializer): + + class Meta: + model = Configuration + fields = [ + 'change_accounting_period', + 'sync_fyle_to_sage_intacct_payments', + 'sync_sage_intacct_to_fyle_payments', + 'auto_create_destination_entity', + 'memo_structure' + ] + + +class GeneralMappingsSerializer(serializers.ModelSerializer): + + netsuite_location = ReadWriteSerializerMethodField() + netsuite_location_level = ReadWriteSerializerMethodField() + department_level = ReadWriteSerializerMethodField() + use_employee_location = ReadWriteSerializerMethodField() + use_employee_department = ReadWriteSerializerMethodField() + use_employee_class = ReadWriteSerializerMethodField() + + class Meta: + model = GeneralMapping + fields = [ + 'netsuite_location', + 'netsuite_location_level', + 'department_level', + 'use_employee_location', + 'use_employee_department', + 'use_employee_class' + ] + + + def get_netsuite_location(self, instance: GeneralMapping): + return { + 'name': instance.location_name, + 'id': instance.location_id + } + + def get_netsuite_location_level(self, instance: GeneralMapping): + return instance.location_level + + def get_department_level(self, instance: GeneralMapping): + return instance.department_level + + def get_default_class(self, instance): + return { + 'name': instance.default_class_name, + 'id': instance.default_class_id + } + + def get_default_project(self, instance): + return { + 'name': instance.default_project_name, + 'id': instance.default_project_id + } + + def get_default_item(self, instance): + return { + 'name': instance.default_item_name, + 'id': instance.default_item_id + } + +class WorkspaceSchedulesSerializer(serializers.ModelSerializer): + emails_selected = serializers.ListField(allow_null=True, required=False) + + class Meta: + model = WorkspaceSchedule + fields = [ + 'enabled', + 'interval_hours', + 'additional_email_options', + 'emails_selected' + ] + +class AdvancedConfigurationsSerializer(serializers.ModelSerializer): + """ + Serializer for the Advanced Configurations Form/API + """ + configurations = ConfigurationSerializer() + general_mappings = GeneralMappingsSerializer() + workspace_schedules = WorkspaceSchedulesSerializer() + workspace_id = serializers.SerializerMethodField() + + class Meta: + model = Workspace + fields = [ + 'configurations', + 'general_mappings', + 'workspace_schedules', + 'workspace_id' + ] + read_only_fields = ['workspace_id'] + + + def get_workspace_id(self, instance): + return instance.id + + def update(self, instance, validated): + configurations = validated.pop('configurations') + general_mappings = validated.pop('general_mappings') + workspace_schedules = validated.pop('workspace_schedules') + + Configuration.objects.update_or_create( + workspace=instance, + defaults={ + 'sync_fyle_to_sage_intacct_payments': configurations.get('sync_fyle_to_sage_intacct_payments'), + 'sync_sage_intacct_to_fyle_payments': configurations.get('sync_sage_intacct_to_fyle_payments'), + 'auto_create_destination_entity': configurations.get('auto_create_destination_entity'), + 'change_accounting_period': configurations.get('change_accounting_period'), + 'memo_structure': configurations.get('memo_structure') + } + ) + + GeneralMapping.objects.update_or_create( + workspace=instance, + defaults={ + 'netsuite_location': general_mappings.get + 'netsuite_location_level', + 'department_level', + 'use_employee_location', + 'use_employee_department', + 'use_employee_class' + } + ) + + diff --git a/apps/workspaces/apis/advanced_settings/triggers.py b/apps/workspaces/apis/advanced_settings/triggers.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/workspaces/apis/advanced_settings/views.py b/apps/workspaces/apis/advanced_settings/views.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/workspaces/migrations/0036_auto_20231027_0709.py b/apps/workspaces/migrations/0036_auto_20231027_0709.py new file mode 100644 index 00000000..b2b40553 --- /dev/null +++ b/apps/workspaces/migrations/0036_auto_20231027_0709.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.14 on 2023-10-27 07:09 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('workspaces', '0035_auto_20231019_1025'), + ] + + operations = [ + migrations.AlterField( + model_name='workspaceschedule', + name='workspace', + field=models.OneToOneField(help_text='Reference to Workspace model', on_delete=django.db.models.deletion.PROTECT, related_name='workspace_schedules', to='workspaces.workspace'), + ), + ] diff --git a/apps/workspaces/models.py b/apps/workspaces/models.py index 1409e6f9..61a26377 100644 --- a/apps/workspaces/models.py +++ b/apps/workspaces/models.py @@ -87,7 +87,7 @@ class WorkspaceSchedule(models.Model): Workspace Schedule """ id = models.AutoField(primary_key=True, help_text='Unique Id to identify a schedule') - workspace = models.OneToOneField(Workspace, on_delete=models.PROTECT, help_text='Reference to Workspace model') + workspace = models.OneToOneField(Workspace, on_delete=models.PROTECT, help_text='Reference to Workspace model', related_name='workspace_schedules') enabled = models.BooleanField(default=False) start_datetime = models.DateTimeField(help_text='Datetime for start of schedule', null=True) interval_hours = models.IntegerField(null=True) From 84ed5c9b0863995195147c025f1c10cfbd73b7a7 Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Tue, 31 Oct 2023 14:01:08 +0530 Subject: [PATCH 19/36] advanced settings v2 api with test case --- .../apis/advanced_settings/serializers.py | 69 +++++++++++-------- .../apis/advanced_settings/triggers.py | 24 +++++++ .../apis/advanced_settings/views.py | 11 +++ apps/workspaces/apis/urls.py | 2 + .../test_advanced_settings/__init__.py | 0 .../test_advanced_settings/fixtures.py | 61 ++++++++++++++++ .../test_advanced_settings/test_views.py | 61 ++++++++++++++++ 7 files changed, 198 insertions(+), 30 deletions(-) create mode 100644 tests/test_workspaces/test_apis/test_advanced_settings/__init__.py create mode 100644 tests/test_workspaces/test_apis/test_advanced_settings/fixtures.py create mode 100644 tests/test_workspaces/test_apis/test_advanced_settings/test_views.py diff --git a/apps/workspaces/apis/advanced_settings/serializers.py b/apps/workspaces/apis/advanced_settings/serializers.py index 33999571..dbe937b1 100644 --- a/apps/workspaces/apis/advanced_settings/serializers.py +++ b/apps/workspaces/apis/advanced_settings/serializers.py @@ -2,7 +2,7 @@ from apps.workspaces.models import Configuration, Workspace, WorkspaceSchedule from apps.mappings.models import GeneralMapping - +from apps.workspaces.apis.advanced_settings.triggers import AdvancedConfigurationsTriggers class ReadWriteSerializerMethodField(serializers.SerializerMethodField): @@ -26,8 +26,8 @@ class Meta: model = Configuration fields = [ 'change_accounting_period', - 'sync_fyle_to_sage_intacct_payments', - 'sync_sage_intacct_to_fyle_payments', + 'sync_fyle_to_netsuite_payments', + 'sync_netsuite_to_fyle_payments', 'auto_create_destination_entity', 'memo_structure' ] @@ -66,23 +66,14 @@ def get_netsuite_location_level(self, instance: GeneralMapping): def get_department_level(self, instance: GeneralMapping): return instance.department_level - def get_default_class(self, instance): - return { - 'name': instance.default_class_name, - 'id': instance.default_class_id - } + def get_use_employee_location(self, instance: GeneralMapping): + return instance.use_employee_location + + def get_use_employee_department(self, instance: GeneralMapping): + return instance.use_employee_department - def get_default_project(self, instance): - return { - 'name': instance.default_project_name, - 'id': instance.default_project_id - } - - def get_default_item(self, instance): - return { - 'name': instance.default_item_name, - 'id': instance.default_item_id - } + def get_use_employee_class(self, instance: GeneralMapping): + return instance.use_employee_class class WorkspaceSchedulesSerializer(serializers.ModelSerializer): emails_selected = serializers.ListField(allow_null=True, required=False) @@ -96,11 +87,11 @@ class Meta: 'emails_selected' ] -class AdvancedConfigurationsSerializer(serializers.ModelSerializer): +class AdvancedSettingsSerializer(serializers.ModelSerializer): """ Serializer for the Advanced Configurations Form/API """ - configurations = ConfigurationSerializer() + configuration = ConfigurationSerializer() general_mappings = GeneralMappingsSerializer() workspace_schedules = WorkspaceSchedulesSerializer() workspace_id = serializers.SerializerMethodField() @@ -108,7 +99,7 @@ class AdvancedConfigurationsSerializer(serializers.ModelSerializer): class Meta: model = Workspace fields = [ - 'configurations', + 'configuration', 'general_mappings', 'workspace_schedules', 'workspace_id' @@ -120,11 +111,11 @@ def get_workspace_id(self, instance): return instance.id def update(self, instance, validated): - configurations = validated.pop('configurations') + configurations = validated.pop('configuration') general_mappings = validated.pop('general_mappings') workspace_schedules = validated.pop('workspace_schedules') - Configuration.objects.update_or_create( + configuration_instance, _ = Configuration.objects.update_or_create( workspace=instance, defaults={ 'sync_fyle_to_sage_intacct_payments': configurations.get('sync_fyle_to_sage_intacct_payments'), @@ -138,13 +129,31 @@ def update(self, instance, validated): GeneralMapping.objects.update_or_create( workspace=instance, defaults={ - 'netsuite_location': general_mappings.get - 'netsuite_location_level', - 'department_level', - 'use_employee_location', - 'use_employee_department', - 'use_employee_class' + 'netsuite_location': general_mappings.get('netsuite_location'), + 'netsuite_location_level': general_mappings.get('netsuite_location_level'), + 'department_level': general_mappings.get('department_level'), + 'use_employee_location': general_mappings.get('use_employee_location'), + 'use_employee_department': general_mappings.get('use_employee_department'), + 'use_employee_class': general_mappings.get('use_employee_class') } ) + AdvancedConfigurationsTriggers.run_post_configurations_triggers(instance.id, workspace_schedule=workspace_schedules, configuration=configuration_instance) + + if instance.onboarding_state == 'ADVANCED_CONFIGURATION': + instance.onboarding_state = 'COMPLETE' + instance.save() + + return instance + + def validate(self, data): + if not data.get('configuration'): + raise serializers.ValidationError('Configurations are required') + + if not data.get('general_mappings'): + raise serializers.ValidationError('General mappings are required') + + if not data.get('workspace_schedules'): + raise serializers.ValidationError('Workspace Schedules are required') + return data diff --git a/apps/workspaces/apis/advanced_settings/triggers.py b/apps/workspaces/apis/advanced_settings/triggers.py index e69de29b..466a0111 100644 --- a/apps/workspaces/apis/advanced_settings/triggers.py +++ b/apps/workspaces/apis/advanced_settings/triggers.py @@ -0,0 +1,24 @@ +from apps.netsuite.helpers import schedule_payment_sync +from apps.workspaces.models import Configuration, WorkspaceSchedule +from apps.workspaces.tasks import schedule_sync + + +class AdvancedConfigurationsTriggers: + """ + Class containing all triggers for advanced_configurations + """ + @staticmethod + def run_post_configurations_triggers(workspace_id, workspace_schedule: WorkspaceSchedule, configuration: Configuration): + """ + Run workspace general settings triggers + """ + + schedule_sync( + workspace_id=workspace_id, + schedule_enabled=workspace_schedule.get('enabled'), + hours=workspace_schedule.get('interval_hours'), + email_added=workspace_schedule.get('additional_email_options'), + emails_selected=workspace_schedule.get('emails_selected') + ) + + schedule_payment_sync(configuration=configuration) diff --git a/apps/workspaces/apis/advanced_settings/views.py b/apps/workspaces/apis/advanced_settings/views.py index e69de29b..0e5b368d 100644 --- a/apps/workspaces/apis/advanced_settings/views.py +++ b/apps/workspaces/apis/advanced_settings/views.py @@ -0,0 +1,11 @@ +from rest_framework import generics + +from apps.workspaces.models import Workspace +from apps.workspaces.apis.advanced_settings.serializers import AdvancedSettingsSerializer + + +class AdvancedSettingsView(generics.RetrieveUpdateAPIView): + serializer_class = AdvancedSettingsSerializer + + def get_object(self): + return Workspace.objects.filter(id=self.kwargs['workspace_id']).first() diff --git a/apps/workspaces/apis/urls.py b/apps/workspaces/apis/urls.py index 024bd3f1..a7294d86 100644 --- a/apps/workspaces/apis/urls.py +++ b/apps/workspaces/apis/urls.py @@ -1,4 +1,5 @@ from django.urls import path +from apps.workspaces.apis.advanced_settings.views import AdvancedSettingsView from apps.workspaces.apis.map_employees.views import MapEmployeesView from apps.workspaces.apis.export_settings.views import ExportSettingsView @@ -10,4 +11,5 @@ path('/map_employees/', MapEmployeesView.as_view(), name='map-employees'), path('/export_settings/', ExportSettingsView.as_view(), name='export-settings'), path('/import_settings/', ImportSettingsView.as_view(), name='import-settings'), + path('/advanced-settings/', AdvancedSettingsView.as_view(), name='advanced-settings'), ] diff --git a/tests/test_workspaces/test_apis/test_advanced_settings/__init__.py b/tests/test_workspaces/test_apis/test_advanced_settings/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_workspaces/test_apis/test_advanced_settings/fixtures.py b/tests/test_workspaces/test_apis/test_advanced_settings/fixtures.py new file mode 100644 index 00000000..7e05bdde --- /dev/null +++ b/tests/test_workspaces/test_apis/test_advanced_settings/fixtures.py @@ -0,0 +1,61 @@ +data = { + "advanced_settings": { + "configuration": { + "change_accounting_period": True, + "sync_fyle_to_netsuite_payments": True, + "sync_netsuite_to_fyle_payments": False, + "auto_create_destination_entity": False, + "memo_structure": ["merchant", "purpose"], + }, + "general_mappings": { + "netsuite_location": { + "name": "Bir Billing", + "id": "13" + }, + "netsuite_location_level": "TRANSACTION_BODY", + "department_level": "", + "use_employee_location": False, + "use_employee_department": False, + "use_employee_class": False + }, + "workspace_schedules": { + "enabled": True, + "interval_hours": 24, + "emails_selected": ["fyle@fyle.in"], + "additional_email_options": {}, + }, + }, + "response": { + "configuration": { + "change_accounting_period": True, + "sync_fyle_to_netsuite_payments": True, + "sync_netsuite_to_fyle_payments": False, + "auto_create_destination_entity": False, + "memo_structure": ["merchant", "purpose"], + }, + "general_mappings": { + "netsuite_location": { + "name": "Bir Billing", + "id": "13" + }, + "netsuite_location_level": "TRANSACTION_BODY", + "department_level": "", + "use_employee_location": False, + "use_employee_department": False, + "use_employee_class": False + }, + "workspace_schedules": { + "enabled": True, + "start_datetime": "now()", + "interval_hours": 24, + "emails_selected": [], + "additional_email_options": [], + }, + "workspace_id": 9, + }, + "validate": { + "workspace_general_settings": {}, + "general_mappings": {}, + "workspace_schedules": {}, + }, +} diff --git a/tests/test_workspaces/test_apis/test_advanced_settings/test_views.py b/tests/test_workspaces/test_apis/test_advanced_settings/test_views.py new file mode 100644 index 00000000..2c9dc8fb --- /dev/null +++ b/tests/test_workspaces/test_apis/test_advanced_settings/test_views.py @@ -0,0 +1,61 @@ +import json +from tests.helper import dict_compare_keys +from apps.workspaces.models import Workspace, Configuration +from .fixtures import data +import pytest +from django.urls import reverse + +@pytest.mark.django_db(databases=['default']) +def test_advanced_settings(api_client, access_token): + + workspace = Workspace.objects.get(id=1) + workspace.onboarding_state = 'ADVANCED_CONFIGURATION' + workspace.save() + + url = reverse( + 'advanced-settings', kwargs={ + 'workspace_id': 1 + } + ) + + api_client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(access_token)) + response = api_client.put( + url, + data=data['advanced_settings'], + format='json' + ) + + assert response.status_code == 200 + + response = json.loads(response.content) + workspace = Workspace.objects.get(id=1) + + assert dict_compare_keys(response, data['response']) == [], 'workspaces api returns a diff in the keys' + assert workspace.onboarding_state == 'COMPLETE' + + response = api_client.put( + url, + data={ + 'general_mappings':{}}, + format='json' + ) + + assert response.status_code == 400 + + response = api_client.put( + url, + data={ + 'configuration':{}}, + format='json' + ) + + assert response.status_code == 400 + + response = api_client.put( + url, + data={ + 'workspace_schedules':{}}, + format='json' + ) + + assert response.status_code == 400 From 18ecafe50b11704e514e34fb2ba5eebddcedc96e Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Tue, 31 Oct 2023 14:38:20 +0530 Subject: [PATCH 20/36] First schedule should be triggered after interval hours --- apps/workspaces/tasks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/workspaces/tasks.py b/apps/workspaces/tasks.py index a573e7e9..139d32dc 100644 --- a/apps/workspaces/tasks.py +++ b/apps/workspaces/tasks.py @@ -56,6 +56,7 @@ def schedule_sync(workspace_id: int, schedule_enabled: bool, hours: int, email_a if email_added: ws_schedule.additional_email_options.append(email_added) + next_run = datetime.now() + timedelta(hours=hours) schedule, _ = Schedule.objects.update_or_create( func='apps.workspaces.tasks.run_sync_schedule', @@ -63,7 +64,7 @@ def schedule_sync(workspace_id: int, schedule_enabled: bool, hours: int, email_a defaults={ 'schedule_type': Schedule.MINUTES, 'minutes': hours * 60, - 'next_run': datetime.now() + 'next_run': next_run } ) From adf33da2bccf06cefd334f6c3543b2ffb249c270 Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Tue, 31 Oct 2023 15:57:02 +0530 Subject: [PATCH 21/36] Handle Admin GET in a safer way --- apps/workspaces/views.py | 13 +++++++------ tests/test_workspaces/test_views.py | 10 +++++++--- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/apps/workspaces/views.py b/apps/workspaces/views.py index 39026d9b..e650bc80 100644 --- a/apps/workspaces/views.py +++ b/apps/workspaces/views.py @@ -458,16 +458,17 @@ def get(self, request, *args, **kwargs): users = workspace.user.all() for user in users: admin = User.objects.get(user_id=user) - name = ExpenseAttribute.objects.get( + employee = ExpenseAttribute.objects.filter( value=admin.email, workspace_id=kwargs['workspace_id'], attribute_type='EMPLOYEE' - ).detail['full_name'] + ).first() - admin_email.append({ - 'name': name, - 'email': admin.email - }) + if employee: + admin_email.append({ + 'name': employee.detail['full_name'], + 'email': admin.email + }) return Response( data=admin_email, diff --git a/tests/test_workspaces/test_views.py b/tests/test_workspaces/test_views.py index 798c31b6..04e51e8f 100644 --- a/tests/test_workspaces/test_views.py +++ b/tests/test_workspaces/test_views.py @@ -317,9 +317,13 @@ def test_post_workspace_schedule(api_client, access_token): api_client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(access_token)) response = api_client.post(url, { - 'schedule_enabled': False, - 'hours': 0 - }) + "hours": 2, + "schedule_enabled": True, + "added_email": None, + "selected_email": [ + "admin1@fyleforbadassashu.in" + ] + }, format='json') assert response.status_code == 200 def test_get_workspace_schedule(api_client, access_token): From 2faa99fe76233906c3c1ab2bbfcad4ebf4563bff Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Thu, 2 Nov 2023 14:04:31 +0530 Subject: [PATCH 22/36] Making reimbursbale expense object nullable and checking edge cases for the same --- apps/fyle/models.py | 4 ++-- apps/mappings/tasks.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/fyle/models.py b/apps/fyle/models.py index 866a54db..5144c681 100644 --- a/apps/fyle/models.py +++ b/apps/fyle/models.py @@ -345,7 +345,7 @@ def create_expense_groups_by_report_id_fund_source(expense_objects: List[Expense reimbursable_expenses = list(filter(lambda expense: expense.fund_source == 'PERSONAL', expense_objects)) - if configuration.reimbursable_expenses_object == 'EXPENSE REPORT' and 'expense_id' not in reimbursable_expense_group_fields: + if (configuration.reimbursable_expenses_object and configuration.reimbursable_expenses_object) == 'EXPENSE REPORT' and 'expense_id' not in reimbursable_expense_group_fields: total_amount = 0 if 'spent_at' in reimbursable_expense_group_fields: grouped_data = defaultdict(list) @@ -367,7 +367,7 @@ def create_expense_groups_by_report_id_fund_source(expense_objects: List[Expense if total_amount < 0: reimbursable_expenses = list(filter(lambda expense: expense.amount > 0, reimbursable_expenses)) - elif configuration.reimbursable_expenses_object != 'JOURNAL ENTRY': + elif configuration.reimbursable_expenses_object and configuration.reimbursable_expenses_object != 'JOURNAL ENTRY': reimbursable_expenses = list(filter(lambda expense: expense.amount > 0, reimbursable_expenses)) expense_groups = _group_expenses(reimbursable_expenses, reimbursable_expense_group_fields, workspace_id) diff --git a/apps/mappings/tasks.py b/apps/mappings/tasks.py index efef0862..e77cc94f 100644 --- a/apps/mappings/tasks.py +++ b/apps/mappings/tasks.py @@ -188,10 +188,10 @@ def sync_expense_categories_and_accounts(configuration: Configuration, netsuite_ :netsuite_connection: NetSuite Connection Object :return: None """ - if configuration.reimbursable_expenses_object == 'EXPENSE REPORT' or configuration.corporate_credit_card_expenses_object == 'EXPENSE REPORT': + if (configuration.reimbursable_expenses_object and configuration.reimbursable_expenses_object == 'EXPENSE REPORT') or configuration.corporate_credit_card_expenses_object == 'EXPENSE REPORT': netsuite_connection.sync_expense_categories() - if configuration.reimbursable_expenses_object in ('BILL', 'JOURNAL ENTRY') or \ + if (configuration.reimbursable_expenses_object and configuration.reimbursable_expenses_object in ('BILL', 'JOURNAL ENTRY')) or \ configuration.corporate_credit_card_expenses_object in ('BILL', 'JOURNAL ENTRY', 'CREDIT CARD CHARGE'): netsuite_connection.sync_accounts() @@ -479,7 +479,7 @@ def auto_create_category_mappings(workspace_id): reimbursable_expenses_object = configuration.reimbursable_expenses_object corporate_credit_card_expenses_object = configuration.corporate_credit_card_expenses_object - if reimbursable_expenses_object == 'EXPENSE REPORT': + if reimbursable_expenses_object and reimbursable_expenses_object == 'EXPENSE REPORT': reimbursable_destination_type = 'EXPENSE_CATEGORY' else: reimbursable_destination_type = 'ACCOUNT' @@ -501,7 +501,7 @@ def auto_create_category_mappings(workspace_id): platform.categories.post_bulk(fyle_payload) platform.categories.sync() - if reimbursable_expenses_object == 'EXPENSE REPORT' and \ + if (reimbursable_expenses_object and reimbursable_expenses_object == 'EXPENSE REPORT') and \ corporate_credit_card_expenses_object in ('BILL', 'JOURNAL ENTRY', 'CREDIT CARD CHARGE'): bulk_create_ccc_category_mappings(workspace_id) From fe4f88b2bc5ee722726f3ae41eb770e2b8885e5c Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Fri, 3 Nov 2023 16:12:47 +0530 Subject: [PATCH 23/36] comment resolved --- apps/fyle/helpers.py | 4 ++-- apps/fyle/models.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/fyle/helpers.py b/apps/fyle/helpers.py index d1d2211a..d4ee923b 100644 --- a/apps/fyle/helpers.py +++ b/apps/fyle/helpers.py @@ -130,9 +130,9 @@ def update_import_card_credits_flag(corporate_credit_card_expenses_object: str, expense_group_settings = ExpenseGroupSettings.objects.get(workspace_id=workspace_id) import_card_credits = None - if (corporate_credit_card_expenses_object == 'EXPENSE REPORT' or reimbursable_expenses_object in ['EXPENSE REPORT', 'JOURNAL ENTRY']) and not expense_group_settings.import_card_credits: + if (corporate_credit_card_expenses_object == 'EXPENSE REPORT' or (reimbursable_expenses_object and reimbursable_expenses_object in ['EXPENSE REPORT', 'JOURNAL ENTRY'])) and not expense_group_settings.import_card_credits: import_card_credits = True - elif (corporate_credit_card_expenses_object != 'EXPENSE REPORT' and reimbursable_expenses_object not in ['EXPENSE REPORT', 'JOURNAL ENTRY']) and expense_group_settings.import_card_credits: + elif (corporate_credit_card_expenses_object != 'EXPENSE REPORT' and (reimbursable_expenses_object and reimbursable_expenses_object in ['EXPENSE REPORT', 'JOURNAL ENTRY'])) and expense_group_settings.import_card_credits: import_card_credits = False if corporate_credit_card_expenses_object == 'CREDIT CARD CHARGE': diff --git a/apps/fyle/models.py b/apps/fyle/models.py index 5144c681..fed23cc2 100644 --- a/apps/fyle/models.py +++ b/apps/fyle/models.py @@ -345,7 +345,7 @@ def create_expense_groups_by_report_id_fund_source(expense_objects: List[Expense reimbursable_expenses = list(filter(lambda expense: expense.fund_source == 'PERSONAL', expense_objects)) - if (configuration.reimbursable_expenses_object and configuration.reimbursable_expenses_object) == 'EXPENSE REPORT' and 'expense_id' not in reimbursable_expense_group_fields: + if configuration.reimbursable_expenses_object == 'EXPENSE REPORT' and 'expense_id' not in reimbursable_expense_group_fields: total_amount = 0 if 'spent_at' in reimbursable_expense_group_fields: grouped_data = defaultdict(list) @@ -367,7 +367,7 @@ def create_expense_groups_by_report_id_fund_source(expense_objects: List[Expense if total_amount < 0: reimbursable_expenses = list(filter(lambda expense: expense.amount > 0, reimbursable_expenses)) - elif configuration.reimbursable_expenses_object and configuration.reimbursable_expenses_object != 'JOURNAL ENTRY': + elif configuration.reimbursable_expenses_object != 'JOURNAL ENTRY': reimbursable_expenses = list(filter(lambda expense: expense.amount > 0, reimbursable_expenses)) expense_groups = _group_expenses(reimbursable_expenses, reimbursable_expense_group_fields, workspace_id) From e7b1d40f10494cfece5c59e0f13d82b3bbc488d3 Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Mon, 6 Nov 2023 14:06:38 +0530 Subject: [PATCH 24/36] resolving comments --- apps/mappings/tasks.py | 9 +++++---- tests/test_mappings/test_tasks.py | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/mappings/tasks.py b/apps/mappings/tasks.py index e77cc94f..c4f9f8dc 100644 --- a/apps/mappings/tasks.py +++ b/apps/mappings/tasks.py @@ -226,8 +226,8 @@ def upload_categories_to_fyle(workspace_id: int, configuration: Configuration, p if configuration.import_categories: netsuite_accounts = DestinationAttribute.objects.filter( workspace_id=workspace_id, - attribute_type='EXPENSE_CATEGORY' if configuration.reimbursable_expenses_object == 'EXPENSE REPORT' else 'ACCOUNT', - display_name='Expense Category' if configuration.reimbursable_expenses_object == 'EXPENSE REPORT' else 'Account' + attribute_type='EXPENSE_CATEGORY' if configuration.employee_field_mapping == 'EMPLOYEE' else 'ACCOUNT', + display_name='Expense Category' if configuration.employee_field_mapping == 'EMPLOYEE' else 'Account' ) if netsuite_accounts: netsuite_attributes = netsuite_accounts @@ -477,9 +477,10 @@ def auto_create_category_mappings(workspace_id): configuration: Configuration = Configuration.objects.get(workspace_id=workspace_id) reimbursable_expenses_object = configuration.reimbursable_expenses_object + employee_field_mapping = configuration.employee_field_mapping corporate_credit_card_expenses_object = configuration.corporate_credit_card_expenses_object - if reimbursable_expenses_object and reimbursable_expenses_object == 'EXPENSE REPORT': + if employee_field_mapping and employee_field_mapping == 'EMPLOYEE': reimbursable_destination_type = 'EXPENSE_CATEGORY' else: reimbursable_destination_type = 'ACCOUNT' @@ -501,7 +502,7 @@ def auto_create_category_mappings(workspace_id): platform.categories.post_bulk(fyle_payload) platform.categories.sync() - if (reimbursable_expenses_object and reimbursable_expenses_object == 'EXPENSE REPORT') and \ + if reimbursable_expenses_object == 'EXPENSE REPORT' and \ corporate_credit_card_expenses_object in ('BILL', 'JOURNAL ENTRY', 'CREDIT CARD CHARGE'): bulk_create_ccc_category_mappings(workspace_id) diff --git a/tests/test_mappings/test_tasks.py b/tests/test_mappings/test_tasks.py index 69d2a862..6210bad4 100644 --- a/tests/test_mappings/test_tasks.py +++ b/tests/test_mappings/test_tasks.py @@ -303,7 +303,7 @@ def test_upload_categories_to_fyle(mocker, db): netsuite_attributes = upload_categories_to_fyle(49, configuration, platform) - assert len(netsuite_attributes) == 137 + assert len(netsuite_attributes) == 36 def test_filter_unmapped_destinations(db, mocker): @@ -410,7 +410,7 @@ def test_auto_create_category_mappings(db, mocker): response = auto_create_category_mappings(workspace_id=49) mappings_count = CategoryMapping.objects.filter(workspace_id=49).count() - assert mappings_count == 53 + assert mappings_count == 36 # Patents & Licenses - Exempted category = ExpenseAttribute.objects.filter(value='Patents & Licenses - Exempted', workspace_id = 49).first() From 48a39fd67d2b1e418a68e087220ce16359708bcd Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Mon, 6 Nov 2023 16:23:22 +0530 Subject: [PATCH 25/36] all comment resolved --- apps/fyle/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/fyle/helpers.py b/apps/fyle/helpers.py index d4ee923b..730e2b6b 100644 --- a/apps/fyle/helpers.py +++ b/apps/fyle/helpers.py @@ -132,7 +132,7 @@ def update_import_card_credits_flag(corporate_credit_card_expenses_object: str, if (corporate_credit_card_expenses_object == 'EXPENSE REPORT' or (reimbursable_expenses_object and reimbursable_expenses_object in ['EXPENSE REPORT', 'JOURNAL ENTRY'])) and not expense_group_settings.import_card_credits: import_card_credits = True - elif (corporate_credit_card_expenses_object != 'EXPENSE REPORT' and (reimbursable_expenses_object and reimbursable_expenses_object in ['EXPENSE REPORT', 'JOURNAL ENTRY'])) and expense_group_settings.import_card_credits: + elif (corporate_credit_card_expenses_object != 'EXPENSE REPORT' and (reimbursable_expenses_object and reimbursable_expenses_object not in ['EXPENSE REPORT', 'JOURNAL ENTRY'])) and expense_group_settings.import_card_credits: import_card_credits = False if corporate_credit_card_expenses_object == 'CREDIT CARD CHARGE': From 2eb6d344ebf76034d7415bf5fd1ab353d00ec2ac Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Mon, 6 Nov 2023 20:42:42 +0530 Subject: [PATCH 26/36] added code in test for the changes --- tests/test_mappings/test_tasks.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_mappings/test_tasks.py b/tests/test_mappings/test_tasks.py index 6210bad4..13158b45 100644 --- a/tests/test_mappings/test_tasks.py +++ b/tests/test_mappings/test_tasks.py @@ -279,6 +279,7 @@ def test_upload_categories_to_fyle(mocker, db): configuration = Configuration.objects.filter(workspace_id=49).first() configuration.reimbursable_expenses_object = 'EXPENSE REPORT' + configuration.employee_field_mapping = 'VENDOR' configuration.corporate_credit_card_expenses_object = 'BILL' configuration.import_categories = True configuration.save() @@ -290,7 +291,7 @@ def test_upload_categories_to_fyle(mocker, db): assert expense_category_count == 36 - assert len(netsuite_attributes) == 36 + assert len(netsuite_attributes) == 137 count_of_accounts = DestinationAttribute.objects.filter( attribute_type='ACCOUNT', workspace_id=49).count() @@ -403,14 +404,14 @@ def test_auto_create_category_mappings(db, mocker): destination_attribute.save() configuration = Configuration.objects.filter(workspace_id=49).first() - + configuration.employee_field_mapping = 'VENDOR' configuration.import_categories = True configuration.save() response = auto_create_category_mappings(workspace_id=49) mappings_count = CategoryMapping.objects.filter(workspace_id=49).count() - assert mappings_count == 36 + assert mappings_count == 53 # Patents & Licenses - Exempted category = ExpenseAttribute.objects.filter(value='Patents & Licenses - Exempted', workspace_id = 49).first() From a21e0c1c3a36da9fc3f237390ef3489e7c8bb279 Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Mon, 6 Nov 2023 20:48:37 +0530 Subject: [PATCH 27/36] added test code for the changes --- tests/test_mappings/test_tasks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_mappings/test_tasks.py b/tests/test_mappings/test_tasks.py index 13158b45..1e6a27c1 100644 --- a/tests/test_mappings/test_tasks.py +++ b/tests/test_mappings/test_tasks.py @@ -298,6 +298,7 @@ def test_upload_categories_to_fyle(mocker, db): assert count_of_accounts == 137 + configuration.employee_field_mapping = 'EMPLOYEE' configuration.reimbursable_expenses_object = 'BILL' configuration.corporate_credit_card_expenses_object = 'BILL' configuration.save() From 6c2c439148156e2814226d6686e8c847e4f562d8 Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Mon, 6 Nov 2023 21:07:06 +0530 Subject: [PATCH 28/36] Error model, API and test --- apps/tasks/migrations/0009_error.py | 35 +++++++++++ apps/tasks/models.py | 28 +++++++++ apps/workspaces/apis/errors/__init__.py | 0 apps/workspaces/apis/errors/serializers.py | 41 ++++++++++++ apps/workspaces/apis/errors/views.py | 16 +++++ apps/workspaces/apis/urls.py | 2 + fyle_netsuite_api/settings.py | 3 +- fyle_netsuite_api/utils.py | 10 +++ requirements.txt | 1 + .../test_apis/test_errors/__init__.py | 0 .../test_apis/test_errors/fixtures.py | 62 +++++++++++++++++++ .../test_apis/test_errors/test_views.py | 60 ++++++++++++++++++ 12 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 apps/tasks/migrations/0009_error.py create mode 100644 apps/workspaces/apis/errors/__init__.py create mode 100644 apps/workspaces/apis/errors/serializers.py create mode 100644 apps/workspaces/apis/errors/views.py create mode 100644 tests/test_workspaces/test_apis/test_errors/__init__.py create mode 100644 tests/test_workspaces/test_apis/test_errors/fixtures.py create mode 100644 tests/test_workspaces/test_apis/test_errors/test_views.py diff --git a/apps/tasks/migrations/0009_error.py b/apps/tasks/migrations/0009_error.py new file mode 100644 index 00000000..5e987736 --- /dev/null +++ b/apps/tasks/migrations/0009_error.py @@ -0,0 +1,35 @@ +# Generated by Django 3.1.14 on 2023-11-06 12:20 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('fyle_accounting_mappings', '0023_auto_20231010_1139'), + ('fyle', '0026_auto_20231025_0913'), + ('workspaces', '0036_auto_20231027_0709'), + ('tasks', '0008_auto_20220301_1300'), + ] + + operations = [ + migrations.CreateModel( + name='Error', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('type', models.CharField(choices=[('EMPLOYEE_MAPPING', 'EMPLOYEE_MAPPING'), ('CATEGORY_MAPPING', 'CATEGORY_MAPPING'), ('TAX_MAPPING', 'TAX_MAPPING'), ('NETSUITE_ERROR', 'NETSUITE_ERROR')], help_text='Error type', max_length=50)), + ('is_resolved', models.BooleanField(default=False, help_text='Is resolved')), + ('error_title', models.CharField(help_text='Error title', max_length=255)), + ('error_detail', models.TextField(help_text='Error detail')), + ('created_at', models.DateTimeField(auto_now_add=True, help_text='Created at datetime')), + ('updated_at', models.DateTimeField(auto_now=True, help_text='Updated at datetime')), + ('expense_attribute', models.OneToOneField(help_text='Reference to Expense Attribute', null=True, on_delete=django.db.models.deletion.PROTECT, to='fyle_accounting_mappings.expenseattribute')), + ('expense_group', models.ForeignKey(help_text='Reference to Expense group', null=True, on_delete=django.db.models.deletion.PROTECT, to='fyle.expensegroup')), + ('workspace', models.ForeignKey(help_text='Reference to Workspace model', on_delete=django.db.models.deletion.PROTECT, to='workspaces.workspace')), + ], + options={ + 'db_table': 'errors', + }, + ), + ] diff --git a/apps/tasks/models.py b/apps/tasks/models.py index 4068834d..ce51519b 100644 --- a/apps/tasks/models.py +++ b/apps/tasks/models.py @@ -5,6 +5,8 @@ from apps.workspaces.models import Workspace from apps.fyle.models import ExpenseGroup +from fyle_accounting_mappings.models import ExpenseAttribute + def get_default(): return { @@ -30,6 +32,7 @@ def get_default(): ('ENQUEUED', 'ENQUEUED') ) +ERROR_TYPE_CHOICES = (('EMPLOYEE_MAPPING', 'EMPLOYEE_MAPPING'), ('CATEGORY_MAPPING', 'CATEGORY_MAPPING'), ('TAX_MAPPING', 'TAX_MAPPING'), ('NETSUITE_ERROR', 'NETSUITE_ERROR')) class TaskLog(models.Model): """ @@ -57,3 +60,28 @@ class TaskLog(models.Model): class Meta: db_table = 'task_logs' + + +class Error(models.Model): + """ + Table to store errors + """ + id = models.AutoField(primary_key=True) + workspace = models.ForeignKey(Workspace, on_delete=models.PROTECT, help_text='Reference to Workspace model') + type = models.CharField(max_length=50, choices=ERROR_TYPE_CHOICES, help_text='Error type') + expense_group = models.ForeignKey( + ExpenseGroup, on_delete=models.PROTECT, + null=True, help_text='Reference to Expense group' + ) + expense_attribute = models.OneToOneField( + ExpenseAttribute, on_delete=models.PROTECT, + null=True, help_text='Reference to Expense Attribute' + ) + is_resolved = models.BooleanField(default=False, help_text='Is resolved') + error_title = models.CharField(max_length=255, help_text='Error title') + error_detail = models.TextField(help_text='Error detail') + created_at = models.DateTimeField(auto_now_add=True, help_text='Created at datetime') + updated_at = models.DateTimeField(auto_now=True, help_text='Updated at datetime') + + class Meta: + db_table = 'errors' diff --git a/apps/workspaces/apis/errors/__init__.py b/apps/workspaces/apis/errors/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/workspaces/apis/errors/serializers.py b/apps/workspaces/apis/errors/serializers.py new file mode 100644 index 00000000..3628d69e --- /dev/null +++ b/apps/workspaces/apis/errors/serializers.py @@ -0,0 +1,41 @@ +from fyle_accounting_mappings.models import ExpenseAttribute +from rest_framework import serializers + +from apps.fyle.models import ExpenseGroup +from apps.fyle.serializers import ExpenseSerializer +from apps.tasks.models import Error + + +class ExpenseAttributeSerializer(serializers.ModelSerializer): + """ + Serializer for Expense Attribute + """ + + class Meta: + model = ExpenseAttribute + fields = '__all__' + + +class ExpenseGroupSerializer(serializers.ModelSerializer): + """ + Serializer for Expense Group + """ + + expenses = ExpenseSerializer(many=True) + + class Meta: + model = ExpenseGroup + fields = '__all__' + + +class ErrorSerializer(serializers.ModelSerializer): + """ + Serializer for the Errors + """ + + expense_attribute = ExpenseAttributeSerializer() + expense_group = ExpenseGroupSerializer() + + class Meta: + model = Error + fields = '__all__' diff --git a/apps/workspaces/apis/errors/views.py b/apps/workspaces/apis/errors/views.py new file mode 100644 index 00000000..a07240db --- /dev/null +++ b/apps/workspaces/apis/errors/views.py @@ -0,0 +1,16 @@ +from django_filters.rest_framework import DjangoFilterBackend +from fyle_netsuite_api.utils import LookupFieldMixin +from rest_framework import generics +from apps.tasks.models import Error +from apps.workspaces.apis.errors.serializers import ErrorSerializer + + +class ErrorsView(LookupFieldMixin, generics.ListAPIView): + + queryset = Error.objects.all() + serializer_class = ErrorSerializer + pagination_class = None + filter_backends = (DjangoFilterBackend,) + filterset_fields = {'type':{'exact'}, 'is_resolved':{'exact'}} + + diff --git a/apps/workspaces/apis/urls.py b/apps/workspaces/apis/urls.py index a7294d86..f8ae3fb4 100644 --- a/apps/workspaces/apis/urls.py +++ b/apps/workspaces/apis/urls.py @@ -4,6 +4,7 @@ from apps.workspaces.apis.map_employees.views import MapEmployeesView from apps.workspaces.apis.export_settings.views import ExportSettingsView from apps.workspaces.apis.import_settings.views import ImportSettingsView +from apps.workspaces.apis.errors.views import ErrorsView @@ -12,4 +13,5 @@ path('/export_settings/', ExportSettingsView.as_view(), name='export-settings'), path('/import_settings/', ImportSettingsView.as_view(), name='import-settings'), path('/advanced-settings/', AdvancedSettingsView.as_view(), name='advanced-settings'), + path('/errors/', ErrorsView.as_view(), name='errors') ] diff --git a/fyle_netsuite_api/settings.py b/fyle_netsuite_api/settings.py index 72704778..8235bfa4 100644 --- a/fyle_netsuite_api/settings.py +++ b/fyle_netsuite_api/settings.py @@ -56,7 +56,8 @@ 'apps.tasks', 'apps.mappings', 'apps.netsuite', - 'django_q' + 'django_q', + 'django_filters', ] MIDDLEWARE = [ diff --git a/fyle_netsuite_api/utils.py b/fyle_netsuite_api/utils.py index fe007608..e973f5ab 100644 --- a/fyle_netsuite_api/utils.py +++ b/fyle_netsuite_api/utils.py @@ -13,3 +13,13 @@ def assert_valid(condition: bool, message: str) -> Response or None: raise ValidationError(detail={ 'message': message }) + +class LookupFieldMixin: + lookup_field = "workspace_id" + + def filter_queryset(self, queryset): + if self.lookup_field in self.kwargs: + lookup_value = self.kwargs[self.lookup_field] + filter_kwargs = {self.lookup_field: lookup_value} + queryset = queryset.filter(**filter_kwargs) + return super().filter_queryset(queryset) diff --git a/requirements.txt b/requirements.txt index b72f180d..e81e291f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -62,3 +62,4 @@ urllib3==1.26.11 wcwidth==0.1.8 wrapt==1.12.1 zeep==4.1.0 +django-filter diff --git a/tests/test_workspaces/test_apis/test_errors/__init__.py b/tests/test_workspaces/test_apis/test_errors/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_workspaces/test_apis/test_errors/fixtures.py b/tests/test_workspaces/test_apis/test_errors/fixtures.py new file mode 100644 index 00000000..4c8e2e17 --- /dev/null +++ b/tests/test_workspaces/test_apis/test_errors/fixtures.py @@ -0,0 +1,62 @@ +data = { + 'errors_response': [ + { + "id": 1, + "expense_attribute": { + "id": 13044, + "attribute_type": "CATEGORY", + "display_name": "Category", + "value": "Internet", + "source_id": "142069", + "auto_mapped": False, + "auto_created": False, + "active": None, + "detail": None, + "created_at": "2022-10-07T06:02:54.076426Z", + "updated_at": "2022-10-07T06:02:54.076429Z", + "workspace": 8 + }, + "expense_group": None, + "type": "CATEGORY_MAPPING", + "is_resolved": False, + "error_title": "Internet", + "error_detail": "Category mapping is missing", + "created_at": "2022-10-07T06:07:32.823778Z", + "updated_at": "2022-10-07T06:31:53.211657Z", + "workspace": 1 + }, + { + "id": 2, + "expense_attribute": { + "id": 13020, + "attribute_type": "EMPLOYEE", + "display_name": "Employee", + "value": "ashwin.t@fyle.in", + "source_id": "ouQmTCQE26dc", + "auto_mapped": False, + "auto_created": False, + "active": None, + "detail": { + "user_id": "usqywo0f3nBY", + "location": None, + "full_name": "Joanna", + "department": None, + "department_id": None, + "employee_code": None, + "department_code": None + }, + "created_at": "2022-10-07T06:02:53.810543Z", + "updated_at": "2022-10-07T06:02:53.810548Z", + "workspace": 8 + }, + "expense_group": None, + "type": "EMPLOYEE_MAPPING", + "is_resolved": False, + "error_title": "ashwin.t@fyle.in", + "error_detail": "Employee mapping is missing", + "created_at": "2022-10-07T06:31:48.338064Z", + "updated_at": "2022-10-07T06:31:48.338082Z", + "workspace": 1 + } + ] +} diff --git a/tests/test_workspaces/test_apis/test_errors/test_views.py b/tests/test_workspaces/test_apis/test_errors/test_views.py new file mode 100644 index 00000000..d269814d --- /dev/null +++ b/tests/test_workspaces/test_apis/test_errors/test_views.py @@ -0,0 +1,60 @@ +import json +from datetime import datetime,timezone +from apps.tasks.models import Error +from .fixtures import data +from tests.helper import dict_compare_keys +import pytest +from django.urls import reverse + +@pytest.mark.django_db(databases=['default']) +def test_errors(api_client, access_token): + + Error.objects.create( + workspace_id=1, + type = 'EMPLOYEE_MAPPING', + expense_attribute_id=8, + is_resolved = False, + error_title = 'ashwin.t@fyle.in', + error_detail = 'Employee mapping is missing', + created_at = datetime.now(tz=timezone.utc), + updated_at = datetime.now(tz=timezone.utc) + ) + + url = reverse( + 'errors', kwargs={ + 'workspace_id': 1 + } + ) + api_client.credentials(HTTP_AUTHORIZATION='Bearer {}'.format(access_token)) + response = api_client.get( + url, + format='json' + ) + + assert response.status_code == 200 + + response = json.loads(response.content) + assert dict_compare_keys(response, data['errors_response']) == [], 'errors api returns a diff in the keys' + + url = '/api/v2/workspaces/1/errors/?is_resolved=False&type=CATEGORY_MAPPING' + response = api_client.get( + url, + format='json' + ) + + assert response.status_code == 200 + + Error.objects.filter( + workspace_id=1, + type='EMPLOYEE_MAPPING', + error_detail = 'Employee mapping is missing', + is_resolved=False + ).update(is_resolved=True) + + url = '/api/v2/workspaces/1/errors/?is_resolved=true&type=EMPLOYEE_MAPPING' + response = api_client.get( + url, + format='json' + ) + + assert response.status_code == 200 From 7c4178235898fe6cc1195947aac8c1eb466386a3 Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Tue, 7 Nov 2023 16:14:48 +0530 Subject: [PATCH 29/36] Export error decorator, creating employee error and resolving. --- apps/mappings/signals.py | 12 +- apps/mappings/tasks.py | 65 ++++- apps/netsuite/exceptions.py | 108 +++++++ apps/netsuite/tasks.py | 563 +++++++++++------------------------- 4 files changed, 349 insertions(+), 399 deletions(-) create mode 100644 apps/netsuite/exceptions.py diff --git a/apps/mappings/signals.py b/apps/mappings/signals.py index 220049f8..9a222587 100644 --- a/apps/mappings/signals.py +++ b/apps/mappings/signals.py @@ -5,7 +5,7 @@ from django.dispatch import receiver from django_q.tasks import async_task -from fyle_accounting_mappings.models import MappingSetting +from fyle_accounting_mappings.models import MappingSetting, EmployeeMapping, Mapping from apps.mappings.tasks import upload_attributes_to_fyle, schedule_cost_centers_creation,\ schedule_fyle_attributes_creation @@ -13,11 +13,21 @@ from apps.netsuite.helpers import schedule_payment_sync from apps.workspaces.models import Configuration from apps.workspaces.tasks import delete_cards_mapping_settings +from apps.tasks.models import Error from .models import GeneralMapping, SubsidiaryMapping from .tasks import schedule_auto_map_ccc_employees +@receiver(post_save, sender=EmployeeMapping) +def resolve_post_employees_mapping_errors(sender, instance: Mapping, **kwargs): + """ + Resolve errors after mapping is created + """ + Error.objects.filter(expense_attribute_id=instance.source_employee_id).update( + is_resolved=True + ) + @receiver(post_save, sender=SubsidiaryMapping) def run_post_subsidiary_mappings(sender, instance: SubsidiaryMapping, **kwargs): diff --git a/apps/mappings/tasks.py b/apps/mappings/tasks.py index c4f9f8dc..9f19486e 100644 --- a/apps/mappings/tasks.py +++ b/apps/mappings/tasks.py @@ -13,13 +13,14 @@ from fyle.platform.exceptions import WrongParamsError, InvalidTokenError from fyle_accounting_mappings.models import Mapping, MappingSetting, ExpenseAttribute, DestinationAttribute,\ - CategoryMapping + CategoryMapping, EmployeeMapping from fyle_accounting_mappings.helpers import EmployeesAutoMappingHelper from fyle_integrations_platform_connector import PlatformConnector from apps.mappings.models import GeneralMapping from apps.netsuite.connector import NetSuiteConnector from apps.workspaces.models import NetSuiteCredentials, FyleCredential, Configuration, Workspace +from apps.tasks.models import Error from .exceptions import handle_exceptions from .constants import FYLE_EXPENSE_SYSTEM_FIELDS, DEFAULT_NETSUITE_IMPORT_TYPES @@ -27,6 +28,63 @@ logger = logging.getLogger(__name__) logger.level = logging.INFO +def get_mapped_attributes_ids(source_attribute_type: str, destination_attribute_type: str, errored_attribute_ids: List[int]): + + mapped_attribute_ids = [] + + if source_attribute_type == "TAX_GROUP": + mapped_attribute_ids: List[int] = Mapping.objects.filter( + source_id__in=errored_attribute_ids + ).values_list('source_id', flat=True) + + elif source_attribute_type == "EMPLOYEE": + params = { + 'source_employee_id__in': errored_attribute_ids, + } + + if destination_attribute_type == "EMPLOYEE": + params['destination_employee_id__isnull'] = False + else: + params['destination_vendor_id__isnull'] = False + mapped_attribute_ids: List[int] = EmployeeMapping.objects.filter( + **params + ).values_list('source_employee_id', flat=True) + + elif source_attribute_type == "CATEGORY": + params = { + 'source_category_id__in': errored_attribute_ids, + } + + if destination_attribute_type == 'EXPENSE_TYPE': + params['destination_expense_head_id__isnull'] = False + else: + params['destination_account_id__isnull'] = False + + mapped_attribute_ids: List[int] = CategoryMapping.objects.filter( + **params + ).values_list('source_category_id', flat=True) + + return mapped_attribute_ids + + +def resolve_expense_attribute_errors( + source_attribute_type: str, workspace_id: int, destination_attribute_type: str = None): + """ + Resolve Expense Attribute Errors + :return: None + """ + errored_attribute_ids: List[int] = Error.objects.filter( + is_resolved=False, + workspace_id=workspace_id, + type='{}_MAPPING'.format(source_attribute_type) + ).values_list('expense_attribute_id', flat=True) + + if errored_attribute_ids: + mapped_attribute_ids = get_mapped_attributes_ids(source_attribute_type, destination_attribute_type, errored_attribute_ids) + + if mapped_attribute_ids: + Error.objects.filter(expense_attribute_id__in=mapped_attribute_ids).update(is_resolved=True) + def remove_duplicates(ns_attributes: List[DestinationAttribute]): unique_attributes = [] @@ -657,6 +715,11 @@ def async_auto_map_employees(workspace_id: int): netsuite_connection.sync_vendors() EmployeesAutoMappingHelper(workspace_id, destination_type, employee_mapping_preference).reimburse_mapping() + resolve_expense_attribute_errors( + source_attribute_type="EMPLOYEE", + workspace_id=workspace_id, + destination_attribute_type=destination_type, + ) def schedule_auto_map_employees(employee_mapping_preference: str, workspace_id: int): diff --git a/apps/netsuite/exceptions.py b/apps/netsuite/exceptions.py new file mode 100644 index 00000000..61a071b4 --- /dev/null +++ b/apps/netsuite/exceptions.py @@ -0,0 +1,108 @@ +import logging +import json +import traceback + +from apps.fyle.models import ExpenseGroup +from apps.tasks.models import TaskLog +from apps.workspaces.models import NetSuiteCredentials + +from netsuitesdk.internal.exceptions import NetSuiteRequestError +from netsuitesdk import NetSuiteRateLimitError, NetSuiteLoginError +from fyle_netsuite_api.exceptions import BulkError + +logger = logging.getLogger(__name__) +logger.level = logging.INFO + +netsuite_error_message = 'NetSuite System Error' + +def __handle_netsuite_connection_error(expense_group: ExpenseGroup, task_log: TaskLog) -> None: + logger.info( + 'NetSuite Credentials not found for workspace_id %s / expense group %s', + expense_group.id, + expense_group.workspace_id + ) + detail = { + 'expense_group_id': expense_group.id, + 'message': 'NetSuite Account not connected' + } + task_log.status = 'FAILED' + task_log.detail = detail + + task_log.save() + + +def __log_error(task_log: TaskLog) -> None: + logger.exception('Something unexpected happened workspace_id: %s %s', task_log.workspace_id, task_log.detail) + + +def handle_netsuite_exceptions(payment=False): + def decorator(func): + def wrapper(*args): + if payment: + entity_object = args[0] + workspace_id = args[1] + object_type = args[2] + task_log, _ = TaskLog.objects.update_or_create( + workspace_id=workspace_id, + task_id='PAYMENT_{}'.format(entity_object['unique_id']), + defaults={ + 'status': 'IN_PROGRESS', + 'type': 'CREATING_VENDOR_PAYMENT' + } + ) + else: + expense_group = args[0] + task_log_id = args[1] + task_log = TaskLog.objects.get(id=task_log_id) + + try: + func(*args) + + except NetSuiteCredentials.DoesNotExist: + __handle_netsuite_connection_error(expense_group, task_log) + + except (NetSuiteRequestError, NetSuiteLoginError) as exception: + all_details = [] + logger.info({'error': exception}) + detail = json.dumps(exception.__dict__) + detail = json.loads(detail) + task_log.status = 'FAILED' + + all_details.append({ + 'expense_group_id': expense_group.id, + 'value': netsuite_error_message, + 'type': detail['code'], + 'message': detail['message'] + }) + task_log.detail = all_details + + task_log.save() + + except BulkError as exception: + logger.info(exception.response) + detail = exception.response + task_log.status = 'FAILED' + task_log.detail = detail + + task_log.save() + + except NetSuiteRateLimitError: + logger.info('Rate limit error, workspace_id - %s', expense_group.workspace_id) + task_log.status = 'FAILED' + task_log.detail = { + 'error': 'Rate limit error' + } + + task_log.save() + + except Exception: + error = traceback.format_exc() + task_log.detail = { + 'error': error + } + task_log.status = 'FATAL' + task_log.save() + __log_error(task_log) + + return wrapper + return decorator diff --git a/apps/netsuite/tasks.py b/apps/netsuite/tasks.py index 4398b6e8..ce53a7b8 100644 --- a/apps/netsuite/tasks.py +++ b/apps/netsuite/tasks.py @@ -9,6 +9,7 @@ from django.db import transaction from django.db.models import Q from django.utils.module_loading import import_string +from apps.netsuite.exceptions import handle_netsuite_exceptions from django_q.models import Schedule from django_q.tasks import Chain, async_task @@ -23,7 +24,7 @@ from apps.fyle.models import ExpenseGroup, Expense, Reimbursement from apps.mappings.models import GeneralMapping, SubsidiaryMapping -from apps.tasks.models import TaskLog +from apps.tasks.models import TaskLog, Error from apps.workspaces.models import NetSuiteCredentials, FyleCredential, Configuration, Workspace from .models import Bill, BillLineitem, ExpenseReport, ExpenseReportLineItem, JournalEntry, JournalEntryLineItem, \ @@ -253,22 +254,7 @@ def create_or_update_employee_mapping(expense_group: ExpenseGroup, netsuite_conn except NetSuiteRequestError as exception: logger.info({'error': exception}) - - -def __handle_netsuite_connection_error(expense_group: ExpenseGroup, task_log: TaskLog) -> None: - logger.info( - 'NetSuite Credentials not found for workspace_id %s / expense group %s', - expense_group.id, - expense_group.workspace_id - ) - detail = { - 'expense_group_id': expense_group.id, - 'message': 'NetSuite Account not connected' - } - task_log.status = 'FAILED' - task_log.detail = detail - - task_log.save() + def construct_payload_and_update_export(expense_id_receipt_url_map: dict, task_log: TaskLog, workspace: Workspace, cluster_domain: str, netsuite_connection: NetSuiteConnector): @@ -401,6 +387,7 @@ def upload_attachments_and_update_export(expenses: List[Expense], task_log: Task ) +@handle_netsuite_exceptions(payment=False) def create_bill(expense_group, task_log_id): task_log = TaskLog.objects.get(id=task_log_id) @@ -413,94 +400,48 @@ def create_bill(expense_group, task_log_id): configuration: Configuration = Configuration.objects.get(workspace_id=expense_group.workspace_id) general_mappings: GeneralMapping = GeneralMapping.objects.filter(workspace_id=expense_group.workspace_id).first() - try: - fyle_credentials = FyleCredential.objects.get(workspace_id=expense_group.workspace_id) - netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=expense_group.workspace_id) - - netsuite_connection = NetSuiteConnector(netsuite_credentials, expense_group.workspace_id) - - if expense_group.fund_source == 'PERSONAL' and configuration.auto_map_employees \ - and configuration.auto_create_destination_entity: - create_or_update_employee_mapping( - expense_group, netsuite_connection, configuration.auto_map_employees, - configuration.employee_field_mapping) - - if general_mappings and general_mappings.use_employee_department and expense_group.fund_source == 'CCC' \ - and configuration.auto_map_employees and configuration.auto_create_destination_entity: - create_or_update_employee_mapping( - expense_group, netsuite_connection, configuration.auto_map_employees, - configuration.employee_field_mapping) - - __validate_expense_group(expense_group, configuration) - with transaction.atomic(): - bill_object = Bill.create_bill(expense_group) - - bill_lineitems_objects = BillLineitem.create_bill_lineitems(expense_group, configuration) - - created_bill = netsuite_connection.post_bill(bill_object, bill_lineitems_objects) - - task_log.detail = created_bill - task_log.bill = bill_object - task_log.status = 'COMPLETE' - - task_log.save() + fyle_credentials = FyleCredential.objects.get(workspace_id=expense_group.workspace_id) + netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=expense_group.workspace_id) - expense_group.exported_at = datetime.now() - expense_group.response_logs = created_bill - expense_group.save() + netsuite_connection = NetSuiteConnector(netsuite_credentials, expense_group.workspace_id) - async_task( - 'apps.netsuite.tasks.upload_attachments_and_update_export', - expense_group.expenses.all(), task_log, fyle_credentials, expense_group.workspace_id - ) + if expense_group.fund_source == 'PERSONAL' and configuration.auto_map_employees \ + and configuration.auto_create_destination_entity: + create_or_update_employee_mapping( + expense_group, netsuite_connection, configuration.auto_map_employees, + configuration.employee_field_mapping) - except NetSuiteCredentials.DoesNotExist: - __handle_netsuite_connection_error(expense_group, task_log) + if general_mappings and general_mappings.use_employee_department and expense_group.fund_source == 'CCC' \ + and configuration.auto_map_employees and configuration.auto_create_destination_entity: + create_or_update_employee_mapping( + expense_group, netsuite_connection, configuration.auto_map_employees, + configuration.employee_field_mapping) - except (NetSuiteRequestError, NetSuiteLoginError) as exception: - all_details = [] - logger.info({'error': exception}) - detail = json.dumps(exception.__dict__) - detail = json.loads(detail) - task_log.status = 'FAILED' + __validate_expense_group(expense_group, configuration) + with transaction.atomic(): + bill_object = Bill.create_bill(expense_group) - all_details.append({ - 'expense_group_id': expense_group.id, - 'value': netsuite_error_message, - 'type': detail['code'], - 'message': detail['message'] - }) - task_log.detail = all_details + bill_lineitems_objects = BillLineitem.create_bill_lineitems(expense_group, configuration) - task_log.save() + created_bill = netsuite_connection.post_bill(bill_object, bill_lineitems_objects) - except BulkError as exception: - logger.info(exception.response) - detail = exception.response - task_log.status = 'FAILED' - task_log.detail = detail + task_log.detail = created_bill + task_log.bill = bill_object + task_log.status = 'COMPLETE' task_log.save() - except NetSuiteRateLimitError: - logger.info('Rate limit error, workspace_id - %s', expense_group.workspace_id) - task_log.status = 'FAILED' - task_log.detail = { - 'error': 'Rate limit error' - } - - task_log.save() + expense_group.exported_at = datetime.now() + expense_group.response_logs = created_bill + expense_group.save() - except Exception: - error = traceback.format_exc() - task_log.detail = { - 'error': error - } - task_log.status = 'FATAL' - task_log.save() - __log_error(task_log) + async_task( + 'apps.netsuite.tasks.upload_attachments_and_update_export', + expense_group.expenses.all(), task_log, fyle_credentials, expense_group.workspace_id + ) +@handle_netsuite_exceptions(payment=False) def create_credit_card_charge(expense_group, task_log_id): task_log = TaskLog.objects.get(id=task_log_id) @@ -513,106 +454,60 @@ def create_credit_card_charge(expense_group, task_log_id): configuration = Configuration.objects.get(workspace_id=expense_group.workspace_id) general_mappings: GeneralMapping = GeneralMapping.objects.filter(workspace_id=expense_group.workspace_id).first() - try: - netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=expense_group.workspace_id) - - netsuite_connection = NetSuiteConnector(netsuite_credentials, expense_group.workspace_id) - - if general_mappings and general_mappings.use_employee_department and expense_group.fund_source == 'CCC' \ - and configuration.auto_map_employees and configuration.auto_create_destination_entity: - create_or_update_employee_mapping( - expense_group, netsuite_connection, configuration.auto_map_employees, - configuration.employee_field_mapping) - - merchant = expense_group.expenses.first().vendor - auto_create_merchants = configuration.auto_create_merchants - get_or_create_credit_card_vendor(expense_group, merchant, auto_create_merchants) - - __validate_expense_group(expense_group, configuration) - with transaction.atomic(): - credit_card_charge_object = CreditCardCharge.create_credit_card_charge(expense_group) - - credit_card_charge_lineitems_object = CreditCardChargeLineItem.create_credit_card_charge_lineitem( - expense_group, configuration - ) - attachment_links = {} - - expense = expense_group.expenses.first() - refund = False - if expense.amount < 0: - refund = True + netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=expense_group.workspace_id) - attachment_link = load_attachments(netsuite_connection, expense, expense_group) + netsuite_connection = NetSuiteConnector(netsuite_credentials, expense_group.workspace_id) - if attachment_link: - attachment_links[expense.expense_id] = attachment_link + if general_mappings and general_mappings.use_employee_department and expense_group.fund_source == 'CCC' \ + and configuration.auto_map_employees and configuration.auto_create_destination_entity: + create_or_update_employee_mapping( + expense_group, netsuite_connection, configuration.auto_map_employees, + configuration.employee_field_mapping) - created_credit_card_charge = netsuite_connection.post_credit_card_charge( - credit_card_charge_object, credit_card_charge_lineitems_object, attachment_links, refund - ) + merchant = expense_group.expenses.first().vendor + auto_create_merchants = configuration.auto_create_merchants + get_or_create_credit_card_vendor(expense_group, merchant, auto_create_merchants) - if refund: - created_credit_card_charge['type'] = 'chargeCardRefund' - else: - created_credit_card_charge['type'] = 'chargeCard' + __validate_expense_group(expense_group, configuration) + with transaction.atomic(): + credit_card_charge_object = CreditCardCharge.create_credit_card_charge(expense_group) - task_log.detail = created_credit_card_charge - task_log.credit_card_charge = credit_card_charge_object - task_log.status = 'COMPLETE' - - task_log.save() + credit_card_charge_lineitems_object = CreditCardChargeLineItem.create_credit_card_charge_lineitem( + expense_group, configuration + ) + attachment_links = {} - expense_group.exported_at = datetime.now() - expense_group.response_logs = created_credit_card_charge - expense_group.save() + expense = expense_group.expenses.first() + refund = False + if expense.amount < 0: + refund = True - except NetSuiteCredentials.DoesNotExist: - __handle_netsuite_connection_error(expense_group, task_log) + attachment_link = load_attachments(netsuite_connection, expense, expense_group) - except (NetSuiteRequestError, NetSuiteLoginError) as exception: - all_details = [] - logger.info({'error': exception}) - detail = json.dumps(exception.__dict__) - detail = json.loads(detail) - task_log.status = 'FAILED' + if attachment_link: + attachment_links[expense.expense_id] = attachment_link - all_details.append({ - 'expense_group_id': expense_group.id, - 'value': netsuite_error_message, - 'type': detail['code'], - 'message': detail['message'] - }) - task_log.detail = all_details + created_credit_card_charge = netsuite_connection.post_credit_card_charge( + credit_card_charge_object, credit_card_charge_lineitems_object, attachment_links, refund + ) - task_log.save() + if refund: + created_credit_card_charge['type'] = 'chargeCardRefund' + else: + created_credit_card_charge['type'] = 'chargeCard' - except BulkError as exception: - logger.info(exception.response) - detail = exception.response - task_log.status = 'FAILED' - task_log.detail = detail + task_log.detail = created_credit_card_charge + task_log.credit_card_charge = credit_card_charge_object + task_log.status = 'COMPLETE' task_log.save() - except NetSuiteRateLimitError: - logger.info('Rate limit error, workspace_id - %s', expense_group.workspace_id) - task_log.status = 'FAILED' - task_log.detail = { - 'error': 'Rate limit error' - } - - task_log.save() - - except Exception: - error = traceback.format_exc() - task_log.detail = { - 'error': error - } - task_log.status = 'FATAL' - task_log.save() - __log_error(task_log) + expense_group.exported_at = datetime.now() + expense_group.response_logs = created_credit_card_charge + expense_group.save() +@handle_netsuite_exceptions(payment=False) def create_expense_report(expense_group, task_log_id): task_log = TaskLog.objects.get(id=task_log_id) @@ -624,90 +519,44 @@ def create_expense_report(expense_group, task_log_id): configuration = Configuration.objects.get(workspace_id=expense_group.workspace_id) - try: - fyle_credentials = FyleCredential.objects.get(workspace_id=expense_group.workspace_id) - netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=expense_group.workspace_id) - netsuite_connection = NetSuiteConnector(netsuite_credentials, expense_group.workspace_id) - - if configuration.auto_map_employees and configuration.auto_create_destination_entity: - create_or_update_employee_mapping( - expense_group, netsuite_connection, configuration.auto_map_employees, - configuration.employee_field_mapping) - - __validate_expense_group(expense_group, configuration) - with transaction.atomic(): - expense_report_object = ExpenseReport.create_expense_report(expense_group) - - expense_report_lineitems_objects = ExpenseReportLineItem.create_expense_report_lineitems( - expense_group, configuration - ) - - created_expense_report = netsuite_connection.post_expense_report( - expense_report_object, expense_report_lineitems_objects - ) - - task_log.detail = created_expense_report - task_log.expense_report = expense_report_object - task_log.status = 'COMPLETE' - - task_log.save() - - expense_group.exported_at = datetime.now() - expense_group.response_logs = created_expense_report - expense_group.save() - - async_task( - 'apps.netsuite.tasks.upload_attachments_and_update_export', - expense_group.expenses.all(), task_log, fyle_credentials, 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) + netsuite_connection = NetSuiteConnector(netsuite_credentials, expense_group.workspace_id) - except NetSuiteCredentials.DoesNotExist: - __handle_netsuite_connection_error(expense_group, task_log) + if configuration.auto_map_employees and configuration.auto_create_destination_entity: + create_or_update_employee_mapping( + expense_group, netsuite_connection, configuration.auto_map_employees, + configuration.employee_field_mapping) - except (NetSuiteRequestError, NetSuiteLoginError) as exception: - all_details = [] - logger.info({'error': exception}) - detail = json.dumps(exception.__dict__) - detail = json.loads(detail) - task_log.status = 'FAILED' + __validate_expense_group(expense_group, configuration) + with transaction.atomic(): + expense_report_object = ExpenseReport.create_expense_report(expense_group) - all_details.append({ - 'expense_group_id': expense_group.id, - 'value': netsuite_error_message, - 'type': detail['code'], - 'message': detail['message'] - }) - task_log.detail = all_details + expense_report_lineitems_objects = ExpenseReportLineItem.create_expense_report_lineitems( + expense_group, configuration + ) - task_log.save() + created_expense_report = netsuite_connection.post_expense_report( + expense_report_object, expense_report_lineitems_objects + ) - except BulkError as exception: - logger.info(exception.response) - detail = exception.response - task_log.status = 'FAILED' - task_log.detail = detail + task_log.detail = created_expense_report + task_log.expense_report = expense_report_object + task_log.status = 'COMPLETE' task_log.save() - except NetSuiteRateLimitError: - logger.info('Rate limit error, workspace_id - %s', expense_group.workspace_id) - task_log.status = 'FAILED' - task_log.detail = { - 'error': 'Rate limit error' - } - - task_log.save() + expense_group.exported_at = datetime.now() + expense_group.response_logs = created_expense_report + expense_group.save() - except Exception: - error = traceback.format_exc() - task_log.detail = { - 'error': error - } - task_log.status = 'FATAL' - task_log.save() - __log_error(task_log) + async_task( + 'apps.netsuite.tasks.upload_attachments_and_update_export', + expense_group.expenses.all(), task_log, fyle_credentials, expense_group.workspace_id + ) +@handle_netsuite_exceptions(payment=False) def create_journal_entry(expense_group, task_log_id): task_log = TaskLog.objects.get(id=task_log_id) @@ -719,88 +568,42 @@ def create_journal_entry(expense_group, task_log_id): configuration = Configuration.objects.get(workspace_id=expense_group.workspace_id) - try: - fyle_credentials = FyleCredential.objects.get(workspace_id=expense_group.workspace_id) - netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=expense_group.workspace_id) - - netsuite_connection = NetSuiteConnector(netsuite_credentials, expense_group.workspace_id) - - if configuration.auto_map_employees and configuration.auto_create_destination_entity: - create_or_update_employee_mapping( - expense_group, netsuite_connection, configuration.auto_map_employees, - configuration.employee_field_mapping) - __validate_expense_group(expense_group, configuration) - with transaction.atomic(): - journal_entry_object = JournalEntry.create_journal_entry(expense_group) - - journal_entry_lineitems_objects = JournalEntryLineItem.create_journal_entry_lineitems( - expense_group, configuration - ) - - created_journal_entry = netsuite_connection.post_journal_entry( - journal_entry_object, journal_entry_lineitems_objects - ) - - task_log.detail = created_journal_entry - task_log.journal_entry = journal_entry_object - task_log.status = 'COMPLETE' - - task_log.save() - expense_group.exported_at = datetime.now() - expense_group.response_logs = created_journal_entry - expense_group.save() - - async_task( - 'apps.netsuite.tasks.upload_attachments_and_update_export', - expense_group.expenses.all(), task_log, fyle_credentials, 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) - except NetSuiteCredentials.DoesNotExist: - __handle_netsuite_connection_error(expense_group, task_log) + netsuite_connection = NetSuiteConnector(netsuite_credentials, expense_group.workspace_id) - except (NetSuiteRequestError, NetSuiteLoginError) as exception: - all_details = [] - logger.info({'error': exception}) - detail = json.dumps(exception.__dict__) - detail = json.loads(detail) - task_log.status = 'FAILED' + if configuration.auto_map_employees and configuration.auto_create_destination_entity: + create_or_update_employee_mapping( + expense_group, netsuite_connection, configuration.auto_map_employees, + configuration.employee_field_mapping) + __validate_expense_group(expense_group, configuration) + with transaction.atomic(): + journal_entry_object = JournalEntry.create_journal_entry(expense_group) - all_details.append({ - 'expense_group_id': expense_group.id, - 'value': netsuite_error_message, - 'type': detail['code'], - 'message': detail['message'] - }) - task_log.detail = all_details + journal_entry_lineitems_objects = JournalEntryLineItem.create_journal_entry_lineitems( + expense_group, configuration + ) - task_log.save() + created_journal_entry = netsuite_connection.post_journal_entry( + journal_entry_object, journal_entry_lineitems_objects + ) - except BulkError as exception: - logger.info(exception.response) - detail = exception.response - task_log.status = 'FAILED' - task_log.detail = detail + task_log.detail = created_journal_entry + task_log.journal_entry = journal_entry_object + task_log.status = 'COMPLETE' task_log.save() - except NetSuiteRateLimitError: - logger.info('Rate limit error, workspace_id - %s', expense_group.workspace_id) - task_log.status = 'FAILED' - task_log.detail = { - 'error': 'Rate limit error' - } + expense_group.exported_at = datetime.now() + expense_group.response_logs = created_journal_entry + expense_group.save() - task_log.save() - - except Exception: - error = traceback.format_exc() - task_log.detail = { - 'error': error - } - task_log.status = 'FATAL' - task_log.save() - __log_error(task_log) + async_task( + 'apps.netsuite.tasks.upload_attachments_and_update_export', + expense_group.expenses.all(), task_log, fyle_credentials, expense_group.workspace_id + ) def __validate_general_mapping(expense_group: ExpenseGroup, configuration: Configuration) -> List[BulkError]: @@ -1014,6 +817,18 @@ def __validate_employee_mapping(expense_group: ExpenseGroup, configuration: Conf 'message': 'Employee mapping not found' }) + if employee: + Error.objects.update_or_create( + workspace_id=expense_group.workspace_id, + expense_attribute=employee, + defaults={ + 'type': 'EMPLOYEE_MAPPING', + 'error_title': employee.value, + 'error_detail': 'Employee mapping is missing', + 'is_resolved': False + } + ) + return bulk_errors @@ -1323,7 +1138,7 @@ def create_netsuite_payment_objects(netsuite_objects, object_type, workspace_id) return netsuite_payment_objects - +@handle_netsuite_exceptions(payment=True) def process_vendor_payment(entity_object, workspace_id, object_type): task_log, _ = TaskLog.objects.update_or_create( workspace_id=workspace_id, @@ -1334,91 +1149,45 @@ def process_vendor_payment(entity_object, workspace_id, object_type): } ) - try: - with transaction.atomic(): - - vendor_payment_object = VendorPayment.create_vendor_payment( - workspace_id, entity_object - ) - - vendor_payment_lineitems = VendorPaymentLineitem.create_vendor_payment_lineitems( - entity_object['line'], vendor_payment_object - ) - - netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=workspace_id) - netsuite_connection = NetSuiteConnector(netsuite_credentials, workspace_id) - - first_object_id = vendor_payment_lineitems[0].doc_id - if object_type == 'BILL': - first_object = netsuite_connection.get_bill(first_object_id) - else: - first_object = netsuite_connection.get_expense_report(first_object_id) - created_vendor_payment = netsuite_connection.post_vendor_payment( - vendor_payment_object, vendor_payment_lineitems, first_object - ) - - lines = entity_object['line'] - expense_group_ids = [line['expense_group'].id for line in lines] - - if object_type == 'BILL': - paid_objects = Bill.objects.filter(expense_group_id__in=expense_group_ids).all() + with transaction.atomic(): - else: - paid_objects = ExpenseReport.objects.filter(expense_group_id__in=expense_group_ids).all() + vendor_payment_object = VendorPayment.create_vendor_payment( + workspace_id, entity_object + ) - for paid_object in paid_objects: - paid_object.payment_synced = True - paid_object.paid_on_netsuite = True - paid_object.save() + vendor_payment_lineitems = VendorPaymentLineitem.create_vendor_payment_lineitems( + entity_object['line'], vendor_payment_object + ) - task_log.detail = created_vendor_payment - task_log.vendor_payment = vendor_payment_object - task_log.status = 'COMPLETE' + netsuite_credentials = NetSuiteCredentials.objects.get(workspace_id=workspace_id) + netsuite_connection = NetSuiteConnector(netsuite_credentials, workspace_id) - task_log.save() - except NetSuiteCredentials.DoesNotExist: - logger.info( - 'NetSuite Credentials not found for workspace_id %s', - workspace_id + first_object_id = vendor_payment_lineitems[0].doc_id + if object_type == 'BILL': + first_object = netsuite_connection.get_bill(first_object_id) + else: + first_object = netsuite_connection.get_expense_report(first_object_id) + created_vendor_payment = netsuite_connection.post_vendor_payment( + vendor_payment_object, vendor_payment_lineitems, first_object ) - detail = { - 'message': 'NetSuite Account not connected' - } - task_log.status = 'FAILED' - task_log.detail = detail - task_log.save() + lines = entity_object['line'] + expense_group_ids = [line['expense_group'].id for line in lines] - except (NetSuiteRequestError, NetSuiteLoginError) as exception: - all_details = [] - logger.info({'error': exception}) - detail = json.dumps(exception.__dict__) - detail = json.loads(detail) - task_log.status = 'FAILED' - - all_details.append({ - 'value': netsuite_error_message, - 'type': detail['code'], - 'message': detail['message'] - }) - task_log.detail = all_details + if object_type == 'BILL': + paid_objects = Bill.objects.filter(expense_group_id__in=expense_group_ids).all() - task_log.save() - - except NetSuiteRateLimitError: - logger.info('Rate limit error, workspace_id - %s', workspace_id) - task_log.status = 'FAILED' - task_log.detail = { - 'error': 'Rate limit error' - } + else: + paid_objects = ExpenseReport.objects.filter(expense_group_id__in=expense_group_ids).all() - task_log.save() + for paid_object in paid_objects: + paid_object.payment_synced = True + paid_object.paid_on_netsuite = True + paid_object.save() - except BulkError as exception: - logger.info(exception.response) - detail = exception.response - task_log.status = 'FAILED' - task_log.detail = detail + task_log.detail = created_vendor_payment + task_log.vendor_payment = vendor_payment_object + task_log.status = 'COMPLETE' task_log.save() From a06936f7ca98d4b153d6daa2969f8342cd5be478 Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Tue, 7 Nov 2023 20:34:45 +0530 Subject: [PATCH 30/36] exception error removed --- apps/mappings/tasks.py | 2 ++ apps/netsuite/exceptions.py | 20 +++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/apps/mappings/tasks.py b/apps/mappings/tasks.py index 9f19486e..a54b83de 100644 --- a/apps/mappings/tasks.py +++ b/apps/mappings/tasks.py @@ -79,6 +79,8 @@ def resolve_expense_attribute_errors( type='{}_MAPPING'.format(source_attribute_type) ).values_list('expense_attribute_id', flat=True) + print('errored_attribute_ids', errored_attribute_ids) + if errored_attribute_ids: mapped_attribute_ids = get_mapped_attributes_ids(source_attribute_type, destination_attribute_type, errored_attribute_ids) diff --git a/apps/netsuite/exceptions.py b/apps/netsuite/exceptions.py index 61a071b4..97e25007 100644 --- a/apps/netsuite/exceptions.py +++ b/apps/netsuite/exceptions.py @@ -59,7 +59,20 @@ def wrapper(*args): func(*args) except NetSuiteCredentials.DoesNotExist: - __handle_netsuite_connection_error(expense_group, task_log) + if payment: + logger.info( + 'NetSuite Credentials not found for workspace_id %s', + workspace_id + ) + detail = { + 'message': 'NetSuite Account not connected' + } + task_log.status = 'FAILED' + task_log.detail = detail + + task_log.save() + else: + __handle_netsuite_connection_error(expense_group, task_log) except (NetSuiteRequestError, NetSuiteLoginError) as exception: all_details = [] @@ -69,11 +82,12 @@ def wrapper(*args): task_log.status = 'FAILED' all_details.append({ - 'expense_group_id': expense_group.id, 'value': netsuite_error_message, 'type': detail['code'], 'message': detail['message'] }) + if not payment: + all_details[0]['expense_group_id'] = expense_group.id task_log.detail = all_details task_log.save() @@ -87,7 +101,7 @@ def wrapper(*args): task_log.save() except NetSuiteRateLimitError: - logger.info('Rate limit error, workspace_id - %s', expense_group.workspace_id) + logger.info('Rate limit error, workspace_id - %s', workspace_id if payment else expense_group.workspace_id) task_log.status = 'FAILED' task_log.detail = { 'error': 'Rate limit error' From 0a4aee3350817486c52893cf425d1501709a6809 Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Tue, 7 Nov 2023 20:35:37 +0530 Subject: [PATCH 31/36] import error resolved --- tests/test_netsuite/test_tasks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_netsuite/test_tasks.py b/tests/test_netsuite/test_tasks.py index 0e93b189..7461a87f 100644 --- a/tests/test_netsuite/test_tasks.py +++ b/tests/test_netsuite/test_tasks.py @@ -15,8 +15,9 @@ from apps.workspaces.models import Configuration, NetSuiteCredentials, FyleCredential from apps.tasks.models import TaskLog from apps.netsuite.tasks import __validate_general_mapping, __validate_subsidiary_mapping, check_netsuite_object_status, create_credit_card_charge, create_journal_entry, create_or_update_employee_mapping, create_vendor_payment, get_all_internal_ids, \ - get_or_create_credit_card_vendor, create_bill, create_expense_report, load_attachments, __handle_netsuite_connection_error, process_reimbursements, process_vendor_payment, schedule_bills_creation, schedule_credit_card_charge_creation, schedule_expense_reports_creation, schedule_journal_entry_creation, schedule_netsuite_objects_status_sync, schedule_reimbursements_sync, schedule_vendor_payment_creation, \ + get_or_create_credit_card_vendor, create_bill, create_expense_report, load_attachments, process_reimbursements, process_vendor_payment, schedule_bills_creation, schedule_credit_card_charge_creation, schedule_expense_reports_creation, schedule_journal_entry_creation, schedule_netsuite_objects_status_sync, schedule_reimbursements_sync, schedule_vendor_payment_creation, \ __validate_tax_group_mapping, check_expenses_reimbursement_status, __validate_expense_group, upload_attachments_and_update_export +from apps.netsuite.exceptions import __handle_netsuite_connection_error from apps.mappings.models import GeneralMapping, SubsidiaryMapping from fyle_accounting_mappings.models import DestinationAttribute, EmployeeMapping, CategoryMapping, ExpenseAttribute, Mapping from .fixtures import data From 469efda078e73557fb7179d635b745afeb5315be Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Wed, 8 Nov 2023 14:44:55 +0530 Subject: [PATCH 32/36] employee mapping error test added --- apps/mappings/tasks.py | 2 -- tests/test_mappings/test_signals.py | 32 +++++++++++++++++++++- tests/test_mappings/test_tasks.py | 41 +++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/apps/mappings/tasks.py b/apps/mappings/tasks.py index a54b83de..9f19486e 100644 --- a/apps/mappings/tasks.py +++ b/apps/mappings/tasks.py @@ -79,8 +79,6 @@ def resolve_expense_attribute_errors( type='{}_MAPPING'.format(source_attribute_type) ).values_list('expense_attribute_id', flat=True) - print('errored_attribute_ids', errored_attribute_ids) - if errored_attribute_ids: mapped_attribute_ids = get_mapped_attributes_ids(source_attribute_type, destination_attribute_type, errored_attribute_ids) diff --git a/tests/test_mappings/test_signals.py b/tests/test_mappings/test_signals.py index 1f33c019..a54a5e38 100644 --- a/tests/test_mappings/test_signals.py +++ b/tests/test_mappings/test_signals.py @@ -2,7 +2,37 @@ from django_q.models import Schedule from apps.workspaces.models import Configuration, Workspace from apps.mappings.models import GeneralMapping -from fyle_accounting_mappings.models import MappingSetting, ExpenseAttribute +from apps.tasks.models import Error +from fyle_accounting_mappings.models import MappingSetting, ExpenseAttribute, EmployeeMapping + + +@pytest.mark.django_db() +def test_resolve_post_employees_mapping_errors(access_token): + source_employee = ExpenseAttribute.objects.filter( + value='approver1@fyleforgotham.in', + workspace_id=1, + attribute_type='EMPLOYEE' + ).first() + + Error.objects.update_or_create( + workspace_id=1, + expense_attribute=source_employee, + defaults={ + 'type': 'EMPLOYEE_MAPPING', + 'error_title': source_employee.value, + 'error_detail': 'Employee mapping is missing', + 'is_resolved': False + } + ) + employee_mapping, _ = EmployeeMapping.objects.update_or_create( + source_employee_id=1, + destination_employee_id=719, + workspace_id=1 + ) + + error = Error.objects.filter(expense_attribute_id=employee_mapping.source_employee_id).first() + + assert error.is_resolved == True @pytest.mark.django_db() def test_run_post_mapping_settings_triggers(access_token): diff --git a/tests/test_mappings/test_tasks.py b/tests/test_mappings/test_tasks.py index 1e6a27c1..77a743b2 100644 --- a/tests/test_mappings/test_tasks.py +++ b/tests/test_mappings/test_tasks.py @@ -1,4 +1,5 @@ import logging +from apps.fyle.models import ExpenseGroup import pytest from unittest import mock from django_q.models import Schedule @@ -17,6 +18,46 @@ logger = logging.getLogger(__name__) logger.level = logging.INFO + +def test_resolve_expense_attribute_errors(db): + workspace_id = 1 + expense_group = ExpenseGroup.objects.get(id=1) + + employee_attribute = ExpenseAttribute.objects.filter( + value=expense_group.description.get('employee_email'), + workspace_id=expense_group.workspace_id, + attribute_type='EMPLOYEE' + ).first() + + error, _ = Error.objects.update_or_create( + workspace_id=expense_group.workspace_id, + expense_attribute=employee_attribute, + defaults={ + 'type': 'EMPLOYEE_MAPPING', + 'error_title': employee_attribute.value, + 'error_detail': 'Employee mapping is missing', + 'is_resolved': False + } + ) + + resolve_expense_attribute_errors('EMPLOYEE', workspace_id, 'EMPLOYEE') + assert Error.objects.get(id=error.id).is_resolved == True + + error, _ = Error.objects.update_or_create( + workspace_id=expense_group.workspace_id, + expense_attribute=employee_attribute, + defaults={ + 'type': 'EMPLOYEE_MAPPING', + 'error_title': employee_attribute.value, + 'error_detail': 'Employee mapping is missing', + 'is_resolved': False + } + ) + + resolve_expense_attribute_errors('EMPLOYEE', workspace_id, 'VENDOR') + assert Error.objects.get(id=error.id).is_resolved == True + + def test_disable_category_for_items_mapping(db ,mocker): workspace_id = 49 configuration = Configuration.objects.filter(workspace_id=workspace_id).first() From dc81ada4a2f927fef7f6e273001013144e70309e Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Tue, 21 Nov 2023 17:10:11 +0530 Subject: [PATCH 33/36] made changes to the decorator for export functions --- apps/netsuite/exceptions.py | 30 +++++++++++++++++++++++++++++- tests/test_mappings/test_tasks.py | 4 ++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/apps/netsuite/exceptions.py b/apps/netsuite/exceptions.py index 97e25007..d812ad91 100644 --- a/apps/netsuite/exceptions.py +++ b/apps/netsuite/exceptions.py @@ -4,12 +4,14 @@ from apps.fyle.models import ExpenseGroup from apps.tasks.models import TaskLog -from apps.workspaces.models import NetSuiteCredentials +from apps.workspaces.models import LastExportDetail, NetSuiteCredentials from netsuitesdk.internal.exceptions import NetSuiteRequestError from netsuitesdk import NetSuiteRateLimitError, NetSuiteLoginError from fyle_netsuite_api.exceptions import BulkError +from django.db.models import Q + logger = logging.getLogger(__name__) logger.level = logging.INFO @@ -35,6 +37,28 @@ def __log_error(task_log: TaskLog) -> None: logger.exception('Something unexpected happened workspace_id: %s %s', task_log.workspace_id, task_log.detail) +def update_last_export_details(workspace_id): + last_export_detail = LastExportDetail.objects.get(workspace_id=workspace_id) + + failed_exports = TaskLog.objects.filter( + ~Q(type__in=['CREATING_VENDOR_PAYMENT','FETCHING_EXPENSES']), workspace_id=workspace_id, status__in=['FAILED', 'FATAL'] + ).count() + + successful_exports = TaskLog.objects.filter( + ~Q(type__in=['CREATING_VENDOR_PAYMENT', 'FETCHING_EXPENSES']), + workspace_id=workspace_id, + status='COMPLETE', + updated_at__gt=last_export_detail.last_exported_at + ).count() + + last_export_detail.failed_expense_groups_count = failed_exports + last_export_detail.successful_expense_groups_count = successful_exports + last_export_detail.total_expense_groups_count = failed_exports + successful_exports + last_export_detail.save() + + return last_export_detail + + def handle_netsuite_exceptions(payment=False): def decorator(func): def wrapper(*args): @@ -54,6 +78,7 @@ def wrapper(*args): expense_group = args[0] task_log_id = args[1] task_log = TaskLog.objects.get(id=task_log_id) + last_export = args[2] try: func(*args) @@ -117,6 +142,9 @@ def wrapper(*args): task_log.status = 'FATAL' task_log.save() __log_error(task_log) + + if not payment and last_export is True: + update_last_export_details(expense_group.workspace_id) return wrapper return decorator diff --git a/tests/test_mappings/test_tasks.py b/tests/test_mappings/test_tasks.py index 76c49672..a3c9ea8d 100644 --- a/tests/test_mappings/test_tasks.py +++ b/tests/test_mappings/test_tasks.py @@ -332,7 +332,7 @@ def test_upload_categories_to_fyle(mocker, db): assert expense_category_count == 36 - assert len(netsuite_attributes) == 137 + assert len(netsuite_attributes) == 36 count_of_accounts = DestinationAttribute.objects.filter( attribute_type='ACCOUNT', workspace_id=49).count() @@ -346,7 +346,7 @@ def test_upload_categories_to_fyle(mocker, db): netsuite_attributes = upload_categories_to_fyle(49, configuration, platform) - assert len(netsuite_attributes) == 36 + assert len(netsuite_attributes) == 137 def test_filter_unmapped_destinations(db, mocker): From 24fabfa221ffdfcedd2c82c93fd10e548c6ea538 Mon Sep 17 00:00:00 2001 From: Ashutosh singh <55102089+Ashutosh619-sudo@users.noreply.github.com> Date: Mon, 27 Nov 2023 13:11:20 +0530 Subject: [PATCH 34/36] Settings category mapping error and resolving (#446) * category mapping error settings and resolution * test added * Setting tax mapping and resolve (#447) * Setting tax mapping and resolve * resolved comments * Setting and Resolving Netsuite Error (#448) * Setting Netsuite Error * resolving netsuite error * setting netsuite error resolved * comment resolved --------- Co-authored-by: Ashutosh619-sudo --------- Co-authored-by: Ashutosh619-sudo --------- Co-authored-by: Ashutosh619-sudo --- apps/mappings/signals.py | 20 +++++++++- apps/mappings/tasks.py | 9 +++++ apps/netsuite/exceptions.py | 34 +++++++++++++++- apps/netsuite/tasks.py | 48 +++++++++++++++++++++++ tests/test_mappings/test_signals.py | 61 ++++++++++++++++++++++++++++- tests/test_mappings/test_tasks.py | 20 ++++++++++ 6 files changed, 189 insertions(+), 3 deletions(-) diff --git a/apps/mappings/signals.py b/apps/mappings/signals.py index 9a222587..c6fd04f5 100644 --- a/apps/mappings/signals.py +++ b/apps/mappings/signals.py @@ -5,7 +5,7 @@ from django.dispatch import receiver from django_q.tasks import async_task -from fyle_accounting_mappings.models import MappingSetting, EmployeeMapping, Mapping +from fyle_accounting_mappings.models import MappingSetting, EmployeeMapping, Mapping, CategoryMapping from apps.mappings.tasks import upload_attributes_to_fyle, schedule_cost_centers_creation,\ schedule_fyle_attributes_creation @@ -18,6 +18,24 @@ from .models import GeneralMapping, SubsidiaryMapping from .tasks import schedule_auto_map_ccc_employees +@receiver(post_save, sender=Mapping) +def resolve_post_mapping_errors(sender, instance: Mapping, **kwargs): + """ + Resolve errors after mapping is created + """ + if instance.source_type == 'TAX_GROUP': + Error.objects.filter(expense_attribute_id=instance.source_id).update( + is_resolved=True + ) + +@receiver(post_save, sender=CategoryMapping) +def resolve_post_category_mapping_errors(sender, instance: Mapping, **kwargs): + """ + Resolve errors after mapping is created + """ + Error.objects.filter(expense_attribute_id=instance.source_category_id).update( + is_resolved=True + ) @receiver(post_save, sender=EmployeeMapping) def resolve_post_employees_mapping_errors(sender, instance: Mapping, **kwargs): diff --git a/apps/mappings/tasks.py b/apps/mappings/tasks.py index 74e67eca..7f6964c9 100644 --- a/apps/mappings/tasks.py +++ b/apps/mappings/tasks.py @@ -525,6 +525,9 @@ def post_tax_groups(platform_connection: PlatformConnector, workspace_id: int): platform_connection.tax_groups.sync() Mapping.bulk_create_mappings(netsuite_attributes, 'TAX_GROUP', 'TAX_ITEM', workspace_id) + resolve_expense_attribute_errors( + source_attribute_type='TAX_GROUP', workspace_id=workspace_id + ) @handle_exceptions(task_name='Import Category to Fyle and Auto Create Mappings') def auto_create_category_mappings(workspace_id): @@ -563,6 +566,12 @@ def auto_create_category_mappings(workspace_id): if reimbursable_expenses_object == 'EXPENSE REPORT' and \ corporate_credit_card_expenses_object in ('BILL', 'JOURNAL ENTRY', 'CREDIT CARD CHARGE'): bulk_create_ccc_category_mappings(workspace_id) + + resolve_expense_attribute_errors( + source_attribute_type="CATEGORY", + destination_attribute_type=reimbursable_destination_type, + workspace_id=workspace_id + ) def auto_import_and_map_fyle_fields(workspace_id): diff --git a/apps/netsuite/exceptions.py b/apps/netsuite/exceptions.py index d812ad91..6b1aacbf 100644 --- a/apps/netsuite/exceptions.py +++ b/apps/netsuite/exceptions.py @@ -3,7 +3,7 @@ import traceback from apps.fyle.models import ExpenseGroup -from apps.tasks.models import TaskLog +from apps.tasks.models import TaskLog, Error from apps.workspaces.models import LastExportDetail, NetSuiteCredentials from netsuitesdk.internal.exceptions import NetSuiteRequestError @@ -27,6 +27,17 @@ def __handle_netsuite_connection_error(expense_group: ExpenseGroup, task_log: Ta 'expense_group_id': expense_group.id, 'message': 'NetSuite Account not connected' } + + Error.objects.update_or_create( + workspace_id=expense_group.workspace_id, + expense_group=expense_group, + defaults={ + 'type': 'NETSUITE_ERROR', + 'error_title': netsuite_error_message, + 'error_detail': detail['message'], + 'is_resolved': False + }) + task_log.status = 'FAILED' task_log.detail = detail @@ -113,6 +124,16 @@ def wrapper(*args): }) if not payment: all_details[0]['expense_group_id'] = expense_group.id + Error.objects.update_or_create( + workspace_id=expense_group.workspace_id, + expense_group=expense_group, + defaults={ + 'type': 'NETSUITE_ERROR', + 'error_title': netsuite_error_message, + 'error_detail': detail['message'], + 'is_resolved': False + } + ) task_log.detail = all_details task_log.save() @@ -126,6 +147,17 @@ def wrapper(*args): task_log.save() except NetSuiteRateLimitError: + if not payment: + Error.objects.update_or_create( + workspace_id=expense_group.workspace_id, + expense_group=expense_group, + defaults={ + 'type': 'NETSUITE_ERROR', + 'error_title': netsuite_error_message, + 'error_detail': f'Rate limit error, workspace_id - {expense_group.workspace_id}', + 'is_resolved': False + } + ) logger.info('Rate limit error, workspace_id - %s', workspace_id if payment else expense_group.workspace_id) task_log.status = 'FAILED' task_log.detail = { diff --git a/apps/netsuite/tasks.py b/apps/netsuite/tasks.py index e2d3cafb..e389c34c 100644 --- a/apps/netsuite/tasks.py +++ b/apps/netsuite/tasks.py @@ -386,6 +386,18 @@ def upload_attachments_and_update_export(expenses: List[Expense], task_log: Task workspace_id, exception, traceback.format_exc() ) + +def resolve_errors_for_exported_expense_group(expense_group, workspace_id=None): + """ + Resolve errors for exported expense group + :param expense_group: Expense group + """ + if isinstance(expense_group, list): + Error.objects.filter(workspace_id=workspace_id, expense_group_id__in=expense_group, is_resolved=False).update(is_resolved=True) + else: + Error.objects.filter(workspace_id=expense_group.workspace_id, expense_group=expense_group, is_resolved=False).update(is_resolved=True) + + @handle_netsuite_exceptions(payment=False) def create_bill(expense_group, task_log_id, last_export): task_log = TaskLog.objects.get(id=task_log_id) @@ -433,6 +445,7 @@ def create_bill(expense_group, task_log_id, last_export): expense_group.exported_at = datetime.now() expense_group.response_logs = created_bill expense_group.save() + resolve_errors_for_exported_expense_group(expense_group) task_log.save() @@ -501,6 +514,7 @@ def create_credit_card_charge(expense_group, task_log_id, last_export): expense_group.exported_at = datetime.now() expense_group.response_logs = created_credit_card_charge expense_group.save() + resolve_errors_for_exported_expense_group(expense_group) @handle_netsuite_exceptions(payment=False) @@ -545,6 +559,7 @@ def create_expense_report(expense_group, task_log_id, last_export): expense_group.exported_at = datetime.now() expense_group.response_logs = created_expense_report expense_group.save() + resolve_errors_for_exported_expense_group(expense_group) task_log.save() @@ -592,6 +607,7 @@ def create_journal_entry(expense_group, task_log_id, last_export): expense_group.exported_at = datetime.now() expense_group.response_logs = created_journal_entry expense_group.save() + resolve_errors_for_exported_expense_group(expense_group) task_log.save() @@ -730,6 +746,18 @@ def __validate_tax_group_mapping(expense_group: ExpenseGroup, configuration: Con 'message': 'Tax Group Mapping not found' }) + if tax_group: + Error.objects.update_or_create( + workspace_id=tax_group.workspace_id, + expense_attribute=tax_group, + defaults={ + 'type': 'TAX_MAPPING', + 'error_title': tax_group.value, + 'error_detail': 'Tax mapping is missing', + 'is_resolved': False + } + ) + row = row + 1 return bulk_errors @@ -836,6 +864,12 @@ def __validate_category_mapping(expense_group: ExpenseGroup, configuration: Conf workspace_id=expense_group.workspace_id ).first() + category_attribute = ExpenseAttribute.objects.filter( + value=category, + workspace_id=expense_group.workspace_id, + attribute_type='CATEGORY' + ).first() + if category_mapping: if expense_group.fund_source == 'PERSONAL': if configuration.reimbursable_expenses_object == 'EXPENSE REPORT': @@ -857,6 +891,19 @@ def __validate_category_mapping(expense_group: ExpenseGroup, configuration: Conf 'message': 'Category Mapping Not Found' }) + if category_attribute: + Error.objects.update_or_create( + workspace_id=expense_group.workspace_id, + expense_attribute=category_attribute, + defaults={ + 'type': 'CATEGORY_MAPPING', + 'error_title': category_attribute.value, + 'error_detail': 'Category mapping is missing', + 'is_resolved': False + } + ) + + row = row + 1 return bulk_errors @@ -1196,6 +1243,7 @@ def process_vendor_payment(entity_object, workspace_id, object_type): task_log.status = 'COMPLETE' task_log.save() + resolve_errors_for_exported_expense_group(expense_group_ids, workspace_id) def create_vendor_payment(workspace_id): diff --git a/tests/test_mappings/test_signals.py b/tests/test_mappings/test_signals.py index a54a5e38..efccc420 100644 --- a/tests/test_mappings/test_signals.py +++ b/tests/test_mappings/test_signals.py @@ -3,9 +3,68 @@ from apps.workspaces.models import Configuration, Workspace from apps.mappings.models import GeneralMapping from apps.tasks.models import Error -from fyle_accounting_mappings.models import MappingSetting, ExpenseAttribute, EmployeeMapping +from fyle_accounting_mappings.models import MappingSetting, ExpenseAttribute, EmployeeMapping, CategoryMapping, Mapping +def test_resolve_post_mapping_errors(access_token): + tax_group = ExpenseAttribute.objects.filter( + value='GST: NCF-AU @0.0%', + workspace_id=1, + attribute_type='TAX_GROUP' + ).first() + + Error.objects.update_or_create( + workspace_id=1, + expense_attribute=tax_group, + defaults={ + 'type': 'TAX_GROUP_MAPPING', + 'error_title': tax_group.value, + 'error_detail': 'Tax group mapping is missing', + 'is_resolved': False + } + ) + + mapping = Mapping( + source_type='TAX_GROUP', + destination_type='TAX_DETAIL', + # source__value=source_value, + source_id=1642, + destination_id=1019, + workspace_id=1 + ) + mapping.save() + error = Error.objects.filter(expense_attribute_id=mapping.source_id).first() + + assert error.is_resolved == True + +@pytest.mark.django_db() +def test_resolve_post_category_mapping_errors(access_token): + source_category = ExpenseAttribute.objects.filter( + id=96, + workspace_id=1, + attribute_type='CATEGORY' + ).first() + + Error.objects.update_or_create( + workspace_id=1, + expense_attribute=source_category, + defaults={ + 'type': 'CATEGORY_MAPPING', + 'error_title': source_category.value, + 'error_detail': 'Category mapping is missing', + 'is_resolved': False + } + ) + category_mapping, _ = CategoryMapping.objects.update_or_create( + source_category_id=96, + destination_account_id=791, + destination_expense_head_id=791, + workspace_id=1 + ) + + error = Error.objects.filter(expense_attribute_id=category_mapping.source_category_id).first() + assert error.is_resolved == True + @pytest.mark.django_db() def test_resolve_post_employees_mapping_errors(access_token): source_employee = ExpenseAttribute.objects.filter( diff --git a/tests/test_mappings/test_tasks.py b/tests/test_mappings/test_tasks.py index a3c9ea8d..c2c56a67 100644 --- a/tests/test_mappings/test_tasks.py +++ b/tests/test_mappings/test_tasks.py @@ -57,6 +57,26 @@ def test_resolve_expense_attribute_errors(db): resolve_expense_attribute_errors('EMPLOYEE', workspace_id, 'VENDOR') assert Error.objects.get(id=error.id).is_resolved == True + source_category = ExpenseAttribute.objects.filter( + id=34, + workspace_id=1, + attribute_type='CATEGORY' + ).first() + + error, _ = Error.objects.update_or_create( + workspace_id=1, + expense_attribute=source_category, + defaults={ + 'type': 'CATEGORY_MAPPING', + 'error_title': source_category.value, + 'error_detail': 'Category mapping is missing', + 'is_resolved': False + } + ) + + resolve_expense_attribute_errors('CATEGORY', workspace_id, 'ACCOUNT') + assert Error.objects.get(id=error.id).is_resolved == True + def test_disable_category_for_items_mapping(db ,mocker): workspace_id = 49 From cc17962bf980a43c2b14b29d3639fafc06ff3e56 Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Mon, 27 Nov 2023 14:23:53 +0530 Subject: [PATCH 35/36] resolved comments --- apps/netsuite/actions.py | 25 +++++++++++++++++++++++ apps/netsuite/exceptions.py | 40 +++++++++---------------------------- 2 files changed, 34 insertions(+), 31 deletions(-) create mode 100644 apps/netsuite/actions.py diff --git a/apps/netsuite/actions.py b/apps/netsuite/actions.py new file mode 100644 index 00000000..2d0cc7aa --- /dev/null +++ b/apps/netsuite/actions.py @@ -0,0 +1,25 @@ +from apps.tasks.models import TaskLog +from apps.workspaces.models import LastExportDetail +from django.db.models import Q + + +def update_last_export_details(workspace_id): + last_export_detail = LastExportDetail.objects.get(workspace_id=workspace_id) + + failed_exports = TaskLog.objects.filter( + ~Q(type__in=['CREATING_VENDOR_PAYMENT','FETCHING_EXPENSES']), workspace_id=workspace_id, status__in=['FAILED', 'FATAL'] + ).count() + + successful_exports = TaskLog.objects.filter( + ~Q(type__in=['CREATING_VENDOR_PAYMENT', 'FETCHING_EXPENSES']), + workspace_id=workspace_id, + status='COMPLETE', + updated_at__gt=last_export_detail.last_exported_at + ).count() + + last_export_detail.failed_expense_groups_count = failed_exports + last_export_detail.successful_expense_groups_count = successful_exports + last_export_detail.total_expense_groups_count = failed_exports + successful_exports + last_export_detail.save() + + return last_export_detail diff --git a/apps/netsuite/exceptions.py b/apps/netsuite/exceptions.py index 6b1aacbf..7cd06537 100644 --- a/apps/netsuite/exceptions.py +++ b/apps/netsuite/exceptions.py @@ -10,6 +10,8 @@ from netsuitesdk import NetSuiteRateLimitError, NetSuiteLoginError from fyle_netsuite_api.exceptions import BulkError +from .actions import update_last_export_details + from django.db.models import Q logger = logging.getLogger(__name__) @@ -24,7 +26,6 @@ def __handle_netsuite_connection_error(expense_group: ExpenseGroup, task_log: Ta expense_group.workspace_id ) detail = { - 'expense_group_id': expense_group.id, 'message': 'NetSuite Account not connected' } @@ -48,28 +49,6 @@ def __log_error(task_log: TaskLog) -> None: logger.exception('Something unexpected happened workspace_id: %s %s', task_log.workspace_id, task_log.detail) -def update_last_export_details(workspace_id): - last_export_detail = LastExportDetail.objects.get(workspace_id=workspace_id) - - failed_exports = TaskLog.objects.filter( - ~Q(type__in=['CREATING_VENDOR_PAYMENT','FETCHING_EXPENSES']), workspace_id=workspace_id, status__in=['FAILED', 'FATAL'] - ).count() - - successful_exports = TaskLog.objects.filter( - ~Q(type__in=['CREATING_VENDOR_PAYMENT', 'FETCHING_EXPENSES']), - workspace_id=workspace_id, - status='COMPLETE', - updated_at__gt=last_export_detail.last_exported_at - ).count() - - last_export_detail.failed_expense_groups_count = failed_exports - last_export_detail.successful_expense_groups_count = successful_exports - last_export_detail.total_expense_groups_count = failed_exports + successful_exports - last_export_detail.save() - - return last_export_detail - - def handle_netsuite_exceptions(payment=False): def decorator(func): def wrapper(*args): @@ -78,13 +57,13 @@ def wrapper(*args): workspace_id = args[1] object_type = args[2] task_log, _ = TaskLog.objects.update_or_create( - workspace_id=workspace_id, - task_id='PAYMENT_{}'.format(entity_object['unique_id']), - defaults={ - 'status': 'IN_PROGRESS', - 'type': 'CREATING_VENDOR_PAYMENT' - } - ) + workspace_id=workspace_id, + task_id='PAYMENT_{}'.format(entity_object['unique_id']), + defaults={ + 'status': 'IN_PROGRESS', + 'type': 'CREATING_VENDOR_PAYMENT' + } + ) else: expense_group = args[0] task_log_id = args[1] @@ -123,7 +102,6 @@ def wrapper(*args): 'message': detail['message'] }) if not payment: - all_details[0]['expense_group_id'] = expense_group.id Error.objects.update_or_create( workspace_id=expense_group.workspace_id, expense_group=expense_group, From 1c66ec13db3771b3966668ee61958d75114e978d Mon Sep 17 00:00:00 2001 From: Ashutosh619-sudo Date: Mon, 27 Nov 2023 15:24:58 +0530 Subject: [PATCH 36/36] resolved comments --- apps/netsuite/exceptions.py | 55 +++++++++++++++---------------- tests/test_netsuite/test_tasks.py | 2 +- 2 files changed, 27 insertions(+), 30 deletions(-) diff --git a/apps/netsuite/exceptions.py b/apps/netsuite/exceptions.py index 7cd06537..21a2d4e6 100644 --- a/apps/netsuite/exceptions.py +++ b/apps/netsuite/exceptions.py @@ -19,25 +19,33 @@ netsuite_error_message = 'NetSuite System Error' -def __handle_netsuite_connection_error(expense_group: ExpenseGroup, task_log: TaskLog) -> None: - logger.info( - 'NetSuite Credentials not found for workspace_id %s / expense group %s', - expense_group.id, - expense_group.workspace_id - ) +def __handle_netsuite_connection_error(expense_group: ExpenseGroup, task_log: TaskLog, workspace_id: int) -> None: + + if expense_group: + logger.info( + 'NetSuite Credentials not found for workspace_id %s / expense group %s', + expense_group.id, + expense_group.workspace_id + ) + else: + logger.info( + 'NetSuite Credentials not found for workspace_id %s', + workspace_id + ) detail = { 'message': 'NetSuite Account not connected' } - Error.objects.update_or_create( - workspace_id=expense_group.workspace_id, - expense_group=expense_group, - defaults={ - 'type': 'NETSUITE_ERROR', - 'error_title': netsuite_error_message, - 'error_detail': detail['message'], - 'is_resolved': False - }) + if expense_group: + Error.objects.update_or_create( + workspace_id=expense_group.workspace_id, + expense_group=expense_group, + defaults={ + 'type': 'NETSUITE_ERROR', + 'error_title': netsuite_error_message, + 'error_detail': detail['message'], + 'is_resolved': False + }) task_log.status = 'FAILED' task_log.detail = detail @@ -56,6 +64,7 @@ def wrapper(*args): entity_object = args[0] workspace_id = args[1] object_type = args[2] + expense_group = None task_log, _ = TaskLog.objects.update_or_create( workspace_id=workspace_id, task_id='PAYMENT_{}'.format(entity_object['unique_id']), @@ -66,6 +75,7 @@ def wrapper(*args): ) else: expense_group = args[0] + workspace_id=expense_group.workspace_id task_log_id = args[1] task_log = TaskLog.objects.get(id=task_log_id) last_export = args[2] @@ -74,20 +84,7 @@ def wrapper(*args): func(*args) except NetSuiteCredentials.DoesNotExist: - if payment: - logger.info( - 'NetSuite Credentials not found for workspace_id %s', - workspace_id - ) - detail = { - 'message': 'NetSuite Account not connected' - } - task_log.status = 'FAILED' - task_log.detail = detail - - task_log.save() - else: - __handle_netsuite_connection_error(expense_group, task_log) + __handle_netsuite_connection_error(expense_group, task_log, workspace_id) except (NetSuiteRequestError, NetSuiteLoginError) as exception: all_details = [] diff --git a/tests/test_netsuite/test_tasks.py b/tests/test_netsuite/test_tasks.py index 9628740c..38f62c6a 100644 --- a/tests/test_netsuite/test_tasks.py +++ b/tests/test_netsuite/test_tasks.py @@ -959,7 +959,7 @@ def test_handle_netsuite_connection_error(db): workspace_id=1 ) - __handle_netsuite_connection_error(expense_group, task_log) + __handle_netsuite_connection_error(expense_group, task_log, workspace_id=1) task_log = TaskLog.objects.filter(workspace_id=1).last()