From 86effd46b9e3035db5ce27c3ac86c239924173ad Mon Sep 17 00:00:00 2001 From: Viswas Haridas <37623357+JustARatherRidiculouslyLongUsername@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:37:44 +0530 Subject: [PATCH] feat: support split expense grouping (#402) * feat: add fields to support split expense grouping * fix: update sql fixture * test: add unit tests for split expense grouping and update existing fixtures (#401) * test: add unit tests for split expense grouping and update existing fixtures * refactor: lint * feat: add `split_expense_grouping` to the expense group settings serializer (#404) * feat: add `split_expense_grouping` to the expense group settings serializer * feat: implement split expense grouping functionality (#405) * feat: implement split expense grouping functionality * fix: skip only `expense_id` and `expense_number` while grouping while defining the fields to group split expenses by, skip only `expense_id` and `expense_number`, NOT `spent_at` or `posted_at` * feat: add script for split expense grouping (#406) Set the default split expense grouping config to `SINGLE_LINE_ITEM` for all old orgs --- .../0022_support_split_expense_grouping.py | 24 +++ apps/fyle/models.py | 64 +++++- .../apis/export_settings/serializers.py | 4 + ...lt-split-expense-grouping-for-old-orgs.sql | 5 + .../reset_db_fixtures/reset_db.sql | 39 ++-- tests/test_fyle/conftest.py | 19 ++ tests/test_fyle/fixtures.py | 188 +++++++++++++++++- tests/test_fyle/test_models.py | 104 ++++++++++ tests/test_workspaces/fixtures.py | 1 + .../test_apis/test_clone_settings/fixtures.py | 2 + .../test_export_settings/fixtures.py | 2 + 11 files changed, 426 insertions(+), 26 deletions(-) create mode 100644 apps/fyle/migrations/0022_support_split_expense_grouping.py create mode 100644 sql/scripts/027-default-split-expense-grouping-for-old-orgs.sql diff --git a/apps/fyle/migrations/0022_support_split_expense_grouping.py b/apps/fyle/migrations/0022_support_split_expense_grouping.py new file mode 100644 index 00000000..37cedc61 --- /dev/null +++ b/apps/fyle/migrations/0022_support_split_expense_grouping.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.14 on 2024-11-18 09:48 + +import apps.fyle.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('fyle', '0021_expense_is_posted_at_null'), + ] + + operations = [ + migrations.AddField( + model_name='expense', + name='bank_transaction_id', + field=models.CharField(blank=True, help_text='Bank Transaction ID', max_length=255, null=True), + ), + migrations.AddField( + model_name='expensegroupsettings', + name='split_expense_grouping', + field=models.CharField(choices=[('SINGLE_LINE_ITEM', 'SINGLE_LINE_ITEM'), ('MULTIPLE_LINE_ITEM', 'MULTIPLE_LINE_ITEM')], default=apps.fyle.models.get_default_split_expense_grouping, help_text='specify line items for split expenses grouping', max_length=100), + ), + ] diff --git a/apps/fyle/models.py b/apps/fyle/models.py index cad6b5ee..7d69499e 100644 --- a/apps/fyle/models.py +++ b/apps/fyle/models.py @@ -14,7 +14,7 @@ from fyle_accounting_mappings.models import ExpenseAttribute from apps.fyle.enums import ExpenseStateEnum, FundSourceEnum, PlatformExpensesEnum -from apps.workspaces.models import Workspace +from apps.workspaces.models import Workspace, WorkspaceGeneralSettings logger = logging.getLogger(__name__) logger.level = logging.INFO @@ -35,6 +35,7 @@ "spent_at", "expense_id", "posted_at", + "bank_transaction_id", ] @@ -62,6 +63,8 @@ PlatformExpensesEnum.PERSONAL_CORPORATE_CREDIT_CARD_ACCOUNT: FundSourceEnum.CCC, } +SPLIT_EXPENSE_GROUPING = (('SINGLE_LINE_ITEM', 'SINGLE_LINE_ITEM'), ('MULTIPLE_LINE_ITEM', 'MULTIPLE_LINE_ITEM')) + def _format_date(date_string) -> datetime: """ @@ -126,6 +129,7 @@ class Expense(models.Model): ) purpose = models.TextField(null=True, blank=True, help_text="Purpose") report_id = models.CharField(max_length=255, help_text="Report ID") + bank_transaction_id = models.CharField(max_length=255, null=True, blank=True, help_text='Bank Transaction ID') billable = models.BooleanField(default=False, help_text="Expense billable or not") file_ids = ArrayField( base_field=models.CharField(max_length=255), null=True, help_text="File IDs" @@ -199,6 +203,7 @@ def create_expense_objects(expenses: List[Dict], workspace_id: int, skip_update: "corporate_card_id": expense["corporate_card_id"], "purpose": expense["purpose"], "report_id": expense["report_id"], + "bank_transaction_id": expense["bank_transaction_id"], "file_ids": expense["file_ids"], "spent_at": expense["spent_at"], "posted_at": expense["posted_at"], @@ -260,6 +265,10 @@ def get_default_ccc_expense_state(): return ExpenseStateEnum.PAID +def get_default_split_expense_grouping(): + return 'MULTIPLE_LINE_ITEM' + + CCC_EXPENSE_STATE = ( (ExpenseStateEnum.APPROVED, ExpenseStateEnum.APPROVED), (ExpenseStateEnum.PAYMENT_PROCESSING, ExpenseStateEnum.PAYMENT_PROCESSING), @@ -314,6 +323,7 @@ class ExpenseGroupSettings(models.Model): import_card_credits = models.BooleanField( help_text="Import Card Credits", default=False ) + split_expense_grouping = models.CharField(max_length=100, default=get_default_split_expense_grouping, choices=SPLIT_EXPENSE_GROUPING, help_text='specify line items for split expenses grouping') workspace = models.OneToOneField( Workspace, on_delete=models.PROTECT, @@ -421,6 +431,7 @@ def update_expense_group_settings(expense_group_settings: Dict, workspace_id: in ], "ccc_export_date_type": expense_group_settings["ccc_export_date_type"], "import_card_credits": import_card_credits, + 'split_expense_grouping': expense_group_settings['split_expense_grouping'], }, ) @@ -506,13 +517,52 @@ def create_expense_groups_by_report_id_fund_source( corporate_credit_card_expenses = list( filter(lambda expense: expense.fund_source == "CCC", expense_objects) ) - corporate_credit_card_expense_groups = _group_expenses( - corporate_credit_card_expenses, - corporate_credit_card_expense_group_field, - workspace_id, - ) - expense_groups.extend(corporate_credit_card_expense_groups) + if corporate_credit_card_expenses: + workspace_general_settings = WorkspaceGeneralSettings.objects.get( + workspace_id=workspace_id + ) + ccc_export_module = workspace_general_settings.corporate_credit_card_expenses_object + + if ccc_export_module == "BANK TRANSACTION" and expense_group_settings.split_expense_grouping == 'MULTIPLE_LINE_ITEM': + ccc_expenses_without_bank_transaction_id = list( + filter(lambda expense: not expense.bank_transaction_id, corporate_credit_card_expenses) + ) + + ccc_expenses_with_bank_transaction_id = list( + filter(lambda expense: expense.bank_transaction_id, corporate_credit_card_expenses) + ) + + if ccc_expenses_without_bank_transaction_id: + groups_without_bank_transaction_id = _group_expenses( + ccc_expenses_without_bank_transaction_id, + corporate_credit_card_expense_group_field, + workspace_id, + ) + expense_groups.extend(groups_without_bank_transaction_id) + + if ccc_expenses_with_bank_transaction_id: + split_expense_group_fields = [ + field for field in corporate_credit_card_expense_group_field + if field not in ('expense_id', 'expense_number') + ] + split_expense_group_fields.append('bank_transaction_id') + + groups_with_bank_transaction_id = _group_expenses( + ccc_expenses_with_bank_transaction_id, + split_expense_group_fields, + workspace_id, + ) + expense_groups.extend(groups_with_bank_transaction_id) + + else: + corporate_credit_card_expense_groups = _group_expenses( + corporate_credit_card_expenses, + corporate_credit_card_expense_group_field, + workspace_id, + ) + + expense_groups.extend(corporate_credit_card_expense_groups) expense_group_objects = [] diff --git a/apps/workspaces/apis/export_settings/serializers.py b/apps/workspaces/apis/export_settings/serializers.py index 83a4d58b..8ebce381 100644 --- a/apps/workspaces/apis/export_settings/serializers.py +++ b/apps/workspaces/apis/export_settings/serializers.py @@ -64,6 +64,9 @@ class ExpenseGroupSettingsSerializer(serializers.ModelSerializer): ccc_expense_state = serializers.CharField( allow_null=True, allow_blank=True, required=False ) + split_expense_grouping = serializers.CharField( + allow_null=False, allow_blank=False, required=True + ) class Meta: model = ExpenseGroupSettings @@ -74,6 +77,7 @@ class Meta: "corporate_credit_card_expense_group_fields", "ccc_export_date_type", "ccc_expense_state", + "split_expense_grouping", ] diff --git a/sql/scripts/027-default-split-expense-grouping-for-old-orgs.sql b/sql/scripts/027-default-split-expense-grouping-for-old-orgs.sql new file mode 100644 index 00000000..89797023 --- /dev/null +++ b/sql/scripts/027-default-split-expense-grouping-for-old-orgs.sql @@ -0,0 +1,5 @@ +rollback; +begin; + +UPDATE expense_group_settings +SET split_expense_grouping = 'SINGLE_LINE_ITEM'; diff --git a/tests/sql_fixtures/reset_db_fixtures/reset_db.sql b/tests/sql_fixtures/reset_db_fixtures/reset_db.sql index cf286609..f3a95cd5 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.9 (Debian 15.9-1.pgdg120+1) --- Dumped by pg_dump version 15.8 (Debian 15.8-1.pgdg120+1) +-- Dumped from database version 15.7 (Debian 15.7-1.pgdg120+1) +-- Dumped by pg_dump version 15.9 (Debian 15.9-1.pgdg120+1) SET statement_timeout = 0; SET lock_timeout = 0; @@ -815,7 +815,8 @@ CREATE TABLE public.expense_group_settings ( ccc_export_date_type character varying(100) NOT NULL, ccc_expense_state character varying(100), reimbursable_expense_state character varying(100), - import_card_credits boolean NOT NULL + import_card_credits boolean NOT NULL, + split_expense_grouping character varying(100) NOT NULL ); @@ -944,7 +945,8 @@ CREATE TABLE public.expenses ( previous_export_state character varying(255), workspace_id integer, paid_on_fyle boolean NOT NULL, - is_posted_at_null boolean NOT NULL + is_posted_at_null boolean NOT NULL, + bank_transaction_id character varying(255) ); @@ -2640,6 +2642,7 @@ COPY public.django_migrations (id, app, name, applied) FROM stdin; 156 fyle 0021_expense_is_posted_at_null 2024-11-17 21:11:41.101604+00 157 tasks 0010_alter_tasklog_expense_group 2024-11-17 21:11:41.133814+00 158 workspaces 0039_alter_workspacegeneralsettings_change_accounting_period 2024-11-18 04:43:45.472917+00 +159 fyle 0022_support_split_expense_grouping 2024-11-18 10:49:49.550689+00 \. @@ -4835,8 +4838,8 @@ COPY public.expense_fields (id, attribute_type, source_field_id, is_enabled, cre -- Data for Name: expense_group_settings; Type: TABLE DATA; Schema: public; Owner: postgres -- -COPY public.expense_group_settings (id, reimbursable_expense_group_fields, corporate_credit_card_expense_group_fields, expense_state, reimbursable_export_date_type, created_at, updated_at, workspace_id, ccc_export_date_type, ccc_expense_state, reimbursable_expense_state, import_card_credits) FROM stdin; -1 {employee_email,report_id,claim_number,fund_source} {report_id,fund_source,employee_email,claim_number,expense_id} PAYMENT_PROCESSING current_date 2022-08-02 20:24:42.329794+00 2022-08-02 20:25:24.6873+00 1 spent_at PAYMENT_PROCESSING PAYMENT_PROCESSING t +COPY public.expense_group_settings (id, reimbursable_expense_group_fields, corporate_credit_card_expense_group_fields, expense_state, reimbursable_export_date_type, created_at, updated_at, workspace_id, ccc_export_date_type, ccc_expense_state, reimbursable_expense_state, import_card_credits, split_expense_grouping) FROM stdin; +1 {employee_email,report_id,claim_number,fund_source} {report_id,fund_source,employee_email,claim_number,expense_id} PAYMENT_PROCESSING current_date 2022-08-02 20:24:42.329794+00 2022-08-02 20:25:24.6873+00 1 spent_at PAYMENT_PROCESSING PAYMENT_PROCESSING t MULTIPLE_LINE_ITEM \. @@ -4880,17 +4883,17 @@ COPY public.expense_groups_expenses (id, expensegroup_id, expense_id) FROM stdin -- Data for Name: expenses; Type: TABLE DATA; Schema: public; Owner: postgres -- -COPY public.expenses (id, employee_email, category, sub_category, project, expense_id, expense_number, claim_number, amount, currency, foreign_amount, foreign_currency, settlement_id, reimbursable, state, vendor, cost_center, purpose, report_id, spent_at, approved_at, expense_created_at, expense_updated_at, created_at, updated_at, fund_source, verified_at, custom_properties, paid_on_xero, org_id, file_ids, corporate_card_id, tax_amount, tax_group_id, billable, employee_name, posted_at, accounting_export_summary, previous_export_state, workspace_id, paid_on_fyle, is_posted_at_null) FROM stdin; -1 ashwin.t@fyle.in Food \N \N txaaVBj3yKGW E/2022/06/T/4 C/2022/06/R/2 1 USD \N \N setrunCck8hLH t PAYMENT_PROCESSING \N \N \N rp9EvDF8Umk6 2022-06-27 17:00:00+00 2022-06-27 09:06:52.951+00 2022-06-27 09:06:13.135764+00 2022-06-27 09:08:23.340321+00 2022-08-02 20:26:22.81033+00 2022-08-02 20:26:22.810363+00 PERSONAL \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "Postman Field": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N {} \N \N f f -2 ashwin.t@fyle.in Food \N \N txB6D8k0Ws8a E/2022/06/T/2 C/2022/06/R/3 4 USD \N \N setrunCck8hLH t PAYMENT_PROCESSING \N \N \N rpNeZt3cv9wz 2022-06-27 17:00:00+00 2022-06-27 09:07:16.556+00 2022-06-27 09:05:45.738+00 2022-06-27 09:08:23.340321+00 2022-08-02 20:26:22.82716+00 2022-08-02 20:26:22.827194+00 PERSONAL \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "Postman Field": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N {} \N \N f f -3 sravan.kumar@fyle.in Food \N Bebe Rexha txGilVGolf60 E/2022/06/T/1 C/2022/06/R/1 10 USD \N \N setlpIUKpdvsT t PAYMENT_PROCESSING \N Adidas \N rpKuJtEv6h0n 2020-01-01 17:00:00+00 2022-06-08 04:28:30.61+00 2022-06-08 04:27:35.274447+00 2022-06-08 04:28:51.237261+00 2022-08-02 20:26:22.835984+00 2022-08-02 20:26:22.83608+00 PERSONAL \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N 1 tg0gTsClGjLp f \N \N {} \N \N f f -4 sravan.kumar@fyle.in Food \N \N txjIqTCtkkC8 E/2022/05/T/21 C/2022/05/R/18 100 USD \N \N set3ZMFXrDPL3 f PAYMENT_PROCESSING \N \N \N rpLawO11bFib 2022-05-25 17:00:00+00 2022-05-25 08:59:25.649+00 2022-05-25 08:59:07.718891+00 2022-05-25 09:04:05.66983+00 2022-08-02 20:26:22.844927+00 2022-08-02 20:26:22.844961+00 CCC \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N {} \N \N f f -5 sravan.kumar@fyle.in WIP \N Bebe Rexha txUPRc3VwxOP E/2022/05/T/19 C/2022/05/R/17 101 USD \N \N setb1pSLMIok8 f PAYMENT_PROCESSING \N Adidas \N rpv1txzAsgr3 2021-01-01 17:00:00+00 2022-05-25 07:24:12.987+00 2022-05-25 07:21:40.598113+00 2022-05-25 07:25:00.848892+00 2022-08-02 20:26:22.857516+00 2022-08-02 20:26:22.857675+00 CCC \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N {} \N \N f f -6 ashwin.t@fyle.in Food \N \N txUDvDmEV4ep E/2022/05/T/18 C/2022/05/R/16 5 USD \N \N set33iAVXO7BA t PAYMENT_PROCESSING \N \N \N rpE2JyATZhDe 2020-05-25 17:00:00+00 2022-05-25 06:05:23.362+00 2022-05-25 06:04:46.557927+00 2022-05-25 06:05:47.36985+00 2022-08-02 20:26:22.870854+00 2022-08-02 20:26:22.87089+00 PERSONAL \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N {} \N \N f f -7 sravan.kumar@fyle.in WIP \N Bebe Rexha tx1FW3uxYZG6 E/2022/05/T/16 C/2022/05/R/15 151 USD \N \N setzFn3FK5t80 f PAYMENT_PROCESSING \N Adidas \N rprwGgzOZyfR 2022-05-25 17:00:00+00 2022-05-25 03:41:49.042+00 2022-05-25 03:41:28.839711+00 2022-05-25 03:42:10.145663+00 2022-08-02 20:26:22.882803+00 2022-08-02 20:26:22.882836+00 CCC \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N {} \N \N f f -8 sravan.kumar@fyle.in WIP \N Bebe Rexha txVXhyVB8mgK E/2022/05/T/15 C/2022/05/R/14 45 USD \N \N setsN8cLD9KIn f PAYMENT_PROCESSING \N Adidas \N rpnG3lZYDsHU 2022-05-25 17:00:00+00 2022-05-25 02:48:53.791+00 2022-05-25 02:48:37.432989+00 2022-05-25 02:49:18.189037+00 2022-08-02 20:26:22.894793+00 2022-08-02 20:26:22.894827+00 CCC \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N {} \N \N f f -9 sravan.kumar@fyle.in WIP \N Bebe Rexha txBMQRkBQciI E/2022/05/T/14 C/2022/05/R/13 10 USD \N \N setanDKqMZfXB f PAYMENT_PROCESSING \N Adidas \N rpVvNQvE2wbm 2022-05-25 17:00:00+00 2022-05-25 02:38:40.858+00 2022-05-25 02:38:25.832419+00 2022-05-25 02:39:08.208877+00 2022-08-02 20:26:22.908632+00 2022-08-02 20:26:22.908661+00 CCC \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N {} \N \N f f -10 sravan.kumar@fyle.in WIP \N Bebe Rexha txkw3dt3umkN E/2022/05/T/12 C/2022/05/R/12 101 USD \N \N setBe6qAlNXPU f PAYMENT_PROCESSING \N Adidas \N rp5lITpxFLxE 2022-05-24 17:00:00+00 2022-05-24 15:59:13.26+00 2022-05-24 15:55:50.369024+00 2022-05-24 16:00:27.982+00 2022-08-02 20:26:22.921466+00 2022-08-02 20:26:22.9215+00 CCC \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N 1 tg0gTsClGjLp f \N \N {} \N \N f f +COPY public.expenses (id, employee_email, category, sub_category, project, expense_id, expense_number, claim_number, amount, currency, foreign_amount, foreign_currency, settlement_id, reimbursable, state, vendor, cost_center, purpose, report_id, spent_at, approved_at, expense_created_at, expense_updated_at, created_at, updated_at, fund_source, verified_at, custom_properties, paid_on_xero, org_id, file_ids, corporate_card_id, tax_amount, tax_group_id, billable, employee_name, posted_at, accounting_export_summary, previous_export_state, workspace_id, paid_on_fyle, is_posted_at_null, bank_transaction_id) FROM stdin; +1 ashwin.t@fyle.in Food \N \N txaaVBj3yKGW E/2022/06/T/4 C/2022/06/R/2 1 USD \N \N setrunCck8hLH t PAYMENT_PROCESSING \N \N \N rp9EvDF8Umk6 2022-06-27 17:00:00+00 2022-06-27 09:06:52.951+00 2022-06-27 09:06:13.135764+00 2022-06-27 09:08:23.340321+00 2022-08-02 20:26:22.81033+00 2022-08-02 20:26:22.810363+00 PERSONAL \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "Postman Field": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N {} \N \N f f \N +2 ashwin.t@fyle.in Food \N \N txB6D8k0Ws8a E/2022/06/T/2 C/2022/06/R/3 4 USD \N \N setrunCck8hLH t PAYMENT_PROCESSING \N \N \N rpNeZt3cv9wz 2022-06-27 17:00:00+00 2022-06-27 09:07:16.556+00 2022-06-27 09:05:45.738+00 2022-06-27 09:08:23.340321+00 2022-08-02 20:26:22.82716+00 2022-08-02 20:26:22.827194+00 PERSONAL \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "Postman Field": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N {} \N \N f f \N +3 sravan.kumar@fyle.in Food \N Bebe Rexha txGilVGolf60 E/2022/06/T/1 C/2022/06/R/1 10 USD \N \N setlpIUKpdvsT t PAYMENT_PROCESSING \N Adidas \N rpKuJtEv6h0n 2020-01-01 17:00:00+00 2022-06-08 04:28:30.61+00 2022-06-08 04:27:35.274447+00 2022-06-08 04:28:51.237261+00 2022-08-02 20:26:22.835984+00 2022-08-02 20:26:22.83608+00 PERSONAL \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N 1 tg0gTsClGjLp f \N \N {} \N \N f f \N +4 sravan.kumar@fyle.in Food \N \N txjIqTCtkkC8 E/2022/05/T/21 C/2022/05/R/18 100 USD \N \N set3ZMFXrDPL3 f PAYMENT_PROCESSING \N \N \N rpLawO11bFib 2022-05-25 17:00:00+00 2022-05-25 08:59:25.649+00 2022-05-25 08:59:07.718891+00 2022-05-25 09:04:05.66983+00 2022-08-02 20:26:22.844927+00 2022-08-02 20:26:22.844961+00 CCC \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N {} \N \N f f \N +5 sravan.kumar@fyle.in WIP \N Bebe Rexha txUPRc3VwxOP E/2022/05/T/19 C/2022/05/R/17 101 USD \N \N setb1pSLMIok8 f PAYMENT_PROCESSING \N Adidas \N rpv1txzAsgr3 2021-01-01 17:00:00+00 2022-05-25 07:24:12.987+00 2022-05-25 07:21:40.598113+00 2022-05-25 07:25:00.848892+00 2022-08-02 20:26:22.857516+00 2022-08-02 20:26:22.857675+00 CCC \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N {} \N \N f f \N +6 ashwin.t@fyle.in Food \N \N txUDvDmEV4ep E/2022/05/T/18 C/2022/05/R/16 5 USD \N \N set33iAVXO7BA t PAYMENT_PROCESSING \N \N \N rpE2JyATZhDe 2020-05-25 17:00:00+00 2022-05-25 06:05:23.362+00 2022-05-25 06:04:46.557927+00 2022-05-25 06:05:47.36985+00 2022-08-02 20:26:22.870854+00 2022-08-02 20:26:22.87089+00 PERSONAL \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N {} \N \N f f \N +7 sravan.kumar@fyle.in WIP \N Bebe Rexha tx1FW3uxYZG6 E/2022/05/T/16 C/2022/05/R/15 151 USD \N \N setzFn3FK5t80 f PAYMENT_PROCESSING \N Adidas \N rprwGgzOZyfR 2022-05-25 17:00:00+00 2022-05-25 03:41:49.042+00 2022-05-25 03:41:28.839711+00 2022-05-25 03:42:10.145663+00 2022-08-02 20:26:22.882803+00 2022-08-02 20:26:22.882836+00 CCC \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N {} \N \N f f \N +8 sravan.kumar@fyle.in WIP \N Bebe Rexha txVXhyVB8mgK E/2022/05/T/15 C/2022/05/R/14 45 USD \N \N setsN8cLD9KIn f PAYMENT_PROCESSING \N Adidas \N rpnG3lZYDsHU 2022-05-25 17:00:00+00 2022-05-25 02:48:53.791+00 2022-05-25 02:48:37.432989+00 2022-05-25 02:49:18.189037+00 2022-08-02 20:26:22.894793+00 2022-08-02 20:26:22.894827+00 CCC \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N {} \N \N f f \N +9 sravan.kumar@fyle.in WIP \N Bebe Rexha txBMQRkBQciI E/2022/05/T/14 C/2022/05/R/13 10 USD \N \N setanDKqMZfXB f PAYMENT_PROCESSING \N Adidas \N rpVvNQvE2wbm 2022-05-25 17:00:00+00 2022-05-25 02:38:40.858+00 2022-05-25 02:38:25.832419+00 2022-05-25 02:39:08.208877+00 2022-08-02 20:26:22.908632+00 2022-08-02 20:26:22.908661+00 CCC \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N \N \N f \N \N {} \N \N f f \N +10 sravan.kumar@fyle.in WIP \N Bebe Rexha txkw3dt3umkN E/2022/05/T/12 C/2022/05/R/12 101 USD \N \N setBe6qAlNXPU f PAYMENT_PROCESSING \N Adidas \N rp5lITpxFLxE 2022-05-24 17:00:00+00 2022-05-24 15:59:13.26+00 2022-05-24 15:55:50.369024+00 2022-05-24 16:00:27.982+00 2022-08-02 20:26:22.921466+00 2022-08-02 20:26:22.9215+00 CCC \N {"Card": "", "Killua": "", "Classes": "", "avc_123": null, "New Field": "", "Multi field": "", "Testing This": "", "abc in [123]": null, "POSTMAN FIELD": "", "Netsuite Class": ""} f orPJvXuoLqvJ {} \N 1 tg0gTsClGjLp f \N \N {} \N \N f f \N \. @@ -5177,7 +5180,7 @@ SELECT pg_catalog.setval('public.django_content_type_id_seq', 40, true); -- Name: django_migrations_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres -- -SELECT pg_catalog.setval('public.django_migrations_id_seq', 158, true); +SELECT pg_catalog.setval('public.django_migrations_id_seq', 159, true); -- diff --git a/tests/test_fyle/conftest.py b/tests/test_fyle/conftest.py index 035000fc..47dc475e 100644 --- a/tests/test_fyle/conftest.py +++ b/tests/test_fyle/conftest.py @@ -32,3 +32,22 @@ def create_temp_workspace(db): reimbursable_export_date_type="current_date", ccc_export_date_type="spent_at", ) + + +@pytest.fixture +def update_config_for_split_expense_grouping(db): + def _update_config_for_split_expense_grouping(general_settings, expense_group_settings): + general_settings.corporate_credit_card_expenses_object = 'BANK TRANSACTION' + general_settings.save() + expense_group_settings.split_expense_grouping = 'SINGLE_LINE_ITEM' + expense_group_settings.corporate_credit_card_expense_group_fields = [ + 'expense_id', + 'claim_number', + 'fund_source', + 'employee_email', + 'report_id', + 'spent_at', + 'report_id' + ] + expense_group_settings.save() + return _update_config_for_split_expense_grouping diff --git a/tests/test_fyle/fixtures.py b/tests/test_fyle/fixtures.py index c26670cf..158e5e3c 100644 --- a/tests/test_fyle/fixtures.py +++ b/tests/test_fyle/fixtures.py @@ -236,7 +236,8 @@ 'tax_amount': None, 'tax_group_id': None, 'previous_export_state': None, - 'accounting_export_summary': [] + 'accounting_export_summary': [], + "bank_transaction_id": None, }, "expenses": [ { @@ -278,8 +279,189 @@ "Vehicle Type": "", "Fyle Categories": "", }, + "bank_transaction_id": None, }, ], + + "ccc_split_expenses": [ + { + "id": 15133, + "employee_email": "admin1@fyleforexport.updates", + "employee_name": "Theresa Brown", + "category": "Food", + "sub_category": None, + "project": "Project 8", + "org_id": "oryoUx39Vz66", + "expense_number": "E/2024/11/T/7", + "claim_number": "C/2024/11/R/3", + "amount": 21.208, + "currency": "USD", + "foreign_amount": None, + "foreign_currency": None, + "settlement_id": None, + "reimbursable": False, + "state": "APPROVED", + "vendor": "Dominos Pizza", + "cost_center": None, + "corporate_card_id": "baccZ5Ww7s9mLu", + "purpose": None, + "report_id": "rprwO90Ul12E", + "billable": False, + "file_ids": [], + "spent_at": '2024-11-04T00:00:00Z', + "approved_at": '2024-11-04T15:45:02.657341Z', + "posted_at": None, + "expense_created_at": '2024-11-04T15:44:46.752178Z', + "expense_updated_at": '2024-11-04T15:45:02.487787Z', + "created_at": '2024-11-07T20:20:05.416618Z', + "updated_at": '2024-11-07T20:20:05.416629Z', + "fund_source": "CCC", + "verified_at": None, + "custom_properties": {"Custom Expense Field": None}, + "paid_on_xero": False, + "paid_on_fyle": False, + "tax_amount": 0.0, + "tax_group_id": None, + "accounting_export_summary": {}, + "previous_export_state": None, + "workspace_id": 399, + "source_account_type": "PERSONAL_CORPORATE_CREDIT_CARD_ACCOUNT", + "bank_transaction_id": "btxnQe02cfpD9n" + }, + { + "id": 15134, + "employee_email": "admin1@fyleforexport.updates", + "employee_name": "Theresa Brown", + "category": "Food", + "sub_category": None, + "project": "Project 10", + "org_id": "oryoUx39Vz66", + "expense_number": "E/2024/11/T/6", + "claim_number": "C/2024/11/R/3", + "amount": 31.812, + "currency": "USD", + "foreign_amount": None, + "foreign_currency": None, + "settlement_id": None, + "reimbursable": False, + "state": "APPROVED", + "vendor": "Dominos Pizza", + "cost_center": None, + "corporate_card_id": "baccZ5Ww7s9mLu", + "purpose": None, + "report_id": "rprwO90Ul12E", + "billable": False, + "file_ids": [], + "spent_at": '2024-11-04T00:00:00Z', + "approved_at": '2024-11-04T15:45:02.657341Z', + "posted_at": None, + "expense_created_at": '2024-11-04T15:44:46.581967Z', + "expense_updated_at": '2024-11-04T15:45:02.283785Z', + "created_at": '2024-11-07T20:20:05.438423Z', + "updated_at": '2024-11-07T20:20:05.438431Z', + "fund_source": "CCC", + "verified_at": None, + "custom_properties": {"Custom Expense Field": None}, + "paid_on_xero": False, + "paid_on_fyle": False, + "tax_amount": 0.0, + "tax_group_id": None, + "accounting_export_summary": {}, + "previous_export_state": None, + "workspace_id": 399, + "source_account_type": "PERSONAL_CORPORATE_CREDIT_CARD_ACCOUNT", + "bank_transaction_id": "btxnQe02cfpD9n" + }, + { + "id": 15135, + "employee_email": "admin1@fyleforexport.updates", + "employee_name": "Theresa Brown", + "category": "Food", + "sub_category": None, + "project": "Project 10", + "org_id": "oryoUx39Vz66", + "expense_number": "E/2024/11/T/8", + "claim_number": "C/2024/11/R/3", + "amount": 23.492, + "currency": "USD", + "foreign_amount": None, + "foreign_currency": None, + "settlement_id": None, + "reimbursable": False, + "state": "APPROVED", + "vendor": "Dominos Pizza", + "cost_center": None, + "corporate_card_id": "baccZ5Ww7s9mLu", + "purpose": None, + "report_id": "rprwO90Ul12E", + "billable": False, + "file_ids": [], + "spent_at": '2024-11-04T00:00:00Z', + "approved_at": '2024-11-04T15:45:02.657341Z', + "posted_at": None, + "expense_created_at": '2024-11-04T15:44:46.581967Z', + "expense_updated_at": '2024-11-04T15:45:02.283785Z', + "created_at": '2024-11-07T20:20:05.438423Z', + "updated_at": '2024-11-07T20:20:05.438431Z', + "fund_source": "CCC", + "verified_at": None, + "custom_properties": {"Custom Expense Field": None}, + "paid_on_xero": False, + "paid_on_fyle": False, + "tax_amount": 0.0, + "tax_group_id": None, + "accounting_export_summary": {}, + "previous_export_state": None, + "workspace_id": 399, + "source_account_type": "PERSONAL_CORPORATE_CREDIT_CARD_ACCOUNT", + "bank_transaction_id": "btxnQe02cfpD9n" + }, + { + "id": 15136, + "employee_email": "admin1@fyleforexport.updates", + "employee_name": "Theresa Brown", + "category": "Food", + "sub_category": None, + "project": "Project 10", + "org_id": "oryoUx39Vz66", + "expense_number": "E/2024/11/T/9", + "claim_number": "C/2024/11/R/3", + "amount": 26.722, + "currency": "USD", + "foreign_amount": None, + "foreign_currency": None, + "settlement_id": None, + "reimbursable": False, + "state": "APPROVED", + "vendor": "Dominos Pizza", + "cost_center": None, + "corporate_card_id": "baccZ5Ww7s9mLu", + "purpose": None, + "report_id": "rprwO90Ul12E", + "billable": False, + "file_ids": [], + "spent_at": '2024-11-04T00:00:00Z', + "approved_at": '2024-11-04T15:45:02.657341Z', + "posted_at": None, + "expense_created_at": '2024-11-04T15:44:46.581967Z', + "expense_updated_at": '2024-11-04T15:45:02.283785Z', + "created_at": '2024-11-07T20:20:05.438423Z', + "updated_at": '2024-11-07T20:20:05.438431Z', + "fund_source": "CCC", + "verified_at": None, + "custom_properties": {"Custom Expense Field": None}, + "paid_on_xero": False, + "paid_on_fyle": False, + "tax_amount": 0.0, + "tax_group_id": None, + "accounting_export_summary": {}, + "previous_export_state": None, + "workspace_id": 399, + "source_account_type": "PERSONAL_CORPORATE_CREDIT_CARD_ACCOUNT", + "bank_transaction_id": "btxnQe02cfpD9n" + }, + ], + "eliminated_expenses": [ { "id": "tx6wOnBVaumk", @@ -320,6 +502,7 @@ "Vehicle Type": "", "Fyle Categories": "", }, + "bank_transaction_id": None, } ], "expense_group_id": { @@ -366,6 +549,7 @@ "import_card_credits": "false", "created_at": "2021-11-15T08:46:16.069944Z", "updated_at": "2021-11-15T08:46:16.069986Z", + "split_expense_grouping": "MULTIPLE_LINE_ITEM", "workspace": 1, }, "reimbursements": [ @@ -396,6 +580,7 @@ "ccc_expense_state": "PAYMENT_PROCESSING", "reimbursable_export_date_type": "spent_at", "ccc_export_date_type": "spent_at", + "split_expense_grouping": "MULTIPLE_LINE_ITEM", }, "expense_fields_response": [ {"attribute_type": "COST_CENTER", "display_name": "Cost Center"}, @@ -594,6 +779,7 @@ "paid_on_xero": False, "tax_amount": "None", "tax_group_id": "None", + "bank_transaction_id": None, } ], "fund_source": "PERSONAL", diff --git a/tests/test_fyle/test_models.py b/tests/test_fyle/test_models.py index ea7e40e1..dfb0a694 100644 --- a/tests/test_fyle/test_models.py +++ b/tests/test_fyle/test_models.py @@ -7,6 +7,7 @@ get_default_expense_group_fields, get_default_expense_state, ) +from apps.workspaces.models import WorkspaceGeneralSettings from tests.test_fyle.fixtures import data @@ -104,3 +105,106 @@ def test_create_expense_groups_by_report_id_fund_source(db): expense_groups = ExpenseGroup.objects.last() assert expense_groups.exported_at == None + + +def test_split_expense_grouping_with_no_bank_transaction_id(db, update_config_for_split_expense_grouping): + ''' + Test for grouping of 2 expenses with no bank transaction id + ''' + workspace_id = 1 + + # Update settings + general_settings = WorkspaceGeneralSettings.objects.get(workspace_id=workspace_id) + expense_group_settings = ExpenseGroupSettings.objects.get(workspace_id=workspace_id) + update_config_for_split_expense_grouping(general_settings, expense_group_settings) + + # Get reference to expense objects + expenses = data['ccc_split_expenses'][:2] + for expense in expenses: + expense['bank_transaction_id'] = None + + Expense.create_expense_objects(expenses, workspace_id=workspace_id) + expense_objects = Expense.objects.filter(expense_id__in=[expense['id'] for expense in expenses]) + + assert len(expense_objects) == 2, f'Expected 2 expenses, got {len(expense_objects)}' + + # Test for SINGLE_LINE_ITEM split expense grouping + groups = ExpenseGroup.create_expense_groups_by_report_id_fund_source(expense_objects, workspace_id) + assert len(groups) == 2, f'Expected 2 groups, got {len(groups)}' + + # Test for MULTIPLE_LINE_ITEM split expense grouping + expense_group_settings.split_expense_grouping = 'MULTIPLE_LINE_ITEM' + expense_group_settings.save() + + groups = ExpenseGroup.create_expense_groups_by_report_id_fund_source(expense_objects, workspace_id) + assert len(groups) == 2, f'Expected 2 groups, got {len(groups)}' + + +def test_split_expense_grouping_with_same_and_different_ids(db, update_config_for_split_expense_grouping): + ''' + Test for grouping of 2 expenses with the same bank transaction id, + and one expense with a different bank transaction id + ''' + workspace_id = 1 + + # Update settings + general_settings = WorkspaceGeneralSettings.objects.get(workspace_id=workspace_id) + expense_group_settings = ExpenseGroupSettings.objects.get(workspace_id=workspace_id) + update_config_for_split_expense_grouping(general_settings, expense_group_settings) + + # Get reference to expense objects + expenses = data['ccc_split_expenses'][:3] + expenses[0]['bank_transaction_id'] = 'sample_1' + expenses[1]['bank_transaction_id'] = 'sample_1' + expenses[2]['bank_transaction_id'] = 'sample_2' + + Expense.create_expense_objects(expenses, workspace_id=workspace_id) + expense_objects = Expense.objects.filter(expense_id__in=[expense['id'] for expense in expenses]) + + assert len(expense_objects) == 3, f'Expected 3 expenses, got {len(expense_objects)}' + + # Test for SINGLE_LINE_ITEM split expense grouping + groups = ExpenseGroup.create_expense_groups_by_report_id_fund_source(expense_objects, workspace_id) + assert len(groups) == 3, f'Expected 3 groups, got {len(groups)}' + + # Test for MULTIPLE_LINE_ITEM split expense grouping + expense_group_settings.split_expense_grouping = 'MULTIPLE_LINE_ITEM' + expense_group_settings.save() + + groups = ExpenseGroup.create_expense_groups_by_report_id_fund_source(expense_objects, workspace_id) + assert len(groups) == 2, f'Expected 2 groups, got {len(groups)}' + + +def test_split_expense_grouping_pairs_of_same_ids(db, update_config_for_split_expense_grouping): + ''' + Test for grouping of 2 pairs of expenses with the same bank transaction ids + ''' + workspace_id = 1 + + # Update settings + general_settings = WorkspaceGeneralSettings.objects.get(workspace_id=workspace_id) + expense_group_settings = ExpenseGroupSettings.objects.get(workspace_id=workspace_id) + update_config_for_split_expense_grouping(general_settings, expense_group_settings) + + # Get reference to expense objects + expenses = data['ccc_split_expenses'][:4] + expenses[0]['bank_transaction_id'] = 'sample_1' + expenses[1]['bank_transaction_id'] = 'sample_1' + expenses[2]['bank_transaction_id'] = 'sample_2' + expenses[3]['bank_transaction_id'] = 'sample_2' + + Expense.create_expense_objects(expenses, workspace_id=workspace_id) + expense_objects = Expense.objects.filter(expense_id__in=[expense['id'] for expense in expenses]) + + assert len(expense_objects) == 4, f'Expected 4 expenses, got {len(expense_objects)}' + + # Test for SINGLE_LINE_ITEM split expense grouping + groups = ExpenseGroup.create_expense_groups_by_report_id_fund_source(expense_objects, workspace_id) + assert len(groups) == 4, f'Expected 4 groups, got {len(groups)}' + + # Test for MULTIPLE_LINE_ITEM split expense grouping + expense_group_settings.split_expense_grouping = 'MULTIPLE_LINE_ITEM' + expense_group_settings.save() + + groups = ExpenseGroup.create_expense_groups_by_report_id_fund_source(expense_objects, workspace_id) + assert len(groups) == 2, f'Expected 2 groups, got {len(groups)}' diff --git a/tests/test_workspaces/fixtures.py b/tests/test_workspaces/fixtures.py index b8ae01ed..3d58f13e 100644 --- a/tests/test_workspaces/fixtures.py +++ b/tests/test_workspaces/fixtures.py @@ -96,6 +96,7 @@ }, "paid_on_qbo": False, "payment_number": "P/2022/05/R/7", + "bank_transaction_id": None, } ], "workspace_schedule": { diff --git a/tests/test_workspaces/test_apis/test_clone_settings/fixtures.py b/tests/test_workspaces/test_apis/test_clone_settings/fixtures.py index 2add1b98..d24a8009 100644 --- a/tests/test_workspaces/test_apis/test_clone_settings/fixtures.py +++ b/tests/test_workspaces/test_apis/test_clone_settings/fixtures.py @@ -27,6 +27,7 @@ "ccc_export_date_type": "spent_at", "ccc_expense_state": "PAYMENT_PROCESSING", "import_card_credits": True, + "split_expense_grouping": "MULTIPLE_LINE_ITEM", }, "general_mappings": { "bank_account": {"id": "10", "name": "Visa"}, @@ -95,6 +96,7 @@ "reimbursable_export_date_type": "current_date", "ccc_expense_state": "PAID", "ccc_export_date_type": "spent_at", + "split_expense_grouping": "MULTIPLE_LINE_ITEM", }, "workspace_general_settings": { "reimbursable_expenses_object": "PURCHASE BILL", 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 df45bd54..2d9d2aba 100644 --- a/tests/test_workspaces/test_apis/test_export_settings/fixtures.py +++ b/tests/test_workspaces/test_apis/test_export_settings/fixtures.py @@ -7,6 +7,7 @@ "import_card_credits": True, "ccc_export_date_type": "", "expense_state": "", + "split_expense_grouping": "MULTIPLE_LINE_ITEM", }, "workspace_general_settings": { "reimbursable_expenses_object": "PURCHASE BILL", @@ -45,6 +46,7 @@ "ccc_export_date_type": "spent_at", "ccc_expense_state": "PAYMENT_PROCESSING", "import_card_credits": True, + "split_expense_grouping": "MULTIPLE_LINE_ITEM", }, "general_mappings": { "bank_account": {"id": "10", "name": "Visa"},