| Type | Name | Status | PK | FK | Columns | Index | Script | Diff Script |
|---|---|---|---|---|---|---|---|---|
| Table | module_permissions | Missing in Target |
|
|||||
| Table | contacts | Missing in Target |
|
|||||
| Table | countries | Missing in Target |
|
|||||
| Table | currencies | Missing in Target |
|
|||||
| Table | __EFMigrationsHistory | Missing in Target |
|
|||||
| Table | company_upis | Missing in Target |
|
|||||
| Table | company_users | Missing in Target |
|
|||||
| Table | customers | Missing in Target |
|
|||||
| Table | default_organization_users | Missing in Target |
|
|||||
| Table | departments | Missing in Target |
|
|||||
| Table | user_roles | Missing in Target |
|
|||||
| Table | description_templates | Missing in Target |
|
|||||
| Table | role_permissions | Missing in Target |
|
|||||
| Table | chart_of_accounts | Missing in Target |
|
|||||
| Table | companies | Missing in Target |
|
|||||
| Table | users | Missing in Target |
|
|||||
| Table | users_bk | Missing in Target |
|
|||||
| Table | v_finance_year_id | Missing in Target |
|
|||||
| Table | v_is_second_last_leaf | Missing in Target |
|
|||||
| Table | v_permission_id | Missing in Target |
|
|||||
| Table | user_permissions | Missing in Target |
|
|||||
| Table | account_categories | Missing in Target |
|
|||||
| Table | account_groups | Missing in Target |
|
|||||
| Table | document_meta_datas | Missing in Target |
|
|||||
| Table | account_opening_balances | Missing in Target |
|
|||||
| Table | bank_reconciliation_links | Missing in Target |
|
|||||
| Table | bank_transfers | Missing in Target |
|
|||||
| Table | journal_voucher_details | Missing in Target |
|
|||||
| Table | schema_versions | Missing in Target |
|
|||||
| Table | states | Missing in Target |
|
|||||
| Table | temp_organization_logs | Missing in Target |
|
|||||
| Table | temp_paymnet_data | Missing in Target |
|
|||||
| Table | bank_reconciliation_stagings | Missing in Target |
|
|||||
| Table | payment_references | Missing in Target |
|
|||||
| Table | journal_narrations | Missing in Target |
|
|||||
| Table | organization_users | Missing in Target |
|
|||||
| Table | transaction_header_2024_2025 | Missing in Target |
|
|||||
| Table | bank_transfer_ids | Missing in Target |
|
|||||
| Table | banks | Missing in Target |
|
|||||
| Table | budget_lines | Missing in Target |
|
|||||
| Table | budget_statuses | Missing in Target |
|
|||||
| Table | budget_workflow | Missing in Target |
|
|||||
| Table | budgets | Missing in Target |
|
|||||
| Table | cities | Missing in Target |
|
|||||
| Table | cron_dummy_log | Missing in Target |
|
|||||
| Table | permission_groups | Missing in Target |
|
|||||
| Table | company_bank_accounts | Missing in Target |
|
|||||
| Table | company_contacts | Missing in Target |
|
|||||
| Table | company_finance_year | Missing in Target |
|
|||||
| Table | journal_entries | Missing in Target |
|
|||||
| Table | warehouses | Missing in Target |
|
|||||
| Table | transaction_header_2022_2023 | Missing in Target |
|
|||||
| Table | transaction_header_2023_2024 | Missing in Target |
|
|||||
| Table | bank_reconciliations | Missing in Target |
|
|||||
| Table | feature_toggles | Missing in Target |
|
|||||
| Table | user_permissions_backup | Missing in Target |
|
|||||
| Table | journal_entries_2022_2023 | Missing in Target |
|
|||||
| Table | feature_toggle_overrides | Missing in Target |
|
|||||
| Table | user_permission_sync_runs | Missing in Target |
|
|||||
| Table | user_permission_sync_deltas | Missing in Target |
|
|||||
| Table | scope_types | Missing in Target |
|
|||||
| Table | journal_entries_2023_2024 | Missing in Target |
|
|||||
| Table | journal_entries_2024_2025 | Missing in Target |
|
|||||
| Table | journal_entries_2025_2026 | Missing in Target |
|
|||||
| Table | journal_voucher_header_id | Missing in Target |
|
|||||
| Table | journal_voucher_headers | Missing in Target |
|
|||||
| Table | actions | Missing in Target |
|
|||||
| Table | feature_toggle_audits | Missing in Target |
|
|||||
| Table | roles | Missing in Target |
|
|||||
| Table | opening_balances | Missing in Target |
|
|||||
| Table | organization_accounts | Missing in Target |
|
|||||
| Table | bank_statements | Missing in Target |
|
|||||
| Table | account_types | Missing in Target |
|
|||||
| Table | addresses | Missing in Target |
|
|||||
| Table | audit_queries | Missing in Target |
|
|||||
| Table | audit_query_responses | Missing in Target |
|
|||||
| Table | audit_query_statuses | Missing in Target |
|
|||||
| Table | bank_accounts | Missing in Target |
|
|||||
| Table | transaction_header_2025_2026 | Missing in Target |
|
|||||
| Table | transaction_headers | Missing in Target |
|
|||||
| Table | logs | Missing in Target |
|
|||||
| Table | messages | Missing in Target |
|
|||||
| Table | notification_types | Missing in Target |
|
|||||
| Table | notifications | Missing in Target |
|
|||||
| Table | organization_types | Missing in Target |
|
|||||
| Table | organizations | Missing in Target |
|
|||||
| Table | permissions | Missing in Target |
|
|||||
| Table | reconciliation_statuses | Missing in Target |
|
|||||
| Table | transaction_headers_19_05_25 | Missing in Target |
|
|||||
| Table | transaction_headers_bk_20_5_25 | Missing in Target |
|
|||||
| Table | vendors | Missing in Target |
|
|||||
| Table | transaction_source_types | Missing in Target |
|
|||||
| Table | upis | Missing in Target |
|
|||||
| Table | user_deletion_request_statuses | Missing in Target |
|
|||||
| Table | user_deletion_requests | Missing in Target |
|
|||||
| Table | user_global_permissions | Missing in Target |
|
|||||
| Table | company_preferences | Missing in Target |
|
|||||
| Table | designations | Missing in Target |
|
|||||
| Table | dummy_log_table | Missing in Target |
|
|||||
| Table | dummy_test_table | Missing in Target |
|
|||||
| Table | email_templates | Missing in Target |
|
|||||
| Table | employees | Missing in Target |
|
|||||
| Table | entry_sources | Missing in Target |
|
|||||
| Table | finance_year | Missing in Target |
|
|||||
| Table | fixed_deposits | Missing in Target |
|
|||||
| Table | general_ledgers | Missing in Target |
|
|||||
| Table | images | Missing in Target |
|
|||||
| Function | pgp_sym_decrypt_bytea | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.pgp_sym_decrypt_bytea(bytea, text, text)
2
RETURNS bytea
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pgp_sym_decrypt_bytea$function$
|
|||||
| Function | pgp_pub_encrypt | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.pgp_pub_encrypt(text, bytea)
2
RETURNS bytea
3
LANGUAGE c
4
PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pgp_pub_encrypt_text$function$
|
|||||
| Function | pgp_pub_encrypt_bytea | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.pgp_pub_encrypt_bytea(bytea, bytea)
2
RETURNS bytea
3
LANGUAGE c
4
PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pgp_pub_encrypt_bytea$function$
|
|||||
| Function | pgp_pub_encrypt | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.pgp_pub_encrypt(text, bytea, text)
2
RETURNS bytea
3
LANGUAGE c
4
PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pgp_pub_encrypt_text$function$
|
|||||
| Function | pgp_pub_decrypt | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.pgp_pub_decrypt(bytea, bytea, text)
2
RETURNS text
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pgp_pub_decrypt_text$function$
|
|||||
| Function | pgp_pub_decrypt_bytea | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.pgp_pub_decrypt_bytea(bytea, bytea, text)
2
RETURNS bytea
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pgp_pub_decrypt_bytea$function$
|
|||||
| Function | pgp_pub_decrypt | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.pgp_pub_decrypt(bytea, bytea, text, text)
2
RETURNS text
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pgp_pub_decrypt_text$function$
|
|||||
| Function | pgp_pub_decrypt_bytea | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.pgp_pub_decrypt_bytea(bytea, bytea, text, text)
2
RETURNS bytea
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pgp_pub_decrypt_bytea$function$
|
|||||
| Function | pgp_key_id | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.pgp_key_id(bytea)
2
RETURNS text
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pgp_key_id_w$function$
|
|||||
| Function | armor | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.armor(bytea)
2
RETURNS text
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pg_armor$function$
|
|||||
| Function | armor | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.armor(bytea, text[], text[])
2
RETURNS text
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pg_armor$function$
|
|||||
| Function | dearmor | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dearmor(text)
2
RETURNS bytea
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pg_dearmor$function$
|
|||||
| Function | pgp_armor_headers | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.pgp_armor_headers(text, OUT key text, OUT value text)
2
RETURNS SETOF record
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pgp_armor_headers$function$
|
|||||
| Function | fips_mode | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.fips_mode()
2
RETURNS boolean
3
LANGUAGE c
4
PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pg_check_fipsmode$function$
|
|||||
| Function | uuid_nil | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.uuid_nil()
2
RETURNS uuid
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/uuid-ossp', $function$uuid_nil$function$
|
|||||
| Function | create_user | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.create_user(p_user_id uuid, p_company_id uuid, p_email text, p_phone_number text, p_first_name text, p_last_name text, p_address_id uuid, p_created_by uuid)
2
RETURNS uuid
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_existing_user_id UUID; -- Variable to store the user ID
7
BEGIN
8
-- Step 1: Check if the user already exists based on email
9
SELECT id INTO v_existing_user_id
10
FROM public.users
11
WHERE email = p_email;
12
13
-- If user already exists, return the existing user_id
14
IF v_existing_user_id IS NOT NULL THEN
15
RETURN v_existing_user_id;
16
END IF;
17
18
-- Step 2: Insert into users table
19
INSERT INTO public.users (
20
id,
21
company_id,
22
email,
23
phone_number,
24
first_name,
25
last_name,
26
password_hash,
27
address_id,
28
created_on_utc,
29
created_by
30
) VALUES (
31
p_user_id, -- Generate a new UUID for the user ID
32
p_company_id,
33
p_email, -- User email
34
p_phone_number, -- User phone number
35
p_first_name, -- User first name
36
p_last_name, -- Default empty last name
37
'', -- Default empty password hash
38
p_address_id, -- Provided Address ID
39
NOW(), -- Current timestamp
40
p_created_by -- Created by
41
);
42
43
-- Return the new user ID
44
RETURN p_user_id;
45
END;
46
$function$
|
|||||
| Function | uuid_ns_dns | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.uuid_ns_dns()
2
RETURNS uuid
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/uuid-ossp', $function$uuid_ns_dns$function$
|
|||||
| Function | uuid_ns_url | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.uuid_ns_url()
2
RETURNS uuid
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/uuid-ossp', $function$uuid_ns_url$function$
|
|||||
| Function | uuid_ns_oid | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.uuid_ns_oid()
2
RETURNS uuid
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/uuid-ossp', $function$uuid_ns_oid$function$
|
|||||
| Function | uuid_ns_x500 | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.uuid_ns_x500()
2
RETURNS uuid
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/uuid-ossp', $function$uuid_ns_x500$function$
|
|||||
| Function | uuid_generate_v1 | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.uuid_generate_v1()
2
RETURNS uuid
3
LANGUAGE c
4
PARALLEL SAFE STRICT
5
AS '$libdir/uuid-ossp', $function$uuid_generate_v1$function$
|
|||||
| Function | uuid_generate_v1mc | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.uuid_generate_v1mc()
2
RETURNS uuid
3
LANGUAGE c
4
PARALLEL SAFE STRICT
5
AS '$libdir/uuid-ossp', $function$uuid_generate_v1mc$function$
|
|||||
| Function | uuid_generate_v3 | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.uuid_generate_v3(namespace uuid, name text)
2
RETURNS uuid
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/uuid-ossp', $function$uuid_generate_v3$function$
|
|||||
| Function | uuid_generate_v4 | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.uuid_generate_v4()
2
RETURNS uuid
3
LANGUAGE c
4
PARALLEL SAFE STRICT
5
AS '$libdir/uuid-ossp', $function$uuid_generate_v4$function$
|
|||||
| Function | uuid_generate_v5 | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.uuid_generate_v5(namespace uuid, name text)
2
RETURNS uuid
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/uuid-ossp', $function$uuid_generate_v5$function$
|
|||||
| Function | generate_permission_description | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.generate_permission_description(p_name text)
2
RETURNS text
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
parts text[];
7
module text;
8
tail text[];
9
action text;
10
object_tokens text[];
11
object_raw text;
12
object_phrase text;
13
is_myspace boolean := false;
14
15
-- scratch vars for “humanize” and “pluralize”
16
t text;
17
obj_lc text;
18
BEGIN
19
-- Expect Dhanman.<Module>...
20
parts := regexp_split_to_array(p_name, '\.');
21
IF array_length(parts, 1) IS NULL OR parts[1] <> 'Dhanman' THEN
22
RETURN NULL; -- not our namespace
23
END IF;
24
25
module := parts[2];
26
is_myspace := (module = 'MySpace');
27
28
IF array_length(parts, 1) < 3 THEN
29
-- e.g., Dhanman.Read / Dhanman.Write / Dhanman.Delete
30
RETURN trim(both ' ' FROM regexp_replace(module, '([a-z])([A-Z])', '\1 \2', 'g')) || ' permission';
31
END IF;
32
33
tail := parts[3:array_length(parts,1)]; -- everything after module
34
35
-- detect multi-word actions first
36
IF tail[array_length(tail,1)] = 'SendForApproval' THEN
37
action := 'SendForApproval';
38
IF array_length(tail,1) > 1 THEN
39
object_tokens := tail[1:array_length(tail,1)-1];
40
ELSE
41
object_tokens := ARRAY[]::text[];
42
END IF;
43
44
ELSIF tail[array_length(tail,1)] IN ('IdRead','TokenRead') THEN
45
action := 'Read';
46
IF array_length(tail,1) > 1 THEN
47
object_tokens := tail[1:array_length(tail,1)-1];
48
ELSE
49
object_tokens := ARRAY[]::text[];
50
END IF;
51
52
ELSE
53
-- simple actions (last token)
54
IF tail[array_length(tail,1)] IN ('Read','Write','Delete','Approve','Cancel','Reject','Resolve','Assign','Copy','Pay','Import') THEN
55
action := tail[array_length(tail,1)];
56
IF array_length(tail,1) > 1 THEN
57
object_tokens := tail[1:array_length(tail,1)-1];
58
ELSE
59
object_tokens := ARRAY[]::text[];
60
END IF;
61
ELSE
62
-- No explicit action => treat the last token as (maybe) object and assume base Read on module
63
action := NULL;
64
object_tokens := tail;
65
END IF;
66
END IF;
67
68
-- join tokens with '.' to get a raw object handle
69
object_raw := NULL;
70
IF object_tokens IS NOT NULL AND array_length(object_tokens,1) IS NOT NULL THEN
71
SELECT string_agg(x, '.' ORDER BY ord)
72
INTO object_raw
73
FROM (
74
SELECT object_tokens[i] AS x, i AS ord
75
FROM generate_subscripts(object_tokens,1) AS i
76
) s;
77
END IF;
78
79
-- If action is NULL, try to interpret tail as a “Read/Write/Delete” on module (rare fallback)
80
IF action IS NULL THEN
81
IF object_raw ILIKE 'Read' THEN
82
t := trim(both ' ' FROM regexp_replace(module, '([a-z])([A-Z])', '\1 \2', 'g'));
83
RETURN 'Read ' || lower(t) || ' data';
84
ELSIF object_raw ILIKE 'Write' THEN
85
t := trim(both ' ' FROM regexp_replace(module, '([a-z])([A-Z])', '\1 \2', 'g'));
86
RETURN 'Add/Update ' || lower(t) || ' data';
87
ELSIF object_raw ILIKE 'Delete' THEN
88
RETURN 'Delete general data';
89
END IF;
90
-- else continue with generic handling below
91
END IF;
92
93
-- ===== normalize object to a nicer phrase =====
94
object_phrase := NULL;
95
IF object_raw IS NOT NULL AND object_raw <> '' THEN
96
obj_lc := lower(object_raw);
97
98
-- specific aliases
99
IF obj_lc ~* '^basic(\.read|\.write|\.delete)?$' THEN
100
object_phrase := 'Basic Info';
101
ELSIF obj_lc LIKE 'ticketstatus%' THEN
102
object_phrase := 'Ticket Status';
103
ELSIF obj_lc = 'invoicetemplate' THEN
104
object_phrase := 'Invoice Template';
105
ELSIF obj_lc = 'groupedinvoice' THEN
106
object_phrase := 'Grouped Invoice';
107
ELSIF obj_lc = 'recurringinvoice' THEN
108
object_phrase := 'Recurring Invoice';
109
ELSIF obj_lc = 'customeraccountconfig' THEN
110
object_phrase := 'Customer Account Configuration';
111
ELSIF obj_lc = 'vendoraccountconfig' THEN
112
object_phrase := 'Vendor Account Configuration';
113
ELSIF obj_lc = 'bankstatement' THEN
114
object_phrase := 'Bank Statement';
115
ELSIF obj_lc = 'banktransfer' THEN
116
object_phrase := 'Bank Transfer';
117
ELSIF obj_lc = 'fixeddeposit' THEN
118
object_phrase := 'Fixed Deposit';
119
ELSE
120
-- generic humanize: add spaces before capitals
121
object_phrase := trim(both ' ' FROM regexp_replace(object_raw, '([a-z])([A-Z])', '\1 \2', 'g'));
122
END IF;
123
END IF;
124
125
-- quick helpers inlined:
126
-- humanize(module)
127
t := trim(both ' ' FROM regexp_replace(module, '([a-z])([A-Z])', '\1 \2', 'g'));
128
-- module phrase
129
t := t || ' module';
130
131
-- MySpace special-casing (root objects)
132
IF is_myspace AND (object_phrase IS NULL OR object_phrase = '') THEN
133
IF action = 'Read' THEN
134
RETURN 'View own My Space data. (myspace)';
135
ELSIF action = 'Write' THEN
136
RETURN 'Create or update own data. (myspace)';
137
ELSIF action = 'Delete' THEN
138
RETURN 'Delete own entries. (myspace)';
139
END IF;
140
END IF;
141
142
-- pluralize (very naive) if we need a plural in messages
143
-- compute plural into obj_lc (reuse var)
144
IF object_phrase IS NOT NULL AND object_phrase <> '' THEN
145
obj_lc := lower(object_phrase);
146
-- don’t pluralize these:
147
IF obj_lc NOT IN ('info','data','ledger','cash flow','journal','basicinfo','basic info') THEN
148
IF obj_lc !~ '(s|x|z|ch|sh)$' THEN
149
object_phrase := object_phrase || 's';
150
END IF;
151
END IF;
152
END IF;
153
154
-- Render phrases by action
155
IF is_myspace THEN
156
-- MySpace + specific objects
157
IF action = 'Read' AND object_phrase IS NOT NULL THEN
158
IF lower(object_phrase) IN ('user', 'users') THEN
159
RETURN 'View user (own) details. (myspace)';
160
ELSIF lower(object_phrase) IN ('due','dues') THEN
161
RETURN 'View dues summary. (myspace)';
162
ELSE
163
RETURN 'View own ' || lower(object_phrase) || '. (myspace)';
164
END IF;
165
END IF;
166
-- For other MySpace actions on specific objects, fall through to generic wording below
167
END IF;
168
169
-- rebuild a fresh humanized module text for generic lines
170
t := trim(both ' ' FROM regexp_replace(module, '([a-z])([A-Z])', '\1 \2', 'g'));
171
t := t || ' module';
172
173
CASE action
174
WHEN 'Read' THEN
175
IF object_phrase IS NULL OR object_phrase = '' THEN
176
RETURN 'Read ' || lower(trim(both ' ' FROM regexp_replace(module, '([a-z])([A-Z])', '\1 \2', 'g'))) || ' data';
177
ELSE
178
RETURN 'Read ' || lower(object_phrase) || ' in ' || t;
179
END IF;
180
181
WHEN 'Write' THEN
182
IF object_phrase IS NULL OR object_phrase = '' THEN
183
RETURN 'Add/Update ' || lower(trim(both ' ' FROM regexp_replace(module, '([a-z])([A-Z])', '\1 \2', 'g'))) || ' data';
184
ELSE
185
RETURN 'Add/Update ' || lower(object_phrase) || ' in ' || t;
186
END IF;
187
188
WHEN 'Delete' THEN
189
IF object_phrase IS NULL OR object_phrase = '' THEN
190
RETURN 'Delete general data';
191
ELSE
192
RETURN 'Delete ' || lower(object_phrase) || ' in ' || t;
193
END IF;
194
195
WHEN 'Approve' THEN
196
IF object_phrase IS NULL OR object_phrase = '' THEN
197
RETURN 'Approve ' || lower(trim(both ' ' FROM regexp_replace(module, '([a-z])([A-Z])', '\1 \2', 'g'))) || ' actions';
198
ELSE
199
RETURN 'Approve ' || lower(object_phrase) || ' in the ' || t;
200
END IF;
201
202
WHEN 'Cancel' THEN
203
RETURN 'Cancel ' || lower(COALESCE(object_phrase,'items')) || ' in ' || t;
204
205
WHEN 'Reject' THEN
206
RETURN 'Reject ' || lower(COALESCE(object_phrase,'items')) || ' in ' || t;
207
208
WHEN 'Resolve' THEN
209
RETURN 'Mark ' || lower(COALESCE(object_phrase,'items')) || ' as resolved in the ' || t;
210
211
WHEN 'Assign' THEN
212
RETURN 'Assign ' || lower(COALESCE(object_phrase,'items')) || ' in the ' || t;
213
214
WHEN 'Copy' THEN
215
RETURN 'Copy ' || lower(COALESCE(object_phrase,'items')) || ' in ' || t;
216
217
WHEN 'Pay' THEN
218
RETURN 'Make payments for ' || lower(COALESCE(object_phrase,'items'));
219
220
WHEN 'SendForApproval' THEN
221
RETURN 'Send ' || lower(COALESCE(object_phrase,'items')) || ' for approval';
222
223
WHEN 'Import' THEN
224
RETURN 'Import ' || lower(COALESCE(object_phrase,'items')) || ' data';
225
226
ELSE
227
-- Fallback
228
IF object_phrase IS NULL OR object_phrase = '' THEN
229
RETURN trim(both ' ' FROM regexp_replace(module, '([a-z])([A-Z])', '\1 \2', 'g')) || ' permission';
230
ELSE
231
RETURN trim(both ' ' FROM regexp_replace(module, '([a-z])([A-Z])', '\1 \2', 'g'))
232
|| ' ' || trim(both ' ' FROM regexp_replace(object_phrase, '([a-z])([A-Z])', '\1 \2', 'g'))
233
|| ' permission';
234
END IF;
235
END CASE;
236
END;
237
$function$
|
|||||
| Function | get_account_expense_monthly_breakdown | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_account_expense_monthly_breakdown(p_company_id uuid, p_finance_year_id integer)
2
RETURNS TABLE(account_id uuid, account_name text, year integer, apr numeric, apr_diff numeric, may numeric, may_diff numeric, jun numeric, jun_diff numeric, jul numeric, jul_diff numeric, aug numeric, aug_diff numeric, sep numeric, sep_diff numeric, oct numeric, oct_diff numeric, nov numeric, nov_diff numeric, "dec" numeric, dec_diff numeric, jan numeric, jan_diff numeric, feb numeric, feb_diff numeric, mar numeric, mar_diff numeric, total numeric, difference numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_organization_id UUID;
7
v_start_date DATE;
8
v_end_date DATE;
9
v_prev_start_date DATE;
10
v_prev_end_date DATE;
11
BEGIN
12
-- Get organization and financial year details
13
SELECT c.organization_id INTO v_organization_id FROM public.companies c WHERE c.id = p_company_id;
14
SELECT fy.start_date, fy.end_date INTO v_start_date, v_end_date FROM public.finance_year fy WHERE fy.id = p_finance_year_id;
15
SELECT pfy.start_date, pfy.end_date INTO v_prev_start_date, v_prev_end_date FROM public.finance_year pfy WHERE pfy.id = (p_finance_year_id - 1);
16
17
RETURN QUERY
18
WITH expense_accounts AS (
19
SELECT DISTINCT coa.id AS account_id, coa.name
20
FROM chart_of_accounts coa
21
JOIN account_types at ON coa.account_type_id = at.id
22
WHERE at.id IN (SELECT id FROM get_account_type_hierarchy(5)) AND coa.organization_id = v_organization_id
23
),
24
years AS (
25
SELECT p_finance_year_id AS year UNION ALL SELECT p_finance_year_id - 1
26
),
27
all_accounts AS (
28
SELECT ea.account_id, ea.name, y.year
29
FROM expense_accounts ea CROSS JOIN years y
30
),
31
monthly_expenses AS (
32
-- Get monthly totals, excluding opening balance entries
33
SELECT
34
je.account_id,
35
CASE
36
WHEN je.transaction_date BETWEEN v_start_date AND v_end_date THEN p_finance_year_id
37
WHEN je.transaction_date BETWEEN v_prev_start_date AND v_prev_end_date THEN (p_finance_year_id - 1)
38
END AS year,
39
EXTRACT(MONTH FROM je.transaction_date) AS month,
40
SUM(je.amount) AS amount
41
FROM journal_entries je
42
JOIN transaction_headers th ON je.transaction_id = th.id
43
WHERE th.company_id = p_company_id
44
AND je.transaction_date BETWEEN v_prev_start_date AND v_end_date
45
AND th.transaction_source_type != 18
46
AND th.is_deleted = FALSE
47
AND je.is_deleted = FALSE
48
GROUP BY je.account_id, year, EXTRACT(MONTH FROM je.transaction_date)
49
),
50
pivoted_expenses AS (
51
-- Pivot data to month-wise columns
52
SELECT
53
aa.account_id, aa.name AS account_name, aa.year,
54
COALESCE(SUM(CASE WHEN me.month = 4 THEN me.amount ELSE 0 END), 0) AS apr,
55
COALESCE(SUM(CASE WHEN me.month = 5 THEN me.amount ELSE 0 END), 0) AS may,
56
COALESCE(SUM(CASE WHEN me.month = 6 THEN me.amount ELSE 0 END), 0) AS jun,
57
COALESCE(SUM(CASE WHEN me.month = 7 THEN me.amount ELSE 0 END), 0) AS jul,
58
COALESCE(SUM(CASE WHEN me.month = 8 THEN me.amount ELSE 0 END), 0) AS aug,
59
COALESCE(SUM(CASE WHEN me.month = 9 THEN me.amount ELSE 0 END), 0) AS sep,
60
COALESCE(SUM(CASE WHEN me.month = 10 THEN me.amount ELSE 0 END), 0) AS oct,
61
COALESCE(SUM(CASE WHEN me.month = 11 THEN me.amount ELSE 0 END), 0) AS nov,
62
COALESCE(SUM(CASE WHEN me.month = 12 THEN me.amount ELSE 0 END), 0) AS "dec",
63
COALESCE(SUM(CASE WHEN me.month = 1 THEN me.amount ELSE 0 END), 0) AS jan,
64
COALESCE(SUM(CASE WHEN me.month = 2 THEN me.amount ELSE 0 END), 0) AS feb,
65
COALESCE(SUM(CASE WHEN me.month = 3 THEN me.amount ELSE 0 END), 0) AS mar,
66
COALESCE(SUM(me.amount), 0) AS total
67
FROM all_accounts aa
68
LEFT JOIN monthly_expenses me ON aa.account_id = me.account_id AND aa.year = me.year
69
GROUP BY aa.account_id, aa.name, aa.year
70
)
71
-- Final table with difference calculation
72
SELECT
73
pe.account_id, pe.account_name, pe.year,
74
pe.apr, COALESCE(pe.apr - LAG(pe.apr) OVER (PARTITION BY pe.account_name ORDER BY pe.year), 0) AS apr_diff,
75
pe.may, COALESCE(pe.may - LAG(pe.may) OVER (PARTITION BY pe.account_name ORDER BY pe.year), 0) AS may_diff,
76
pe.jun, COALESCE(pe.jun - LAG(pe.jun) OVER (PARTITION BY pe.account_name ORDER BY pe.year), 0) AS jun_diff,
77
pe.jul, COALESCE(pe.jul - LAG(pe.jul) OVER (PARTITION BY pe.account_name ORDER BY pe.year), 0) AS jul_diff,
78
pe.aug, COALESCE(pe.aug - LAG(pe.aug) OVER (PARTITION BY pe.account_name ORDER BY pe.year), 0) AS aug_diff,
79
pe.sep, COALESCE(pe.sep - LAG(pe.sep) OVER (PARTITION BY pe.account_name ORDER BY pe.year), 0) AS sep_diff,
80
pe.oct, COALESCE(pe.oct - LAG(pe.oct) OVER (PARTITION BY pe.account_name ORDER BY pe.year), 0) AS oct_diff,
81
pe.nov, COALESCE(pe.nov - LAG(pe.nov) OVER (PARTITION BY pe.account_name ORDER BY pe.year), 0) AS nov_diff,
82
pe.dec, COALESCE(pe.dec - LAG(pe.dec) OVER (PARTITION BY pe.account_name ORDER BY pe.year), 0) AS dec_diff,
83
pe.jan, COALESCE(pe.jan - LAG(pe.jan) OVER (PARTITION BY pe.account_name ORDER BY pe.year), 0) AS jan_diff,
84
pe.feb, COALESCE(pe.feb - LAG(pe.feb) OVER (PARTITION BY pe.account_name ORDER BY pe.year), 0) AS feb_diff,
85
pe.mar, COALESCE(pe.mar - LAG(pe.mar) OVER (PARTITION BY pe.account_name ORDER BY pe.year), 0) AS mar_diff,
86
pe.total,
87
COALESCE(pe.total - LAG(pe.total) OVER (PARTITION BY pe.account_name ORDER BY pe.year), 0) AS difference
88
FROM pivoted_expenses pe
89
ORDER BY pe.account_name, pe.year;
90
END;
91
$function$
|
|||||
| Function | get_permissions | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_permissions()
2
RETURNS TABLE(id uuid, name text, parent_permission_id uuid, tree_name text, description text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
WITH RECURSIVE permissions_cte AS (
8
SELECT
9
p.id,
10
p.name,
11
p.parent_permission_id,
12
0 AS level,
13
p.name::text AS tree_name,
14
p.name::text AS sort_key,
15
p.description
16
FROM
17
public.permissions p
18
WHERE
19
p.parent_permission_id = '00000000-0000-0000-0000-000000000000'
20
21
UNION ALL
22
23
SELECT
24
p.id,
25
p.name,
26
p.parent_permission_id,
27
cte.level + 1 AS level,
28
(LPAD('', (cte.level + 1) * 4, ' ') || p.name::text)::text AS tree_name,
29
(cte.sort_key || ' > ' || p.name::text)::text AS sort_key,
30
p.description
31
FROM
32
public.permissions p
33
INNER JOIN
34
permissions_cte cte ON cte.id = p.parent_permission_id
35
)
36
SELECT
37
cte.id,
38
cte.name,
39
cte.parent_permission_id,
40
cte.tree_name::text,
41
cte.description
42
FROM
43
permissions_cte cte
44
ORDER BY
45
cte.sort_key;
46
END;
47
$function$
|
|||||
| Function | get_five_years_expenses | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_five_years_expenses(p_company_id uuid, p_finance_year_id integer)
2
RETURNS TABLE(id uuid, financial_year_range text, account_name text, y1_apr numeric, y1_may numeric, y1_jun numeric, y1_jul numeric, y1_aug numeric, y1_sep numeric, y1_oct numeric, y1_nov numeric, y1_dec numeric, y1_jan numeric, y1_feb numeric, y1_mar numeric, y2_apr numeric, y2_may numeric, y2_jun numeric, y2_jul numeric, y2_aug numeric, y2_sep numeric, y2_oct numeric, y2_nov numeric, y2_dec numeric, y2_jan numeric, y2_feb numeric, y2_mar numeric, y3_apr numeric, y3_may numeric, y3_jun numeric, y3_jul numeric, y3_aug numeric, y3_sep numeric, y3_oct numeric, y3_nov numeric, y3_dec numeric, y3_jan numeric, y3_feb numeric, y3_mar numeric, y4_apr numeric, y4_may numeric, y4_jun numeric, y4_jul numeric, y4_aug numeric, y4_sep numeric, y4_oct numeric, y4_nov numeric, y4_dec numeric, y4_jan numeric, y4_feb numeric, y4_mar numeric, y5_apr numeric, y5_may numeric, y5_jun numeric, y5_jul numeric, y5_aug numeric, y5_sep numeric, y5_oct numeric, y5_nov numeric, y5_dec numeric, y5_jan numeric, y5_feb numeric, y5_mar numeric)
3
LANGUAGE plpgsql
4
STABLE PARALLEL SAFE
5
AS $function$
6
DECLARE
7
current_year_start DATE;
8
current_year_end DATE;
9
previous_year1_start DATE;
10
previous_year1_end DATE;
11
previous_year2_start DATE;
12
previous_year2_end DATE;
13
previous_year3_start DATE;
14
previous_year3_end DATE;
15
previous_year4_start DATE;
16
previous_year4_end DATE;
17
CONST_EXPENSE_TYPE_ID INTEGER := 5;
18
BEGIN
19
-- Get start/end of current year
20
SELECT fy.start_date, fy.end_date
21
INTO current_year_start, current_year_end
22
FROM public.finance_year fy
23
WHERE fy.id = p_finance_year_id;
24
25
IF current_year_start IS NULL OR current_year_end IS NULL THEN
26
RAISE EXCEPTION 'Financial year with ID % not found', p_finance_year_id;
27
END IF;
28
29
-- Previous year 1
30
SELECT fy.start_date, fy.end_date
31
INTO previous_year1_start, previous_year1_end
32
FROM public.finance_year fy
33
WHERE fy.end_date < current_year_start
34
ORDER BY fy.end_date DESC
35
LIMIT 1;
36
37
-- Previous year 2
38
SELECT fy.start_date, fy.end_date
39
INTO previous_year2_start, previous_year2_end
40
FROM public.finance_year fy
41
WHERE fy.end_date < previous_year1_start
42
ORDER BY fy.end_date DESC
43
LIMIT 1;
44
45
-- Previous year 3
46
SELECT fy.start_date, fy.end_date
47
INTO previous_year3_start, previous_year3_end
48
FROM public.finance_year fy
49
WHERE fy.end_date < previous_year2_start
50
ORDER BY fy.end_date DESC
51
LIMIT 1;
52
53
-- Previous year 4
54
SELECT fy.start_date, fy.end_date
55
INTO previous_year4_start, previous_year4_end
56
FROM public.finance_year fy
57
WHERE fy.end_date < previous_year3_start
58
ORDER BY fy.end_date DESC
59
LIMIT 1;
60
61
RETURN QUERY
62
WITH RECURSIVE AccountHierarchy AS (
63
SELECT coa.id AS account_id, coa.name, coa.parent_account_id
64
FROM public.chart_of_accounts coa
65
WHERE coa.account_type_id = CONST_EXPENSE_TYPE_ID
66
UNION ALL
67
SELECT coa.id, coa.name, coa.parent_account_id
68
FROM public.chart_of_accounts coa
69
INNER JOIN AccountHierarchy ah ON coa.parent_account_id = ah.account_id
70
),
71
MonthlyExpenses AS (
72
SELECT
73
ah.name AS account_name,
74
TO_CHAR(je.transaction_date, 'MM') AS transaction_month,
75
CASE
76
WHEN je.transaction_date BETWEEN previous_year4_start AND previous_year3_start - INTERVAL '1 day' THEN 'y1'
77
WHEN je.transaction_date BETWEEN previous_year3_start AND previous_year2_start - INTERVAL '1 day' THEN 'y2'
78
WHEN je.transaction_date BETWEEN previous_year2_start AND previous_year1_start - INTERVAL '1 day' THEN 'y3'
79
WHEN je.transaction_date BETWEEN previous_year1_start AND current_year_start - INTERVAL '1 day' THEN 'y4'
80
WHEN je.transaction_date BETWEEN current_year_start AND current_year_end THEN 'y5'
81
END AS year_group,
82
SUM(je.amount) AS monthly_amount
83
FROM AccountHierarchy ah
84
LEFT JOIN public.journal_entries je ON je.account_id = ah.account_id
85
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
86
WHERE th.company_id = p_company_id
87
AND je.transaction_date BETWEEN previous_year4_start AND current_year_end
88
AND je.is_deleted = FALSE
89
GROUP BY ah.name, year_group, TO_CHAR(je.transaction_date, 'MM')
90
),
91
AggregatedExpenses AS (
92
SELECT
93
me.account_name,
94
-- y1
95
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '04' THEN me.monthly_amount ELSE 0 END) AS y1_apr,
96
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '05' THEN me.monthly_amount ELSE 0 END) AS y1_may,
97
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '06' THEN me.monthly_amount ELSE 0 END) AS y1_jun,
98
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '07' THEN me.monthly_amount ELSE 0 END) AS y1_jul,
99
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '08' THEN me.monthly_amount ELSE 0 END) AS y1_aug,
100
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '09' THEN me.monthly_amount ELSE 0 END) AS y1_sep,
101
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '10' THEN me.monthly_amount ELSE 0 END) AS y1_oct,
102
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '11' THEN me.monthly_amount ELSE 0 END) AS y1_nov,
103
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '12' THEN me.monthly_amount ELSE 0 END) AS y1_dec,
104
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '01' THEN me.monthly_amount ELSE 0 END) AS y1_jan,
105
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '02' THEN me.monthly_amount ELSE 0 END) AS y1_feb,
106
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '03' THEN me.monthly_amount ELSE 0 END) AS y1_mar,
107
-- y2
108
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '04' THEN me.monthly_amount ELSE 0 END) AS y2_apr,
109
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '05' THEN me.monthly_amount ELSE 0 END) AS y2_may,
110
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '06' THEN me.monthly_amount ELSE 0 END) AS y2_jun,
111
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '07' THEN me.monthly_amount ELSE 0 END) AS y2_jul,
112
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '08' THEN me.monthly_amount ELSE 0 END) AS y2_aug,
113
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '09' THEN me.monthly_amount ELSE 0 END) AS y2_sep,
114
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '10' THEN me.monthly_amount ELSE 0 END) AS y2_oct,
115
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '11' THEN me.monthly_amount ELSE 0 END) AS y2_nov,
116
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '12' THEN me.monthly_amount ELSE 0 END) AS y2_dec,
117
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '01' THEN me.monthly_amount ELSE 0 END) AS y2_jan,
118
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '02' THEN me.monthly_amount ELSE 0 END) AS y2_feb,
119
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '03' THEN me.monthly_amount ELSE 0 END) AS y2_mar,
120
-- y3
121
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '04' THEN me.monthly_amount ELSE 0 END) AS y3_apr,
122
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '05' THEN me.monthly_amount ELSE 0 END) AS y3_may,
123
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '06' THEN me.monthly_amount ELSE 0 END) AS y3_jun,
124
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '07' THEN me.monthly_amount ELSE 0 END) AS y3_jul,
125
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '08' THEN me.monthly_amount ELSE 0 END) AS y3_aug,
126
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '09' THEN me.monthly_amount ELSE 0 END) AS y3_sep,
127
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '10' THEN me.monthly_amount ELSE 0 END) AS y3_oct,
128
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '11' THEN me.monthly_amount ELSE 0 END) AS y3_nov,
129
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '12' THEN me.monthly_amount ELSE 0 END) AS y3_dec,
130
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '01' THEN me.monthly_amount ELSE 0 END) AS y3_jan,
131
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '02' THEN me.monthly_amount ELSE 0 END) AS y3_feb,
132
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '03' THEN me.monthly_amount ELSE 0 END) AS y3_mar,
133
-- y4
134
SUM(CASE WHEN me.year_group = 'y4' AND me.transaction_month = '04' THEN me.monthly_amount ELSE 0 END) AS y4_apr,
135
SUM(CASE WHEN me.year_group = 'y4' AND me.transaction_month = '05' THEN me.monthly_amount ELSE 0 END) AS y4_may,
136
SUM(CASE WHEN me.year_group = 'y4' AND me.transaction_month = '06' THEN me.monthly_amount ELSE 0 END) AS y4_jun,
137
SUM(CASE WHEN me.year_group = 'y4' AND me.transaction_month = '07' THEN me.monthly_amount ELSE 0 END) AS y4_jul,
138
SUM(CASE WHEN me.year_group = 'y4' AND me.transaction_month = '08' THEN me.monthly_amount ELSE 0 END) AS y4_aug,
139
SUM(CASE WHEN me.year_group = 'y4' AND me.transaction_month = '09' THEN me.monthly_amount ELSE 0 END) AS y4_sep,
140
SUM(CASE WHEN me.year_group = 'y4' AND me.transaction_month = '10' THEN me.monthly_amount ELSE 0 END) AS y4_oct,
141
SUM(CASE WHEN me.year_group = 'y4' AND me.transaction_month = '11' THEN me.monthly_amount ELSE 0 END) AS y4_nov,
142
SUM(CASE WHEN me.year_group = 'y4' AND me.transaction_month = '12' THEN me.monthly_amount ELSE 0 END) AS y4_dec,
143
SUM(CASE WHEN me.year_group = 'y4' AND me.transaction_month = '01' THEN me.monthly_amount ELSE 0 END) AS y4_jan,
144
SUM(CASE WHEN me.year_group = 'y4' AND me.transaction_month = '02' THEN me.monthly_amount ELSE 0 END) AS y4_feb,
145
SUM(CASE WHEN me.year_group = 'y4' AND me.transaction_month = '03' THEN me.monthly_amount ELSE 0 END) AS y4_mar,
146
-- y5
147
SUM(CASE WHEN me.year_group = 'y5' AND me.transaction_month = '04' THEN me.monthly_amount ELSE 0 END) AS y5_apr,
148
SUM(CASE WHEN me.year_group = 'y5' AND me.transaction_month = '05' THEN me.monthly_amount ELSE 0 END) AS y5_may,
149
SUM(CASE WHEN me.year_group = 'y5' AND me.transaction_month = '06' THEN me.monthly_amount ELSE 0 END) AS y5_jun,
150
SUM(CASE WHEN me.year_group = 'y5' AND me.transaction_month = '07' THEN me.monthly_amount ELSE 0 END) AS y5_jul,
151
SUM(CASE WHEN me.year_group = 'y5' AND me.transaction_month = '08' THEN me.monthly_amount ELSE 0 END) AS y5_aug,
152
SUM(CASE WHEN me.year_group = 'y5' AND me.transaction_month = '09' THEN me.monthly_amount ELSE 0 END) AS y5_sep,
153
SUM(CASE WHEN me.year_group = 'y5' AND me.transaction_month = '10' THEN me.monthly_amount ELSE 0 END) AS y5_oct,
154
SUM(CASE WHEN me.year_group = 'y5' AND me.transaction_month = '11' THEN me.monthly_amount ELSE 0 END) AS y5_nov,
155
SUM(CASE WHEN me.year_group = 'y5' AND me.transaction_month = '12' THEN me.monthly_amount ELSE 0 END) AS y5_dec,
156
SUM(CASE WHEN me.year_group = 'y5' AND me.transaction_month = '01' THEN me.monthly_amount ELSE 0 END) AS y5_jan,
157
SUM(CASE WHEN me.year_group = 'y5' AND me.transaction_month = '02' THEN me.monthly_amount ELSE 0 END) AS y5_feb,
158
SUM(CASE WHEN me.year_group = 'y5' AND me.transaction_month = '03' THEN me.monthly_amount ELSE 0 END) AS y5_mar
159
FROM MonthlyExpenses me
160
GROUP BY me.account_name
161
)
162
SELECT
163
gen_random_uuid() AS id,
164
CONCAT(EXTRACT(YEAR FROM previous_year4_start), '-', EXTRACT(YEAR FROM current_year_end)) AS financial_year_range,
165
ae.account_name,
166
-- y1 months
167
ae.y1_apr, ae.y1_may, ae.y1_jun, ae.y1_jul, ae.y1_aug, ae.y1_sep, ae.y1_oct, ae.y1_nov, ae.y1_dec, ae.y1_jan, ae.y1_feb, ae.y1_mar,
168
-- y2 months
169
ae.y2_apr, ae.y2_may, ae.y2_jun, ae.y2_jul, ae.y2_aug, ae.y2_sep, ae.y2_oct, ae.y2_nov, ae.y2_dec, ae.y2_jan, ae.y2_feb, ae.y2_mar,
170
-- y3 months
171
ae.y3_apr, ae.y3_may, ae.y3_jun, ae.y3_jul, ae.y3_aug, ae.y3_sep, ae.y3_oct, ae.y3_nov, ae.y3_dec, ae.y3_jan, ae.y3_feb, ae.y3_mar,
172
-- y4 months
173
ae.y4_apr, ae.y4_may, ae.y4_jun, ae.y4_jul, ae.y4_aug, ae.y4_sep, ae.y4_oct, ae.y4_nov, ae.y4_dec, ae.y4_jan, ae.y4_feb, ae.y4_mar,
174
-- y5 months
175
ae.y5_apr, ae.y5_may, ae.y5_jun, ae.y5_jul, ae.y5_aug, ae.y5_sep, ae.y5_oct, ae.y5_nov, ae.y5_dec, ae.y5_jan, ae.y5_feb, ae.y5_mar
176
FROM AggregatedExpenses ae
177
ORDER BY
178
COALESCE(ae.y1_apr,0)+COALESCE(ae.y1_may,0)+COALESCE(ae.y1_jun,0)+COALESCE(ae.y1_jul,0)+COALESCE(ae.y1_aug,0)+COALESCE(ae.y1_sep,0)+COALESCE(ae.y1_oct,0)+COALESCE(ae.y1_nov,0)+COALESCE(ae.y1_dec,0)+COALESCE(ae.y1_jan,0)+COALESCE(ae.y1_feb,0)+COALESCE(ae.y1_mar,0)
179
+COALESCE(ae.y2_apr,0)+COALESCE(ae.y2_may,0)+COALESCE(ae.y2_jun,0)+COALESCE(ae.y2_jul,0)+COALESCE(ae.y2_aug,0)+COALESCE(ae.y2_sep,0)+COALESCE(ae.y2_oct,0)+COALESCE(ae.y2_nov,0)+COALESCE(ae.y2_dec,0)+COALESCE(ae.y2_jan,0)+COALESCE(ae.y2_feb,0)+COALESCE(ae.y2_mar,0)
180
+COALESCE(ae.y3_apr,0)+COALESCE(ae.y3_may,0)+COALESCE(ae.y3_jun,0)+COALESCE(ae.y3_jul,0)+COALESCE(ae.y3_aug,0)+COALESCE(ae.y3_sep,0)+COALESCE(ae.y3_oct,0)+COALESCE(ae.y3_nov,0)+COALESCE(ae.y3_dec,0)+COALESCE(ae.y3_jan,0)+COALESCE(ae.y3_feb,0)+COALESCE(ae.y3_mar,0)
181
+COALESCE(ae.y4_apr,0)+COALESCE(ae.y4_may,0)+COALESCE(ae.y4_jun,0)+COALESCE(ae.y4_jul,0)+COALESCE(ae.y4_aug,0)+COALESCE(ae.y4_sep,0)+COALESCE(ae.y4_oct,0)+COALESCE(ae.y4_nov,0)+COALESCE(ae.y4_dec,0)+COALESCE(ae.y4_jan,0)+COALESCE(ae.y4_feb,0)+COALESCE(ae.y4_mar,0)
182
+COALESCE(ae.y5_apr,0)+COALESCE(ae.y5_may,0)+COALESCE(ae.y5_jun,0)+COALESCE(ae.y5_jul,0)+COALESCE(ae.y5_aug,0)+COALESCE(ae.y5_sep,0)+COALESCE(ae.y5_oct,0)+COALESCE(ae.y5_nov,0)+COALESCE(ae.y5_dec,0)+COALESCE(ae.y5_jan,0)+COALESCE(ae.y5_feb,0)+COALESCE(ae.y5_mar,0)
183
DESC
184
LIMIT 12;
185
END;
186
$function$
|
|||||
| Function | get_three_years_expenses | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_three_years_expenses(p_company_id uuid, p_finance_year_id integer)
2
RETURNS TABLE(id uuid, financial_year_range text, account_name text, y1_apr numeric, y1_may numeric, y1_jun numeric, y1_jul numeric, y1_aug numeric, y1_sep numeric, y1_oct numeric, y1_nov numeric, y1_dec numeric, y1_jan numeric, y1_feb numeric, y1_mar numeric, y2_apr numeric, y2_may numeric, y2_jun numeric, y2_jul numeric, y2_aug numeric, y2_sep numeric, y2_oct numeric, y2_nov numeric, y2_dec numeric, y2_jan numeric, y2_feb numeric, y2_mar numeric, y3_apr numeric, y3_may numeric, y3_jun numeric, y3_jul numeric, y3_aug numeric, y3_sep numeric, y3_oct numeric, y3_nov numeric, y3_dec numeric, y3_jan numeric, y3_feb numeric, y3_mar numeric)
3
LANGUAGE plpgsql
4
STABLE PARALLEL SAFE
5
AS $function$
6
7
DECLARE
8
current_year_start DATE;
9
current_year_end DATE;
10
previous_year1_start DATE;
11
previous_year1_end DATE;
12
previous_year2_start DATE;
13
previous_year2_end DATE;
14
CONST_EXPENSE_TYPE_ID INTEGER := 5;
15
BEGIN
16
-- Fetch the start and end dates for the current financial year
17
SELECT fy.start_date, fy.end_date
18
INTO current_year_start, current_year_end
19
FROM public.finance_year fy
20
WHERE fy.id = p_finance_year_id;
21
22
-- Validate financial year existence
23
IF current_year_start IS NULL OR current_year_end IS NULL THEN
24
RAISE EXCEPTION 'Financial year with ID % not found', p_finance_year_id;
25
END IF;
26
27
-- Fetch the previous financial year's start and end dates
28
SELECT fy.start_date, fy.end_date
29
INTO previous_year1_start, previous_year1_end
30
FROM public.finance_year fy
31
WHERE fy.end_date < current_year_start
32
ORDER BY fy.end_date DESC
33
LIMIT 1;
34
35
-- Fetch the year before the previous financial year
36
SELECT fy.start_date, fy.end_date
37
INTO previous_year2_start, previous_year2_end
38
FROM public.finance_year fy
39
WHERE fy.end_date < previous_year1_start
40
ORDER BY fy.end_date DESC
41
LIMIT 1;
42
43
-- Generate financial year range dynamically
44
RETURN QUERY
45
WITH RECURSIVE AccountHierarchy AS (
46
-- Base case: Select root accounts (account_type_id = 5 indicates expense accounts)
47
SELECT
48
coa.id AS account_id,
49
coa.name,
50
coa.parent_account_id
51
FROM public.chart_of_accounts coa
52
WHERE coa.account_type_id = CONST_EXPENSE_TYPE_ID
53
54
UNION ALL
55
56
-- Select all child accounts recursively under the root accounts
57
SELECT
58
coa.id AS account_id,
59
coa.name,
60
coa.parent_account_id
61
FROM public.chart_of_accounts coa
62
INNER JOIN AccountHierarchy ah ON coa.parent_account_id = ah.account_id
63
),
64
MonthlyExpenses AS (
65
SELECT
66
ah.name AS account_name,
67
TO_CHAR(je.transaction_date, 'MM') AS transaction_month,
68
CASE
69
WHEN je.transaction_date BETWEEN previous_year2_start AND previous_year1_start - INTERVAL '1 day' THEN 'y1'
70
WHEN je.transaction_date BETWEEN previous_year1_start AND current_year_start - INTERVAL '1 day' THEN 'y2'
71
WHEN je.transaction_date BETWEEN current_year_start AND current_year_end THEN 'y3'
72
END AS year_group,
73
SUM(je.amount) AS monthly_amount
74
FROM AccountHierarchy ah
75
LEFT JOIN public.journal_entries je ON je.account_id = ah.account_id
76
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
77
WHERE th.company_id = p_company_id
78
AND je.transaction_date BETWEEN previous_year2_start AND current_year_end
79
AND je.is_deleted = FALSE
80
GROUP BY ah.name, year_group, TO_CHAR(je.transaction_date, 'MM')
81
),
82
AggregatedExpenses AS (
83
SELECT
84
me.account_name,
85
-- Populate y1 values
86
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '04' THEN me.monthly_amount ELSE 0 END) AS y1_apr,
87
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '05' THEN me.monthly_amount ELSE 0 END) AS y1_may,
88
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '06' THEN me.monthly_amount ELSE 0 END) AS y1_jun,
89
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '07' THEN me.monthly_amount ELSE 0 END) AS y1_jul,
90
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '08' THEN me.monthly_amount ELSE 0 END) AS y1_aug,
91
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '09' THEN me.monthly_amount ELSE 0 END) AS y1_sep,
92
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '10' THEN me.monthly_amount ELSE 0 END) AS y1_oct,
93
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '11' THEN me.monthly_amount ELSE 0 END) AS y1_nov,
94
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '12' THEN me.monthly_amount ELSE 0 END) AS y1_dec,
95
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '01' THEN me.monthly_amount ELSE 0 END) AS y1_jan,
96
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '02' THEN me.monthly_amount ELSE 0 END) AS y1_feb,
97
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '03' THEN me.monthly_amount ELSE 0 END) AS y1_mar,
98
-- Similarly populate y2 and y3 values
99
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '04' THEN me.monthly_amount ELSE 0 END) AS y2_apr,
100
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '05' THEN me.monthly_amount ELSE 0 END) AS y2_may,
101
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '06' THEN me.monthly_amount ELSE 0 END) AS y2_jun,
102
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '07' THEN me.monthly_amount ELSE 0 END) AS y2_jul,
103
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '08' THEN me.monthly_amount ELSE 0 END) AS y2_aug,
104
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '09' THEN me.monthly_amount ELSE 0 END) AS y2_sep,
105
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '10' THEN me.monthly_amount ELSE 0 END) AS y2_oct,
106
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '11' THEN me.monthly_amount ELSE 0 END) AS y2_nov,
107
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '12' THEN me.monthly_amount ELSE 0 END) AS y2_dec,
108
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '01' THEN me.monthly_amount ELSE 0 END) AS y2_jan,
109
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '02' THEN me.monthly_amount ELSE 0 END) AS y2_feb,
110
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '03' THEN me.monthly_amount ELSE 0 END) AS y2_mar,
111
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '04' THEN me.monthly_amount ELSE 0 END) AS y3_apr,
112
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '05' THEN me.monthly_amount ELSE 0 END) AS y3_may,
113
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '06' THEN me.monthly_amount ELSE 0 END) AS y3_jun,
114
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '07' THEN me.monthly_amount ELSE 0 END) AS y3_jul,
115
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '08' THEN me.monthly_amount ELSE 0 END) AS y3_aug,
116
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '09' THEN me.monthly_amount ELSE 0 END) AS y3_sep,
117
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '10' THEN me.monthly_amount ELSE 0 END) AS y3_oct,
118
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '11' THEN me.monthly_amount ELSE 0 END) AS y3_nov,
119
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '12' THEN me.monthly_amount ELSE 0 END) AS y3_dec,
120
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '01' THEN me.monthly_amount ELSE 0 END) AS y3_jan,
121
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '02' THEN me.monthly_amount ELSE 0 END) AS y3_feb,
122
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '03' THEN me.monthly_amount ELSE 0 END) AS y3_mar
123
FROM MonthlyExpenses me
124
GROUP BY me.account_name
125
)
126
SELECT
127
gen_random_uuid() AS id,
128
CONCAT(EXTRACT(YEAR FROM previous_year2_start), '-', EXTRACT(YEAR FROM current_year_end)) AS financial_year_range,
129
ae.account_name,
130
ae.y1_apr, ae.y1_may, ae.y1_jun, ae.y1_jul, ae.y1_aug, ae.y1_sep, ae.y1_oct, ae.y1_nov, ae.y1_dec, ae.y1_jan, ae.y1_feb, ae.y1_mar,
131
ae.y2_apr, ae.y2_may, ae.y2_jun, ae.y2_jul, ae.y2_aug, ae.y2_sep, ae.y2_oct, ae.y2_nov, ae.y2_dec, ae.y2_jan, ae.y2_feb, ae.y2_mar,
132
ae.y3_apr, ae.y3_may, ae.y3_jun, ae.y3_jul, ae.y3_aug, ae.y3_sep, ae.y3_oct, ae.y3_nov, ae.y3_dec, ae.y3_jan, ae.y3_feb, ae.y3_mar
133
FROM AggregatedExpenses ae
134
ORDER BY
135
COALESCE(ae.y1_apr, 0) + COALESCE(ae.y1_may, 0) + COALESCE(ae.y1_jun, 0) + COALESCE(ae.y1_jul, 0) +
136
COALESCE(ae.y1_aug, 0) + COALESCE(ae.y1_sep, 0) + COALESCE(ae.y1_oct, 0) + COALESCE(ae.y1_nov, 0) +
137
COALESCE(ae.y1_dec, 0) + COALESCE(ae.y1_jan, 0) + COALESCE(ae.y1_feb, 0) + COALESCE(ae.y1_mar, 0) +
138
COALESCE(ae.y2_apr, 0) + COALESCE(ae.y2_may, 0) + COALESCE(ae.y2_jun, 0) + COALESCE(ae.y2_jul, 0) +
139
COALESCE(ae.y2_aug, 0) + COALESCE(ae.y2_sep, 0) + COALESCE(ae.y2_oct, 0) + COALESCE(ae.y2_nov, 0) +
140
COALESCE(ae.y2_dec, 0) + COALESCE(ae.y2_jan, 0) + COALESCE(ae.y2_feb, 0) + COALESCE(ae.y2_mar, 0) +
141
COALESCE(ae.y3_apr, 0) + COALESCE(ae.y3_may, 0) + COALESCE(ae.y3_jun, 0) + COALESCE(ae.y3_jul, 0) +
142
COALESCE(ae.y3_aug, 0) + COALESCE(ae.y3_sep, 0) + COALESCE(ae.y3_oct, 0) + COALESCE(ae.y3_nov, 0) +
143
COALESCE(ae.y3_dec, 0) + COALESCE(ae.y3_jan, 0) + COALESCE(ae.y3_feb, 0) + COALESCE(ae.y3_mar, 0) DESC
144
LIMIT 12;
145
END;
146
$function$
|
|||||
| Function | get_all_coa | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_all_coa(p_organization_id uuid)
2
RETURNS TABLE(id uuid, account_type text, account_number integer, name text, opening_balance numeric, level integer, order_sequence character varying, is_default_account boolean, schedule text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN query
7
WITH RECURSIVE coa_view AS (
8
SELECT
9
coa.id,
10
act.name as account_type,
11
coa.account_number::integer,
12
coa.name,
13
coa.parent_account_id,
14
0 AS level,
15
coa.account_number::text AS order_sequence,
16
coa.is_default_account, -- Include is_default_account,
17
NULL::text AS schedule
18
FROM
19
public.chart_of_accounts coa
20
INNER JOIN
21
public.account_types act ON coa.account_type_id = act.id
22
WHERE
23
coa.organization_id = p_organization_id
24
AND (coa.parent_account_id = '00000000-0000-0000-0000-000000000000' or coa.parent_account_id is null)
25
AND coa.is_deleted = FALSE
26
UNION ALL
27
SELECT
28
coa.id,
29
act.name as account_type,
30
coa.account_number::integer,
31
coa.name,
32
coa.parent_account_id,
33
view.level + 1 AS level,
34
view.order_sequence || '.' || coa.account_number::text AS order_sequence,
35
coa.is_default_account, -- Include is_default_account,
36
NULL::text AS schedule
37
FROM
38
public.chart_of_accounts coa
39
INNER JOIN
40
coa_view view ON view.id = coa.parent_account_id
41
INNER JOIN
42
public.account_types act ON coa.account_type_id = act.id
43
WHERE
44
coa.organization_id = p_organization_id
45
AND coa.is_deleted = FALSE
46
)
47
SELECT
48
view.id,
49
view.account_type,
50
view.account_number,
51
LPAD('', view.level * 4, ' ') || view.name AS name,
52
0::numeric AS opening_balance,
53
view.level,
54
view.order_sequence::character varying AS order_sequence,
55
view.is_default_account, -- Include is_default_account in final output
56
view.schedule
57
FROM
58
coa_view view
59
ORDER BY
60
view.order_sequence;
61
END;
62
$function$
|
|||||
| Function | pg_get_tabledef | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.pg_get_tabledef(p_table_name text)
2
RETURNS text
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
col RECORD;
7
col_defs TEXT := '';
8
pk_cols TEXT := '';
9
result TEXT;
10
BEGIN
11
FOR col IN
12
SELECT
13
column_name,
14
data_type,
15
character_maximum_length,
16
numeric_precision,
17
numeric_scale,
18
is_nullable,
19
column_default
20
FROM information_schema.columns
21
WHERE table_schema = 'public' AND table_name = p_table_name
22
ORDER BY ordinal_position
23
LOOP
24
col_defs := col_defs ||
25
format('"%s" %s%s%s%s, ',
26
col.column_name,
27
CASE
28
WHEN col.data_type = 'character varying' THEN format('varchar(%s)', col.character_maximum_length)
29
WHEN col.data_type = 'numeric' THEN format('numeric(%s,%s)', col.numeric_precision, col.numeric_scale)
30
ELSE col.data_type
31
END,
32
CASE WHEN col.column_default IS NOT NULL THEN ' DEFAULT ' || col.column_default ELSE '' END,
33
CASE WHEN col.is_nullable = 'NO' THEN ' NOT NULL' ELSE '' END,
34
''
35
);
36
END LOOP;
37
38
-- Get primary key columns
39
SELECT string_agg(format('"%s"', kcu.column_name), ', ')
40
INTO pk_cols
41
FROM information_schema.table_constraints tc
42
JOIN information_schema.key_column_usage kcu
43
ON tc.constraint_name = kcu.constraint_name
44
WHERE tc.table_schema = 'public'
45
AND tc.table_name = p_table_name
46
AND tc.constraint_type = 'PRIMARY KEY';
47
48
IF pk_cols IS NOT NULL THEN
49
col_defs := col_defs || format('PRIMARY KEY (%s), ', pk_cols);
50
END IF;
51
52
col_defs := left(col_defs, length(col_defs) - 2);
53
result := format('CREATE TABLE "%s" (%s);', p_table_name, col_defs);
54
RETURN result;
55
END;
56
$function$
|
|||||
| Function | get_cash_flow_statement | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_cash_flow_statement(p_company_id uuid, p_finance_year_id integer, p_is_get_for_organization boolean)
2
RETURNS TABLE(account_id uuid, account_category text, account_type_id integer, account_type text, account_name text, net_cash_flow numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_financial_year_start DATE;
7
v_financial_year_end DATE;
8
v_company_ids UUID[];
9
v_organization_id UUID;
10
BEGIN
11
-- Fetch the Organization ID for the given Company ID
12
SELECT organization_id INTO v_organization_id
13
FROM public.companies
14
WHERE id = p_company_id;
15
16
IF v_organization_id IS NULL THEN
17
RAISE EXCEPTION 'Organization ID not found for company ID %', p_company_id;
18
END IF;
19
20
-- Fetch the start and end dates of the financial year
21
SELECT fy.start_date, fy.end_date INTO v_financial_year_start, v_financial_year_end
22
FROM public.finance_year fy
23
WHERE fy.id = p_finance_year_id;
24
25
IF v_financial_year_start IS NULL OR v_financial_year_end IS NULL THEN
26
RAISE EXCEPTION 'Financial year with ID % not found', p_finance_year_id;
27
END IF;
28
29
-- Determine the company IDs to include (for all companies in the same organization or a single company)
30
IF p_is_get_for_organization THEN
31
SELECT ARRAY_AGG(id) INTO v_company_ids
32
FROM public.companies
33
WHERE organization_id = v_organization_id;
34
ELSE
35
v_company_ids := ARRAY[p_company_id];
36
END IF;
37
38
-- Combining Operating, Investing, and Financing Activities into a single result
39
RETURN QUERY
40
WITH cash_flow_activities AS (
41
-- Operating Activities: Assets, Liabilities, Revenue, and Expenses
42
SELECT
43
coa.id AS account_id,
44
'Operating' AS account_category,
45
at.id as account_type_id,
46
at.name as account_type,
47
coa.name AS account_name,
48
SUM(CASE
49
-- Assets
50
WHEN je.entry_type = 'D' AND coa.account_type_id IN (6, 8, 9) THEN je.amount -- Debit for Assets increases balance
51
WHEN je.entry_type = 'C' AND coa.account_type_id IN (6, 8, 9) THEN -je.amount -- Credit for Assets decreases balance
52
53
-- Liabilities
54
WHEN je.entry_type = 'C' AND coa.account_type_id = 10 THEN je.amount -- Credit for Liabilities increases balance
55
WHEN je.entry_type = 'D' AND coa.account_type_id = 10 THEN -je.amount -- Debit for Liabilities decreases balance
56
57
-- Revenue
58
WHEN je.entry_type = 'C' AND coa.account_type_id IN (14, 15, 19, 20) THEN je.amount -- Credit for Revenue increases balance
59
WHEN je.entry_type = 'D' AND coa.account_type_id IN (14, 15, 19, 20) THEN -je.amount -- Debit for Revenue decreases balance
60
61
-- Expenses
62
WHEN je.entry_type = 'D' AND coa.account_type_id IN (16, 17, 18, 21, 22) THEN je.amount -- Debit for Expenses increases balance
63
WHEN je.entry_type = 'C' AND coa.account_type_id IN (16, 17, 18, 21, 22) THEN -je.amount -- Credit for Expenses decreases balance
64
ELSE 0
65
END) AS net_cash_flow
66
FROM
67
public.chart_of_accounts coa
68
INNER JOIN public.account_types at ON coa.account_type_id = at.id
69
INNER JOIN public.journal_entries je ON je.account_id = coa.id
70
INNER JOIN public.transaction_headers th ON th.id = je.transaction_id
71
WHERE
72
th.company_id = ANY(v_company_ids)
73
AND th.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
74
AND NOT th.is_deleted
75
AND NOT je.is_deleted
76
AND coa.account_type_id IN (6, 8, 9, 10, 14, 15, 16, 17, 18, 19, 20, 21, 22)
77
GROUP BY
78
coa.id, at.id, at.name, coa.name
79
80
UNION ALL
81
82
-- Investing Activities: Non-Current Assets
83
SELECT
84
coa.id AS account_id,
85
'Investing' AS account_category,
86
at.id as account_type_id,
87
at.name as account_type,
88
coa.name AS account_name,
89
SUM(CASE
90
WHEN je.entry_type = 'D' AND coa.account_type_id = 7 THEN je.amount -- Debit for Non-Current Assets increases balance
91
WHEN je.entry_type = 'C' AND coa.account_type_id = 7 THEN -je.amount -- Credit for Non-Current Assets decreases balance
92
ELSE 0
93
END) AS net_cash_flow
94
FROM
95
public.chart_of_accounts coa
96
INNER JOIN public.account_types at ON coa.account_type_id = at.id
97
INNER JOIN public.journal_entries je ON je.account_id = coa.id
98
INNER JOIN public.transaction_headers th ON th.id = je.transaction_id
99
WHERE
100
th.company_id = ANY(v_company_ids)
101
AND th.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
102
AND NOT th.is_deleted
103
AND NOT je.is_deleted
104
AND coa.account_type_id = 7
105
GROUP BY
106
coa.id, at.id, at.name, coa.name
107
108
UNION ALL
109
110
-- Financing Activities: Equity and Long-Term Liabilities
111
SELECT
112
coa.id AS account_id,
113
'Financing' AS account_category,
114
at.id as account_type_id,
115
at.name as account_type,
116
coa.name AS account_name,
117
SUM(CASE
118
-- Liabilities
119
WHEN je.entry_type = 'C' AND coa.account_type_id = 11 THEN je.amount -- Credit for Liabilities increases balance
120
WHEN je.entry_type = 'D' AND coa.account_type_id = 11 THEN -je.amount -- Debit for Liabilities decreases balance
121
122
-- Equity
123
WHEN je.entry_type = 'C' AND coa.account_type_id IN (12, 13) THEN je.amount -- Credit for Equity increases balance
124
WHEN je.entry_type = 'D' AND coa.account_type_id IN (12, 13) THEN -je.amount -- Debit for Equity decreases balance
125
ELSE 0
126
END) AS net_cash_flow
127
FROM
128
public.chart_of_accounts coa
129
INNER JOIN public.account_types at ON coa.account_type_id = at.id
130
INNER JOIN public.journal_entries je ON je.account_id = coa.id
131
INNER JOIN public.transaction_headers th ON th.id = je.transaction_id
132
WHERE
133
th.company_id = ANY(v_company_ids)
134
AND th.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
135
AND NOT th.is_deleted
136
AND NOT je.is_deleted
137
AND coa.account_type_id IN (11, 12, 13)
138
GROUP BY
139
coa.id, at.id, at.name, coa.name
140
)
141
SELECT
142
cfa.account_id,
143
cfa.account_category,
144
cfa.account_type_id,
145
cfa.account_type,
146
cfa.account_name,
147
cfa.net_cash_flow
148
FROM cash_flow_activities cfa
149
WHERE cfa.net_cash_flow != 0 -- Exclude records with zero net cash flow
150
ORDER BY cfa.account_category, cfa.account_type_id, cfa.account_type, cfa.account_name;
151
END;
152
$function$
|
|||||
| Function | get_bank_statements | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_bank_statements(p_company_id uuid, p_finyear_id integer, p_date_from timestamp without time zone DEFAULT NULL::timestamp without time zone, p_date_to timestamp without time zone DEFAULT NULL::timestamp without time zone)
2
RETURNS TABLE(id integer, company_id uuid, bank_id uuid, bank_name text, txn_date timestamp without time zone, cheque_number character varying, description text, value_date timestamp without time zone, branch_code character varying, debit_amount numeric, credit_amount numeric, balance numeric, has_reconciled boolean, created_by uuid, created_on_utc timestamp without time zone, modified_by uuid, modified_on_utc timestamp without time zone, deleted_on_utc timestamp without time zone, is_deleted boolean, rec_status_id integer, rec_status text, rec_confidence_score numeric, rec_strategy_name text, rec_matched_party_name text, rec_party_id uuid, rec_party_type text, rec_reviewed_by uuid, rec_reviewed_on_utc timestamp without time zone, rec_matched_amount numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
bs.id,
9
bs.company_id,
10
bs.bank_id,
11
coa.name AS bank_name,
12
bs.txn_date::timestamp,
13
bs.cheque_number,
14
bs.description,
15
bs.value_date::timestamp,
16
bs.branch_code,
17
bs.debit_amount,
18
bs.credit_amount,
19
bs.balance,
20
bs.has_reconciled,
21
bs.created_by,
22
bs.created_on_utc,
23
bs.modified_by,
24
bs.modified_on_utc,
25
bs.deleted_on_utc,
26
bs.is_deleted,
27
28
-- ✅ Finalized Reconciliation Status
29
COALESCE(br.reconciliation_status_id, 0) AS rec_status_id,
30
31
-- Determine rec_status based on staging status and confidence score
32
CASE
33
WHEN br.reconciliation_status_id IS NOT NULL THEN
34
COALESCE(rs.status::text, 'unmatched')
35
WHEN brs.confidence_score >= 70.99 THEN
36
'matched' -- Auto-match based on high confidence score
37
WHEN brs.status = 'pending' THEN
38
'pending' -- Pending review status
39
ELSE
40
'unmatched'
41
END AS rec_status,
42
43
-- Latest staging details (AI / manual reconciliation suggestion)
44
COALESCE(brs.confidence_score, 0) AS rec_confidence_score,
45
COALESCE(brs.strategy_name::text, 'No Match Found') AS rec_strategy_name,
46
brs.matched_party_name::text AS rec_matched_party_name,
47
brs.party_id AS rec_party_id,
48
brs.party_type::text AS rec_party_type,
49
brs.reviewed_by AS rec_reviewed_by,
50
brs.reviewed_on_utc AS rec_reviewed_on_utc,
51
COALESCE(brs.matched_amount, 0) AS rec_matched_amount
52
53
FROM public.bank_statements bs
54
INNER JOIN public.chart_of_accounts coa
55
ON coa.id = bs.bank_id
56
AND coa.account_type_id = 9
57
AND coa.is_deleted = FALSE
58
INNER JOIN public.finance_year fy
59
ON fy.id = p_finyear_id
60
61
-- 🟢 Latest staging entry (AI / manual reconciliation suggestion)
62
LEFT JOIN LATERAL (
63
SELECT brs_inner.*
64
FROM public.bank_reconciliation_stagings brs_inner
65
WHERE brs_inner.company_id = bs.company_id
66
AND brs_inner.bank_statement_id = bs.id
67
AND brs_inner.is_deleted = FALSE
68
ORDER BY brs_inner.created_on_utc DESC
69
LIMIT 1
70
) brs ON TRUE
71
72
-- 🟢 Finalized reconciliations
73
LEFT JOIN public.bank_reconciliations br
74
ON br.bank_statement_id = bs.id
75
AND br.company_id = bs.company_id
76
77
-- 🟢 Status lookup
78
LEFT JOIN public.reconciliation_statuses rs
79
ON rs.id = br.reconciliation_status_id
80
81
WHERE bs.company_id = p_company_id
82
AND bs.txn_date BETWEEN fy.start_date AND fy.end_date
83
AND (p_date_from IS NULL OR bs.txn_date >= p_date_from)
84
AND (p_date_to IS NULL OR bs.txn_date <= p_date_to)
85
AND bs.is_deleted = FALSE
86
87
ORDER BY bs.txn_date, bs.id;
88
END;
89
$function$
|
|||||
| Function | get_comparative_accounts_overview | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_comparative_accounts_overview(p_company_id uuid, p_fin_year_id integer)
2
RETURNS TABLE(account_id uuid, account_name text, parent_account_id uuid, hierarchy_path text[], order_sequence text, financial_year integer, apr numeric, may numeric, jun numeric, jul numeric, aug numeric, sep numeric, oct numeric, nov numeric, "dec" numeric, jan numeric, feb numeric, mar numeric, total_sum numeric, difference numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_current_year_id INTEGER := p_fin_year_id;
7
v_previous_year_id INTEGER;
8
v_organization_id UUID;
9
BEGIN
10
-- Get organization
11
SELECT c.organization_id INTO v_organization_id
12
FROM public.companies c
13
WHERE c.id = p_company_id;
14
15
-- Find previous financial year (based on start_date)
16
SELECT fy.id
17
INTO v_previous_year_id
18
FROM finance_year fy
19
WHERE fy.start_date < (
20
SELECT start_date FROM finance_year WHERE id = v_current_year_id
21
)
22
ORDER BY fy.start_date DESC
23
LIMIT 1;
24
25
RETURN QUERY
26
WITH RECURSIVE account_with_path AS (
27
-- Root accounts
28
SELECT
29
coa.id,
30
coa.name,
31
coa.parent_account_id,
32
ARRAY[coa.name]::text[] AS path,
33
coa.account_number::text AS order_sequence
34
FROM chart_of_accounts coa
35
WHERE coa.organization_id = v_organization_id
36
AND (coa.parent_account_id IS NULL OR coa.parent_account_id = '00000000-0000-0000-0000-000000000000')
37
AND coa.is_deleted = FALSE
38
39
UNION ALL
40
41
-- Children
42
SELECT
43
c.id,
44
c.name,
45
c.parent_account_id,
46
awp.path || c.name,
47
awp.order_sequence || '.' || c.account_number::text
48
FROM chart_of_accounts c
49
JOIN account_with_path awp ON c.parent_account_id = awp.id
50
WHERE c.organization_id = v_organization_id
51
AND c.is_deleted = FALSE
52
),
53
leaf_accounts AS (
54
SELECT a.id
55
FROM account_with_path a
56
WHERE NOT EXISTS (
57
SELECT 1 FROM chart_of_accounts c
58
WHERE c.parent_account_id = a.id
59
AND c.organization_id = v_organization_id
60
AND c.is_deleted = FALSE
61
)
62
),
63
financial_years AS (
64
SELECT id AS fin_year_id, start_date, end_date
65
FROM finance_year
66
WHERE id IN (v_current_year_id, v_previous_year_id)
67
),
68
aggregated_data AS (
69
SELECT
70
awp.id,
71
awp.name,
72
awp.parent_account_id,
73
awp.path AS hierarchy_path,
74
awp.order_sequence,
75
fy.fin_year_id,
76
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 4 THEN je.amount END), 0) AS apr,
77
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 5 THEN je.amount END), 0) AS may,
78
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 6 THEN je.amount END), 0) AS jun,
79
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 7 THEN je.amount END), 0) AS jul,
80
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 8 THEN je.amount END), 0) AS aug,
81
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 9 THEN je.amount END), 0) AS sep,
82
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 10 THEN je.amount END), 0) AS oct,
83
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 11 THEN je.amount END), 0) AS nov,
84
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 12 THEN je.amount END), 0) AS "dec",
85
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 1 THEN je.amount END), 0) AS jan,
86
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 2 THEN je.amount END), 0) AS feb,
87
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 3 THEN je.amount END), 0) AS mar
88
FROM account_with_path awp
89
JOIN leaf_accounts la ON la.id = awp.id -- ✅ only leaf accounts get financial years
90
CROSS JOIN financial_years fy
91
LEFT JOIN journal_entries je
92
ON je.account_id = awp.id
93
AND je.transaction_date BETWEEN fy.start_date AND fy.end_date
94
LEFT JOIN transaction_headers th
95
ON th.id = je.transaction_id
96
AND th.company_id = p_company_id
97
WHERE (th.transaction_source_type IS NULL OR th.transaction_source_type != 18)
98
GROUP BY awp.id, awp.name, awp.parent_account_id, awp.path, awp.order_sequence, fy.fin_year_id
99
)
100
SELECT
101
a1.id AS account_id,
102
a1.name AS account_name,
103
a1.parent_account_id,
104
a1.hierarchy_path,
105
a1.order_sequence,
106
a1.fin_year_id AS financial_year,
107
a1.apr, a1.may, a1.jun, a1.jul, a1.aug, a1.sep, a1.oct, a1.nov, a1."dec",
108
a1.jan, a1.feb, a1.mar,
109
(COALESCE(a1.apr,0) + COALESCE(a1.may,0) + COALESCE(a1.jun,0) +
110
COALESCE(a1.jul,0) + COALESCE(a1.aug,0) + COALESCE(a1.sep,0) +
111
COALESCE(a1.oct,0) + COALESCE(a1.nov,0) + COALESCE(a1."dec",0) +
112
COALESCE(a1.jan,0) + COALESCE(a1.feb,0) + COALESCE(a1.mar,0)) AS total_sum,
113
((COALESCE(a1.apr,0) + COALESCE(a1.may,0) + COALESCE(a1.jun,0) +
114
COALESCE(a1.jul,0) + COALESCE(a1.aug,0) + COALESCE(a1.sep,0) +
115
COALESCE(a1.oct,0) + COALESCE(a1.nov,0) + COALESCE(a1."dec",0) +
116
COALESCE(a1.jan,0) + COALESCE(a1.feb,0) + COALESCE(a1.mar,0))
117
-
118
(COALESCE(a2.apr,0) + COALESCE(a2.may,0) + COALESCE(a2.jun,0) +
119
COALESCE(a2.jul,0) + COALESCE(a2.aug,0) + COALESCE(a2.sep,0) +
120
COALESCE(a2.oct,0) + COALESCE(a2.nov,0) + COALESCE(a2."dec",0) +
121
COALESCE(a2.jan,0) + COALESCE(a2.feb,0) + COALESCE(a2.mar,0))
122
) AS difference
123
FROM aggregated_data a1
124
LEFT JOIN aggregated_data a2
125
ON a1.id = a2.id
126
AND a1.fin_year_id = v_current_year_id
127
AND a2.fin_year_id = v_previous_year_id
128
ORDER BY a1.order_sequence, a1.fin_year_id;
129
END;
130
$function$
|
|||||
| Function | get_expense_categorization | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_expense_categorization(p_company_id uuid, p_period_type integer, p_finance_id integer)
2
RETURNS TABLE(period text, year numeric, account_id uuid, account_name text, total_expense numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_financial_year_start DATE;
7
v_financial_year_end DATE;
8
v_organization_id uuid;
9
CONST_MONTHLY CONSTANT INTEGER := 1;
10
CONST_QUARTERLY CONSTANT INTEGER := 2;
11
CONST_HALF_YEARLY CONSTANT INTEGER := 3;
12
CONST_YEARLY CONSTANT INTEGER := 4;
13
CONST_YTD CONSTANT INTEGER := 5;
14
BEGIN
15
-- Get financial year boundaries
16
SELECT start_date, end_date INTO v_financial_year_start, v_financial_year_end
17
FROM public.finance_year
18
WHERE id = p_finance_id;
19
20
-- Get organization_id for the given company_id
21
SELECT organization_id INTO v_organization_id
22
FROM public.companies
23
WHERE id = p_company_id;
24
25
IF v_financial_year_start IS NULL OR v_financial_year_end IS NULL THEN
26
RAISE EXCEPTION 'Financial year with ID % not found', p_finance_id;
27
END IF;
28
IF v_organization_id IS NULL THEN
29
RAISE EXCEPTION 'Company % not found or missing organization_id', p_company_id;
30
END IF;
31
32
IF p_period_type = CONST_MONTHLY THEN
33
RETURN QUERY
34
WITH months AS (
35
SELECT
36
TO_CHAR(m, 'Mon') AS period,
37
EXTRACT(YEAR FROM m) AS year,
38
EXTRACT(MONTH FROM m) AS month_num
39
FROM generate_series(v_financial_year_start, v_financial_year_end, '1 month'::interval) AS m
40
)
41
SELECT
42
months.period,
43
months.year,
44
coa.id AS account_id,
45
coa.name AS account_name,
46
COALESCE(SUM(je.amount), 0) AS total_expense
47
FROM months
48
LEFT JOIN public.journal_entries je
49
ON EXTRACT(MONTH FROM je.transaction_date) = months.month_num
50
AND EXTRACT(YEAR FROM je.transaction_date) = months.year
51
AND je.is_deleted = FALSE
52
INNER JOIN public.transaction_headers th
53
ON je.transaction_id = th.id
54
AND th.company_id = p_company_id
55
INNER JOIN public.chart_of_accounts coa
56
ON je.account_id = coa.id
57
AND coa.organization_id = v_organization_id
58
AND coa.is_deleted = FALSE
59
WHERE coa.account_type_id IN (SELECT id FROM get_account_type_hierarchy(5))
60
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
61
AND je.entry_type = 'D'
62
GROUP BY months.year, months.month_num, months.period, coa.id, coa.name
63
ORDER BY months.year, months.month_num, coa.name;
64
65
ELSIF p_period_type = CONST_QUARTERLY THEN
66
RETURN QUERY
67
WITH quarters AS (
68
SELECT 'Q1' AS period, v_financial_year_start AS start_date, v_financial_year_start + interval '2 month' AS end_date
69
UNION ALL
70
SELECT 'Q2', v_financial_year_start + interval '3 month', v_financial_year_start + interval '5 month'
71
UNION ALL
72
SELECT 'Q3', v_financial_year_start + interval '6 month', v_financial_year_start + interval '8 month'
73
UNION ALL
74
SELECT 'Q4', v_financial_year_start + interval '9 month', v_financial_year_end
75
)
76
SELECT
77
quarters.period,
78
EXTRACT(YEAR FROM quarters.start_date) AS year,
79
coa.id AS account_id,
80
coa.name AS account_name,
81
COALESCE(SUM(je.amount), 0) AS total_expense
82
FROM quarters
83
LEFT JOIN public.journal_entries je
84
ON je.transaction_date BETWEEN quarters.start_date AND quarters.end_date
85
AND je.is_deleted = FALSE
86
INNER JOIN public.transaction_headers th
87
ON je.transaction_id = th.id
88
AND th.company_id = p_company_id
89
INNER JOIN public.chart_of_accounts coa
90
ON je.account_id = coa.id
91
AND coa.organization_id = v_organization_id
92
AND coa.is_deleted = FALSE
93
WHERE coa.account_type_id IN (SELECT id FROM get_account_type_hierarchy(5))
94
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
95
AND je.entry_type = 'D'
96
GROUP BY quarters.period, year, coa.id, coa.name
97
ORDER BY year, quarters.period, coa.name;
98
99
ELSIF p_period_type = CONST_HALF_YEARLY THEN
100
RETURN QUERY
101
WITH half_years AS (
102
SELECT 'H1' AS period, v_financial_year_start AS start_date, v_financial_year_start + interval '5 month' AS end_date
103
UNION ALL
104
SELECT 'H2', v_financial_year_start + interval '6 month', v_financial_year_end
105
)
106
SELECT
107
half_years.period,
108
EXTRACT(YEAR FROM half_years.start_date) AS year,
109
coa.id AS account_id,
110
coa.name AS account_name,
111
COALESCE(SUM(je.amount), 0) AS total_expense
112
FROM half_years
113
LEFT JOIN public.journal_entries je
114
ON je.transaction_date BETWEEN half_years.start_date AND half_years.end_date
115
AND je.is_deleted = FALSE
116
INNER JOIN public.transaction_headers th
117
ON je.transaction_id = th.id
118
AND th.company_id = p_company_id
119
INNER JOIN public.chart_of_accounts coa
120
ON je.account_id = coa.id
121
AND coa.organization_id = v_organization_id
122
AND coa.is_deleted = FALSE
123
WHERE coa.account_type_id IN (SELECT id FROM get_account_type_hierarchy(5))
124
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
125
AND je.entry_type = 'D'
126
GROUP BY half_years.period, year, coa.id, coa.name
127
ORDER BY year, half_years.period, coa.name;
128
129
ELSIF p_period_type = CONST_YEARLY THEN
130
RETURN QUERY
131
SELECT
132
'Year' AS period,
133
EXTRACT(YEAR FROM je.transaction_date) AS year,
134
coa.id AS account_id,
135
coa.name AS account_name,
136
COALESCE(SUM(je.amount), 0) AS total_expense
137
FROM public.journal_entries je
138
INNER JOIN public.transaction_headers th
139
ON je.transaction_id = th.id
140
AND th.company_id = p_company_id
141
INNER JOIN public.chart_of_accounts coa
142
ON je.account_id = coa.id
143
AND coa.organization_id = v_organization_id
144
AND coa.is_deleted = FALSE
145
WHERE coa.account_type_id IN (SELECT id FROM get_account_type_hierarchy(5))
146
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
147
AND je.entry_type = 'D'
148
AND je.is_deleted = FALSE
149
GROUP BY year, coa.id, coa.name
150
ORDER BY year, coa.name;
151
152
ELSIF p_period_type = CONST_YTD THEN
153
RETURN QUERY
154
WITH ytd_fin_years AS (
155
SELECT fy.start_date, fy.end_date
156
FROM public.finance_year fy
157
WHERE EXTRACT(YEAR FROM fy.start_date) BETWEEN EXTRACT(YEAR FROM CURRENT_DATE) - 4 AND EXTRACT(YEAR FROM CURRENT_DATE)
158
ORDER BY fy.start_date DESC
159
LIMIT 5
160
)
161
SELECT
162
'YTD' AS period,
163
NULL::numeric AS year,
164
coa.id AS account_id,
165
coa.name AS account_name,
166
COALESCE(SUM(je.amount), 0) AS total_expense
167
FROM ytd_fin_years
168
LEFT JOIN public.journal_entries je
169
ON je.transaction_date BETWEEN ytd_fin_years.start_date AND LEAST(
170
ytd_fin_years.end_date,
171
CASE WHEN CURRENT_DATE BETWEEN ytd_fin_years.start_date AND ytd_fin_years.end_date
172
THEN CURRENT_DATE ELSE ytd_fin_years.end_date END
173
)
174
AND je.is_deleted = FALSE
175
LEFT JOIN public.transaction_headers th ON je.transaction_id = th.id
176
AND th.company_id = p_company_id
177
LEFT JOIN public.chart_of_accounts coa ON je.account_id = coa.id
178
AND coa.organization_id = v_organization_id
179
AND coa.is_deleted = FALSE
180
WHERE coa.account_type_id IN (SELECT id FROM get_account_type_hierarchy(5))
181
AND (coa.name IS NULL OR coa.name NOT IN ('Rounding Gain'))
182
GROUP BY coa.id, coa.name
183
ORDER BY total_expense DESC;
184
185
END IF;
186
END;
187
$function$
|
|||||
| Function | get_income_expense_monthly_breakdown | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_income_expense_monthly_breakdown(p_company_id uuid, p_finance_year_id integer)
2
RETURNS TABLE(id uuid, account_type text, account_name text, parent_account_id uuid, level integer, year integer, apr numeric, apr_diff numeric, may numeric, may_diff numeric, jun numeric, jun_diff numeric, jul numeric, jul_diff numeric, aug numeric, aug_diff numeric, sep numeric, sep_diff numeric, oct numeric, oct_diff numeric, nov numeric, nov_diff numeric, "dec" numeric, dec_diff numeric, jan numeric, jan_diff numeric, feb numeric, feb_diff numeric, mar numeric, mar_diff numeric, total numeric, difference numeric, order_sequence character varying)
3
LANGUAGE plpgsql
4
AS $function$
5
6
DECLARE
7
v_organization_id UUID;
8
v_start_date DATE;
9
v_end_date DATE;
10
v_prev_start_date DATE;
11
v_prev_end_date DATE;
12
BEGIN
13
-- Get the organization_id for the given company_id
14
SELECT c.organization_id INTO v_organization_id
15
FROM public.companies c
16
WHERE c.id = p_company_id;
17
18
-- Get the start and end dates for the selected financial year
19
SELECT fy.start_date, fy.end_date INTO v_start_date, v_end_date
20
FROM public.finance_year fy
21
WHERE fy.id = p_finance_year_id;
22
23
-- Get the start and end dates for the previous financial year
24
SELECT pfy.start_date, pfy.end_date INTO v_prev_start_date, v_prev_end_date
25
FROM public.finance_year pfy
26
WHERE pfy.id = (p_finance_year_id - 1);
27
28
RETURN QUERY
29
WITH RECURSIVE account_hierarchy AS (
30
-- Get all income and expense accounts in a hierarchical order
31
SELECT
32
coa.id AS id,
33
act.name AS account_type,
34
coa.name AS account_name,
35
coa.parent_account_id,
36
0 AS level,
37
coa.account_number::TEXT::CHARACTER VARYING AS order_sequence -- ✅ Cast explicitly
38
FROM chart_of_accounts coa
39
JOIN account_types act ON coa.account_type_id = act.id
40
WHERE coa.organization_id = v_organization_id
41
AND act.id IN (4, 5)
42
AND (coa.parent_account_id IS NULL OR coa.parent_account_id = '00000000-0000-0000-0000-000000000000')
43
AND coa.is_deleted = FALSE
44
45
UNION ALL
46
47
SELECT
48
coa.id AS id,
49
act.name AS account_type,
50
coa.name AS account_name,
51
coa.parent_account_id,
52
parent.level + 1 AS level,
53
(parent.order_sequence || '.' || coa.account_number::TEXT)::CHARACTER VARYING -- ✅ Cast explicitly
54
FROM chart_of_accounts coa
55
JOIN account_hierarchy parent ON parent.id = coa.parent_account_id
56
JOIN account_types act ON coa.account_type_id = act.id
57
WHERE coa.organization_id = v_organization_id
58
AND coa.is_deleted = FALSE
59
),
60
years AS (
61
-- Ensure two years are always present
62
SELECT p_finance_year_id AS year
63
UNION ALL
64
SELECT p_finance_year_id - 1
65
),
66
all_accounts AS (
67
-- Ensure all accounts have both years
68
SELECT ah.id, ah.account_name, ah.account_type, ah.parent_account_id, ah.level, ah.order_sequence, y.year
69
FROM account_hierarchy ah
70
CROSS JOIN years y
71
),
72
monthly_totals AS (
73
-- Get monthly totals for the selected and previous financial years
74
SELECT
75
je.account_id AS id,
76
CASE
77
WHEN je.transaction_date BETWEEN v_start_date AND v_end_date THEN p_finance_year_id
78
WHEN je.transaction_date BETWEEN v_prev_start_date AND v_prev_end_date THEN (p_finance_year_id - 1)
79
END AS year,
80
EXTRACT(MONTH FROM je.transaction_date) AS month,
81
SUM(je.amount) AS amount
82
FROM journal_entries je
83
JOIN transaction_headers th ON je.transaction_id = th.id
84
WHERE th.company_id = p_company_id
85
AND je.transaction_date BETWEEN v_prev_start_date AND v_end_date
86
GROUP BY je.account_id, year, EXTRACT(MONTH FROM je.transaction_date)
87
),
88
pivoted_data AS (
89
-- Pivoting data to month-wise columns
90
SELECT
91
aa.id,
92
aa.account_type,
93
aa.account_name,
94
aa.parent_account_id,
95
aa.level,
96
aa.year,
97
aa.order_sequence,
98
COALESCE(SUM(CASE WHEN mt.month = 4 THEN mt.amount ELSE 0 END), 0) AS apr,
99
COALESCE(SUM(CASE WHEN mt.month = 5 THEN mt.amount ELSE 0 END), 0) AS may,
100
COALESCE(SUM(CASE WHEN mt.month = 6 THEN mt.amount ELSE 0 END), 0) AS jun,
101
COALESCE(SUM(CASE WHEN mt.month = 7 THEN mt.amount ELSE 0 END), 0) AS jul,
102
COALESCE(SUM(CASE WHEN mt.month = 8 THEN mt.amount ELSE 0 END), 0) AS aug,
103
COALESCE(SUM(CASE WHEN mt.month = 9 THEN mt.amount ELSE 0 END), 0) AS sep,
104
COALESCE(SUM(CASE WHEN mt.month = 10 THEN mt.amount ELSE 0 END), 0) AS oct,
105
COALESCE(SUM(CASE WHEN mt.month = 11 THEN mt.amount ELSE 0 END), 0) AS nov,
106
COALESCE(SUM(CASE WHEN mt.month = 12 THEN mt.amount ELSE 0 END), 0) AS "dec",
107
COALESCE(SUM(CASE WHEN mt.month = 1 THEN mt.amount ELSE 0 END), 0) AS jan,
108
COALESCE(SUM(CASE WHEN mt.month = 2 THEN mt.amount ELSE 0 END), 0) AS feb,
109
COALESCE(SUM(CASE WHEN mt.month = 3 THEN mt.amount ELSE 0 END), 0) AS mar,
110
COALESCE(SUM(mt.amount), 0) AS total
111
FROM all_accounts aa
112
LEFT JOIN monthly_totals mt ON aa.id = mt.id AND aa.year = mt.year
113
GROUP BY aa.id, aa.account_type, aa.account_name, aa.parent_account_id, aa.level, aa.order_sequence, aa.year
114
)
115
-- Final table with the difference calculation
116
SELECT
117
pd.id,
118
pd.account_type,
119
LPAD('', pd.level * 4, ' ') || pd.account_name,
120
pd.parent_account_id,
121
pd.level,
122
pd.year,
123
pd.apr, COALESCE(pd.apr - LAG(pd.apr) OVER (PARTITION BY pd.id ORDER BY pd.year), 0) AS apr_diff,
124
pd.may, COALESCE(pd.may - LAG(pd.may) OVER (PARTITION BY pd.id ORDER BY pd.year), 0) AS may_diff,
125
pd.jun, COALESCE(pd.jun - LAG(pd.jun) OVER (PARTITION BY pd.id ORDER BY pd.year), 0) AS jun_diff,
126
pd.jul, COALESCE(pd.jul - LAG(pd.jul) OVER (PARTITION BY pd.id ORDER BY pd.year), 0) AS jul_diff,
127
pd.aug, COALESCE(pd.aug - LAG(pd.aug) OVER (PARTITION BY pd.id ORDER BY pd.year), 0) AS aug_diff,
128
pd.sep, COALESCE(pd.sep - LAG(pd.sep) OVER (PARTITION BY pd.id ORDER BY pd.year), 0) AS sep_diff,
129
pd.oct, COALESCE(pd.oct - LAG(pd.oct) OVER (PARTITION BY pd.id ORDER BY pd.year), 0) AS oct_diff,
130
pd.nov, COALESCE(pd.nov - LAG(pd.nov) OVER (PARTITION BY pd.id ORDER BY pd.year), 0) AS nov_diff,
131
pd.dec, COALESCE(pd.dec - LAG(pd.dec) OVER (PARTITION BY pd.id ORDER BY pd.year), 0) AS dec_diff,
132
pd.jan, COALESCE(pd.jan - LAG(pd.jan) OVER (PARTITION BY pd.id ORDER BY pd.year), 0) AS jan_diff,
133
pd.feb, COALESCE(pd.feb - LAG(pd.feb) OVER (PARTITION BY pd.id ORDER BY pd.year), 0) AS feb_diff,
134
pd.mar, COALESCE(pd.mar - LAG(pd.mar) OVER (PARTITION BY pd.id ORDER BY pd.year), 0) AS mar_diff,
135
pd.total,
136
COALESCE(pd.total - LAG(pd.total) OVER (PARTITION BY pd.id ORDER BY pd.year), 0) AS difference,
137
pd.order_sequence::CHARACTER VARYING -- ✅ Ensures type consistency
138
FROM pivoted_data pd
139
ORDER BY pd.order_sequence;
140
141
END;
142
$function$
|
|||||
| Function | fn_check_account_id_exists | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.fn_check_account_id_exists(account_id uuid)
2
RETURNS boolean
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN EXISTS (
7
SELECT 1 FROM public.vendors WHERE id = account_id
8
) OR EXISTS (
9
SELECT 1 FROM public.customers WHERE id = account_id
10
) OR EXISTS (
11
SELECT 1 FROM public.chart_of_accounts WHERE id = account_id
12
);
13
END;
14
$function$
|
|||||
| Function | create_notification_with_message | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.create_notification_with_message(p_user_id uuid, p_message text, p_route text, p_short_desc text, p_created_by uuid, p_created_time timestamp without time zone, p_additional_parameter_id text)
2
RETURNS integer
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_notification_id INT;
7
BEGIN
8
-- Insert notification and retrieve generated ID
9
INSERT INTO notifications (
10
user_id, message, route, short_description, additional_parameter_id,
11
created_on_utc, created_by, is_deleted
12
) VALUES (
13
p_user_id, p_message, p_route, p_short_desc,p_additional_parameter_id,
14
p_created_time, p_created_by, FALSE
15
)
16
RETURNING id INTO v_notification_id;
17
RETURN v_notification_id;
18
END;
19
$function$
|
|||||
| Function | approve_visitor_for_unit | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.approve_visitor_for_unit(p_visitor_log_id integer, p_unit_id integer, p_resident_user uuid, p_resident_id integer)
2
RETURNS TABLE(visitor_log_id integer, unit_id integer, approver_resident_id integer, final_status_id integer)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
local_total_visits INT;
7
local_approved_count INT;
8
local_rejected_count INT;
9
local_new_visitor_status INT;
10
BEGIN
11
-- Step 1: Approve for this unit only
12
UPDATE public.multi_unit_visits
13
SET visitor_statuses = 2, -- Approved
14
modified_on_utc = NOW(),
15
modified_by = p_resident_user
16
WHERE visitor_log_id = p_visitor_log_id
17
AND unit_id = p_unit_id
18
AND is_deleted = FALSE;
19
20
IF NOT FOUND THEN
21
-- No update happened (invalid visitor_log_id or unit_id)
22
RETURN;
23
END IF;
24
25
-- Step 2: Recalculate parent visitor log status
26
SELECT
27
COUNT(*),
28
COUNT(*) FILTER (WHERE muv.visitor_statuses = 2), -- Approved
29
COUNT(*) FILTER (WHERE muv.visitor_statuses = 6) -- Rejected
30
INTO
31
local_total_visits,
32
local_approved_count,
33
local_rejected_count
34
FROM public.multi_unit_visits muv
35
WHERE muv.visitor_log_id = p_visitor_log_id
36
AND muv.is_deleted = FALSE;
37
38
-- Step 3: Decide new parent status
39
IF local_approved_count = local_total_visits AND local_total_visits > 0 THEN
40
local_new_visitor_status := 2; -- All Approved
41
ELSIF local_approved_count > 0 THEN
42
local_new_visitor_status := 7; -- Partial Approved
43
ELSIF local_rejected_count > 0 THEN
44
local_new_visitor_status := 6; -- Rejected
45
ELSE
46
local_new_visitor_status := 1; -- Pending
47
END IF;
48
49
UPDATE public.visitor_logs
50
SET visitor_status_id = local_new_visitor_status,
51
modified_on_utc = NOW(),
52
modified_by = p_resident_user
53
WHERE id = p_visitor_log_id;
54
55
-- Step 4: Return minimal information for the service layer
56
RETURN QUERY
57
SELECT
58
p_visitor_log_id,
59
p_unit_id,
60
p_resident_id, -- approver resident
61
local_new_visitor_status;
62
63
END;
64
$function$
|
|||||
| Function | get_address_id | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_address_id(p_country_id uuid, p_state_id uuid, p_city_id uuid, p_address_line1 text, p_zip_code text, p_created_by uuid, p_address_line2 text DEFAULT NULL::text)
2
RETURNS uuid
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_address_id UUID;
7
BEGIN
8
-- Insert into addresses table
9
INSERT INTO public.addresses (
10
id,
11
country_id,
12
state_id,
13
city_id,
14
address_line1,
15
address_line2,
16
zip_code,
17
created_on_utc,
18
created_by
19
) VALUES (
20
gen_random_uuid(), -- Generated address ID
21
p_country_id, -- Fetched country ID
22
p_state_id, -- Fetched state ID
23
p_city_id, -- City ID
24
p_address_line1, -- Address Line 1
25
p_address_line2,
26
p_zip_code, -- Zip Code
27
NOW(), -- Created on timestamp
28
p_created_by -- Created by
29
)RETURNING id INTO v_address_id;
30
31
RETURN v_address_id;
32
END;
33
$function$
|
|||||
| Function | get_all_child_accounts | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_all_child_accounts(p_organization_id uuid, p_group_id uuid)
2
RETURNS TABLE(id uuid)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
IF p_organization_id IS NULL OR p_group_id IS NULL THEN
7
RAISE EXCEPTION 'Organization ID and COA ID cannot be null';
8
END IF;
9
10
RETURN QUERY
11
WITH RECURSIVE account_group_view AS (
12
SELECT
13
ag.code,
14
ag.group_code,
15
ag.top_group_code,
16
ag.parent_group_code
17
FROM
18
public.account_groups ag
19
LEFT OUTER JOIN public.account_groups tgc
20
ON tgc.code = ag.top_group_code
21
WHERE
22
ag.organization_id = p_organization_id AND
23
ag.id = p_group_id
24
UNION ALL
25
SELECT
26
parent.code,
27
parent.group_code,
28
parent.top_group_code,
29
parent.parent_group_code
30
FROM
31
public.account_groups parent
32
LEFT OUTER JOIN public.account_groups tgc
33
ON tgc.code = parent.top_group_code
34
JOIN
35
account_group_view ag
36
ON parent.parent_group_code = ag.code
37
AND parent.organization_id = p_organization_id
38
)
39
40
SELECT
41
coa.id
42
FROM
43
public.chart_of_accounts coa
44
INNER JOIN account_group_view agv on coa.account_group_code = agv.code;
45
END;
46
$function$
|
|||||
| Function | get_opening_balances | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_opening_balances(p_organization_id uuid, p_fin_year_id integer)
2
RETURNS TABLE(account_id uuid, balance numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
ob.account_id,
9
SUM(
10
CASE
11
WHEN ob.entry_type = 'D' THEN ob.balance
12
ELSE -ob.balance
13
END
14
) AS balance
15
FROM public.opening_balances ob
16
WHERE ob.organization_id = p_organization_id
17
AND ob.finyear_id = p_fin_year_id
18
AND ob.is_deleted = false
19
group by ob.account_id;
20
END;
21
$function$
|
|||||
| Function | get_trial_balance_of_company_fy | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_trial_balance_of_company_fy(p_company_id uuid, p_finance_year_id integer)
2
RETURNS TABLE(account_id uuid, account_type text, account_category text, account_number text, account_name text, total_debits numeric, total_credits numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
WITH RECURSIVE coa_hierarchy AS (
8
-- Base case: Fetch the top-level (root) accounts with no parent (e.g., Assets, Liabilities)
9
SELECT
10
coa.id,
11
act.name AS account_type,
12
coa.account_number::integer,
13
coa.name,
14
coa.parent_account_id,
15
0 AS level,
16
coa.account_number::text AS order_sequence
17
FROM
18
public.chart_of_accounts coa
19
INNER JOIN
20
public.account_types act ON coa.account_type_id = act.id
21
WHERE
22
coa.is_deleted = FALSE
23
AND coa.parent_account_id = '00000000-0000-0000-0000-000000000000' -- Root-level accounts
24
25
UNION ALL
26
27
-- Recursive case: Fetch all child accounts for each parent
28
SELECT
29
coa.id,
30
act.name AS account_type,
31
coa.account_number::integer,
32
coa.name,
33
coa.parent_account_id,
34
ch.level + 1 AS level,
35
ch.order_sequence || '.' || coa.account_number::text AS order_sequence
36
FROM
37
public.chart_of_accounts coa
38
INNER JOIN
39
public.account_types act ON coa.account_type_id = act.id
40
INNER JOIN
41
coa_hierarchy ch ON ch.id = coa.parent_account_id
42
WHERE
43
coa.is_deleted = FALSE
44
),
45
coa_with_balances AS (
46
-- Fetch the current financial year start and end date from the finance_year table
47
SELECT
48
fy.start_date::DATE, -- Cast start_date to DATE
49
fy.end_date::DATE, -- Cast end_date to DATE
50
fy.year
51
FROM
52
public.finance_year fy
53
WHERE
54
fy.id = p_finance_year_id
55
LIMIT 1
56
),
57
journal_entries_filtered AS (
58
-- Fetch accounts that have non-zero balances within the financial year range
59
SELECT
60
je.account_id,
61
SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END) AS total_debits,
62
SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END) AS total_credits
63
FROM
64
public.journal_entries je
65
JOIN
66
public.transaction_headers th ON je.transaction_id = th.id
67
JOIN
68
coa_with_balances fy ON th.transaction_date BETWEEN fy.start_date AND fy.end_date -- Filter by financial year
69
WHERE
70
th.company_id = p_company_id
71
AND je.is_deleted = FALSE
72
GROUP BY
73
je.account_id
74
HAVING
75
SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END) > 0 OR
76
SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END) > 0
77
),
78
relevant_accounts AS (
79
-- Select accounts that have balances directly
80
SELECT
81
ch.id,
82
ch.account_type,
83
ch.account_number,
84
ch.name,
85
ch.level,
86
ch.order_sequence,
87
ch.parent_account_id,
88
wb.total_debits,
89
wb.total_credits
90
FROM
91
coa_hierarchy ch
92
LEFT JOIN
93
journal_entries_filtered wb ON ch.id = wb.account_id
94
WHERE
95
wb.account_id IS NOT NULL -- Only accounts with actual balances
96
97
UNION ALL
98
99
-- Recursively select parent accounts, without recalculating balances
100
SELECT
101
ch.id,
102
ch.account_type,
103
ch.account_number,
104
ch.name,
105
ch.level,
106
ch.order_sequence,
107
ch.parent_account_id,
108
NULL AS total_debits,
109
NULL AS total_credits
110
FROM
111
coa_hierarchy ch
112
INNER JOIN
113
relevant_accounts ra ON ch.id = ra.parent_account_id -- Propagate balance up to parent accounts
114
)
115
-- Final output
116
SELECT DISTINCT ON (ra.order_sequence)
117
ra.id::uuid AS account_id, -- Chart of Accounts ID
118
ra.account_type::TEXT AS account_type, -- Account type
119
ac.name::TEXT AS account_category, -- Account category
120
ra.account_number::TEXT AS account_number, -- Account number
121
LPAD('', ra.level * 4, ' ') || ra.name::TEXT AS account_name, -- Indented account name
122
COALESCE(ra.total_debits, 0) AS total_debits, -- Sum of debits
123
COALESCE(ra.total_credits, 0) AS total_credits -- Sum of credits
124
-- Cast end date of the financial year to DATE
125
FROM
126
relevant_accounts ra
127
LEFT JOIN
128
public.chart_of_accounts coa ON ra.id = coa.id -- Join chart of accounts
129
LEFT JOIN
130
public.account_types at ON coa.account_type_id = at.id -- Join account types
131
LEFT JOIN
132
public.account_categories ac ON at.account_category_id = ac.id -- Join account categories
133
CROSS JOIN
134
coa_with_balances fy -- Include the financial year details
135
ORDER BY
136
ra.order_sequence; -- Maintain hierarchy in the output
137
END;
138
$function$
|
|||||
| Function | pgp_pub_encrypt_bytea | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.pgp_pub_encrypt_bytea(bytea, bytea, text)
2
RETURNS bytea
3
LANGUAGE c
4
PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pgp_pub_encrypt_bytea$function$
|
|||||
| Function | pgp_pub_decrypt | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.pgp_pub_decrypt(bytea, bytea)
2
RETURNS text
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pgp_pub_decrypt_text$function$
|
|||||
| Function | pgp_pub_decrypt_bytea | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.pgp_pub_decrypt_bytea(bytea, bytea)
2
RETURNS bytea
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pgp_pub_decrypt_bytea$function$
|
|||||
| Function | search_function_with_paramter | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.search_function_with_paramter(keyword text)
2
RETURNS TABLE(function_name text, function_definition text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
proname AS function_name,
9
prosrc AS function_definition
10
FROM pg_proc
11
WHERE prosrc ILIKE '%' || keyword || '%';
12
END;
13
$function$
|
|||||
| Function | get_balance_sheet | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_balance_sheet(p_company_id uuid, p_finance_year_id integer, p_as_on_date date, p_is_get_all_for_organizations boolean)
2
RETURNS TABLE(account_id uuid, account_type text, account_name text, amount numeric, category text)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_start_date DATE;
7
v_end_date DATE;
8
v_company_ids UUID[];
9
v_organization_id UUID;
10
asset_account_ids integer[];
11
liability_account_ids integer[];
12
equity_account_ids integer[];
13
BEGIN
14
-- Fetch financial year start and end dates
15
SELECT fy.start_date, fy.end_date
16
INTO v_start_date, v_end_date
17
FROM public.finance_year fy
18
WHERE fy.id = p_finance_year_id;
19
20
-- Cap the end date by the given as_on_date
21
v_end_date := LEAST(v_end_date, p_as_on_date);
22
23
-- Get organization id of company
24
SELECT c.organization_id INTO v_organization_id
25
FROM public.companies c
26
WHERE c.id = p_company_id;
27
28
-- Get account type hierarchies
29
asset_account_ids := ARRAY(SELECT id FROM get_account_type_hierarchy(1)); -- Assets
30
liability_account_ids := ARRAY(SELECT id FROM get_account_type_hierarchy(2)); -- Liabilities
31
equity_account_ids := ARRAY(SELECT id FROM get_account_type_hierarchy(3)); -- Equity
32
33
-- Company IDs based on flag
34
IF p_is_get_all_for_organizations THEN
35
SELECT ARRAY_AGG(id)
36
INTO v_company_ids
37
FROM public.companies
38
WHERE organization_id = v_organization_id;
39
ELSE
40
v_company_ids := ARRAY[p_company_id];
41
END IF;
42
43
-- Assets
44
RETURN QUERY
45
SELECT
46
coa.id,
47
at.name,
48
coa.name,
49
COALESCE(SUM(
50
CASE
51
WHEN je.entry_type = 'D' THEN je.amount
52
WHEN je.entry_type = 'C' THEN -je.amount
53
ELSE 0
54
END
55
),0) AS amount,
56
'Assets'
57
FROM public.chart_of_accounts coa
58
JOIN public.account_types at ON coa.account_type_id = at.id
59
JOIN public.journal_entries je ON je.account_id = coa.id
60
JOIN public.transaction_headers th ON th.id = je.transaction_id
61
WHERE th.company_id = ANY(v_company_ids)
62
AND th.transaction_date BETWEEN v_start_date AND v_end_date
63
AND NOT th.is_deleted
64
AND NOT je.is_deleted
65
AND coa.account_type_id = ANY(asset_account_ids)
66
GROUP BY coa.id, at.name, coa.name, coa.account_number, at.id
67
ORDER BY coa.account_number, at.id;
68
69
-- Liabilities
70
RETURN QUERY
71
SELECT
72
coa.id,
73
at.name,
74
coa.name,
75
COALESCE(SUM(
76
CASE
77
WHEN je.entry_type = 'C' THEN je.amount
78
WHEN je.entry_type = 'D' THEN -je.amount
79
ELSE 0
80
END
81
),0) AS amount,
82
'Liabilities'
83
FROM public.chart_of_accounts coa
84
JOIN public.account_types at ON coa.account_type_id = at.id
85
JOIN public.journal_entries je ON je.account_id = coa.id
86
JOIN public.transaction_headers th ON th.id = je.transaction_id
87
WHERE th.company_id = ANY(v_company_ids)
88
AND th.transaction_date BETWEEN v_start_date AND v_end_date
89
AND NOT th.is_deleted
90
AND NOT je.is_deleted
91
AND coa.account_type_id = ANY(liability_account_ids)
92
GROUP BY coa.id, at.name, coa.name, coa.account_number, at.id
93
ORDER BY coa.account_number, at.id;
94
95
-- Equity
96
RETURN QUERY
97
SELECT
98
coa.id,
99
at.name,
100
coa.name,
101
COALESCE(SUM(
102
CASE
103
WHEN je.entry_type = 'C' THEN je.amount
104
WHEN je.entry_type = 'D' THEN -je.amount
105
ELSE 0
106
END
107
),0) AS amount,
108
'Equity'
109
FROM public.chart_of_accounts coa
110
JOIN public.account_types at ON coa.account_type_id = at.id
111
JOIN public.journal_entries je ON je.account_id = coa.id
112
JOIN public.transaction_headers th ON th.id = je.transaction_id
113
WHERE th.company_id = ANY(v_company_ids)
114
AND th.transaction_date BETWEEN v_start_date AND v_end_date
115
AND NOT th.is_deleted
116
AND NOT je.is_deleted
117
AND coa.account_type_id = ANY(equity_account_ids)
118
GROUP BY coa.id, at.name, coa.name, coa.account_number, at.id
119
ORDER BY coa.account_number, at.id;
120
121
END;
122
$function$
|
|||||
| Function | get_all_account_groups | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_all_account_groups(p_organization_id uuid)
2
RETURNS TABLE(id uuid, organization_id uuid, code integer, group_code integer, top_group_code integer, parent_group_code integer, name text, level integer, order_sequence character varying, schedule text, is_main_group boolean)
3
LANGUAGE plpgsql
4
AS $function$
5
6
#variable_conflict use_column
7
begin
8
RETURN query
9
WITH RECURSIVE account_group_view AS (
10
SELECT
11
id,
12
organization_id,
13
code,
14
group_code,
15
top_group_code,
16
parent_group_code,
17
name,
18
0 AS level,
19
CAST(code AS varchar(50)) AS order_sequence,
20
schedule,
21
is_main_group
22
FROM
23
public.account_groups
24
WHERE
25
organization_id = p_organization_id AND
26
parent_group_code is null
27
UNION ALL
28
SELECT
29
parent.id,
30
parent.organization_id,
31
parent.code,
32
parent.group_code,
33
parent.top_group_code,
34
parent.parent_group_code,
35
parent.name,
36
level + 1 AS level,
37
CAST(order_sequence || '_' || CAST(parent.code AS VARCHAR (50)) AS VARCHAR(50)) AS order_sequence,
38
parent.schedule,
39
parent.is_main_group
40
FROM
41
public.account_groups parent
42
JOIN
43
account_group_view ag
44
ON parent.parent_group_code = ag.code AND parent.organization_id = p_organization_id
45
)
46
47
SELECT
48
id,
49
organization_id,
50
code,
51
group_code,
52
top_group_code,
53
parent_group_code,
54
RIGHT(' ',level*3) || name AS name,
55
level,
56
order_sequence,
57
schedule,
58
is_main_group
59
FROM
60
account_group_view
61
ORDER BY
62
order_sequence;
63
64
end
65
66
$function$
|
|||||
| Function | get_user_notifications_with_message | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_user_notifications_with_message(p_user_id uuid)
2
RETURNS TABLE(notification_id integer, message text, short_desc text, route text, message_created_time timestamp without time zone, additional_parameter_id text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
nt.id,
9
nt.message,
10
nt.short_description,
11
nt.route,
12
msg.created_time,
13
msg.additional_parameter_id
14
FROM notification_types nt
15
LEFT JOIN LATERAL (
16
SELECT m.*
17
FROM messages m
18
WHERE m.notification_type_id = nt.id AND m.is_deleted = FALSE
19
ORDER BY m.created_time DESC
20
LIMIT 1
21
) msg ON true
22
WHERE nt.user_id = p_user_id
23
--AND nt.created_on_utc >= NOW() - INTERVAL '24 hours'
24
AND nt.is_deleted = FALSE;
25
END;
26
$function$
|
|||||
| Function | get_city_id | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_city_id(p_city_name text, p_zip_code text, p_state_id uuid, p_created_by uuid)
2
RETURNS uuid
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_city_id UUID;
7
BEGIN
8
SELECT id INTO v_city_id
9
FROM public.cities
10
WHERE LOWER(name) = LOWER(p_city_name) AND zip_code = p_zip_code;
11
12
IF v_city_id IS NULL THEN
13
INSERT INTO public.cities (id, name, state_id, zip_code, created_on_utc, created_by)
14
VALUES (gen_random_uuid(), p_city_name, p_state_id, p_zip_code, NOW(), p_created_by)
15
RETURNING id INTO v_city_id;
16
END IF;
17
18
RETURN v_city_id;
19
END;
20
$function$
|
|||||
| Function | grant_full_schema_access | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.grant_full_schema_access(p_schema text, p_user text)
2
RETURNS void
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
obj RECORD;
7
BEGIN
8
-- Grant on tables
9
FOR obj IN
10
SELECT table_name
11
FROM information_schema.tables
12
WHERE table_schema = p_schema
13
AND table_type = 'BASE TABLE'
14
LOOP
15
EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE %I.%I TO %I;', p_schema, obj.table_name, p_user);
16
RAISE NOTICE 'GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE %.% TO %;', p_schema, obj.table_name, p_user;
17
END LOOP;
18
19
-- Grant on sequences (USAGE + SELECT + UPDATE: full coverage)
20
FOR obj IN
21
SELECT c.relname AS sequence_name
22
FROM pg_class c
23
JOIN pg_namespace n ON n.oid = c.relnamespace
24
WHERE c.relkind = 'S'
25
AND n.nspname = p_schema
26
LOOP
27
EXECUTE format('GRANT USAGE, SELECT, UPDATE ON SEQUENCE %I.%I TO %I;', p_schema, obj.sequence_name, p_user);
28
RAISE NOTICE 'GRANT USAGE, SELECT, UPDATE ON SEQUENCE %.% TO %;', p_schema, obj.sequence_name, p_user;
29
END LOOP;
30
31
-- Grant on all functions (handles all argument types)
32
FOR obj IN
33
SELECT
34
p.proname AS function_name,
35
pg_get_function_identity_arguments(p.oid) AS args
36
FROM
37
pg_proc p
38
JOIN pg_namespace n ON p.pronamespace = n.oid
39
WHERE
40
n.nspname = p_schema
41
AND p.prokind = 'f' -- f = function
42
LOOP
43
EXECUTE format(
44
'GRANT EXECUTE ON FUNCTION %I.%I(%s) TO %I;',
45
p_schema, obj.function_name, obj.args, p_user
46
);
47
RAISE NOTICE 'GRANT EXECUTE ON FUNCTION %.%(%) TO %;', p_schema, obj.function_name, obj.args, p_user;
48
END LOOP;
49
50
-- Grant on all procedures (Postgres 11+)
51
FOR obj IN
52
SELECT
53
p.proname AS procedure_name,
54
pg_get_function_identity_arguments(p.oid) AS args
55
FROM
56
pg_proc p
57
JOIN pg_namespace n ON p.pronamespace = n.oid
58
WHERE
59
n.nspname = p_schema
60
AND p.prokind = 'p' -- p = procedure
61
LOOP
62
EXECUTE format(
63
'GRANT EXECUTE ON PROCEDURE %I.%I(%s) TO %I;',
64
p_schema, obj.procedure_name, obj.args, p_user
65
);
66
RAISE NOTICE 'GRANT EXECUTE ON PROCEDURE %.%(%) TO %;', p_schema, obj.procedure_name, obj.args, p_user;
67
END LOOP;
68
69
END;
70
$function$
|
|||||
| Function | create_budget | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.create_budget(p_company_id uuid, p_finance_year_id integer, p_name text, p_status_id integer, p_created_by uuid, p_budget_lines_json jsonb)
2
RETURNS uuid
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_budget_id uuid;
7
v_now timestamp := now();
8
line jsonb;
9
v_line_id uuid;
10
BEGIN
11
RAISE NOTICE '---- create_budget() START ----';
12
RAISE NOTICE '---- create_budget() START 123----';
13
RAISE NOTICE 'Company: %, FinanceYear: %, Name: %', p_company_id, p_finance_year_id, p_name;
14
RAISE NOTICE 'Input budget lines JSON: %', p_budget_lines_json;
15
16
-- Step 1: Check if a budget already exists
17
SELECT id INTO v_budget_id
18
FROM budgets
19
WHERE company_id = p_company_id
20
AND finance_year_id = p_finance_year_id
21
AND is_deleted = false
22
LIMIT 1;
23
24
IF v_budget_id IS NOT NULL THEN
25
RAISE NOTICE 'Existing budget found: %', v_budget_id;
26
UPDATE budgets
27
SET name = p_name,
28
status_id = p_status_id,
29
modified_by = p_created_by,
30
modified_on_utc = v_now
31
WHERE id = v_budget_id;
32
ELSE
33
v_budget_id := gen_random_uuid();
34
RAISE NOTICE 'Inserting new budget: %', v_budget_id;
35
INSERT INTO budgets (
36
id, company_id, finance_year_id, name, status_id,
37
created_by, created_on_utc, is_deleted
38
)
39
VALUES (
40
v_budget_id, p_company_id, p_finance_year_id, p_name, p_status_id,
41
p_created_by, v_now, false
42
);
43
END IF;
44
45
-- Step 2: Loop through each line
46
FOR line IN SELECT * FROM jsonb_array_elements(p_budget_lines_json)
47
LOOP
48
RAISE NOTICE 'Processing line: %', line;
49
50
v_line_id := NULLIF(line->>'id', '')::uuid;
51
IF v_line_id IS NULL OR v_line_id = '00000000-0000-0000-0000-000000000000'::uuid THEN
52
v_line_id := gen_random_uuid();
53
RAISE NOTICE 'Generated new line ID: %', v_line_id;
54
ELSE
55
RAISE NOTICE 'Using provided line ID: %', v_line_id;
56
END IF;
57
58
IF EXISTS (
59
SELECT 1 FROM budget_lines
60
WHERE id = v_line_id AND budget_id = v_budget_id AND is_deleted = false
61
) THEN
62
RAISE NOTICE 'Updating existing budget line: %', v_line_id;
63
UPDATE budget_lines
64
SET account_id = (line->>'accountId')::uuid,
65
period_start = (line->>'periodStart')::timestamp::date,
66
period_end = (line->>'periodEnd')::timestamp::date,
67
amount = (line->>'amount')::numeric,
68
modified_by = p_created_by,
69
modified_on_utc = v_now,
70
is_deleted = false
71
WHERE id = v_line_id AND budget_id = v_budget_id;
72
ELSE
73
RAISE NOTICE 'Inserting new budget line: %', v_line_id;
74
INSERT INTO budget_lines (
75
id, budget_id, account_id, period_start, period_end,
76
amount, created_by, created_on_utc, is_deleted
77
)
78
VALUES (
79
v_line_id,
80
v_budget_id,
81
(line->>'accountId')::uuid,
82
(line->>'periodStart')::timestamp::date,
83
(line->>'periodEnd')::timestamp::date,
84
(line->>'amount')::numeric,
85
p_created_by,
86
v_now,
87
false
88
);
89
END IF;
90
END LOOP;
91
92
RAISE NOTICE '---- create_budget() END ----';
93
RETURN v_budget_id;
94
END;
95
$function$
|
|||||
| Function | dummy_hello_function | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.dummy_hello_function(name text)
2
RETURNS text
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN 'Hello, ' || name || '!';
7
END;
8
$function$
|
|||||
| Function | get_account_total_amount_by_date_range | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_account_total_amount_by_date_range(p_company_id uuid, p_finance_id integer, p_account_id uuid, p_start_date date DEFAULT NULL::date, p_end_date date DEFAULT NULL::date)
2
RETURNS numeric
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_financial_year_start DATE;
7
v_financial_year_end DATE;
8
v_total_amount NUMERIC := 0;
9
v_organization_id UUID;
10
BEGIN
11
-- Step 1: Get the organization ID for the given company
12
SELECT organization_id
13
INTO v_organization_id
14
FROM public.companies
15
WHERE id = p_company_id;
16
17
-- Step 2: Validate if the organization ID exists
18
IF v_organization_id IS NULL THEN
19
RAISE EXCEPTION 'Organization ID not found for company ID %', p_company_id;
20
END IF;
21
22
-- Step 3: Get the financial year start and end dates
23
SELECT fy.start_date, fy.end_date
24
INTO v_financial_year_start, v_financial_year_end
25
FROM public.finance_year fy
26
WHERE fy.id = p_finance_id;
27
28
-- Step 4: Check if the financial year exists
29
IF v_financial_year_start IS NULL OR v_financial_year_end IS NULL THEN
30
RAISE EXCEPTION 'Financial year with ID % not found', p_finance_id;
31
END IF;
32
33
-- Step 5: Calculate the total amount for the specific account within the given date range
34
SELECT COALESCE(SUM(je.amount), 0) INTO v_total_amount
35
FROM public.journal_entries je
36
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
37
INNER JOIN public.chart_of_accounts coa ON je.account_id = coa.id
38
WHERE th.company_id = p_company_id
39
AND je.account_id = p_account_id
40
AND coa.organization_id = v_organization_id -- Match organization ID
41
AND je.transaction_date BETWEEN COALESCE(p_start_date, v_financial_year_start) -- Use provided start date or financial year start
42
AND COALESCE(p_end_date, v_financial_year_end) -- Use provided end date or financial year end
43
AND je.is_deleted = FALSE;
44
45
-- Step 6: Return the total amount
46
RETURN v_total_amount;
47
END;
48
$function$
|
|||||
| Function | get_all_users | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_all_users()
2
RETURNS TABLE(id uuid, first_name character varying, last_name character varying, email character varying, phone_number character varying, user_name text)
3
LANGUAGE sql
4
AS $function$
5
SELECT id, first_name, last_name, email, phone_number, user_name
6
FROM public.users;
7
$function$
|
|||||
| Function | get_balance_sheet_3 | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_balance_sheet_3(p_company_id uuid, p_finance_year_id integer, p_is_get_all_for_organizations boolean)
2
RETURNS TABLE(account_type text, account_name text, amount numeric, category text)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_start_date DATE;
7
v_end_date DATE;
8
v_company_ids UUID[];
9
v_organization_id UUID;
10
asset_account_ids integer[];
11
liability_account_ids integer[];
12
equity_account_ids integer[];
13
BEGIN
14
-- Fetch financial year start and end dates
15
SELECT fy.start_date, fy.end_date
16
INTO v_start_date, v_end_date
17
FROM public.finance_year fy
18
WHERE fy.id = p_finance_year_id;
19
20
select c.organization_id into v_organization_id
21
from public.companies c
22
where c.id = p_company_id;
23
24
-- Get account type hierarchies for assets, liabilities, and equity
25
asset_account_ids := ARRAY(SELECT id FROM get_account_type_hierarchy(1)); -- Assets
26
liability_account_ids := ARRAY(SELECT id FROM get_account_type_hierarchy(2)); -- Liabilities
27
equity_account_ids := ARRAY(SELECT id FROM get_account_type_hierarchy(3)); -- Equity
28
29
-- Determine company IDs to fetch data for (single or all organizations)
30
IF p_is_get_all_for_organizations THEN
31
SELECT ARRAY_AGG(id)
32
INTO v_company_ids
33
FROM public.companies
34
WHERE organization_id = (SELECT organization_id FROM public.companies WHERE id = p_company_id);
35
ELSE
36
v_company_ids := ARRAY[p_company_id];
37
END IF;
38
39
-- Return Assets
40
RETURN QUERY
41
SELECT
42
at.name AS account_type,
43
coa.name AS account_name,
44
COALESCE(
45
SUM(
46
CASE
47
WHEN ob.entry_type = 'D' THEN ob.balance
48
ELSE -ob.balance
49
END
50
), 0) +
51
COALESCE(
52
SUM(CASE
53
WHEN je.entry_type = 'D' THEN je.amount -- Debit increases asset balance
54
WHEN je.entry_type = 'C' THEN -je.amount -- Credit decreases asset balance
55
ELSE 0
56
END), 0
57
) AS amount,
58
'Assets' AS category
59
FROM public.chart_of_accounts coa
60
LEFT JOIN public.journal_entries je ON je.account_id = coa.id
61
LEFT JOIN public.transaction_headers th ON je.transaction_id = th.id
62
LEFT JOIN public.opening_balances ob ON ob.account_id = coa.id
63
AND ob.organization_id = v_organization_id
64
AND ob.finyear_id = p_finance_year_id
65
AND ob.is_deleted = FALSE
66
INNER JOIN public.account_types at ON coa.account_type_id = at.id
67
WHERE th.company_id = ANY(v_company_ids)
68
AND th.transaction_date BETWEEN v_start_date AND v_end_date
69
AND NOT th.is_deleted
70
AND NOT je.is_deleted
71
AND coa.account_type_id = ANY(asset_account_ids)
72
GROUP BY at.name, coa.name, coa.account_number, at.id
73
ORDER BY coa.account_number, at.id;
74
75
-- Return Liabilities
76
RETURN QUERY
77
SELECT
78
at.name AS account_type,
79
coa.name AS account_name,
80
COALESCE(
81
SUM(
82
CASE
83
WHEN ob.entry_type = 'C' THEN ob.balance
84
ELSE -ob.balance
85
END
86
), 0) +
87
COALESCE(
88
SUM(CASE
89
WHEN je.entry_type = 'C' THEN je.amount
90
WHEN je.entry_type = 'D' THEN -je.amount
91
ELSE 0
92
END), 0
93
) AS amount,
94
'Liabilities' AS category
95
FROM public.chart_of_accounts coa
96
LEFT JOIN public.journal_entries je ON je.account_id = coa.id
97
LEFT JOIN public.transaction_headers th ON je.transaction_id = th.id
98
LEFT JOIN public.opening_balances ob ON ob.account_id = coa.id
99
AND ob.organization_id = v_organization_id
100
AND ob.finyear_id = p_finance_year_id
101
AND ob.is_deleted = FALSE
102
INNER JOIN public.account_types at ON coa.account_type_id = at.id
103
WHERE th.company_id = ANY(v_company_ids)
104
AND th.transaction_date BETWEEN v_start_date AND v_end_date
105
AND NOT th.is_deleted
106
AND NOT je.is_deleted
107
AND coa.account_type_id = ANY(liability_account_ids)
108
GROUP BY at.name, coa.name, coa.account_number, at.id
109
ORDER BY coa.account_number, at.id;
110
111
-- Return Equity
112
RETURN QUERY
113
SELECT
114
at.name AS account_type,
115
coa.name AS account_name,
116
COALESCE(
117
SUM(
118
CASE
119
WHEN ob.entry_type = 'C' THEN ob.balance
120
ELSE -ob.balance
121
END
122
), 0) +
123
COALESCE(
124
SUM(CASE
125
WHEN je.entry_type = 'C' THEN je.amount
126
WHEN je.entry_type = 'D' THEN -je.amount
127
ELSE 0
128
END), 0
129
) AS amount,
130
'Equity' AS category
131
FROM public.chart_of_accounts coa
132
LEFT JOIN public.journal_entries je ON je.account_id = coa.id
133
LEFT JOIN public.transaction_headers th ON je.transaction_id = th.id
134
LEFT JOIN public.opening_balances ob ON ob.account_id = coa.id
135
AND ob.organization_id = v_organization_id
136
AND ob.finyear_id = p_finance_year_id
137
AND ob.is_deleted = FALSE
138
INNER JOIN public.account_types at ON coa.account_type_id = at.id
139
WHERE th.company_id = ANY(v_company_ids)
140
AND th.transaction_date BETWEEN v_start_date AND v_end_date
141
AND NOT th.is_deleted
142
AND NOT je.is_deleted
143
AND coa.account_type_id = ANY(equity_account_ids)
144
GROUP BY at.name, coa.name, coa.account_number, at.id
145
ORDER BY coa.account_number, at.id;
146
147
END;
148
$function$
|
|||||
| Function | get_customer_dues | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_customer_dues(p_company_id uuid)
2
RETURNS TABLE(customer_name text, coa_name text, amount numeric, due_date date, aging_bucket text)
3
LANGUAGE plpgsql
4
AS $function$
5
6
BEGIN
7
RETURN QUERY
8
SELECT
9
c.name::text AS customer_name,
10
coa.name AS coa_name,
11
je.amount,
12
(th.transaction_date + INTERVAL '30 days')::date AS due_date, -- Assuming a 30-day payment term
13
CASE
14
WHEN (th.transaction_date + INTERVAL '30 days') <= CURRENT_DATE THEN 'Current'
15
WHEN (th.transaction_date + INTERVAL '30 days') BETWEEN CURRENT_DATE - INTERVAL '30 days' AND CURRENT_DATE THEN '1-30 Days Past Due'
16
WHEN (th.transaction_date + INTERVAL '60 days') BETWEEN CURRENT_DATE - INTERVAL '60 days' AND CURRENT_DATE - INTERVAL '31 days' THEN '31-60 Days Past Due'
17
ELSE 'Over 60 Days Past Due'
18
END AS aging_bucket
19
FROM
20
public.journal_entries je
21
JOIN
22
public.transaction_headers th ON je.transaction_id = th.id
23
JOIN
24
public.chart_of_accounts coa ON je.account_id = coa.id
25
JOIN
26
public.customers c ON th.customer_id = c.id
27
WHERE
28
coa.account_type_id = (SELECT id FROM public.account_types WHERE name = 'Accounts Receivable')
29
AND je.is_deleted = false
30
AND (th.transaction_date + INTERVAL '30 days') <= CURRENT_DATE
31
AND th.company_id = p_company_id
32
ORDER BY
33
(th.transaction_date + INTERVAL '30 days')::date;
34
END;
35
36
$function$
|
|||||
| Function | get_total_expense | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_total_expense(p_company_id uuid, p_finance_id integer)
2
RETURNS numeric
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_financial_year_start DATE;
7
v_financial_year_end DATE;
8
v_total_expense NUMERIC := 0;
9
v_organization_id UUID;
10
BEGIN
11
-- Step 1: Get the organization ID for the given company
12
SELECT organization_id
13
INTO v_organization_id
14
FROM public.companies
15
WHERE id = p_company_id;
16
17
-- Step 2: Validate if the organization ID exists
18
IF v_organization_id IS NULL THEN
19
RAISE EXCEPTION 'Organization ID not found for company ID %', p_company_id;
20
END IF;
21
22
-- Step 3: Get the financial year start and end dates
23
SELECT fy.start_date, fy.end_date
24
INTO v_financial_year_start, v_financial_year_end
25
FROM public.finance_year fy
26
WHERE fy.id = p_finance_id;
27
28
-- Step 4: Validate if the financial year exists
29
IF v_financial_year_start IS NULL OR v_financial_year_end IS NULL THEN
30
RAISE EXCEPTION 'Financial year with ID % not found', p_finance_id;
31
END IF;
32
33
-- Step 5: Calculate the total expense
34
SELECT COALESCE(SUM(je.amount), 0) INTO v_total_expense
35
FROM public.journal_entries je
36
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
37
INNER JOIN public.chart_of_accounts coa ON je.account_id = coa.id
38
WHERE th.company_id = p_company_id
39
AND coa.organization_id = v_organization_id -- Match organization ID
40
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
41
AND je.is_deleted = FALSE
42
AND EXISTS (
43
SELECT 1
44
FROM public.account_types at
45
WHERE at.id = coa.account_type_id
46
AND at.account_category_id = 5 -- Expense category
47
);
48
49
-- Step 6: Return the total expense
50
RETURN v_total_expense;
51
END;
52
$function$
|
|||||
| Function | get_income_expense_overview | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_income_expense_overview(p_company_id uuid, p_period_type integer, p_finance_id integer)
2
RETURNS TABLE(period text, year numeric, total_income numeric, total_expense numeric, actual_income numeric, actual_expense numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_financial_year_start DATE;
7
v_financial_year_end DATE;
8
v_finance_year_start NUMERIC;
9
v_finance_year_end NUMERIC;
10
11
CONST_MONTHLY CONSTANT INTEGER := 1;
12
CONST_QUARTERLY CONSTANT INTEGER := 2;
13
CONST_HALF_YEARLY CONSTANT INTEGER := 3;
14
CONST_YEARLY CONSTANT INTEGER := 4;
15
CONST_YTD CONSTANT INTEGER := 5;
16
BEGIN
17
IF p_period_type != CONST_YTD THEN
18
SELECT start_date, end_date,
19
EXTRACT(YEAR FROM start_date),
20
EXTRACT(YEAR FROM end_date)
21
INTO v_financial_year_start, v_financial_year_end,
22
v_finance_year_start, v_finance_year_end
23
FROM public.finance_year
24
WHERE id = p_finance_id;
25
END IF;
26
27
IF p_period_type = CONST_YTD THEN
28
-- Returns last 5 years, fiscal quarters (Apr–Jun, Jul–Sep, Oct–Dec, Jan–Mar)
29
RETURN QUERY
30
WITH recent_years AS (
31
SELECT fy.id, fy.start_date, fy.end_date,
32
EXTRACT(YEAR FROM fy.start_date) AS fiscal_year
33
FROM public.finance_year fy
34
WHERE EXTRACT(YEAR FROM fy.start_date) >= EXTRACT(YEAR FROM CURRENT_DATE) - 4
35
AND EXTRACT(YEAR FROM fy.start_date) <= EXTRACT(YEAR FROM CURRENT_DATE)
36
ORDER BY fy.start_date DESC
37
LIMIT 5
38
),
39
quarters AS (
40
SELECT id AS finance_id, fiscal_year, 'Q1' AS period,
41
make_date(fiscal_year::int, 4, 1) AS period_start,
42
make_date(fiscal_year::int, 6, 30) AS period_end
43
FROM recent_years
44
UNION ALL
45
SELECT id, fiscal_year, 'Q2',
46
make_date(fiscal_year::int, 7, 1),
47
make_date(fiscal_year::int, 9, 30)
48
FROM recent_years
49
UNION ALL
50
SELECT id, fiscal_year, 'Q3',
51
make_date(fiscal_year::int, 10, 1),
52
make_date(fiscal_year::int, 12, 31)
53
FROM recent_years
54
UNION ALL
55
-- For Q4, fiscal_year stays the same, but period_start/period_end move to the next calendar year
56
SELECT id, fiscal_year, 'Q4',
57
make_date((fiscal_year::int) + 1, 1, 1),
58
make_date((fiscal_year::int) + 1, 3, 31)
59
FROM recent_years
60
),
61
filtered_je AS (
62
SELECT
63
je.amount,
64
je.entry_type,
65
tr.company_id,
66
je.transaction_date,
67
coa.account_type_id
68
FROM journal_entries je
69
JOIN transaction_headers tr ON je.transaction_id = tr.id
70
JOIN chart_of_accounts coa ON je.account_id = coa.id
71
WHERE je.is_deleted = FALSE
72
AND tr.company_id = p_company_id
73
)
74
SELECT
75
CONCAT(q.period, ' ', q.fiscal_year) AS period,
76
q.fiscal_year,
77
COALESCE(SUM(CASE WHEN je.entry_type = 'C' AND je.account_type_id IN (4,14,15,19,20)
78
AND je.transaction_date BETWEEN q.period_start AND q.period_end THEN je.amount ELSE 0 END), 0) AS total_income,
79
COALESCE(SUM(CASE WHEN je.entry_type = 'D' AND je.account_type_id IN (5,16,17,18,21,22)
80
AND je.transaction_date BETWEEN q.period_start AND q.period_end THEN je.amount ELSE 0 END), 0) AS total_expense,
81
COALESCE(SUM(CASE WHEN je.entry_type = 'D' AND je.account_type_id IN (8,9)
82
AND je.transaction_date BETWEEN q.period_start AND q.period_end THEN je.amount ELSE 0 END), 0) AS actual_income,
83
COALESCE(SUM(CASE WHEN je.entry_type = 'C' AND je.account_type_id IN (8,9)
84
AND je.transaction_date BETWEEN q.period_start AND q.period_end THEN je.amount ELSE 0 END), 0) AS actual_expense
85
FROM quarters q
86
LEFT JOIN filtered_je je
87
ON je.transaction_date BETWEEN q.period_start AND q.period_end
88
GROUP BY q.period, q.fiscal_year
89
HAVING
90
SUM(CASE WHEN je.entry_type = 'C' AND je.account_type_id IN (4,14,15,19,20)
91
AND je.transaction_date BETWEEN q.period_start AND q.period_end THEN je.amount ELSE 0 END) > 0
92
OR SUM(CASE WHEN je.entry_type = 'D' AND je.account_type_id IN (5,16,17,18,21,22)
93
AND je.transaction_date BETWEEN q.period_start AND q.period_end THEN je.amount ELSE 0 END) > 0
94
OR SUM(CASE WHEN je.entry_type = 'D' AND je.account_type_id IN (8,9)
95
AND je.transaction_date BETWEEN q.period_start AND q.period_end THEN je.amount ELSE 0 END) > 0
96
OR SUM(CASE WHEN je.entry_type = 'C' AND je.account_type_id IN (8,9)
97
AND je.transaction_date BETWEEN q.period_start AND q.period_end THEN je.amount ELSE 0 END) > 0
98
ORDER BY q.fiscal_year, q.period;
99
100
ELSIF p_period_type = CONST_MONTHLY THEN
101
RETURN QUERY
102
WITH months AS (
103
SELECT
104
generate_series::date AS month_start,
105
(generate_series + interval '1 month')::date AS month_end
106
FROM generate_series(
107
v_financial_year_start,
108
v_financial_year_end,
109
interval '1 month'
110
)
111
),
112
filtered_je AS (
113
SELECT
114
je.amount,
115
je.entry_type,
116
je.transaction_date,
117
coa.account_type_id
118
FROM journal_entries je
119
JOIN transaction_headers tr ON je.transaction_id = tr.id
120
JOIN chart_of_accounts coa ON je.account_id = coa.id
121
WHERE tr.company_id = p_company_id
122
AND je.is_deleted = FALSE
123
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
124
)
125
SELECT
126
to_char(m.month_start, 'Mon YYYY') AS period,
127
EXTRACT(YEAR FROM m.month_start) AS year,
128
COALESCE(SUM(CASE WHEN je.entry_type = 'C' AND je.account_type_id IN (4,14,15,19,20)
129
AND je.transaction_date >= m.month_start AND je.transaction_date < m.month_end THEN je.amount ELSE 0 END), 0) AS total_income,
130
COALESCE(SUM(CASE WHEN je.entry_type = 'D' AND je.account_type_id IN (5,16,17,18,21,22)
131
AND je.transaction_date >= m.month_start AND je.transaction_date < m.month_end THEN je.amount ELSE 0 END), 0) AS total_expense,
132
COALESCE(SUM(CASE WHEN je.entry_type = 'D' AND je.account_type_id IN (8,9)
133
AND je.transaction_date >= m.month_start AND je.transaction_date < m.month_end THEN je.amount ELSE 0 END), 0) AS actual_income,
134
COALESCE(SUM(CASE WHEN je.entry_type = 'C' AND je.account_type_id IN (8,9)
135
AND je.transaction_date >= m.month_start AND je.transaction_date < m.month_end THEN je.amount ELSE 0 END), 0) AS actual_expense
136
FROM months m
137
LEFT JOIN filtered_je je
138
ON je.transaction_date >= m.month_start AND je.transaction_date < m.month_end
139
GROUP BY m.month_start
140
ORDER BY m.month_start;
141
142
ELSIF p_period_type = CONST_QUARTERLY THEN
143
-- Single year, use explicit fiscal quarters
144
RETURN QUERY
145
WITH fiscal_quarters AS (
146
SELECT 'Q1' AS period, v_financial_year_start AS period_start, make_date(v_finance_year_start::int, 6, 30) AS period_end, v_finance_year_start AS year
147
UNION ALL
148
SELECT 'Q2', make_date(v_finance_year_start::int, 7, 1), make_date(v_finance_year_start::int, 9, 30), v_finance_year_start
149
UNION ALL
150
SELECT 'Q3', make_date(v_finance_year_start::int, 10, 1), make_date(v_finance_year_start::int, 12, 31), v_finance_year_start
151
UNION ALL
152
SELECT 'Q4', make_date((v_finance_year_start::int) + 1, 1, 1), v_financial_year_end, v_finance_year_start
153
),
154
filtered_je AS (
155
SELECT
156
je.amount,
157
je.entry_type,
158
je.transaction_date,
159
coa.account_type_id
160
FROM journal_entries je
161
JOIN transaction_headers tr ON je.transaction_id = tr.id
162
JOIN chart_of_accounts coa ON je.account_id = coa.id
163
WHERE tr.company_id = p_company_id
164
AND je.is_deleted = FALSE
165
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
166
)
167
SELECT
168
CONCAT(fq.period, ' ', fq.year) AS period,
169
fq.year,
170
COALESCE(SUM(CASE WHEN je.entry_type = 'C' AND je.account_type_id IN (4,14,15,19,20)
171
AND je.transaction_date BETWEEN fq.period_start AND fq.period_end THEN je.amount ELSE 0 END), 0) AS total_income,
172
COALESCE(SUM(CASE WHEN je.entry_type = 'D' AND je.account_type_id IN (5,16,17,18,21,22)
173
AND je.transaction_date BETWEEN fq.period_start AND fq.period_end THEN je.amount ELSE 0 END), 0) AS total_expense,
174
COALESCE(SUM(CASE WHEN je.entry_type = 'D' AND je.account_type_id IN (8,9)
175
AND je.transaction_date BETWEEN fq.period_start AND fq.period_end THEN je.amount ELSE 0 END), 0) AS actual_income,
176
COALESCE(SUM(CASE WHEN je.entry_type = 'C' AND je.account_type_id IN (8,9)
177
AND je.transaction_date BETWEEN fq.period_start AND fq.period_end THEN je.amount ELSE 0 END), 0) AS actual_expense
178
FROM fiscal_quarters fq
179
LEFT JOIN filtered_je je
180
ON je.transaction_date BETWEEN fq.period_start AND fq.period_end
181
GROUP BY fq.period, fq.year
182
ORDER BY fq.year, fq.period;
183
184
ELSIF p_period_type = CONST_HALF_YEARLY THEN
185
RETURN QUERY
186
WITH half_years AS (
187
SELECT 'H1' AS period, v_financial_year_start AS period_start, make_date(v_finance_year_start::int, 9, 30) AS period_end, v_finance_year_start AS year
188
UNION ALL
189
SELECT 'H2', make_date(v_finance_year_start::int, 10, 1), v_financial_year_end, v_finance_year_start
190
),
191
filtered_je AS (
192
SELECT
193
je.amount,
194
je.entry_type,
195
je.transaction_date,
196
coa.account_type_id
197
FROM journal_entries je
198
JOIN transaction_headers tr ON je.transaction_id = tr.id
199
JOIN chart_of_accounts coa ON je.account_id = coa.id
200
WHERE tr.company_id = p_company_id
201
AND je.is_deleted = FALSE
202
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
203
)
204
SELECT
205
CONCAT(h.period, ' ', h.year) AS period,
206
h.year,
207
COALESCE(SUM(CASE WHEN je.entry_type = 'C' AND je.account_type_id IN (4,14,15,19,20)
208
AND je.transaction_date BETWEEN h.period_start AND h.period_end THEN je.amount ELSE 0 END), 0) AS total_income,
209
COALESCE(SUM(CASE WHEN je.entry_type = 'D' AND je.account_type_id IN (5,16,17,18,21,22)
210
AND je.transaction_date BETWEEN h.period_start AND h.period_end THEN je.amount ELSE 0 END), 0) AS total_expense,
211
COALESCE(SUM(CASE WHEN je.entry_type = 'D' AND je.account_type_id IN (8,9)
212
AND je.transaction_date BETWEEN h.period_start AND h.period_end THEN je.amount ELSE 0 END), 0) AS actual_income,
213
COALESCE(SUM(CASE WHEN je.entry_type = 'C' AND je.account_type_id IN (8,9)
214
AND je.transaction_date BETWEEN h.period_start AND h.period_end THEN je.amount ELSE 0 END), 0) AS actual_expense
215
FROM half_years h
216
JOIN filtered_je je
217
ON je.transaction_date BETWEEN h.period_start AND h.period_end
218
GROUP BY h.period, h.year
219
ORDER BY h.year, h.period;
220
221
ELSIF p_period_type = CONST_YEARLY THEN
222
RETURN QUERY
223
WITH filtered_je AS (
224
SELECT
225
je.amount,
226
je.entry_type,
227
je.transaction_date,
228
coa.account_type_id
229
FROM journal_entries je
230
JOIN transaction_headers tr ON je.transaction_id = tr.id
231
JOIN chart_of_accounts coa ON je.account_id = coa.id
232
WHERE tr.company_id = p_company_id
233
AND je.is_deleted = FALSE
234
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
235
)
236
SELECT
237
CONCAT('Year ', v_finance_year_start) AS period,
238
v_finance_year_start AS year,
239
COALESCE(SUM(CASE WHEN je.entry_type = 'C' AND je.account_type_id IN (4,14,15,19,20) THEN je.amount ELSE 0 END), 0) AS total_income,
240
COALESCE(SUM(CASE WHEN je.entry_type = 'D' AND je.account_type_id IN (5,16,17,18,21,22) THEN je.amount ELSE 0 END), 0) AS total_expense,
241
COALESCE(SUM(CASE WHEN je.entry_type = 'D' AND je.account_type_id IN (8,9) THEN je.amount ELSE 0 END), 0) AS actual_income,
242
COALESCE(SUM(CASE WHEN je.entry_type = 'C' AND je.account_type_id IN (8,9) THEN je.amount ELSE 0 END), 0) AS actual_expense
243
FROM filtered_je je;
244
245
ELSE
246
RAISE EXCEPTION 'Unsupported period type %', p_period_type;
247
END IF;
248
END;
249
$function$
|
|||||
| Function | get_vendor_opening_balance | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_vendor_opening_balance(p_vendor_id uuid, p_finyear_id integer)
2
RETURNS TABLE(transaction_date timestamp without time zone, vendor_id uuid, vendor_name text, description text, credit numeric, debit numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_year_text text;
7
BEGIN
8
-- Fetch the year text for the given finance year id
9
SELECT year INTO v_year_text FROM public.finance_year WHERE id = p_finyear_id;
10
IF v_year_text IS NULL THEN
11
RAISE EXCEPTION 'Finance year with id % not found!', p_finyear_id;
12
END IF;
13
14
RETURN QUERY
15
SELECT
16
th.transaction_date,
17
th.vendor_id,
18
v.name::text AS vendor_name, -- Explicit cast to match function signature
19
th.description,
20
COALESCE(CASE WHEN je.entry_type = 'C' THEN je.amount END, 0) AS credit,
21
COALESCE(CASE WHEN je.entry_type = 'D' THEN je.amount END, 0) AS debit
22
FROM public.transaction_headers th
23
JOIN public.vendors v ON th.vendor_id = v.id
24
JOIN public.journal_entries je ON th.id = je.transaction_id
25
JOIN public.chart_of_accounts sc ON sc.name ILIKE '%Sundry Creditors%'
26
WHERE th.vendor_id = p_vendor_id
27
AND je.account_id = sc.id
28
AND th.transaction_source_type = 13
29
AND th.description LIKE 'Opening Balance for %' || v_year_text || '%'
30
ORDER BY th.transaction_date;
31
END;
32
$function$
|
|||||
| Function | test | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.test(p_company_id uuid, p_financial_year integer, p_chart_of_account_id uuid)
2
RETURNS TABLE(account_id uuid, account_name text, parent_account_id uuid, account_type text, apr numeric, may numeric, jun numeric, jul numeric, aug numeric, sep numeric, oct numeric, nov numeric, "dec" numeric, jan numeric, feb numeric, mar numeric, total_current_year numeric, total_last_year numeric, difference numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_start_date TIMESTAMP DEFAULT NULL;
7
v_end_date TIMESTAMP DEFAULT NULL;
8
v_prev_start_date TIMESTAMP DEFAULT NULL;
9
v_prev_end_date TIMESTAMP DEFAULT NULL;
10
BEGIN
11
-- Get start and end date for the given financial year
12
SELECT start_date, end_date
13
INTO v_start_date, v_end_date
14
FROM finance_year
15
WHERE EXTRACT(YEAR FROM start_date) = p_financial_year
16
LIMIT 1;
17
18
-- If no result, raise an error
19
IF v_start_date IS NULL OR v_end_date IS NULL THEN
20
RAISE EXCEPTION 'Financial year % not found in finance_year table', p_financial_year;
21
END IF;
22
23
-- Get the previous financial year's start and end date
24
SELECT start_date, end_date
25
INTO v_prev_start_date, v_prev_end_date
26
FROM finance_year
27
WHERE EXTRACT(YEAR FROM start_date) = (p_financial_year - 1)
28
LIMIT 1;
29
30
-- If previous year is missing, use the current year as a fallback
31
IF v_prev_start_date IS NULL OR v_prev_end_date IS NULL THEN
32
v_prev_start_date := v_start_date;
33
v_prev_end_date := v_end_date;
34
END IF;
35
36
-- Debugging: Print financial year start and end dates
37
RAISE NOTICE 'Start Date: %, End Date: %, Prev Start: %, Prev End: %', v_start_date, v_end_date, v_prev_start_date, v_prev_end_date;
38
39
-- Return the final query
40
RETURN QUERY
41
WITH direct_children AS (
42
-- Step 1: Get Direct Child Accounts
43
SELECT coa.id AS child_id, coa.name AS child_name, coa.parent_account_id AS direct_parent_id, coa.account_type_id
44
FROM chart_of_accounts coa
45
WHERE coa.parent_account_id = p_chart_of_account_id
46
),
47
all_descendants AS (
48
-- Step 2: Get All Descendants of Each Direct Child
49
WITH RECURSIVE hierarchy AS (
50
SELECT ca.id AS descendant_id, ca.parent_account_id AS ancestor_parent_id
51
FROM chart_of_accounts ca
52
WHERE ca.parent_account_id IN (SELECT child_id FROM direct_children)
53
54
UNION ALL
55
56
SELECT coa.id AS descendant_id, coa.parent_account_id AS ancestor_parent_id
57
FROM chart_of_accounts coa
58
INNER JOIN hierarchy h ON coa.parent_account_id = h.descendant_id
59
)
60
SELECT descendant_id, ancestor_parent_id FROM hierarchy
61
)
62
SELECT
63
dc.child_id AS account_id,
64
dc.child_name AS account_name,
65
dc.direct_parent_id AS parent_account_id,
66
at.name AS account_type,
67
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 4 THEN je.amount ELSE 0 END), 0) AS apr,
68
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 5 THEN je.amount ELSE 0 END), 0) AS may,
69
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 6 THEN je.amount ELSE 0 END), 0) AS jun,
70
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 7 THEN je.amount ELSE 0 END), 0) AS jul,
71
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 8 THEN je.amount ELSE 0 END), 0) AS aug,
72
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 9 THEN je.amount ELSE 0 END), 0) AS sep,
73
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 10 THEN je.amount ELSE 0 END), 0) AS oct,
74
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 11 THEN je.amount ELSE 0 END), 0) AS nov,
75
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 12 THEN je.amount ELSE 0 END), 0) AS "dec",
76
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 1 THEN je.amount ELSE 0 END), 0) AS jan,
77
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 2 THEN je.amount ELSE 0 END), 0) AS feb,
78
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 3 THEN je.amount ELSE 0 END), 0) AS mar,
79
80
-- Total current year transactions
81
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date THEN je.amount ELSE 0 END), 0) AS total_current_year,
82
83
-- Total last year transactions
84
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_prev_start_date AND v_prev_end_date THEN je.amount ELSE 0 END), 0) AS total_last_year,
85
86
-- Difference between the two years
87
(COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date THEN je.amount ELSE 0 END), 0) -
88
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_prev_start_date AND v_prev_end_date THEN je.amount ELSE 0 END), 0))
89
AS difference
90
91
FROM direct_children dc
92
LEFT JOIN all_descendants ad ON dc.child_id = ad.ancestor_parent_id OR dc.child_id = ad.descendant_id
93
LEFT JOIN journal_entries je ON (je.account_id = ad.descendant_id OR je.account_id = dc.child_id)
94
LEFT JOIN account_types at ON at.id = dc.account_type_id
95
GROUP BY dc.child_id, dc.child_name, dc.direct_parent_id, at.name;
96
97
END;
98
$function$
|
|||||
| Function | get_all_companies | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_all_companies(p_organization_id uuid)
2
RETURNS TABLE(id uuid, organization_id uuid, name character varying, contact_name character varying, contact_count integer, mobile_number character varying, gst_in character varying, email character varying, city_name character varying, is_apartment boolean, created_by uuid, created_by_name character varying, modified_by uuid, modified_by_name character varying, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
cmp.id,
9
cmp.organization_id,
10
cmp.name::character varying,
11
COALESCE(
12
(SELECT con.first_name || ' ' || con.last_name
13
FROM public.contacts con
14
JOIN public.company_contacts cc
15
ON con.id = cc.contact_id
16
WHERE cc.company_id = cmp.id AND con.is_primary = true
17
LIMIT 1), ''
18
)::character varying AS primary_contact_name,
19
(SELECT COUNT(*)
20
FROM public.contacts con
21
JOIN public.company_contacts cc
22
ON con.id = cc.contact_id
23
WHERE cc.company_id = cmp.id)::integer AS contact_count,
24
COALESCE(
25
(SELECT con.mobile_number
26
FROM public.contacts con
27
JOIN public.company_contacts cc
28
ON con.id = cc.contact_id
29
WHERE cc.company_id = cmp.id AND con.is_primary = true
30
LIMIT 1), ''
31
)::character varying AS primary_contact_mobile,
32
cmp.gstin::character varying AS gst_in,
33
COALESCE(
34
(SELECT con.email
35
FROM public.contacts con
36
JOIN public.company_contacts cc
37
ON con.id = cc.contact_id
38
WHERE cc.company_id = cmp.id AND con.is_primary = true
39
LIMIT 1), ''
40
)::character varying AS primary_contact_email,
41
COALESCE(
42
(SELECT ct.name
43
FROM public.cities ct
44
WHERE ct.id = (
45
SELECT ad.city_id
46
FROM public.addresses ad
47
WHERE ad.id = cmp.billing_address_id
48
LIMIT 1)
49
LIMIT 1), ''
50
)::character varying AS city_name,
51
cmp.is_apartment,
52
cmp.created_by,
53
COALESCE(
54
(SELECT u.first_name || ' ' || u.last_name
55
FROM public.users u
56
WHERE u.id = cmp.created_by), ''
57
)::character varying AS created_by_name,
58
cmp.modified_by,
59
COALESCE(
60
(SELECT u.first_name || ' ' || u.last_name
61
FROM public.users u
62
WHERE u.id = cmp.modified_by), ''
63
)::character varying AS modified_by_name,
64
cmp.created_on_utc,
65
cmp.modified_on_utc
66
FROM
67
public.companies cmp
68
WHERE
69
cmp.organization_id = p_organization_id
70
AND cmp.is_deleted = false;
71
END;
72
$function$
|
|||||
| Function | get_customer_ledger | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_customer_ledger(p_company_id uuid, p_organization_id uuid, p_customer_id uuid, p_start_date date, p_end_date date)
2
RETURNS TABLE(transaction_date date, account_id uuid, account_name text, debit numeric, credit numeric, balance numeric, document_number text, document_id uuid, transaction_source_type integer)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_opening_equity_id UUID;
7
v_sundary_detors_account_id UUID;
8
v_opening_balance_equity_account_id UUID;
9
BEGIN
10
11
SELECT accounts_receivable_account_id, opening_balance_equity_account_id
12
INTO v_sundary_detors_account_id, v_opening_balance_equity_account_id
13
FROM public.organization_accounts
14
WHERE organization_id = p_organization_id
15
LIMIT 1;
16
17
RETURN QUERY
18
WITH all_transactions AS (
19
SELECT
20
th.transaction_date::date,
21
coa.name AS account_name,
22
coa.id AS account_id, -- ✅ Include account_id
23
CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END AS debit,
24
CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END AS credit,
25
th.document_number,
26
th.document_id,
27
th.transaction_source_type,
28
tst.sort_order,
29
th.id AS transaction_id,
30
je.id AS journal_entry_id
31
FROM
32
public.transaction_headers th
33
JOIN public.journal_entries je ON th.id = je.transaction_id
34
JOIN public.transaction_source_types tst ON th.transaction_source_type = tst.id
35
JOIN public.chart_of_accounts coa ON je.account_id = coa.id
36
WHERE
37
th.company_id = p_company_id
38
AND th.customer_id = p_customer_id
39
AND je.account_id = v_sundary_detors_account_id
40
AND th.transaction_date BETWEEN p_start_date AND p_end_date
41
AND th.is_deleted = false
42
AND je.is_deleted = false
43
)
44
SELECT
45
at.transaction_date,
46
at.account_id, -- ✅ Added here
47
at.account_name,
48
at.debit,
49
at.credit,
50
SUM(at.debit - at.credit) OVER (
51
ORDER BY
52
at.transaction_date,
53
at.sort_order,
54
at.transaction_id,
55
at.journal_entry_id
56
) AS balance,
57
at.document_number,
58
at.document_id,
59
at.transaction_source_type
60
FROM all_transactions AS at
61
ORDER BY
62
at.transaction_date,
63
at.sort_order,
64
at.transaction_id,
65
at.journal_entry_id;
66
67
END;
68
$function$
|
|||||
| Function | get_general_ledger | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_general_ledger(p_company_id uuid, p_organization_id uuid, p_start_date date, p_end_date date)
2
RETURNS TABLE(transaction_date date, account_name text, debit numeric, credit numeric, balance numeric, transaction_source_type integer, document_number text, document_id uuid, customer_id uuid, customer_name text, employee_id uuid, employee_name text, vendor_id uuid, vendor_name text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
WITH general_transactions AS (
8
SELECT
9
th.transaction_date::date AS transaction_date,
10
coa.name AS account_name,
11
CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END AS debit,
12
CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END AS credit,
13
th.company_id,
14
je.account_id,
15
th.transaction_source_type,
16
tst.sort_order,
17
th.document_number,
18
th.document_id,
19
th.customer_id,
20
th.employee_id,
21
th.vendor_id,
22
th.id AS transaction_id,
23
je.id AS journal_entry_id
24
FROM
25
public.transaction_headers th
26
INNER JOIN
27
public.journal_entries je ON th.id = je.transaction_id
28
INNER JOIN
29
public.chart_of_accounts coa ON je.account_id = coa.id
30
INNER JOIN
31
public.transaction_source_types tst ON th.transaction_source_type = tst.id
32
WHERE
33
th.company_id = p_company_id
34
AND coa.organization_id = p_organization_id
35
AND th.is_deleted = false
36
AND je.is_deleted = false
37
AND th.transaction_date BETWEEN p_start_date AND p_end_date
38
)
39
SELECT
40
gt.transaction_date,
41
gt.account_name,
42
gt.debit,
43
gt.credit,
44
SUM(gt.debit - gt.credit) OVER (
45
ORDER BY
46
gt.transaction_date,
47
gt.sort_order,
48
gt.transaction_id,
49
gt.journal_entry_id
50
) AS balance,
51
gt.transaction_source_type,
52
gt.document_number,
53
gt.document_id,
54
c.id AS customer_id,
55
c.name::text AS customer_name,
56
e.id AS employee_id,
57
CASE
58
WHEN e.id IS NOT NULL THEN (e.first_name || ' ' || e.last_name)::text
59
ELSE NULL
60
END AS employee_name,
61
v.id AS vendor_id,
62
v.name::text AS vendor_name
63
FROM
64
general_transactions gt
65
LEFT JOIN public.customers c ON gt.customer_id = c.id AND c.company_id = p_company_id
66
LEFT JOIN public.employees e ON gt.employee_id = e.id AND e.company_id = p_company_id
67
LEFT JOIN public.vendors v ON gt.vendor_id = v.id AND v.company_id = p_company_id
68
WHERE
69
NOT (gt.debit = 0 AND gt.credit = 0)
70
ORDER BY
71
gt.transaction_date,
72
gt.sort_order,
73
gt.transaction_id,
74
gt.journal_entry_id;
75
END;
76
$function$
|
|||||
| Function | get_vendor_ledger | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_vendor_ledger(p_vendor_id uuid, p_company_id uuid, p_organization_id uuid, p_start_date date, p_end_date date)
2
RETURNS TABLE(transaction_date date, account_id uuid, account_name text, debit numeric, credit numeric, balance numeric, transaction_source_type integer, document_number text, document_id uuid)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_sundary_creditors_account_id UUID;
7
v_vendor_advance_account_id UUID;
8
BEGIN
9
SELECT
10
accounts_payable_account_id,
11
vendor_advance_account_id
12
INTO
13
v_sundary_creditors_account_id,
14
v_vendor_advance_account_id
15
FROM public.organization_accounts
16
WHERE organization_id = p_organization_id
17
LIMIT 1;
18
19
RETURN QUERY
20
WITH all_transactions AS (
21
SELECT
22
th.transaction_date::date,
23
coa.id AS account_id,
24
coa.name AS account_name,
25
CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END AS debit,
26
CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END AS credit,
27
th.document_number,
28
th.document_id,
29
th.transaction_source_type,
30
tst.sort_order,
31
th.id AS transaction_id,
32
je.id AS journal_entry_id
33
FROM public.transaction_headers th
34
JOIN public.journal_entries je ON th.id = je.transaction_id
35
JOIN public.transaction_source_types tst ON th.transaction_source_type = tst.id
36
JOIN public.chart_of_accounts coa ON je.account_id = coa.id
37
WHERE
38
th.company_id = p_company_id
39
AND th.vendor_id = p_vendor_id
40
AND je.account_id IN (v_sundary_creditors_account_id, v_vendor_advance_account_id)
41
AND th.transaction_date BETWEEN p_start_date AND p_end_date
42
AND th.is_deleted = false
43
AND je.is_deleted = false
44
)
45
SELECT
46
at.transaction_date,
47
at.account_id,
48
at.account_name,
49
at.debit,
50
at.credit,
51
SUM(at.debit - at.credit) OVER (
52
ORDER BY
53
at.transaction_date,
54
at.sort_order,
55
at.transaction_id,
56
at.journal_entry_id
57
) AS balance,
58
at.transaction_source_type,
59
at.document_number,
60
at.document_id
61
FROM all_transactions AS at
62
ORDER BY
63
at.transaction_date,
64
at.sort_order,
65
at.transaction_id,
66
at.journal_entry_id;
67
END;
68
$function$
|
|||||
| Function | get_party_totals | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_party_totals(p_organization_id uuid, p_company_id uuid, p_start_date date, p_end_date date, p_party_type integer)
2
RETURNS TABLE(id uuid, party_name text, company_id uuid, company_name text, total_invoiced numeric, total_received numeric, total_billed numeric, total_paid numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
CUSTOMER CONSTANT int := 1;
7
VENDOR CONSTANT int := 2;
8
BEGIN
9
-- Customers
10
11
IF p_party_type = CUSTOMER THEN
12
RETURN QUERY
13
SELECT
14
th.customer_id AS id,
15
MAX(cust.name) AS party_name,
16
th.company_id,
17
MAX(comp.name) AS company_name,
18
COALESCE(SUM(CASE WHEN th.transaction_source_type = 1 AND je.entry_type = 'D' THEN je.amount ELSE 0 END), 0) AS total_invoiced,
19
COALESCE(SUM(CASE WHEN th.transaction_source_type = 3 AND je.entry_type = 'D' THEN je.amount ELSE 0 END), 0) AS total_received,
20
0::numeric AS total_billed,
21
0::numeric AS total_paid
22
FROM public.transaction_headers th
23
JOIN public.journal_entries je ON je.transaction_id = th.id
24
JOIN public.companies comp ON comp.id = th.company_id
25
LEFT JOIN public.customers cust ON cust.id = th.customer_id
26
WHERE comp.organization_id = p_organization_id
27
AND (p_company_id IS NULL OR th.company_id = p_company_id)
28
AND (p_start_date IS NULL OR th.transaction_date >= p_start_date)
29
AND (p_end_date IS NULL OR th.transaction_date <= p_end_date)
30
AND th.customer_id IS NOT NULL
31
GROUP BY th.customer_id, th.company_id;
32
33
-- Vendors
34
ELSIF p_party_type = VENDOR THEN
35
RETURN QUERY
36
SELECT
37
th.vendor_id AS id,
38
MAX(vend.name) AS party_name,
39
th.company_id,
40
MAX(comp.name) AS company_name,
41
0::numeric AS total_invoiced,
42
0::numeric AS total_received,
43
COALESCE(SUM(CASE WHEN th.transaction_source_type = 2 AND je.entry_type = 'D' THEN je.amount ELSE 0 END), 0) AS total_billed,
44
COALESCE(SUM(CASE WHEN th.transaction_source_type = 4 AND je.entry_type = 'D' THEN je.amount ELSE 0 END), 0) AS total_paid
45
FROM public.transaction_headers th
46
JOIN public.journal_entries je ON je.transaction_id = th.id
47
JOIN public.companies comp ON comp.id = th.company_id
48
LEFT JOIN public.vendors vend ON vend.id = th.vendor_id
49
WHERE comp.organization_id = p_organization_id
50
AND (p_company_id IS NULL OR th.company_id = p_company_id)
51
AND (p_start_date IS NULL OR th.transaction_date >= p_start_date)
52
AND (p_end_date IS NULL OR th.transaction_date <= p_end_date)
53
AND th.vendor_id IS NOT NULL
54
GROUP BY th.vendor_id, th.company_id;
55
56
END IF;
57
END;
58
$function$
|
|||||
| Function | get_accounts_by_classification | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_accounts_by_classification(classification_type text)
2
RETURNS TABLE(account_id uuid, account_name text, account_type_name text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
coa.id AS account_id,
9
coa.name AS account_name,
10
at.name AS account_type_name
11
FROM
12
chart_of_accounts coa
13
INNER JOIN
14
account_types at
15
ON
16
coa.account_type_id = at.id
17
WHERE
18
at.classification = classification_type; -- Filter by classification parameter
19
END;
20
$function$
|
|||||
| Function | get_comparative_accounts_overview | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_comparative_accounts_overview(p_company_id uuid, p_fin_year_id integer, p_chart_of_account_id uuid)
2
RETURNS TABLE(account_id uuid, parent_account_name text, parent_account_id uuid, financial_year integer, apr numeric, may numeric, jun numeric, jul numeric, aug numeric, sep numeric, oct numeric, nov numeric, "dec" numeric, jan numeric, feb numeric, mar numeric, total_sum numeric, difference numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_current_year INTEGER := p_fin_year_id;
7
v_previous_year INTEGER := p_fin_year_id - 1;
8
v_organization_id UUID;
9
v_is_second_last_leaf BOOLEAN;
10
BEGIN
11
-- Get the organization_id for the given company_id
12
SELECT c.organization_id
13
INTO v_organization_id
14
FROM public.companies c
15
WHERE c.id = p_company_id;
16
17
-- Search for 2nd last leaf account
18
SELECT NOT EXISTS (
19
SELECT 1 FROM chart_of_accounts ca1
20
WHERE ca1.parent_account_id = p_chart_of_account_id
21
)
22
INTO v_is_second_last_leaf;
23
24
25
26
RAISE notice 'it is leaf account %', v_is_second_last_leaf;
27
28
-- If it is a LEAF account, fetch its transactions directly
29
IF v_is_second_last_leaf THEN
30
RETURN QUERY
31
WITH RECURSIVE child_accounts AS (
32
-- Get the given parent account and all its child accounts
33
SELECT id, parent_account_id, name
34
FROM chart_of_accounts
35
WHERE parent_account_id = p_chart_of_account_id
36
37
UNION ALL
38
39
-- Recursively get all child accounts
40
SELECT ca.id, ca.parent_account_id, ca.name
41
FROM chart_of_accounts ca
42
INNER JOIN child_accounts c ON ca.parent_account_id = c.id
43
),
44
financial_years AS (
45
-- Get previous and current financial years
46
SELECT fy.id AS fin_year_id, fy.start_date, fy.end_date
47
FROM finance_year fy
48
WHERE fy.id IN (v_previous_year, v_current_year)
49
)
50
SELECT
51
je.id AS transaction_id,
52
je.account_id,
53
ca.name AS account_name,
54
ca.parent_account_id,
55
fy.fin_year_id AS financial_year,
56
je.transaction_date,
57
EXTRACT(MONTH FROM je.transaction_date) AS calendar_month,
58
59
-- Convert Calendar Month to Financial Month (April - March Cycle)
60
CASE
61
WHEN EXTRACT(MONTH FROM je.transaction_date) = 4 THEN 'Apr'
62
WHEN EXTRACT(MONTH FROM je.transaction_date) = 5 THEN 'May'
63
WHEN EXTRACT(MONTH FROM je.transaction_date) = 6 THEN 'Jun'
64
WHEN EXTRACT(MONTH FROM je.transaction_date) = 7 THEN 'Jul'
65
WHEN EXTRACT(MONTH FROM je.transaction_date) = 8 THEN 'Aug'
66
WHEN EXTRACT(MONTH FROM je.transaction_date) = 9 THEN 'Sep'
67
WHEN EXTRACT(MONTH FROM je.transaction_date) = 10 THEN 'Oct'
68
WHEN EXTRACT(MONTH FROM je.transaction_date) = 11 THEN 'Nov'
69
WHEN EXTRACT(MONTH FROM je.transaction_date) = 12 THEN 'Dec'
70
WHEN EXTRACT(MONTH FROM je.transaction_date) = 1 THEN 'Jan'
71
WHEN EXTRACT(MONTH FROM je.transaction_date) = 2 THEN 'Feb'
72
WHEN EXTRACT(MONTH FROM je.transaction_date) = 3 THEN 'Mar'
73
END AS financial_month,
74
75
je.amount,
76
je.description
77
FROM journal_entries je
78
INNER JOIN child_accounts ca ON je.account_id = ca.id
79
INNER JOIN financial_years fy ON je.transaction_date BETWEEN fy.start_date AND fy.end_date
80
ORDER BY fy.fin_year_id, financial_month, ca.name, je.transaction_date;
81
82
ELSE
83
-- If it is NOT a leaf account, use the ORIGINAL FUNCTIONALITY (recursive aggregation)
84
RETURN QUERY
85
WITH direct_children AS (
86
-- Step 1: Get Direct Children of Given Parent
87
SELECT ca.id AS account_id, ca.parent_account_id, ca.name
88
FROM chart_of_accounts ca
89
WHERE ca.parent_account_id = p_chart_of_account_id
90
AND ca.organization_id = v_organization_id
91
),
92
93
descendant_accounts AS (
94
-- Step 2: Recursively Get All Descendants of Each Direct Child
95
WITH RECURSIVE hierarchy AS (
96
-- Start with direct children
97
SELECT dc.account_id, dc.parent_account_id, dc.name
98
FROM direct_children dc
99
100
UNION ALL
101
102
-- Then, recursively get all their descendants
103
SELECT coa.id AS account_id, coa.parent_account_id, coa.name
104
FROM chart_of_accounts coa
105
INNER JOIN hierarchy h ON coa.parent_account_id = h.account_id
106
)
107
SELECT * FROM hierarchy
108
),
109
110
financial_years AS (
111
-- Step 3: Get Start and End Dates for Given Financial Years
112
SELECT id AS fin_year_id, start_date, end_date
113
FROM finance_year
114
WHERE id IN (v_previous_year, v_current_year)
115
),
116
117
aggregated_data AS (
118
-- Step 4: Ensure Each Account Appears for Both Financial Years
119
SELECT
120
dc.account_id, -- Include Account ID
121
dc.name AS parent_account_name,
122
dc.parent_account_id, -- Ensure Parent Account ID is properly referenced
123
fy.fin_year_id AS financial_year,
124
125
-- Monthly Values (April to March)
126
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 4 THEN je.amount ELSE 0 END), 0) AS apr,
127
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 5 THEN je.amount ELSE 0 END), 0) AS may,
128
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 6 THEN je.amount ELSE 0 END), 0) AS jun,
129
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 7 THEN je.amount ELSE 0 END), 0) AS jul,
130
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 8 THEN je.amount ELSE 0 END), 0) AS aug,
131
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 9 THEN je.amount ELSE 0 END), 0) AS sep,
132
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 10 THEN je.amount ELSE 0 END), 0) AS oct,
133
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 11 THEN je.amount ELSE 0 END), 0) AS nov,
134
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 12 THEN je.amount ELSE 0 END), 0) AS "dec",
135
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 1 THEN je.amount ELSE 0 END), 0) AS jan,
136
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 2 THEN je.amount ELSE 0 END), 0) AS feb,
137
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 3 THEN je.amount ELSE 0 END), 0) AS mar,
138
139
-- Total Sum of All Months
140
COALESCE(SUM(je.amount), 0) AS total_sum
141
142
FROM direct_children dc
143
CROSS JOIN financial_years fy -- Ensures each account appears for both years
144
LEFT JOIN descendant_accounts da ON da.parent_account_id = dc.account_id
145
LEFT JOIN journal_entries je
146
ON je.account_id = da.account_id
147
AND je.transaction_date BETWEEN fy.start_date AND fy.end_date -- Ensure correct year filtering
148
-- AND je.company_id = p_company_id -- Filter by Company ID
149
150
GROUP BY dc.account_id, dc.parent_account_id, dc.name, fy.fin_year_id
151
)
152
153
-- Step 5: Compute Difference Between Two Financial Years
154
SELECT
155
a1.account_id,
156
a1.parent_account_name,
157
a1.parent_account_id,
158
a1.financial_year,
159
160
-- Monthly Values
161
a1.apr, a1.may, a1.jun, a1.jul, a1.aug, a1.sep, a1.oct, a1.nov, a1."dec",
162
a1.jan, a1.feb, a1.mar,
163
164
-- Total Sum for Financial Year
165
a1.total_sum,
166
167
-- Difference (Current Year - Previous Year)
168
(COALESCE(a1.total_sum, 0) - COALESCE(a2.total_sum, 0)) AS difference
169
170
FROM aggregated_data a1
171
LEFT JOIN aggregated_data a2
172
ON a1.account_id = a2.account_id
173
AND a1.financial_year = v_current_year -- Current Year
174
AND a2.financial_year = v_previous_year -- Previous Year
175
176
ORDER BY a1.parent_account_name, a1.financial_year;
177
END IF;
178
END;
179
$function$
|
|||||
| Function | get_collection | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_collection(p_company_id uuid)
2
RETURNS TABLE(year numeric, total_income numeric, total_expense numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
financial_year_start date;
7
financial_year_end date;
8
current_year integer;
9
BEGIN
10
-- Determine the current year
11
current_year := EXTRACT(YEAR FROM CURRENT_DATE);
12
13
-- Determine the financial year start and end dates
14
IF EXTRACT(MONTH FROM CURRENT_DATE) >= 4 THEN
15
-- If the current month is April or later, financial year starts from April 1 of the current year
16
financial_year_start := TO_DATE(current_year || '-04-01', 'YYYY-MM-DD');
17
financial_year_end := TO_DATE((current_year + 1) || '-03-31', 'YYYY-MM-DD');
18
ELSE
19
-- If the current month is before April, financial year starts from April 1 of the previous year
20
financial_year_start := TO_DATE((current_year - 1) || '-04-01', 'YYYY-MM-DD');
21
financial_year_end := TO_DATE(current_year || '-03-31', 'YYYY-MM-DD');
22
END IF;
23
24
-- Return the yearly income and expense overview
25
RETURN QUERY
26
SELECT
27
EXTRACT(YEAR FROM je.transaction_date) AS year,
28
SUM(CASE
29
WHEN ac.id = 4 THEN je.amount -- Revenue
30
ELSE 0
31
END) AS total_income,
32
SUM(CASE
33
WHEN ac.id = 5 THEN je.amount -- Expenses
34
ELSE 0
35
END) AS total_expense
36
FROM
37
journal_entries je
38
JOIN
39
chart_of_accounts coa ON je.account_id = coa.id
40
JOIN
41
account_types act ON coa.account_type_id = act.id
42
JOIN
43
account_categories ac ON act.account_category_id = ac.id
44
WHERE
45
je.transaction_date BETWEEN financial_year_start AND financial_year_end
46
AND je.company_id = p_company_id
47
GROUP BY
48
year
49
ORDER BY
50
year;
51
END;
52
$function$
|
|||||
| Function | update_basic_company | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.update_basic_company(p_company_id uuid, p_organization_id uuid, p_name text, p_gst_in text, p_short_name text, p_currency text, p_pan text, p_tan text, p_outstanding_limit numeric, p_is_apartment boolean, p_is_non_work boolean, p_interest_percentage numeric, p_has_gst_in boolean, p_email text, p_mobile_number text)
2
RETURNS void
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_contact_id UUID;
7
BEGIN
8
-- 1️⃣ Get contact_id from company_contacts
9
SELECT contact_id INTO v_contact_id
10
FROM company_contacts
11
WHERE company_id = p_company_id
12
LIMIT 1;
13
14
-- 2️⃣ Update company table
15
UPDATE companies
16
SET
17
organization_id = p_organization_id,
18
name = p_name,
19
gst_in = p_gst_in,
20
short_name = p_short_name,
21
currency = p_currency,
22
pan = p_pan,
23
tan = p_tan,
24
outstanding_limit = p_outstanding_limit,
25
is_apartment = p_is_apartment,
26
is_non_work = p_is_non_work,
27
interest_percentage = p_interest_percentage,
28
has_gst_in = p_has_gst_in,
29
modified_on_utc = NOW()
30
WHERE id = p_company_id;
31
32
-- 3️⃣ Update contact table if mapping exists
33
IF v_contact_id IS NOT NULL THEN
34
UPDATE contacts
35
SET
36
email = p_email,
37
mobile_number = p_mobile_number,
38
modified_on_utc = NOW()
39
WHERE id = v_contact_id;
40
END IF;
41
42
END;
43
$function$
|
|||||
| Function | create_notification_with_userid | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.create_notification_with_userid(p_user_id uuid, p_message text, p_route text, p_short_desc text, p_created_by uuid, p_created_time timestamp without time zone, p_additional_parameter_id text)
2
RETURNS integer
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_notification_id INT;
7
BEGIN
8
-- Insert notification and retrieve generated ID
9
INSERT INTO public.notification_types (
10
user_id, message, route, short_description, additional_parameter_id,
11
created_on_utc, created_by, is_deleted
12
) VALUES (
13
p_user_id, p_message, p_route, p_short_desc, p_additional_parameter_id,
14
NOW()::timestamp without time zone, p_created_by, FALSE -- Cast NOW() to match parameter type
15
)
16
RETURNING id INTO v_notification_id;
17
18
-- Return the inserted notification id
19
RETURN v_notification_id;
20
END;
21
$function$
|
|||||
| Function | get_expense_account_types | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_expense_account_types()
2
RETURNS TABLE(account_id uuid, account_name text, account_type_name text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
coa.id AS account_id,
9
coa.name AS account_name,
10
at.name AS account_type_name
11
FROM
12
chart_of_accounts coa
13
INNER JOIN
14
account_types at
15
ON
16
coa.account_type_id = at.id
17
WHERE
18
at.classification = 'Expense'; -- Filter for expense account types
19
END;
20
$function$
|
|||||
| Function | pgp_sym_decrypt | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.pgp_sym_decrypt(bytea, text)
2
RETURNS text
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pgp_sym_decrypt_text$function$
|
|||||
| Function | pgp_sym_decrypt_bytea | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.pgp_sym_decrypt_bytea(bytea, text)
2
RETURNS bytea
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pgp_sym_decrypt_bytea$function$
|
|||||
| Function | get_module_permissions | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_module_permissions()
2
RETURNS TABLE(id integer, feature_name text, read_permission_id uuid, create_permission_id uuid, update_permission_id uuid, delete_permission_id uuid, full_access_permission_id uuid, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone, deleted_on_utc timestamp without time zone, is_deleted boolean, created_by uuid, modified_by uuid)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
mp.id,
9
mp.feature_name,
10
mp.read_permission_id,
11
mp.create_permission_id,
12
mp.update_permission_id,
13
mp.delete_permission_id,
14
mp.full_access_permission_id,
15
mp.created_on_utc,
16
mp.modified_on_utc,
17
mp.deleted_on_utc,
18
mp.is_deleted,
19
mp.created_by,
20
mp.modified_by
21
FROM module_permissions mp
22
WHERE mp.is_deleted = false
23
ORDER BY mp.feature_name;
24
END;
25
$function$
|
|||||
| Function | get_profit_loss_statement | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_profit_loss_statement(p_company_id uuid, p_fin_year_id integer, p_start_date date, p_end_date date, p_is_get_for_organization boolean)
2
RETURNS TABLE(account_id uuid, account_type text, account_name text, amount numeric, category text)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_start_date date;
7
v_end_date date;
8
v_fy_start_date date;
9
v_fy_end_date date;
10
v_company_ids UUID[];
11
v_organization_id UUID;
12
income_account_ids integer[];
13
expense_account_ids integer[];
14
BEGIN
15
-- Get the financial year dates
16
SELECT start_date::date, end_date::date
17
INTO v_fy_start_date, v_fy_end_date
18
FROM public.finance_year
19
WHERE id = p_fin_year_id
20
LIMIT 1;
21
22
-- Apply filter logic for new date params
23
v_start_date := COALESCE(p_start_date, v_fy_start_date);
24
v_end_date := COALESCE(p_end_date, v_fy_end_date);
25
26
-- Get organization id
27
SELECT organization_id INTO v_organization_id
28
FROM public.companies
29
WHERE id = p_company_id;
30
31
-- Get account type hierarchies
32
income_account_ids := ARRAY(SELECT id FROM get_account_type_hierarchy(4));
33
expense_account_ids := ARRAY(SELECT id FROM get_account_type_hierarchy(5));
34
35
-- Populate company IDs based on org flag
36
IF p_is_get_for_organization THEN
37
SELECT ARRAY(
38
SELECT id FROM companies
39
WHERE organization_id = v_organization_id
40
)
41
INTO v_company_ids;
42
ELSE
43
v_company_ids := ARRAY[p_company_id];
44
END IF;
45
46
-- Return Income
47
RETURN QUERY
48
SELECT
49
coa.id as account_id, -- GUID column
50
at.name AS account_type,
51
coa.name AS account_name,
52
COALESCE(
53
SUM(
54
CASE
55
WHEN ob.entry_type = 'C' THEN ob.balance
56
ELSE -ob.balance
57
END
58
),0
59
) +
60
COALESCE(
61
SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE -je.amount END), 0
62
) AS amount,
63
'Income' AS category
64
FROM public.chart_of_accounts coa
65
INNER JOIN public.journal_entries je ON je.account_id = coa.id
66
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
67
INNER JOIN public.account_types at ON coa.account_type_id = at.id
68
LEFT JOIN public.opening_balances ob ON ob.account_id = coa.id
69
AND ob.organization_id = v_organization_id
70
AND ob.finyear_id = p_fin_year_id
71
AND ob.is_deleted = FALSE
72
WHERE th.company_id = ANY(v_company_ids)
73
AND th.transaction_date BETWEEN v_start_date AND v_end_date
74
AND NOT th.is_deleted
75
AND NOT je.is_deleted
76
AND coa.account_type_id = ANY(income_account_ids)
77
GROUP BY coa.id, at.name, coa.name, coa.account_number, at.id
78
ORDER BY coa.account_number, at.id;
79
80
-- Return Expenses
81
RETURN QUERY
82
SELECT
83
coa.id as account_id, -- GUID column
84
at.name AS account_type,
85
coa.name AS account_name,
86
COALESCE(
87
SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE -je.amount END), 0
88
) AS amount,
89
'Expenses' AS category
90
FROM public.chart_of_accounts coa
91
INNER JOIN public.journal_entries je ON je.account_id = coa.id
92
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
93
INNER JOIN public.account_types at ON coa.account_type_id = at.id
94
WHERE th.company_id = ANY(v_company_ids)
95
AND th.transaction_date BETWEEN v_start_date AND v_end_date
96
AND NOT th.is_deleted
97
AND NOT je.is_deleted
98
AND coa.account_type_id = ANY(expense_account_ids)
99
GROUP BY coa.id, at.name, coa.name, coa.account_number, at.id
100
ORDER BY coa.account_number, at.id;
101
END;
102
$function$
|
|||||
| Function | get_user_notifications_with_timespan | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_user_notifications_with_timespan(p_user_id uuid, p_start_date timestamp with time zone, p_end_date timestamp with time zone)
2
RETURNS TABLE(id integer, message text, short_desc text, route text, created_time timestamp with time zone, additional_parameter_id text)
3
LANGUAGE plpgsql
4
AS $function$
5
6
BEGIN
7
RETURN QUERY
8
SELECT
9
ntf.id AS id,
10
ntf.message,
11
ntf.short_description AS short_desc,
12
ntf.route,
13
ntf.created_on_utc::timestamp with time zone,
14
ntf.additional_parameter_id
15
FROM
16
public.notifications ntf
17
WHERE
18
ntf.user_id = p_user_id
19
AND ntf.created_on_utc BETWEEN p_start_date AND p_end_date -- Use the time span here
20
AND ntf.is_deleted = false
21
AND ntf.is_deleted = false
22
ORDER BY
23
ntf.created_on_utc DESC;
24
END;
25
$function$
|
|||||
| Function | get_trial_balance_by_date_range | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_trial_balance_by_date_range(p_company_id uuid, p_fin_year_id integer, p_start_date date DEFAULT NULL::date, p_end_date date DEFAULT NULL::date, p_is_get_for_organization boolean DEFAULT false)
2
RETURNS TABLE(account_type text, account_number text, account_name text, opening_balance numeric, debit numeric, credit numeric, closing_balance numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_start_date DATE;
7
v_end_date DATE;
8
v_organization_id UUID;
9
v_company_ids UUID[];
10
BEGIN
11
-- Fetch the organization ID from the company ID
12
SELECT organization_id INTO v_organization_id
13
FROM public.companies
14
WHERE id = p_company_id
15
LIMIT 1;
16
17
-- Get financial year start and end dates
18
SELECT start_date, end_date
19
INTO v_start_date, v_end_date
20
FROM public.get_financial_year_dates(p_fin_year_id);
21
22
-- Override with provided values if not NULL
23
v_start_date := COALESCE(p_start_date, v_start_date);
24
v_end_date := COALESCE(p_end_date, v_end_date);
25
26
-- Determine whether to fetch for all companies in the organization
27
IF p_is_get_for_organization THEN
28
SELECT ARRAY_AGG(id)
29
INTO v_company_ids
30
FROM public.companies
31
WHERE organization_id = v_organization_id;
32
ELSE
33
v_company_ids := ARRAY[p_company_id];
34
END IF;
35
36
RETURN QUERY
37
WITH coa_hierarchy AS (
38
SELECT * FROM public.get_chart_of_accounts_hierarchy(v_organization_id)
39
),
40
opening_balances AS (
41
SELECT * FROM public.get_opening_balances(v_organization_id, p_fin_year_id)
42
),
43
journal_entries_filtered AS (
44
SELECT je.account_id,
45
SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END) AS debit,
46
SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END) AS credit
47
FROM public.journal_entries je
48
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
49
WHERE th.company_id = ANY(v_company_ids)
50
AND th.transaction_date BETWEEN v_start_date AND v_end_date
51
GROUP BY je.account_id
52
)
53
SELECT
54
ch.account_type,
55
ch.account_number,
56
ch.account_name,
57
COALESCE(ob.balance, 0) AS opening_balance,
58
COALESCE(jf.debit, 0) AS debit,
59
COALESCE(jf.credit, 0) AS credit,
60
(COALESCE(ob.balance, 0) + COALESCE(jf.debit, 0) - COALESCE(jf.credit, 0)) AS closing_balance
61
FROM coa_hierarchy ch
62
LEFT JOIN opening_balances ob ON ch.id = ob.account_id
63
LEFT JOIN journal_entries_filtered jf ON ch.id = jf.account_id
64
ORDER BY ch.account_number;
65
END;
66
$function$
|
|||||
| Function | get_trial_balance_net | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_trial_balance_net(p_company_id uuid, p_finance_year_id integer)
2
RETURNS TABLE(account_type text, account_category text, account_number text, account_name text, balance numeric, balance_type text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
WITH financial_year_range AS (
8
SELECT
9
fy.start_date::DATE AS start_date,
10
fy.end_date::DATE AS end_date
11
FROM
12
public.finance_year fy
13
WHERE
14
fy.id = p_finance_year_id
15
LIMIT 1
16
),
17
account_balances AS (
18
SELECT
19
je.account_id,
20
SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END) AS total_debits,
21
SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END) AS total_credits
22
FROM
23
public.journal_entries je
24
JOIN
25
public.transaction_headers th ON je.transaction_id = th.id
26
CROSS JOIN
27
financial_year_range fy
28
WHERE
29
th.company_id = p_company_id
30
AND je.is_deleted = FALSE
31
AND th.transaction_date BETWEEN fy.start_date AND fy.end_date
32
GROUP BY
33
je.account_id
34
)
35
SELECT
36
at.name AS account_type,
37
ac.name AS account_category,
38
coa.account_number,
39
coa.name AS account_name,
40
(COALESCE(ab.total_debits, 0) - COALESCE(ab.total_credits, 0)) AS balance,
41
CASE
42
WHEN (COALESCE(ab.total_debits, 0) - COALESCE(ab.total_credits, 0)) > 0 THEN 'Debit'
43
WHEN (COALESCE(ab.total_debits, 0) - COALESCE(ab.total_credits, 0)) < 0 THEN 'Credit'
44
ELSE ''
45
END AS balance_type
46
FROM
47
public.chart_of_accounts coa
48
INNER JOIN
49
public.account_types at ON coa.account_type_id = at.id
50
LEFT JOIN
51
public.account_categories ac ON at.account_category_id = ac.id
52
LEFT JOIN
53
account_balances ab ON coa.id = ab.account_id
54
WHERE
55
coa.organization_id = p_company_id
56
AND coa.is_deleted = FALSE
57
AND (COALESCE(ab.total_debits, 0) - COALESCE(ab.total_credits, 0)) <> 0 -- Only show accounts with a non-zero balance
58
ORDER BY
59
coa.account_number;
60
END;
61
$function$
|
|||||
| Function | pgp_sym_encrypt_bytea | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.pgp_sym_encrypt_bytea(bytea, text, text)
2
RETURNS bytea
3
LANGUAGE c
4
PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pgp_sym_encrypt_bytea$function$
|
|||||
| Function | get_accounts_by_company_and_type | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_accounts_by_company_and_type(p_company_id uuid, p_account_type integer)
2
RETURNS TABLE(id uuid, account_type_id integer, name text, description text, is_default_account boolean)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_organization_id UUID;
7
v_is_apartment BOOLEAN;
8
9
v_default_sales UUID;
10
v_default_purchase UUID;
11
v_default_cash UUID;
12
v_default_bank UUID;
13
BEGIN
14
-- ✅ Fix ambiguous column: use alias 'c'
15
SELECT c.organization_id, c.is_apartment
16
INTO v_organization_id, v_is_apartment
17
FROM companies c
18
WHERE c.id = p_company_id;
19
20
-- ✅ Fix ambiguous column: use alias 'cp'
21
SELECT cp.default_sales_account_id,
22
cp.default_purchase_account_id,
23
cp.default_cash_account_id,
24
cp.default_bank_account_id
25
INTO v_default_sales, v_default_purchase, v_default_cash, v_default_bank
26
FROM company_preferences cp
27
WHERE cp.company_id = p_company_id;
28
29
-- ✅ Main query using table alias 'coa'
30
RETURN QUERY
31
SELECT
32
coa.id,
33
coa.account_type_id,
34
coa.name,
35
coa.description,
36
CASE
37
WHEN coa.id = v_default_sales THEN TRUE
38
WHEN coa.id = v_default_purchase THEN TRUE
39
WHEN coa.id = v_default_cash THEN TRUE
40
WHEN coa.id = v_default_bank THEN TRUE
41
ELSE FALSE
42
END AS is_default_account
43
FROM chart_of_accounts coa
44
WHERE coa.organization_id = v_organization_id
45
AND NOT EXISTS (
46
SELECT 1 FROM chart_of_accounts ca2 WHERE ca2.parent_account_id = coa.id
47
)
48
AND (
49
(
50
v_is_apartment = TRUE AND (
51
(p_account_type = 14 AND coa.account_type_id IN (19, 20)) OR -- SALES_REVENUE
52
(p_account_type IN (16, 17) AND coa.account_type_id IN (21, 22)) OR -- COGS or OPERATING
53
(p_account_type = 9 AND coa.account_type_id = 9) OR -- BANK
54
(p_account_type = 8 AND coa.account_type_id = 8) OR -- CASH
55
(p_account_type = 23 AND coa.account_type_id IN (8, 9)) -- BANK_AND_CASH
56
)
57
)
58
OR
59
(
60
v_is_apartment = FALSE AND (
61
(p_account_type = coa.account_type_id) OR
62
(p_account_type = 23 AND coa.account_type_id IN (8, 9)) -- BANK_AND_CASH
63
)
64
)
65
);
66
END;
67
$function$
|
|||||
| Function | get_trial_balance_of_company_fin_year | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_trial_balance_of_company_fin_year(p_company_id uuid, p_finance_year_id integer)
2
RETURNS TABLE(account_type text, account_category text, account_number text, account_name text, total_debits numeric, total_credits numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
WITH RECURSIVE coa_hierarchy AS (
8
-- Base case: Fetch the top-level (root) accounts
9
SELECT
10
coa.id,
11
act.name AS account_type,
12
act.classification AS account_classification,
13
coa.account_number::integer,
14
coa.name,
15
coa.parent_account_id,
16
0 AS level,
17
coa.account_number::text AS order_sequence
18
FROM
19
public.chart_of_accounts coa
20
INNER JOIN
21
public.account_types act ON coa.account_type_id = act.id
22
WHERE
23
coa.is_deleted = FALSE
24
AND coa.parent_account_id = '00000000-0000-0000-0000-000000000000' -- Root-level accounts
25
26
UNION ALL
27
28
-- Recursive case: Fetch child accounts
29
SELECT
30
coa.id,
31
act.name AS account_type,
32
act.classification AS account_classification,
33
coa.account_number::integer,
34
coa.name,
35
coa.parent_account_id,
36
ch.level + 1 AS level,
37
ch.order_sequence || '.' || coa.account_number::text AS order_sequence
38
FROM
39
public.chart_of_accounts coa
40
INNER JOIN
41
public.account_types act ON coa.account_type_id = act.id
42
INNER JOIN
43
coa_hierarchy ch ON ch.id = coa.parent_account_id
44
WHERE
45
coa.is_deleted = FALSE
46
),
47
coa_with_balances AS (
48
-- Fetch financial year start and end date
49
SELECT
50
fy.start_date::DATE,
51
fy.end_date::DATE
52
FROM
53
public.finance_year fy
54
WHERE
55
fy.id = p_finance_year_id
56
LIMIT 1
57
),
58
journal_entries_filtered AS (
59
-- Get total debits and credits per account
60
SELECT
61
je.account_id,
62
SUM(CASE
63
WHEN je.entry_type = 'D' THEN je.amount
64
ELSE 0
65
END) AS total_debits,
66
SUM(CASE
67
WHEN je.entry_type = 'C' THEN je.amount
68
ELSE 0
69
END) AS total_credits
70
FROM
71
public.journal_entries je
72
JOIN
73
public.transaction_headers th ON je.transaction_id = th.id
74
JOIN
75
coa_with_balances fy ON th.transaction_date BETWEEN fy.start_date AND fy.end_date
76
WHERE
77
th.company_id = p_company_id
78
AND je.is_deleted = FALSE
79
GROUP BY
80
je.account_id
81
HAVING
82
SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END) > 0 OR
83
SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END) > 0
84
),
85
relevant_accounts AS (
86
-- Select accounts that have balances
87
SELECT
88
ch.id,
89
ch.account_type,
90
ch.account_classification,
91
ch.account_number,
92
ch.name,
93
ch.level,
94
ch.order_sequence,
95
ch.parent_account_id,
96
wb.total_debits,
97
wb.total_credits
98
FROM
99
coa_hierarchy ch
100
LEFT JOIN
101
journal_entries_filtered wb ON ch.id = wb.account_id
102
WHERE
103
wb.account_id IS NOT NULL
104
105
UNION ALL
106
107
-- Recursively select parent accounts
108
SELECT
109
ch.id,
110
ch.account_type,
111
ch.account_classification,
112
ch.account_number,
113
ch.name,
114
ch.level,
115
ch.order_sequence,
116
ch.parent_account_id,
117
NULL AS total_debits,
118
NULL AS total_credits
119
FROM
120
coa_hierarchy ch
121
INNER JOIN
122
relevant_accounts ra ON ch.id = ra.parent_account_id
123
)
124
-- Final output
125
SELECT DISTINCT ON (ra.order_sequence)
126
ra.account_type::TEXT AS account_type,
127
ac.name::TEXT AS account_category,
128
ra.account_number::TEXT AS account_number,
129
LPAD('', ra.level * 4, ' ') || ra.name::TEXT AS account_name,
130
CASE
131
WHEN ra.account_classification = 'EXPENSES' THEN COALESCE(ra.total_credits, 0) -- Expenses should be on the debit side
132
ELSE COALESCE(ra.total_debits, 0)
133
END AS total_debits,
134
CASE
135
WHEN ra.account_classification = 'EXPENSES' THEN COALESCE(ra.total_debits, 0) -- Expenses should not appear on the credit side
136
ELSE COALESCE(ra.total_credits, 0)
137
END AS total_credits
138
FROM
139
relevant_accounts ra
140
LEFT JOIN
141
public.chart_of_accounts coa ON ra.id = coa.id
142
LEFT JOIN
143
public.account_types at ON coa.account_type_id = at.id
144
LEFT JOIN
145
public.account_categories ac ON at.account_category_id = ac.id
146
CROSS JOIN
147
coa_with_balances fy
148
ORDER BY
149
ra.order_sequence;
150
END;
151
$function$
|
|||||
| Function | pgp_sym_decrypt | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.pgp_sym_decrypt(bytea, text, text)
2
RETURNS text
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pgp_sym_decrypt_text$function$
|
|||||
| Function | get_user_by_email_or_phone | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_user_by_email_or_phone(p_email text, p_phone text)
2
RETURNS TABLE(id uuid, email text, phone_number text)
3
LANGUAGE sql
4
AS $function$
5
SELECT
6
u.id AS id,
7
u.email AS email,
8
u.phone_number AS phone_number
9
FROM public.users u
10
WHERE u.is_deleted = false
11
AND(
12
(
13
COALESCE(NULLIF(p_email, ''), NULL) IS NOT NULL
14
AND u.email ILIKE COALESCE(NULLIF(p_email, ''), NULL)
15
)
16
OR (
17
COALESCE(NULLIF(p_phone, ''), NULL) IS NOT NULL
18
AND u.phone_number = COALESCE(NULLIF(p_phone, ''), NULL)
19
)
20
)
21
LIMIT 1;
22
$function$
|
|||||
| Function | digest | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.digest(text, text)
2
RETURNS bytea
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pg_digest$function$
|
|||||
| Function | digest | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.digest(bytea, text)
2
RETURNS bytea
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pg_digest$function$
|
|||||
| Function | hmac | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.hmac(text, text, text)
2
RETURNS bytea
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pg_hmac$function$
|
|||||
| Function | hmac | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.hmac(bytea, bytea, text)
2
RETURNS bytea
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pg_hmac$function$
|
|||||
| Function | crypt | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.crypt(text, text)
2
RETURNS text
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pg_crypt$function$
|
|||||
| Function | gen_salt | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.gen_salt(text)
2
RETURNS text
3
LANGUAGE c
4
PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pg_gen_salt$function$
|
|||||
| Function | gen_salt | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.gen_salt(text, integer)
2
RETURNS text
3
LANGUAGE c
4
PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pg_gen_salt_rounds$function$
|
|||||
| Function | encrypt | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.encrypt(bytea, bytea, text)
2
RETURNS bytea
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pg_encrypt$function$
|
|||||
| Function | decrypt | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.decrypt(bytea, bytea, text)
2
RETURNS bytea
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pg_decrypt$function$
|
|||||
| Function | encrypt_iv | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.encrypt_iv(bytea, bytea, bytea, text)
2
RETURNS bytea
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pg_encrypt_iv$function$
|
|||||
| Function | decrypt_iv | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.decrypt_iv(bytea, bytea, bytea, text)
2
RETURNS bytea
3
LANGUAGE c
4
IMMUTABLE PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pg_decrypt_iv$function$
|
|||||
| Function | gen_random_bytes | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.gen_random_bytes(integer)
2
RETURNS bytea
3
LANGUAGE c
4
PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pg_random_bytes$function$
|
|||||
| Function | gen_random_uuid | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.gen_random_uuid()
2
RETURNS uuid
3
LANGUAGE c
4
PARALLEL SAFE
5
AS '$libdir/pgcrypto', $function$pg_random_uuid$function$
|
|||||
| Function | pgp_sym_encrypt | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.pgp_sym_encrypt(text, text)
2
RETURNS bytea
3
LANGUAGE c
4
PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pgp_sym_encrypt_text$function$
|
|||||
| Function | pgp_sym_encrypt_bytea | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.pgp_sym_encrypt_bytea(bytea, text)
2
RETURNS bytea
3
LANGUAGE c
4
PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pgp_sym_encrypt_bytea$function$
|
|||||
| Function | pgp_sym_encrypt | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.pgp_sym_encrypt(text, text, text)
2
RETURNS bytea
3
LANGUAGE c
4
PARALLEL SAFE STRICT
5
AS '$libdir/pgcrypto', $function$pgp_sym_encrypt_text$function$
|
|||||
| Function | get_bank_statements_for_status | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_bank_statements_for_status(p_company_id uuid, p_finyear_id integer, p_date_from timestamp without time zone DEFAULT NULL::timestamp without time zone, p_date_to timestamp without time zone DEFAULT NULL::timestamp without time zone)
2
RETURNS TABLE(id integer, company_id uuid, bank_id integer, bank_name text, txn_date timestamp without time zone, cheque_number character varying, description text, value_date timestamp without time zone, branch_code character varying, debit_amount numeric, credit_amount numeric, balance numeric, has_reconciled boolean, reconciliation_status_id integer, reconciliation_status text, reconciliation_description text, created_by uuid, created_on_utc timestamp without time zone, modified_by uuid, modified_on_utc timestamp without time zone, deleted_on_utc timestamp without time zone, is_deleted boolean)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
bs.id,
9
bs.company_id,
10
bs.bank_id,
11
b.name AS bank_name,
12
bs.txn_date::timestamp,
13
bs.cheque_number,
14
bs.description,
15
bs.value_date::timestamp,
16
bs.branch_code,
17
bs.debit_amount,
18
bs.credit_amount,
19
bs.balance,
20
21
-- 🧠 Smart has_reconciled logic
22
CASE
23
WHEN br.id IS NOT NULL
24
AND rs.status IN ('partial', 'matched', 'audited') THEN TRUE
25
ELSE FALSE
26
END AS has_reconciled,
27
28
-- 🧩 New reconciliation info
29
rs.id AS reconciliation_status_id,
30
rs.status::text AS reconciliation_status,
31
rs.description::text AS reconciliation_description,
32
33
bs.created_by,
34
bs.created_on_utc,
35
bs.modified_by,
36
bs.modified_on_utc,
37
bs.deleted_on_utc,
38
bs.is_deleted
39
FROM public.bank_statements bs
40
INNER JOIN public.banks b
41
ON b.id = bs.bank_id
42
AND b.is_deleted = false
43
INNER JOIN public.finance_year fy
44
ON fy.id = p_finyear_id
45
LEFT JOIN LATERAL (
46
SELECT br.*
47
FROM public.bank_reconciliations br
48
WHERE br.bank_statement_id = bs.id
49
ORDER BY br.matched_on_utc DESC
50
LIMIT 1
51
) br ON TRUE
52
LEFT JOIN public.reconciliation_statuses rs
53
ON rs.id = br.reconciliation_status_id
54
WHERE bs.company_id = p_company_id
55
AND bs.txn_date BETWEEN fy.start_date AND fy.end_date
56
AND (p_date_from IS NULL OR bs.txn_date >= p_date_from)
57
AND (p_date_to IS NULL OR bs.txn_date <= p_date_to)
58
AND bs.is_deleted = false
59
ORDER BY bs.txn_date, bs.id;
60
END;
61
$function$
|
|||||
| Function | get_sundry_creditors_ledger | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_sundry_creditors_ledger(p_company_id uuid, p_fin_year_id integer, p_start_date date DEFAULT NULL::date, p_end_date date DEFAULT NULL::date)
2
RETURNS TABLE(sl_no integer, ledger text, general_ledger text, opening_balance numeric, debit numeric, credit numeric, closing_balance numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_start_date DATE;
7
v_end_date DATE;
8
BEGIN
9
-- Get financial year start and end date if not provided
10
SELECT fy.start_date, fy.end_date
11
INTO v_start_date, v_end_date
12
FROM public.finance_year fy
13
WHERE fy.id = p_fin_year_id
14
LIMIT 1;
15
16
-- Override with provided values if they are not NULL
17
v_start_date := COALESCE(p_start_date, v_start_date);
18
v_end_date := COALESCE(p_end_date, v_end_date);
19
20
RETURN QUERY
21
SELECT ROW_NUMBER() OVER()::INTEGER AS sl_no,
22
v.name::TEXT AS ledger,
23
'Sundry Creditors'::TEXT AS general_ledger,
24
0::NUMERIC AS opening_balance,
25
COALESCE(
26
SUM(CASE
27
WHEN je.entry_type = 'C' THEN je.amount -- Credit increases liability balance
28
ELSE 0
29
END), 0
30
) AS credit,
31
COALESCE(
32
SUM(CASE
33
WHEN je.entry_type = 'D' THEN je.amount -- Debit decreases liability balance
34
ELSE 0
35
END), 0
36
) AS debit,
37
COALESCE(
38
SUM(CASE
39
WHEN je.entry_type = 'C' THEN je.amount
40
ELSE 0
41
END), 0
42
) -
43
COALESCE(
44
SUM(CASE
45
WHEN je.entry_type = 'D' THEN je.amount
46
ELSE 0
47
END), 0
48
) AS closing_balance
49
FROM public.journal_entries je
50
JOIN public.transaction_headers th ON je.transaction_id = th.id
51
JOIN public.vendors v ON th.vendor_id = v.id
52
WHERE th.company_id = p_company_id
53
AND th.transaction_date BETWEEN v_start_date AND v_end_date
54
GROUP BY v.name
55
ORDER BY v.name;
56
END;
57
$function$
|
|||||
| Function | get_account_expense | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_account_expense(p_company_id uuid, p_finance_id integer, p_period_type integer, p_period integer)
2
RETURNS TABLE(account_name text, account_number text, total_expense numeric, financial_year text)
3
LANGUAGE plpgsql
4
STABLE PARALLEL SAFE
5
AS $function$
6
DECLARE
7
v_financial_year_start DATE;
8
v_financial_year_end DATE;
9
v_finance_year TEXT;
10
11
-- Period type constants
12
CONST_MONTHLY CONSTANT INTEGER := 1;
13
CONST_QUARTERLY CONSTANT INTEGER := 2;
14
CONST_HALF_YEARLY CONSTANT INTEGER := 3;
15
CONST_YEARLY CONSTANT INTEGER := 4;
16
EXPENSE_CATEGORY_ID CONSTANT INTEGER := 5; -- Expense category ID
17
18
BEGIN
19
-- Fetch financial year details
20
SELECT fy.start_date, fy.end_date, fy.year
21
INTO v_financial_year_start, v_financial_year_end, v_finance_year
22
FROM public.finance_year fy
23
WHERE fy.id = p_finance_id;
24
25
-- Validate financial year
26
IF v_financial_year_start IS NULL OR v_financial_year_end IS NULL THEN
27
RAISE EXCEPTION 'Invalid financial year ID: %', p_finance_id;
28
END IF;
29
30
-- Handling different period types
31
IF p_period_type = CONST_MONTHLY THEN
32
-- Handle Monthly
33
RETURN QUERY
34
SELECT
35
coa.name AS account_name,
36
coa.account_number,
37
ROUND(COALESCE(SUM(je.amount), 0), 2) AS total_expense,
38
v_finance_year AS financial_year
39
FROM
40
public.journal_entries je
41
JOIN
42
public.transaction_headers th ON je.transaction_id = th.id
43
JOIN
44
public.chart_of_accounts coa ON je.account_id = coa.id
45
JOIN
46
public.account_types act ON coa.account_type_id = act.id
47
JOIN
48
public.account_categories ac ON act.account_category_id = ac.id
49
WHERE
50
th.company_id = p_company_id
51
AND ac.id = EXPENSE_CATEGORY_ID
52
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
53
AND EXTRACT(MONTH FROM je.transaction_date) = (p_period + 3) % 12 + 1
54
AND je.is_deleted = FALSE
55
AND coa.name NOT IN ('Rounding Gain') -- Exclude specific accounts
56
GROUP BY
57
coa.name, coa.account_number, v_finance_year
58
ORDER BY
59
total_expense DESC;
60
61
ELSIF p_period_type = CONST_QUARTERLY THEN
62
-- Handle Quarterly
63
RETURN QUERY
64
SELECT
65
coa.name AS account_name,
66
coa.account_number,
67
ROUND(COALESCE(SUM(je.amount), 0), 2) AS total_expense,
68
v_finance_year AS financial_year
69
FROM
70
public.journal_entries je
71
JOIN
72
public.transaction_headers th ON je.transaction_id = th.id
73
JOIN
74
public.chart_of_accounts coa ON je.account_id = coa.id
75
JOIN
76
public.account_types act ON coa.account_type_id = act.id
77
JOIN
78
public.account_categories ac ON act.account_category_id = ac.id
79
WHERE
80
th.company_id = p_company_id
81
AND ac.id = EXPENSE_CATEGORY_ID
82
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
83
AND (
84
(p_period = 1 AND EXTRACT(MONTH FROM je.transaction_date) IN (4, 5, 6)) OR
85
(p_period = 2 AND EXTRACT(MONTH FROM je.transaction_date) IN (7, 8, 9)) OR
86
(p_period = 3 AND EXTRACT(MONTH FROM je.transaction_date) IN (10, 11, 12)) OR
87
(p_period = 4 AND EXTRACT(MONTH FROM je.transaction_date) IN (1, 2, 3))
88
)
89
AND je.is_deleted = FALSE
90
AND coa.name NOT IN ('Rounding Gain') -- Exclude specific accounts
91
GROUP BY
92
coa.name, coa.account_number, v_finance_year
93
ORDER BY
94
total_expense DESC;
95
96
ELSIF p_period_type = CONST_HALF_YEARLY THEN
97
-- Handle Half-Yearly
98
RETURN QUERY
99
SELECT
100
coa.name AS account_name,
101
coa.account_number,
102
ROUND(COALESCE(SUM(je.amount), 0), 2) AS total_expense,
103
v_finance_year AS financial_year
104
FROM
105
public.journal_entries je
106
JOIN
107
public.transaction_headers th ON je.transaction_id = th.id
108
JOIN
109
public.chart_of_accounts coa ON je.account_id = coa.id
110
JOIN
111
public.account_types act ON coa.account_type_id = act.id
112
JOIN
113
public.account_categories ac ON act.account_category_id = ac.id
114
WHERE
115
th.company_id = p_company_id
116
AND ac.id = EXPENSE_CATEGORY_ID
117
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
118
AND (
119
(p_period = 1 AND EXTRACT(MONTH FROM je.transaction_date) BETWEEN 4 AND 9) OR
120
(p_period = 2 AND (EXTRACT(MONTH FROM je.transaction_date) BETWEEN 10 AND 12 OR EXTRACT(MONTH FROM je.transaction_date) BETWEEN 1 AND 3))
121
)
122
AND je.is_deleted = FALSE
123
AND coa.name NOT IN ('Rounding Gain') -- Exclude specific accounts
124
GROUP BY
125
coa.name, coa.account_number, v_finance_year
126
ORDER BY
127
total_expense DESC;
128
129
ELSIF p_period_type = CONST_YEARLY THEN
130
-- Handle Yearly
131
RETURN QUERY
132
SELECT
133
coa.name AS account_name,
134
coa.account_number,
135
ROUND(COALESCE(SUM(je.amount), 0), 2) AS total_expense,
136
v_finance_year AS financial_year
137
FROM
138
public.journal_entries je
139
JOIN
140
public.transaction_headers th ON je.transaction_id = th.id
141
JOIN
142
public.chart_of_accounts coa ON je.account_id = coa.id
143
JOIN
144
public.account_types act ON coa.account_type_id = act.id
145
JOIN
146
public.account_categories ac ON act.account_category_id = ac.id
147
WHERE
148
th.company_id = p_company_id
149
AND ac.id = EXPENSE_CATEGORY_ID
150
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
151
AND je.is_deleted = FALSE
152
AND coa.name NOT IN ('Rounding Gain') -- Exclude specific accounts
153
GROUP BY
154
coa.name, coa.account_number, v_finance_year
155
ORDER BY
156
total_expense DESC;
157
158
ELSE
159
RAISE EXCEPTION 'Invalid period type: %', p_period_type;
160
END IF;
161
162
END;
163
$function$
|
|||||
| Function | get_account_expense_breakdown | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_account_expense_breakdown(p_company_id uuid, p_finance_id integer, p_period_type integer, p_period integer)
2
RETURNS TABLE(account_id uuid, account_name text, account_number text, total_expense numeric, financial_year text)
3
LANGUAGE plpgsql
4
STABLE PARALLEL SAFE
5
AS $function$
6
DECLARE
7
v_financial_year_start DATE;
8
v_financial_year_end DATE;
9
v_finance_year TEXT;
10
CONST_MONTHLY CONSTANT INTEGER := 1;
11
CONST_QUARTERLY CONSTANT INTEGER := 2;
12
CONST_HALF_YEARLY CONSTANT INTEGER := 3;
13
CONST_YEARLY CONSTANT INTEGER := 4;
14
CONST_YTD CONSTANT INTEGER := 5;
15
BEGIN
16
-- Fetch financial year details
17
SELECT fy.start_date, fy.end_date, fy.year
18
INTO v_financial_year_start, v_financial_year_end, v_finance_year
19
FROM public.finance_year fy
20
WHERE fy.id = p_finance_id;
21
22
IF v_financial_year_start IS NULL OR v_financial_year_end IS NULL THEN
23
RAISE EXCEPTION 'Invalid financial year ID: %', p_finance_id;
24
END IF;
25
26
IF p_period_type = CONST_MONTHLY THEN
27
RETURN QUERY
28
SELECT
29
coa.id AS account_id,
30
coa.name AS account_name,
31
coa.account_number,
32
ROUND(COALESCE(SUM(je.amount), 0), 2) AS total_expense,
33
v_finance_year AS financial_year
34
FROM public.journal_entries je
35
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
36
INNER JOIN public.chart_of_accounts coa ON je.account_id = coa.id
37
WHERE th.company_id = p_company_id
38
AND coa.account_type_id IN (SELECT id FROM get_account_type_hierarchy(5))
39
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
40
AND EXTRACT(MONTH FROM je.transaction_date) = (p_period + 3) % 12 + 1
41
AND je.is_deleted = FALSE
42
AND coa.name NOT IN ('Rounding Gain')
43
GROUP BY coa.id, coa.name, coa.account_number, v_finance_year
44
ORDER BY total_expense DESC;
45
46
ELSIF p_period_type = CONST_QUARTERLY THEN
47
RETURN QUERY
48
SELECT
49
coa.id AS account_id,
50
coa.name AS account_name,
51
coa.account_number,
52
ROUND(COALESCE(SUM(je.amount), 0), 2) AS total_expense,
53
v_finance_year AS financial_year
54
FROM public.journal_entries je
55
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
56
INNER JOIN public.chart_of_accounts coa ON je.account_id = coa.id
57
WHERE th.company_id = p_company_id
58
AND coa.account_type_id IN (SELECT id FROM get_account_type_hierarchy(5))
59
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
60
AND (
61
(p_period = 1 AND EXTRACT(MONTH FROM je.transaction_date) IN (4, 5, 6)) OR
62
(p_period = 2 AND EXTRACT(MONTH FROM je.transaction_date) IN (7, 8, 9)) OR
63
(p_period = 3 AND EXTRACT(MONTH FROM je.transaction_date) IN (10, 11, 12)) OR
64
(p_period = 4 AND EXTRACT(MONTH FROM je.transaction_date) IN (1, 2, 3))
65
)
66
AND je.is_deleted = FALSE
67
AND coa.name NOT IN ('Rounding Gain')
68
GROUP BY coa.id, coa.name, coa.account_number, v_finance_year
69
ORDER BY total_expense DESC;
70
71
ELSIF p_period_type = CONST_HALF_YEARLY THEN
72
RETURN QUERY
73
SELECT
74
coa.id AS account_id,
75
coa.name AS account_name,
76
coa.account_number,
77
ROUND(COALESCE(SUM(je.amount), 0), 2) AS total_expense,
78
v_finance_year AS financial_year
79
FROM public.journal_entries je
80
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
81
INNER JOIN public.chart_of_accounts coa ON je.account_id = coa.id
82
WHERE th.company_id = p_company_id
83
AND coa.account_type_id IN (SELECT id FROM get_account_type_hierarchy(5))
84
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
85
AND (
86
(p_period = 1 AND EXTRACT(MONTH FROM je.transaction_date) BETWEEN 4 AND 9) OR
87
(p_period = 2 AND (EXTRACT(MONTH FROM je.transaction_date) BETWEEN 10 AND 12 OR EXTRACT(MONTH FROM je.transaction_date) BETWEEN 1 AND 3))
88
)
89
AND je.is_deleted = FALSE
90
AND coa.name NOT IN ('Rounding Gain')
91
GROUP BY coa.id, coa.name, coa.account_number, v_finance_year
92
ORDER BY total_expense DESC;
93
94
ELSIF p_period_type = CONST_YEARLY THEN
95
RETURN QUERY
96
SELECT
97
coa.id AS account_id,
98
coa.name AS account_name,
99
coa.account_number,
100
ROUND(COALESCE(SUM(je.amount), 0), 2) AS total_expense,
101
v_finance_year AS financial_year
102
FROM public.journal_entries je
103
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
104
INNER JOIN public.chart_of_accounts coa ON je.account_id = coa.id
105
WHERE th.company_id = p_company_id
106
AND coa.account_type_id IN (SELECT id FROM get_account_type_hierarchy(5))
107
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
108
AND je.is_deleted = FALSE
109
AND coa.name NOT IN ('Rounding Gain')
110
GROUP BY coa.id, coa.name, coa.account_number, v_finance_year
111
ORDER BY total_expense DESC;
112
113
ELSIF p_period_type = CONST_YTD THEN
114
RETURN QUERY
115
WITH recent_years AS (
116
SELECT fy.id, fy.start_date, fy.end_date,
117
fy.year AS financial_year_text
118
FROM public.finance_year fy
119
WHERE EXTRACT(YEAR FROM fy.start_date) >= EXTRACT(YEAR FROM CURRENT_DATE) - 4
120
AND EXTRACT(YEAR FROM fy.start_date) <= EXTRACT(YEAR FROM CURRENT_DATE)
121
ORDER BY fy.start_date DESC
122
LIMIT 5
123
),
124
base AS (
125
SELECT
126
coa.id AS account_id,
127
coa.name AS account_name,
128
coa.account_number,
129
ROUND(COALESCE(SUM(je.amount), 0), 2) AS total_expense,
130
ry.financial_year_text AS financial_year
131
FROM recent_years ry
132
LEFT JOIN public.journal_entries je
133
ON je.transaction_date BETWEEN ry.start_date AND LEAST(
134
ry.end_date,
135
CASE
136
WHEN CURRENT_DATE BETWEEN ry.start_date AND ry.end_date
137
THEN CURRENT_DATE
138
ELSE ry.end_date
139
END
140
)
141
LEFT JOIN public.transaction_headers th
142
ON je.transaction_id = th.id
143
AND th.company_id = p_company_id
144
LEFT JOIN public.chart_of_accounts coa
145
ON je.account_id = coa.id
146
WHERE
147
coa.account_type_id IN (SELECT id FROM get_account_type_hierarchy(5))
148
AND (je.id IS NULL OR je.is_deleted = FALSE)
149
AND (coa.name IS NULL OR coa.name NOT IN ('Rounding Gain'))
150
GROUP BY coa.id, coa.name, coa.account_number, ry.financial_year_text
151
)
152
SELECT
153
base.account_id,
154
base.account_name,
155
base.account_number,
156
SUM(base.total_expense) AS total_expense,
157
base.financial_year
158
FROM base
159
GROUP BY base.account_id, base.account_name, base.account_number, base.financial_year
160
ORDER BY base.financial_year DESC, total_expense DESC;
161
162
ELSE
163
RAISE EXCEPTION 'Invalid period type: %', p_period_type;
164
END IF;
165
END;
166
$function$
|
|||||
| Function | verify_user_password | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.verify_user_password(p_user_name text, p_password text)
2
RETURNS uuid
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_user_id UUID;
7
BEGIN
8
SELECT id
9
INTO v_user_id
10
FROM users
11
WHERE user_name = p_user_name
12
AND is_deleted = false
13
AND password_hash = crypt(p_password, password_hash);
14
15
RETURN v_user_id;
16
END;
17
$function$
|
|||||
| Function | get_account_ledger | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_account_ledger(p_account_id uuid, p_company_id uuid, p_organization_id uuid, p_start_date date, p_end_date date)
2
RETURNS TABLE(transaction_date date, account_name text, debit numeric, credit numeric, balance numeric, transaction_source_type integer, document_number text, document_id uuid, customer_id uuid, customer_name text, employee_id uuid, employee_name text, vendor_id uuid, vendor_name text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
WITH RECURSIVE account_tree AS (
8
SELECT id
9
FROM public.chart_of_accounts
10
WHERE id = p_account_id
11
AND organization_id = p_organization_id
12
AND is_deleted = false
13
UNION ALL
14
SELECT coa.id
15
FROM public.chart_of_accounts coa
16
INNER JOIN account_tree at ON coa.parent_account_id = at.id
17
WHERE coa.organization_id = p_organization_id
18
AND coa.is_deleted = false
19
),
20
account_transactions AS (
21
SELECT
22
th.transaction_date::date AS transaction_date,
23
coa.name AS account_name,
24
coa.id AS account_id,
25
CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END AS debit,
26
CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END AS credit,
27
th.document_number,
28
th.document_id,
29
th.transaction_source_type,
30
tst.sort_order,
31
th.id AS transaction_id,
32
je.id AS journal_entry_id,
33
th.customer_id,
34
th.employee_id,
35
th.vendor_id
36
FROM
37
public.transaction_headers th
38
INNER JOIN
39
public.journal_entries je ON th.id = je.transaction_id
40
INNER JOIN
41
public.chart_of_accounts coa ON je.account_id = coa.id
42
INNER JOIN
43
account_tree at ON je.account_id = at.id
44
INNER JOIN
45
public.transaction_source_types tst ON th.transaction_source_type = tst.id
46
WHERE
47
th.company_id = p_company_id
48
AND coa.organization_id = p_organization_id
49
AND th.is_deleted = false
50
AND je.is_deleted = false
51
AND th.transaction_date BETWEEN p_start_date AND p_end_date
52
)
53
SELECT
54
at.transaction_date,
55
at.account_name,
56
at.debit,
57
at.credit,
58
SUM(at.debit - at.credit) OVER (
59
ORDER BY
60
at.transaction_date,
61
at.sort_order,
62
at.transaction_id,
63
at.journal_entry_id
64
) AS balance,
65
at.transaction_source_type,
66
at.document_number,
67
at.document_id,
68
c.id AS customer_id,
69
c.name::text AS customer_name,
70
e.id AS employee_id,
71
CASE
72
WHEN e.id IS NOT NULL THEN (e.first_name || ' ' || e.last_name)::text
73
ELSE NULL
74
END AS employee_name,
75
v.id AS vendor_id,
76
v.name::text AS vendor_name
77
FROM
78
account_transactions at
79
LEFT JOIN public.customers c ON at.customer_id = c.id AND c.company_id = p_company_id
80
LEFT JOIN public.employees e ON at.employee_id = e.id AND e.company_id = p_company_id
81
LEFT JOIN public.vendors v ON at.vendor_id = v.id AND v.company_id = p_company_id
82
WHERE
83
NOT (at.debit = 0 AND at.credit = 0)
84
ORDER BY
85
at.transaction_date,
86
at.sort_order,
87
at.transaction_id,
88
at.journal_entry_id;
89
END;
90
$function$
|
|||||
| Function | get_account_total_amount | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_account_total_amount(p_company_id uuid, p_finance_id integer, p_account_id uuid)
2
RETURNS numeric
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_financial_year_start DATE;
7
v_financial_year_end DATE;
8
v_total_amount NUMERIC := 0;
9
v_organization_id UUID;
10
BEGIN
11
-- Step 1: Get the organization ID for the given company
12
SELECT organization_id
13
INTO v_organization_id
14
FROM public.companies
15
WHERE id = p_company_id;
16
17
-- Step 2: Validate if the organization ID exists
18
IF v_organization_id IS NULL THEN
19
RAISE EXCEPTION 'Organization ID not found for company ID %', p_company_id;
20
END IF;
21
22
-- Step 3: Get the financial year start and end dates
23
SELECT fy.start_date, fy.end_date
24
INTO v_financial_year_start, v_financial_year_end
25
FROM public.finance_year fy
26
WHERE fy.id = p_finance_id;
27
28
-- Step 4: Check if the financial year exists
29
IF v_financial_year_start IS NULL OR v_financial_year_end IS NULL THEN
30
RAISE EXCEPTION 'Financial year with ID % not found', p_finance_id;
31
END IF;
32
33
-- Step 5: Calculate the total amount for the specific account within the financial year,
34
-- ensuring the `organization_id` matches
35
SELECT COALESCE(SUM(je.amount), 0) INTO v_total_amount
36
FROM public.journal_entries je
37
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
38
INNER JOIN public.chart_of_accounts coa ON je.account_id = coa.id
39
WHERE th.company_id = p_company_id
40
AND je.account_id = p_account_id
41
AND coa.organization_id = v_organization_id -- Match organization ID
42
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
43
AND je.is_deleted = FALSE;
44
45
-- Step 6: Return the total amount
46
RETURN v_total_amount;
47
END;
48
$function$
|
|||||
| Function | get_account_transactions | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_account_transactions(p_company_id uuid, p_fin_year_id integer, p_chart_of_account_id uuid)
2
RETURNS TABLE(account_id uuid, account_name text, parent_account_id uuid, account_type text, year integer, apr numeric, may numeric, jun numeric, jul numeric, aug numeric, sep numeric, oct numeric, nov numeric, "dec" numeric, jan numeric, feb numeric, mar numeric, total_current_year numeric, total_last_year numeric, difference numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_start_date TIMESTAMP DEFAULT NULL;
7
v_end_date TIMESTAMP DEFAULT NULL;
8
v_prev_start_date TIMESTAMP DEFAULT NULL;
9
v_prev_end_date TIMESTAMP DEFAULT NULL;
10
BEGIN
11
-- Get start and end date for the given financial year(fn)
12
SELECT fn.start_date, fn.end_date
13
INTO v_start_date, v_end_date
14
FROM finance_year fn
15
WHERE fn.id = p_fin_year_id
16
LIMIT 1;
17
18
-- If no result, raise an error
19
IF v_start_date IS NULL OR v_end_date IS NULL THEN
20
RAISE EXCEPTION 'Financial year % not found in finance_year table', p_fin_year_id;
21
END IF;
22
23
-- Get the previous financial year's(pfn) start and end date
24
SELECT pfn.start_date, pfn.end_date
25
INTO v_prev_start_date, v_prev_end_date
26
FROM finance_year pfn
27
WHERE pfn.id = (p_fin_year_id - 1)
28
LIMIT 1;
29
30
-- Return the final query
31
RETURN QUERY
32
WITH direct_children AS (
33
SELECT coa.id AS child_id, coa.name AS child_name, coa.parent_account_id AS direct_parent_id, coa.account_type_id
34
FROM chart_of_accounts coa
35
WHERE coa.parent_account_id = p_chart_of_account_id
36
),
37
all_descendants AS (
38
WITH RECURSIVE hierarchy AS (
39
SELECT ca.id AS descendant_id, ca.parent_account_id AS ancestor_parent_id
40
FROM chart_of_accounts ca
41
WHERE ca.parent_account_id IN (SELECT child_id FROM direct_children)
42
43
UNION ALL
44
45
SELECT coa.id AS descendant_id, coa.parent_account_id AS ancestor_parent_id
46
FROM chart_of_accounts coa
47
INNER JOIN hierarchy h ON coa.parent_account_id = h.descendant_id
48
)
49
SELECT descendant_id, ancestor_parent_id FROM hierarchy
50
),
51
years AS (
52
SELECT p_fin_year_id AS year
53
UNION ALL
54
SELECT (p_fin_year_id - 1) AS year
55
),
56
all_accounts AS (
57
SELECT
58
dc.child_id AS account_id,
59
dc.child_name AS account_name,
60
dc.direct_parent_id AS parent_account_id,
61
dc.account_type_id,
62
y.year,
63
dc.child_id AS summation_base_id
64
FROM direct_children dc
65
CROSS JOIN years y
66
67
UNION ALL
68
69
SELECT
70
ad.descendant_id AS account_id,
71
dc.child_name AS account_name,
72
dc.direct_parent_id AS parent_account_id,
73
dc.account_type_id,
74
y.year,
75
dc.child_id AS summation_base_id
76
FROM direct_children dc
77
JOIN all_descendants ad ON dc.child_id = ad.ancestor_parent_id
78
CROSS JOIN years y
79
)
80
SELECT
81
aa.summation_base_id AS account_id,
82
aa.account_name,
83
aa.parent_account_id,
84
at.name AS account_type,
85
aa.year,
86
87
-- Monthly values, ensuring correct year filtering
88
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date AND EXTRACT(MONTH FROM je.transaction_date) = 4 THEN je.amount ELSE 0 END), 0) AS apr,
89
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date AND EXTRACT(MONTH FROM je.transaction_date) = 5 THEN je.amount ELSE 0 END), 0) AS may,
90
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date AND EXTRACT(MONTH FROM je.transaction_date) = 6 THEN je.amount ELSE 0 END), 0) AS jun,
91
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date AND EXTRACT(MONTH FROM je.transaction_date) = 7 THEN je.amount ELSE 0 END), 0) AS jul,
92
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date AND EXTRACT(MONTH FROM je.transaction_date) = 8 THEN je.amount ELSE 0 END), 0) AS aug,
93
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date AND EXTRACT(MONTH FROM je.transaction_date) = 9 THEN je.amount ELSE 0 END), 0) AS sep,
94
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date AND EXTRACT(MONTH FROM je.transaction_date) = 10 THEN je.amount ELSE 0 END), 0) AS oct,
95
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date AND EXTRACT(MONTH FROM je.transaction_date) = 11 THEN je.amount ELSE 0 END), 0) AS nov,
96
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date AND EXTRACT(MONTH FROM je.transaction_date) = 12 THEN je.amount ELSE 0 END), 0) AS "dec",
97
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date AND EXTRACT(MONTH FROM je.transaction_date) = 1 THEN je.amount ELSE 0 END), 0) AS jan,
98
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date AND EXTRACT(MONTH FROM je.transaction_date) = 2 THEN je.amount ELSE 0 END), 0) AS feb,
99
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date AND EXTRACT(MONTH FROM je.transaction_date) = 3 THEN je.amount ELSE 0 END), 0) AS mar,
100
101
-- Totals for each year
102
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date THEN je.amount ELSE 0 END), 0) AS total_current_year,
103
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_prev_start_date AND v_prev_end_date THEN je.amount ELSE 0 END), 0) AS total_last_year,
104
105
-- Difference calculation
106
(COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date THEN je.amount ELSE 0 END), 0) -
107
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_prev_start_date AND v_prev_end_date THEN je.amount ELSE 0 END), 0))
108
AS difference
109
110
FROM all_accounts aa
111
LEFT JOIN journal_entries je
112
ON je.account_id = aa.account_id
113
AND (je.transaction_date BETWEEN v_start_date AND v_end_date OR
114
je.transaction_date BETWEEN v_prev_start_date AND v_prev_end_date)
115
LEFT JOIN account_types at ON at.id = aa.account_type_id
116
GROUP BY aa.year, aa.summation_base_id, aa.account_name, aa.parent_account_id, at.name
117
ORDER BY aa.summation_base_id, aa.year;
118
119
END;
120
$function$
|
|||||
| Function | get_account_type_hierarchy | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_account_type_hierarchy(parent_id integer)
2
RETURNS TABLE(id integer, name text, parent_account_type_id integer)
3
LANGUAGE plpgsql
4
AS $function$
5
6
BEGIN
7
RETURN QUERY
8
WITH RECURSIVE account_hierarchy AS (
9
-- Base case: Start with the given parent account type
10
SELECT
11
p.id,
12
p.name,
13
p.parent_account_type_id
14
FROM
15
public.account_types p
16
WHERE
17
p.id = parent_id
18
19
UNION ALL
20
21
-- Recursive case: Include child account types
22
SELECT
23
at.id,
24
at.name,
25
at.parent_account_type_id
26
FROM
27
public.account_types at
28
INNER JOIN
29
account_hierarchy ah ON at.parent_account_type_id = ah.id
30
)
31
SELECT
32
acch.id,
33
acch.name,
34
acch.parent_account_type_id
35
FROM
36
account_hierarchy acch
37
ORDER BY acch.id;
38
END;
39
$function$
|
|||||
| Function | get_coa_by_account_type | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_coa_by_account_type(account_type_id_input integer)
2
RETURNS TABLE(coa_id uuid, coa_name text, coa_account_number text, coa_description text, coa_current_balance numeric, coa_opening_balance numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
coa.id AS coa_id,
9
coa.name AS coa_name,
10
coa.account_number AS coa_account_number,
11
coa.description AS coa_description,
12
coa.current_balance AS coa_current_balance,
13
coa.opening_balance AS coa_opening_balance
14
FROM
15
chart_of_accounts coa
16
WHERE
17
coa.account_type_id = account_type_id_input
18
AND coa.is_deleted = FALSE; -- Ensuring only active records are returned
19
END;
20
$function$
|
|||||
| Function | get_all_bank_transfers_by_company_and_datespan | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_all_bank_transfers_by_company_and_datespan(p_company_id uuid, p_start_date date, p_end_date date)
2
RETURNS TABLE(id uuid, company_id uuid, company_name text, source_account_id uuid, target_account_id uuid, amount numeric, transfer_date timestamp without time zone, cheque_number text, description text, is_bulk boolean, created_by uuid, created_by_name character varying, modified_by uuid, modified_by_name character varying, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone, deleted_on_utc timestamp without time zone, is_deleted boolean)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
bt.id,
9
bt.company_id,
10
c.name as company_name,
11
bt.source_account_id,
12
bt.target_account_id,
13
bt.amount,
14
bt.transfer_date,
15
bt.cheque_number,
16
bt.description,
17
bt.is_bulk,
18
bt.created_by,
19
CONCAT(cu.first_name, ' ', cu.last_name)::character varying AS created_by_name,
20
bt.modified_by,
21
CONCAT(mu.first_name, ' ', mu.last_name)::character varying AS modified_by_name,
22
bt.created_on_utc,
23
bt.modified_on_utc,
24
bt.deleted_on_utc,
25
bt.is_deleted
26
FROM public.bank_transfers AS bt
27
join public.companies as c on bt.company_id = c.id
28
LEFT JOIN users cu ON cu.id = bt.created_by
29
LEFT JOIN users mu ON mu.id = bt.modified_by
30
WHERE
31
bt.company_id = p_company_id
32
AND bt.transfer_date::date >= p_start_date
33
AND bt.transfer_date::date <= p_end_date
34
AND bt.is_deleted = false;
35
END;
36
$function$
|
|||||
| Function | get_all_expense_categories | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_all_expense_categories(p_company_id uuid, p_finance_year_id integer)
2
RETURNS TABLE(account_name text, account_number text, total_expense numeric)
3
LANGUAGE plpgsql
4
STABLE PARALLEL SAFE
5
AS $function$
6
DECLARE
7
v_financial_year_start date;
8
v_financial_year_end date;
9
BEGIN
10
-- Get financial year start and end dates
11
SELECT fy.start_date, fy.end_date
12
INTO v_financial_year_start, v_financial_year_end
13
FROM public.finance_year fy
14
WHERE fy.id = p_finance_year_id;
15
16
-- Handle case where no financial year is found
17
IF v_financial_year_start IS NULL OR v_financial_year_end IS NULL THEN
18
RAISE EXCEPTION 'Financial year with ID % not found', p_finance_year_id;
19
END IF;
20
21
RETURN QUERY
22
WITH RECURSIVE AccountHierarchy AS (
23
-- Base case: Select root accounts (only considering expense accounts, i.e., account_type_id = 5)
24
SELECT
25
coa.id AS account_id,
26
coa.name,
27
coa.account_number,
28
coa.parent_account_id
29
FROM
30
public.chart_of_accounts coa
31
WHERE
32
coa.account_type_id = 5
33
34
UNION ALL
35
36
-- Recursive case: Select all child accounts under the root accounts
37
SELECT
38
coa.id AS account_id,
39
coa.name,
40
coa.account_number,
41
coa.parent_account_id
42
FROM
43
public.chart_of_accounts coa
44
INNER JOIN
45
AccountHierarchy ah ON coa.parent_account_id = ah.account_id
46
)
47
48
-- Calculate total expenses grouped by account, filtering by company_id in transaction_headers
49
SELECT
50
ah.name AS account_name,
51
ah.account_number,
52
COALESCE(SUM(je.amount), 0) AS total_expense
53
FROM
54
AccountHierarchy ah
55
JOIN
56
public.journal_entries je ON je.account_id = ah.account_id
57
JOIN
58
public.transaction_headers th ON je.transaction_id = th.id
59
WHERE
60
th.company_id = p_company_id
61
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
62
AND je.is_deleted = FALSE
63
GROUP BY
64
ah.name, ah.account_number
65
ORDER BY
66
total_expense DESC;
67
END;
68
$function$
|
|||||
| Function | get_all_coa | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_all_coa(p_organization_id uuid, p_finyear_id integer)
2
RETURNS TABLE(id uuid, account_type text, account_number integer, name text, opening_balance numeric, level integer, order_sequence character varying, is_default_account boolean, schedule text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
WITH RECURSIVE coa_view AS (
8
SELECT
9
coa.id,
10
act.name as account_type,
11
coa.account_number::integer,
12
coa.name,
13
coa.parent_account_id,
14
0 AS level,
15
coa.account_number::text AS order_sequence,
16
coa.is_default_account,
17
NULL::text AS schedule
18
FROM public.chart_of_accounts coa
19
INNER JOIN public.account_types act ON coa.account_type_id = act.id
20
WHERE coa.organization_id = p_organization_id
21
AND (coa.parent_account_id = '00000000-0000-0000-0000-000000000000' OR coa.parent_account_id IS NULL)
22
AND coa.is_deleted = FALSE
23
24
UNION ALL
25
26
SELECT
27
coa.id,
28
act.name as account_type,
29
coa.account_number::integer,
30
coa.name,
31
coa.parent_account_id,
32
view.level + 1 AS level,
33
view.order_sequence || '.' || coa.account_number::text AS order_sequence,
34
coa.is_default_account,
35
NULL::text AS schedule
36
FROM public.chart_of_accounts coa
37
INNER JOIN coa_view view ON view.id = coa.parent_account_id
38
INNER JOIN public.account_types act ON coa.account_type_id = act.id
39
WHERE coa.organization_id = p_organization_id
40
AND coa.is_deleted = FALSE
41
),
42
balances AS (
43
SELECT
44
je.account_id,
45
SUM(
46
CASE WHEN je.entry_type = 'D' THEN je.amount
47
ELSE -je.amount END
48
) AS balance
49
FROM public.journal_entries je
50
INNER JOIN public.transaction_headers th
51
ON th.id = je.transaction_id
52
INNER JOIN public.companies c
53
ON th.company_id = c.id
54
AND c.organization_id = p_organization_id
55
INNER JOIN public.finance_year fy
56
ON th.transaction_date BETWEEN fy.start_date AND fy.end_date
57
AND fy.id = p_finyear_id
58
WHERE th.is_deleted = FALSE
59
GROUP BY je.account_id
60
)
61
SELECT
62
view.id,
63
view.account_type,
64
view.account_number,
65
LPAD('', view.level * 4, ' ') || view.name AS name,
66
COALESCE(balances.balance, 0) AS opening_balance,
67
view.level,
68
view.order_sequence::character varying AS order_sequence,
69
view.is_default_account,
70
view.schedule
71
FROM coa_view view
72
LEFT JOIN balances ON balances.account_id = view.id
73
ORDER BY view.order_sequence;
74
END;
75
$function$
|
|||||
| Function | get_all_coa_by_account_type_id | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_all_coa_by_account_type_id(p_organization_id uuid, p_account_type_id integer)
2
RETURNS TABLE(id uuid, account_type text, account_number integer, name text, opening_balance numeric, level integer, order_sequence character varying, schedule text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN query
7
WITH RECURSIVE coa_view AS (
8
SELECT
9
coa.id,
10
act.name as account_type,
11
coa.account_number::integer,
12
coa.name,
13
coa.parent_account_id,
14
0 AS level,
15
coa.account_number::text AS order_sequence,
16
NULL::text AS schedule
17
FROM
18
public.chart_of_accounts coa
19
INNER JOIN
20
public.account_types act ON coa.account_type_id = act.id
21
WHERE
22
coa.parent_account_id IS NULL
23
AND coa.organization_id = p_organization_id
24
AND coa.account_type_id = p_account_type_id
25
UNION ALL
26
SELECT
27
coa.id,
28
act.name as account_type,
29
coa.account_number::integer,
30
coa.name,
31
coa.parent_account_id,
32
view.level + 1 AS level,
33
view.order_sequence || '.' || coa.account_number::text AS order_sequence,
34
NULL::text AS schedule
35
FROM
36
public.chart_of_accounts coa
37
INNER JOIN
38
coa_view view ON view.id = coa.parent_account_id
39
INNER JOIN
40
public.account_types act ON coa.account_type_id = act.id
41
WHERE
42
coa.organization_id = p_organization_id
43
AND coa.account_type_id = p_account_type_id
44
)
45
SELECT
46
view.id,
47
view.account_type,
48
view.account_number,
49
LPAD('', view.level * 4, ' ') || view.name AS name,
50
0::numeric AS opening_balance,
51
view.level,
52
view.order_sequence::character varying AS order_sequence,
53
view.schedule
54
FROM
55
coa_view view
56
ORDER BY
57
view.order_sequence;
58
END;
59
$function$
|
|||||
| Function | get_all_coa_with_pi | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_all_coa_with_pi(p_organization_id uuid)
2
RETURNS TABLE(id uuid, account_type text, account_number integer, name text, opening_balance numeric, level integer, order_sequence character varying, schedule text, parent_account_id uuid)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN query
7
WITH RECURSIVE coa_view AS (
8
SELECT
9
coa.id,
10
act.name as account_type,
11
coa.account_number::integer,
12
coa.name,
13
coa.parent_account_id,
14
0 AS level,
15
coa.account_number::text AS order_sequence,
16
NULL::text AS schedule
17
FROM
18
public.chart_of_accounts coa
19
INNER JOIN
20
public.account_types act ON coa.account_type_id = act.id
21
WHERE
22
coa.organization_id = p_organization_id
23
AND coa.parent_account_id = '00000000-0000-0000-0000-000000000000'
24
AND coa.is_deleted = FALSE
25
UNION ALL
26
SELECT
27
coa.id,
28
act.name as account_type,
29
coa.account_number::integer,
30
coa.name,
31
coa.parent_account_id,
32
view.level + 1 AS level,
33
view.order_sequence || '.' || coa.account_number::text AS order_sequence,
34
NULL::text AS schedule
35
FROM
36
public.chart_of_accounts coa
37
INNER JOIN
38
coa_view view ON view.id = coa.parent_account_id
39
INNER JOIN
40
public.account_types act ON coa.account_type_id = act.id
41
WHERE
42
coa.organization_id = p_organization_id
43
AND coa.is_deleted = FALSE
44
)
45
SELECT
46
view.id,
47
view.account_type,
48
view.account_number,
49
LPAD('', view.level * 4, ' ') || view.name AS name,
50
0::numeric AS opening_balance,
51
view.level,
52
view.order_sequence::character varying AS order_sequence,
53
view.schedule,
54
view.parent_account_id
55
FROM
56
coa_view view
57
ORDER BY
58
view.order_sequence;
59
END;
60
$function$
|
|||||
| Function | change_user_password | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.change_user_password(p_user_name text, p_current_password text, p_new_password text, p_modified_by uuid)
2
RETURNS boolean
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_user_id UUID;
7
BEGIN
8
-- Step 1: verify current password
9
SELECT id
10
INTO v_user_id
11
FROM users
12
WHERE user_name = p_user_name
13
AND is_deleted = false
14
AND password_hash = crypt(p_current_password, password_hash);
15
16
IF v_user_id IS NULL THEN
17
RETURN FALSE;
18
END IF;
19
20
-- Step 2: update password
21
UPDATE users
22
SET password_hash = crypt(p_new_password, gen_salt('bf', 12)),
23
modified_on_utc = NOW(),
24
modified_by = p_modified_by
25
WHERE id = v_user_id;
26
27
RETURN TRUE;
28
END;
29
$function$
|
|||||
| Function | get_coa_opening_balance | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_coa_opening_balance(p_coa_id uuid, p_finyear_id integer)
2
RETURNS TABLE(transaction_date timestamp without time zone, coa_id uuid, coa_name text, description text, debit numeric, credit numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_year_text text;
7
BEGIN
8
-- Fetch the year text for the given finance year id
9
SELECT year INTO v_year_text FROM public.finance_year WHERE id = p_finyear_id;
10
IF v_year_text IS NULL THEN
11
RAISE EXCEPTION 'Finance year with id % not found!', p_finyear_id;
12
END IF;
13
14
RETURN QUERY
15
SELECT
16
th.transaction_date,
17
th.document_id AS coa_id,
18
coa.name AS coa_name,
19
th.description,
20
COALESCE(CASE WHEN je.entry_type = 'D' THEN je.amount END, 0) AS debit,
21
COALESCE(CASE WHEN je.entry_type = 'C' THEN je.amount END, 0) AS credit
22
FROM public.transaction_headers th
23
JOIN public.journal_entries je ON th.id = je.transaction_id
24
JOIN public.chart_of_accounts coa ON th.document_id = coa.id
25
WHERE th.document_id = p_coa_id
26
AND je.account_id = th.document_id
27
AND th.transaction_source_type = 13
28
AND th.description LIKE 'Opening Balance for %' || v_year_text || '%'
29
ORDER BY th.transaction_date;
30
END;
31
$function$
|
|||||
| Function | get_budget_planning_data | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_budget_planning_data(p_company_id uuid, p_current_finance_year_id integer)
2
RETURNS TABLE(row_id bigint, budget_id uuid, budget_status_id integer, budget_status_name text, account_id uuid, acc_number text, account_name text, acc_type text, acc_level integer, last_year_actual numeric, current_budget_amount numeric, line_id uuid)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
sub.rownum AS row_id,
9
sub.draft_budget_id AS budget_id,
10
sub.draft_status_id AS budget_status_id,
11
sub.draft_status_name AS budget_status_name,
12
sub.account_id,
13
sub.acc_number,
14
sub.account_name,
15
sub.acc_type,
16
sub.acc_level,
17
sub.last_year_actual,
18
sub.current_budget_amount,
19
sub.line_id
20
FROM (
21
WITH curr_year AS (
22
SELECT * FROM public.finance_year WHERE id = p_current_finance_year_id
23
),
24
25
prev_year AS (
26
SELECT fy.*
27
FROM public.finance_year fy
28
JOIN public.company_finance_year cfy ON cfy.finance_year_id = fy.id
29
WHERE cfy.company_id = p_company_id
30
AND fy.start_date < (SELECT start_date FROM curr_year)
31
ORDER BY fy.start_date DESC
32
LIMIT 1
33
),
34
35
coa_hierarchy AS (
36
SELECT
37
id AS coa_id,
38
account_number::text,
39
name,
40
account_type,
41
level
42
FROM public.get_all_coa((SELECT organization_id FROM companies WHERE id = p_company_id))
43
),
44
45
draft_budget AS (
46
SELECT
47
b.id AS draft_budget_id,
48
b.status_id AS draft_status_id,
49
s.name AS draft_status_name
50
FROM public.budgets b
51
JOIN public.budget_statuses s ON b.status_id = s.id
52
WHERE b.company_id = p_company_id
53
AND b.finance_year_id = p_current_finance_year_id
54
AND b.is_deleted = false
55
AND b.status_id = 1 -- Draft
56
LIMIT 1
57
),
58
59
last_year_actuals AS (
60
SELECT
61
je.account_id,
62
SUM(je.amount) AS actual_amount
63
FROM public.journal_entries je
64
JOIN public.transaction_headers th ON je.transaction_id = th.id
65
JOIN prev_year fy ON je.transaction_date BETWEEN fy.start_date AND fy.end_date
66
WHERE th.company_id = p_company_id
67
AND je.is_deleted = false
68
GROUP BY je.account_id
69
),
70
71
current_budget_lines AS (
72
SELECT
73
bl.account_id,
74
SUM(bl.amount) AS budget_amount,
75
(ARRAY_AGG(bl.id))[1] AS line_id -- ✅ Fixed aggregation for UUID
76
FROM public.budget_lines bl
77
JOIN public.budgets b ON bl.budget_id = b.id
78
WHERE b.company_id = p_company_id
79
AND b.finance_year_id = p_current_finance_year_id
80
AND b.is_deleted = false
81
AND bl.is_deleted = false
82
GROUP BY bl.account_id
83
)
84
85
SELECT
86
ROW_NUMBER() OVER (ORDER BY coa.account_number) AS rownum,
87
db.draft_budget_id,
88
db.draft_status_id,
89
db.draft_status_name,
90
coa.coa_id AS account_id,
91
coa.account_number AS acc_number,
92
coa.name AS account_name,
93
coa.account_type AS acc_type,
94
coa.level AS acc_level,
95
COALESCE(lya.actual_amount, 0) AS last_year_actual,
96
COALESCE(cbl.budget_amount, 0) AS current_budget_amount,
97
cbl.line_id
98
FROM coa_hierarchy coa
99
CROSS JOIN draft_budget db
100
LEFT JOIN last_year_actuals lya ON lya.account_id = coa.coa_id
101
LEFT JOIN current_budget_lines cbl ON cbl.account_id = coa.coa_id
102
) sub
103
ORDER BY sub.acc_number;
104
END;
105
$function$
|
|||||
| Function | get_cash_flow | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_cash_flow()
2
RETURNS TABLE(transaction_date date, account_number text, name text, net_cash_flow numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
je.transaction_date,
9
coa.account_number,
10
coa.name,
11
SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE -je.amount END) AS net_cash_flow
12
FROM
13
public.journal_entries je
14
JOIN
15
public.chart_of_accounts coa ON je.account_id = coa.id
16
WHERE
17
coa.account_type_id = 1 -- Current Assets, focusing on cash accounts
18
AND je.is_deleted = false
19
GROUP BY
20
je.transaction_date, coa.account_number, coa.name
21
ORDER BY
22
coa.account_number, je.transaction_date ;
23
END;
24
$function$
|
|||||
| Function | get_cash_flow | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_cash_flow(p_company_id uuid)
2
RETURNS TABLE(transaction_date date, account_number text, name text, net_cash_flow numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
6
BEGIN
7
RETURN QUERY
8
SELECT
9
je.transaction_date::date,
10
coa.account_number,
11
coa.name,
12
SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE -je.amount END) AS net_cash_flow
13
FROM
14
public.journal_entries je
15
JOIN
16
public.chart_of_accounts coa ON je.account_id = coa.id
17
JOIN
18
public.transaction_headers th ON je.transaction_id = th.id
19
WHERE
20
coa.account_type_id = 1 -- Current Assets, focusing on cash accounts
21
AND je.is_deleted = false
22
AND th.company_id = p_company_id
23
GROUP BY
24
je.transaction_date, coa.account_number, coa.name
25
ORDER BY
26
coa.account_number, je.transaction_date ;
27
END;
28
29
$function$
|
|||||
| Function | get_chart_of_accounts_hierarchy | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_chart_of_accounts_hierarchy(p_organization_id uuid)
2
RETURNS TABLE(id uuid, account_type text, account_number text, account_name text, parent_account_id uuid, is_default_account boolean, level integer)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
WITH RECURSIVE coa_hierarchy AS (
8
SELECT
9
coa.id,
10
act.name AS account_type,
11
coa.account_number,
12
coa.name as account_name,
13
coa.parent_account_id,
14
coa.is_default_account,
15
0 AS level
16
FROM
17
public.chart_of_accounts coa
18
INNER JOIN
19
public.account_types act ON coa.account_type_id = act.id
20
WHERE
21
coa.is_deleted = false
22
AND coa.parent_account_id = '00000000-0000-0000-0000-000000000000'
23
AND coa.organization_id = p_organization_id
24
25
UNION ALL
26
27
SELECT
28
coa.id,
29
act.name AS account_type,
30
coa.account_number,
31
coa.name as account_name,
32
coa.parent_account_id,
33
coa.is_default_account,
34
ch.level + 1 AS level
35
FROM
36
public.chart_of_accounts coa
37
INNER JOIN
38
public.account_types act ON coa.account_type_id = act.id
39
INNER JOIN
40
coa_hierarchy ch ON ch.id = coa.parent_account_id
41
WHERE
42
coa.is_deleted = FALSE
43
)
44
SELECT
45
ch.id,
46
ch.account_type,
47
ch.account_number,
48
LPAD(ch.account_name, LENGTH(ch.account_name) + ch.level * 4, ' ') AS account_name,
49
ch.parent_account_id,
50
ch.is_default_account,
51
ch.level
52
FROM coa_hierarchy ch
53
ORDER BY ch.account_number;
54
END;
55
$function$
|
|||||
| Function | get_company_details | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_company_details(p_company_id uuid)
2
RETURNS TABLE(id uuid, organization_id uuid, name text, short_name text, gst_in text, pan text, tan text, currency text, proprietor_name text, outstanding_limit numeric, is_non_work boolean, is_apartment boolean, interest_percentage numeric, billing_address_id uuid, billing_address jsonb, shipping_address_id uuid, shipping_address jsonb, bank_accounts jsonb, contacts jsonb, description text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
com.id,
9
com.organization_id,
10
com.name,
11
com.short_name,
12
com.gstin,
13
com.pan,
14
com.tan,
15
com.currency,
16
com.proprietor_name,
17
com.outstanding_limit,
18
com.is_non_work,
19
com.is_apartment,
20
com.interest_percentage,
21
com.billing_address_id,
22
CASE
23
WHEN ba.id IS NOT NULL
24
THEN jsonb_build_object(
25
'AddressLine1', ba.address_line1,
26
'AddressLine2', ba.address_line2,
27
'ZipCode', ba.zip_code,
28
'CountryId', ba.country_id,
29
'CountryName', bc.name,
30
'StateId', ba.state_id,
31
'StateName', bs.name,
32
'CityId', ba.city_id,
33
'CityName', bci.name
34
)
35
ELSE NULL
36
END AS billing_address,
37
com.shipping_address_id,
38
CASE
39
WHEN sa.id IS NOT NULL
40
THEN jsonb_build_object(
41
'AddressLine1', sa.address_line1,
42
'AddressLine2', sa.address_line2,
43
'ZipCode', sa.zip_code,
44
'CountryId', sa.country_id,
45
'CountryName', sc.name,
46
'StateId', sa.state_id,
47
'StateName', ss.name,
48
'CityId', sa.city_id,
49
'CityName', sci.name
50
)
51
ELSE NULL
52
END AS shipping_address,
53
COALESCE(
54
(
55
SELECT jsonb_agg(
56
jsonb_build_object(
57
'Id', ba.id,
58
'BankId', ba.bank_id,
59
'BankName', b.name,
60
'BranchName', ba.branch_name,
61
'AccountNumber', ba.account_number,
62
'IFSC', ba.ifsc
63
)
64
)
65
FROM company_bank_accounts AS cba
66
JOIN bank_accounts AS ba ON ba.id = cba.bank_account_id
67
JOIN banks AS b ON b.id = ba.bank_id
68
WHERE cba.company_id = com.id AND ba.is_deleted = false
69
),
70
'[]'::jsonb
71
) AS bank_accounts,
72
COALESCE(
73
(
74
SELECT jsonb_agg(
75
jsonb_build_object(
76
'Id', c.id,
77
'Salutation', c.salutation,
78
'FirstName', c.first_name,
79
'LastName', c.last_name,
80
'Email', c.email,
81
'PhoneNumber', c.phone_number,
82
'MobileNumber', c.mobile_number,
83
'IsPrimary', c.is_primary
84
)
85
)
86
FROM company_contacts AS cc
87
JOIN contacts AS c ON c.id = cc.contact_id
88
WHERE cc.company_id = com.id AND c.is_deleted = false
89
),
90
'[]'::jsonb
91
) AS contacts,
92
com.description
93
FROM companies AS com
94
LEFT JOIN addresses AS ba ON ba.id = com.billing_address_id
95
LEFT JOIN countries AS bc ON bc.id = ba.country_id
96
LEFT JOIN states AS bs ON bs.id = ba.state_id
97
LEFT JOIN cities AS bci ON bci.id = ba.city_id
98
LEFT JOIN addresses AS sa ON sa.id = com.shipping_address_id
99
LEFT JOIN countries AS sc ON sc.id = sa.country_id
100
LEFT JOIN states AS ss ON ss.id = sa.state_id
101
LEFT JOIN cities AS sci ON sci.id = sa.city_id
102
WHERE com.id = p_company_id AND com.is_deleted = false;
103
END;
104
$function$
|
|||||
| Function | get_customer_dues | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_customer_dues()
2
RETURNS TABLE(type text, name text, account_name text, amount numeric, due_date date, aging_bucket text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
'Customer' AS type,
9
c.name AS name,
10
coa.name AS account_name,
11
je.amount,
12
th.transaction_date + INTERVAL '30 days' AS due_date, -- Assuming a 30-day payment term
13
CASE
14
WHEN th.transaction_date + INTERVAL '30 days' <= CURRENT_DATE THEN 'Current'
15
WHEN th.transaction_date + INTERVAL '30 days' BETWEEN CURRENT_DATE - INTERVAL '30 days' AND CURRENT_DATE THEN '1-30 Days Past Due'
16
WHEN th.transaction_date + INTERVAL '30 days' BETWEEN CURRENT_DATE - INTERVAL '60 days' AND CURRENT_DATE - INTERVAL '31 days' THEN '31-60 Days Past Due'
17
ELSE 'Over 60 Days Past Due'
18
END AS aging_bucket
19
FROM
20
public.journal_entries je
21
JOIN
22
public.transaction_headers th ON je.transaction_id = th.id
23
JOIN
24
public.chart_of_accounts coa ON je.account_id = coa.id
25
JOIN
26
public.customers c ON th.customer_id = c.id
27
WHERE
28
coa.account_type_id = (SELECT id FROM account_types WHERE name = 'Accounts Receivable')
29
AND je.is_deleted = false
30
AND th.transaction_date + INTERVAL '30 days' <= CURRENT_DATE
31
ORDER BY
32
name, due_date;
33
END;
34
$function$
|
|||||
| Function | get_customer_opening_balance | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_customer_opening_balance(p_customer_id uuid, p_finyear_id integer)
2
RETURNS TABLE(transaction_date timestamp without time zone, customer_id uuid, customer_name text, description text, debit numeric, credit numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_year_text text;
7
BEGIN
8
-- Fetch the year text for the given finance year id
9
SELECT year INTO v_year_text FROM public.finance_year WHERE id = p_finyear_id;
10
IF v_year_text IS NULL THEN
11
RAISE EXCEPTION 'Finance year with id % not found!', p_finyear_id;
12
END IF;
13
14
RETURN QUERY
15
SELECT
16
th.transaction_date,
17
th.customer_id,
18
c.name::text AS customer_name, -- explicit cast here!
19
th.description,
20
COALESCE(CASE WHEN je.entry_type = 'D' THEN je.amount END, 0) AS debit,
21
COALESCE(CASE WHEN je.entry_type = 'C' THEN je.amount END, 0) AS credit
22
FROM public.transaction_headers th
23
JOIN public.customers c ON th.customer_id = c.id
24
JOIN public.journal_entries je ON th.id = je.transaction_id
25
JOIN public.chart_of_accounts sd ON sd.name ILIKE '%Sundry Debtors%'
26
WHERE th.customer_id = p_customer_id
27
AND je.account_id = sd.id
28
AND th.transaction_source_type = 13
29
AND th.description LIKE 'Opening Balance for %' || v_year_text || '%'
30
ORDER BY th.transaction_date;
31
END;
32
$function$
|
|||||
| Function | get_customer_outstanding | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_customer_outstanding()
2
RETURNS TABLE(customer_name text, account_name text, amount numeric, due_date date, aging_bucket text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
c.name AS customer_name,
9
coa.name AS account_name,
10
je.amount,
11
th.transaction_date + INTERVAL '30 days' AS due_date, -- Assuming a 30-day payment term
12
CASE
13
WHEN th.transaction_date + INTERVAL '30 days' <= CURRENT_DATE THEN 'Current'
14
WHEN th.transaction_date + INTERVAL '30 days' BETWEEN CURRENT_DATE - INTERVAL '30 days' AND CURRENT_DATE THEN '1-30 Days Past Due'
15
WHEN th.transaction_date + INTERVAL '30 days' BETWEEN CURRENT_DATE - INTERVAL '60 days' AND CURRENT_DATE - INTERVAL '31 days' THEN '31-60 Days Past Due'
16
ELSE 'Over 60 Days Past Due'
17
END AS aging_bucket
18
FROM
19
public.journal_entries je
20
JOIN
21
public.transaction_headers th ON je.transaction_id = th.id
22
JOIN
23
public.chart_of_accounts coa ON je.account_id = coa.id
24
JOIN
25
public.customers c ON th.customer_id = c.id
26
WHERE
27
coa.account_type_id = (SELECT id FROM account_types WHERE name = 'Accounts Receivable')
28
AND je.is_deleted = false
29
ORDER BY
30
c.name, th.transaction_date;
31
END;
32
$function$
|
|||||
| Function | get_trial_balance_test | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_trial_balance_test(p_company_id uuid, p_finance_year_id integer)
2
RETURNS TABLE(account_type text, account_category text, account_number text, account_name text, total_debits numeric, total_credits numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
WITH financial_year_range AS (
8
SELECT
9
fy.start_date::DATE AS start_date,
10
fy.end_date::DATE AS end_date
11
FROM
12
public.finance_year fy
13
WHERE
14
fy.id = p_finance_year_id
15
LIMIT 1
16
),
17
account_balances AS (
18
SELECT
19
je.account_id,
20
SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END) AS total_debits,
21
SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END) AS total_credits
22
FROM
23
public.journal_entries je
24
JOIN
25
public.transaction_headers th ON je.transaction_id = th.id
26
CROSS JOIN
27
financial_year_range fy
28
WHERE
29
th.company_id = p_company_id
30
AND je.is_deleted = FALSE
31
AND th.transaction_date BETWEEN fy.start_date AND fy.end_date
32
GROUP BY
33
je.account_id
34
)
35
SELECT
36
at.name AS account_type,
37
ac.name::text AS account_category, -- Explicitly cast to text
38
coa.account_number,
39
coa.name AS account_name,
40
COALESCE(ab.total_debits, 0) AS total_debits,
41
COALESCE(ab.total_credits, 0) AS total_credits
42
FROM
43
public.chart_of_accounts coa
44
INNER JOIN
45
public.account_types at ON coa.account_type_id = at.id
46
LEFT JOIN
47
public.account_categories ac ON at.account_category_id = ac.id
48
LEFT JOIN
49
account_balances ab ON coa.id = ab.account_id
50
WHERE
51
coa.organization_id = p_company_id
52
AND coa.is_deleted = FALSE
53
ORDER BY
54
coa.account_number;
55
END;
56
$function$
|
|||||
| Function | get_dashboard_totals_by_company_id | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_dashboard_totals_by_company_id(p_company_id uuid, p_finance_year_id integer)
2
RETURNS TABLE(income_total numeric, expense_total numeric, pending_dues_total numeric, pending_payments_total numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_organization_id UUID;
7
v_financial_year_start DATE;
8
v_financial_year_end DATE;
9
v_finance_year TEXT;
10
BEGIN
11
-- Fetch organization ID
12
SELECT organization_id INTO v_organization_id
13
FROM public.companies
14
WHERE id = p_company_id;
15
16
IF NOT FOUND THEN
17
RAISE EXCEPTION 'Invalid company ID: %', p_company_id;
18
END IF;
19
20
-- Fetch financial year details
21
SELECT start_date, end_date, year INTO v_financial_year_start, v_financial_year_end, v_finance_year
22
FROM public.finance_year
23
WHERE id = p_finance_year_id;
24
25
IF NOT FOUND THEN
26
RAISE EXCEPTION 'Invalid finance year ID: %', p_finance_year_id;
27
END IF;
28
29
-- Calculate totals
30
RETURN QUERY
31
SELECT
32
-- Income Total
33
ROUND(
34
(
35
SELECT COALESCE(SUM(je.amount), 0)
36
FROM public.journal_entries je
37
INNER JOIN public.chart_of_accounts ca ON je.account_id = ca.id
38
INNER JOIN public.account_types at ON ca.account_type_id = at.id
39
INNER JOIN public.account_categories ac ON at.account_category_id = ac.id
40
INNER JOIN public.transaction_headers tr ON je.transaction_id = tr.id
41
WHERE ac.id = 4
42
AND tr.company_id = p_company_id
43
AND ca.organization_id = v_organization_id
44
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
45
AND je.is_deleted = FALSE
46
), 2
47
) AS income_total,
48
49
-- Expense Total
50
ROUND(
51
(
52
SELECT COALESCE(SUM(je.amount), 0)
53
FROM public.journal_entries je
54
INNER JOIN public.chart_of_accounts ca ON je.account_id = ca.id
55
INNER JOIN public.account_types at ON ca.account_type_id = at.id
56
INNER JOIN public.account_categories ac ON at.account_category_id = ac.id
57
INNER JOIN public.transaction_headers tr ON je.transaction_id = tr.id
58
WHERE ac.id = 5
59
AND tr.company_id = p_company_id
60
AND ca.organization_id = v_organization_id
61
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
62
AND je.is_deleted = FALSE
63
), 2
64
) AS expense_total,
65
66
ROUND(
67
(
68
SELECT
69
COALESCE(SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END), 0) -
70
COALESCE(SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END), 0)
71
FROM public.journal_entries je
72
INNER JOIN public.chart_of_accounts ca ON je.account_id = ca.id
73
INNER JOIN public.account_types at ON ca.account_type_id = at.id
74
INNER JOIN public.account_categories ac ON at.account_category_id = ac.id
75
INNER JOIN public.transaction_headers tr ON je.transaction_id = tr.id
76
WHERE at.name IN ('Accounts Receivable', 'Current Assets') -- Filter specifically for receivables
77
AND tr.company_id = p_company_id
78
AND ca.organization_id = v_organization_id
79
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
80
AND je.is_deleted = FALSE
81
), 2
82
) AS pending_dues_total,
83
84
-- Pending Payments Total
85
ROUND(
86
(
87
SELECT COALESCE(SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END), 0) -
88
COALESCE(SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END), 0)
89
FROM public.journal_entries je
90
INNER JOIN public.chart_of_accounts ca ON je.account_id = ca.id
91
INNER JOIN public.account_types at ON ca.account_type_id = at.id
92
INNER JOIN public.transaction_headers tr ON je.transaction_id = tr.id
93
WHERE ca.account_type_id IN (
94
SELECT id
95
FROM public.get_account_type_hierarchy(2)
96
)
97
AND tr.company_id = p_company_id
98
AND ca.organization_id = v_organization_id
99
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
100
AND je.is_deleted = FALSE
101
AND je.entry_type IN ('C', 'D') -- Ensure only valid entry types are included
102
), 2
103
) AS pending_payments_total
104
105
106
;
107
108
END;
109
$function$
|
|||||
| Function | get_dashboard_totals_test | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_dashboard_totals_test(p_company_id uuid, p_finance_year_id integer)
2
RETURNS TABLE(income_total numeric, expense_total numeric, pending_dues_total numeric, pending_payments_total numeric, prev_income_total numeric, prev_expense_total numeric, prev_pending_dues_total numeric, prev_pending_payments_total numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_organization_id UUID;
7
v_financial_year_start DATE;
8
v_financial_year_end DATE;
9
v_prev_financial_year_start DATE;
10
v_prev_financial_year_end DATE;
11
v_finance_year INTEGER;
12
v_today DATE := CURRENT_DATE;
13
BEGIN
14
-- Fetch organization ID
15
SELECT organization_id INTO v_organization_id
16
FROM public.companies
17
WHERE id = p_company_id;
18
19
IF NOT FOUND THEN
20
RAISE EXCEPTION 'Invalid company ID: %', p_company_id;
21
END IF;
22
23
-- Get financial year integer from 'YYYY-YY'
24
SELECT LEFT(year, 4)::INTEGER INTO v_finance_year
25
FROM public.finance_year
26
WHERE id = p_finance_year_id
27
LIMIT 1;
28
29
IF NOT FOUND THEN
30
RAISE EXCEPTION 'Invalid finance year ID: %', p_finance_year_id;
31
END IF;
32
33
-- Define financial year range
34
v_financial_year_start := MAKE_DATE(v_finance_year, 4, 1);
35
v_financial_year_end := MAKE_DATE(v_finance_year + 1, 3, 31);
36
37
-- Define previous financial year range
38
v_prev_financial_year_start := MAKE_DATE(v_finance_year - 1, 4, 1);
39
v_prev_financial_year_end := MAKE_DATE(v_finance_year, 3, 31);
40
41
-- Adjust financial year ends if the year is ongoing
42
IF v_today < v_financial_year_end THEN
43
v_financial_year_end := v_today;
44
END IF;
45
46
IF v_today < v_prev_financial_year_end THEN
47
v_prev_financial_year_end := v_today - INTERVAL '1 year';
48
END IF;
49
50
-- Align both end dates to same day/month by limiting to shorter period
51
v_financial_year_end := LEAST(v_financial_year_end, v_prev_financial_year_end + INTERVAL '1 year');
52
v_prev_financial_year_end := v_financial_year_end - INTERVAL '1 year';
53
54
-- Return calculated data
55
RETURN QUERY
56
SELECT
57
-- Current Year Income
58
ROUND(COALESCE((
59
SELECT SUM(je.amount)
60
FROM journal_entries je
61
JOIN chart_of_accounts ca ON je.account_id = ca.id
62
JOIN account_types at ON ca.account_type_id = at.id
63
JOIN account_categories ac ON at.account_category_id = ac.id
64
JOIN transaction_headers tr ON je.transaction_id = tr.id
65
WHERE ac.id = 4
66
AND tr.company_id = p_company_id
67
AND ca.organization_id = v_organization_id
68
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
69
AND je.is_deleted = FALSE
70
), 0), 2),
71
72
-- Current Year Expense
73
ROUND(COALESCE((
74
SELECT SUM(je.amount)
75
FROM journal_entries je
76
JOIN chart_of_accounts ca ON je.account_id = ca.id
77
JOIN account_types at ON ca.account_type_id = at.id
78
JOIN account_categories ac ON at.account_category_id = ac.id
79
JOIN transaction_headers tr ON je.transaction_id = tr.id
80
WHERE ac.id = 5
81
and tr.transaction_source_type = 2
82
AND tr.company_id = p_company_id
83
AND ca.organization_id = v_organization_id
84
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
85
AND je.is_deleted = FALSE
86
), 0), 2),
87
88
-- Pending Dues
89
ROUND(COALESCE((
90
SELECT SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END) -
91
SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END)
92
FROM journal_entries je
93
JOIN chart_of_accounts ca ON je.account_id = ca.id
94
JOIN account_types at ON ca.account_type_id = at.id
95
JOIN transaction_headers tr ON je.transaction_id = tr.id
96
WHERE at.name IN ('Accounts Receivable', 'Current Assets')
97
AND tr.company_id = p_company_id
98
AND ca.organization_id = v_organization_id
99
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
100
AND je.is_deleted = FALSE
101
), 0), 2),
102
103
-- Pending Payments
104
ROUND(COALESCE((
105
SELECT SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END) -
106
SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END)
107
FROM journal_entries je
108
JOIN chart_of_accounts ca ON je.account_id = ca.id
109
JOIN account_types at ON ca.account_type_id = at.id
110
JOIN transaction_headers tr ON je.transaction_id = tr.id
111
WHERE ca.account_type_id IN (SELECT id FROM get_account_type_hierarchy(2))
112
AND tr.company_id = p_company_id
113
AND ca.organization_id = v_organization_id
114
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
115
AND je.is_deleted = FALSE
116
), 0), 2),
117
118
-- Previous Year Income
119
ROUND(COALESCE((
120
SELECT SUM(je.amount)
121
FROM journal_entries je
122
JOIN chart_of_accounts ca ON je.account_id = ca.id
123
JOIN account_types at ON ca.account_type_id = at.id
124
JOIN account_categories ac ON at.account_category_id = ac.id
125
JOIN transaction_headers tr ON je.transaction_id = tr.id
126
WHERE ac.id = 4
127
AND tr.company_id = p_company_id
128
AND ca.organization_id = v_organization_id
129
AND je.transaction_date BETWEEN v_prev_financial_year_start AND v_prev_financial_year_end
130
AND je.is_deleted = FALSE
131
), 0), 2),
132
133
-- Previous Year Expense
134
ROUND(COALESCE((
135
SELECT SUM(je.amount)
136
FROM journal_entries je
137
JOIN chart_of_accounts ca ON je.account_id = ca.id
138
JOIN account_types at ON ca.account_type_id = at.id
139
JOIN account_categories ac ON at.account_category_id = ac.id
140
JOIN transaction_headers tr ON je.transaction_id = tr.id
141
WHERE ac.id = 5
142
AND tr.company_id = p_company_id
143
AND ca.organization_id = v_organization_id
144
AND je.transaction_date BETWEEN v_prev_financial_year_start AND v_prev_financial_year_end
145
AND je.is_deleted = FALSE
146
), 0), 2),
147
148
-- Previous Year Pending Dues
149
ROUND(COALESCE((
150
SELECT SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END) -
151
SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END)
152
FROM journal_entries je
153
JOIN chart_of_accounts ca ON je.account_id = ca.id
154
JOIN account_types at ON ca.account_type_id = at.id
155
JOIN transaction_headers tr ON je.transaction_id = tr.id
156
WHERE at.name IN ('Accounts Receivable', 'Current Assets')
157
AND tr.company_id = p_company_id
158
AND ca.organization_id = v_organization_id
159
AND je.transaction_date BETWEEN v_prev_financial_year_start AND v_prev_financial_year_end
160
AND je.is_deleted = FALSE
161
), 0), 2),
162
163
-- Previous Year Pending Payments
164
ROUND(COALESCE((
165
SELECT SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END) -
166
SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END)
167
FROM journal_entries je
168
JOIN chart_of_accounts ca ON je.account_id = ca.id
169
JOIN account_types at ON ca.account_type_id = at.id
170
JOIN transaction_headers tr ON je.transaction_id = tr.id
171
WHERE ca.account_type_id IN (SELECT id FROM get_account_type_hierarchy(2))
172
AND tr.company_id = p_company_id
173
AND ca.organization_id = v_organization_id
174
AND je.transaction_date BETWEEN v_prev_financial_year_start AND v_prev_financial_year_end
175
AND je.is_deleted = FALSE
176
), 0), 2);
177
END;
178
$function$
|
|||||
| Function | get_user_deletion_request_by_email | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_user_deletion_request_by_email(email_input text)
2
RETURNS TABLE(user_id uuid, first_name text, last_name text, email text, deletion_request_id integer, status_id integer, status_name text, created_on_utc timestamp without time zone)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT u.id AS user_id,
8
u.first_name::TEXT AS first_name, -- Explicit cast to TEXT
9
u.last_name::TEXT AS last_name, -- Explicit cast to TEXT
10
u.email::TEXT AS email, -- Explicit cast to TEXT
11
ur.id AS deletion_request_id,
12
ur.status_id,
13
s.name::TEXT AS status_name, -- Explicit cast to TEXT
14
ur.created_on_utc
15
FROM public.users u
16
JOIN public.user_deletion_requests ur ON u.id = ur.user_id
17
JOIN public.user_deletion_request_statuses s ON ur.status_id = s.id
18
WHERE u.email = email_input
19
AND u.is_deleted = FALSE
20
LIMIT 1;
21
END;
22
$function$
|
|||||
| Function | get_journal_voucher_by_header_id | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_journal_voucher_by_header_id(p_header_id uuid)
2
RETURNS TABLE(id uuid, date timestamp without time zone, amount numeric, note text, account_id uuid, account_name text, is_debit boolean, details jsonb)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
jvh.id,
9
jvh.date,
10
jvh.amount,
11
jvh.note,
12
jvh.account_id,
13
coa_header.name AS account_name,
14
jvh.is_debit,
15
jsonb_agg(
16
jsonb_build_object(
17
'id', jvd.id,
18
'credit_account_id', jvd.credit_account_id,
19
'credit_account_name', coa_credit.name,
20
'debit_account_id', jvd.debit_account_id,
21
'debit_account_name', coa_debit.name,
22
'amount', jvd.amount,
23
'tds_tcs', jvd.tds_tcs,
24
'narration', jvd.narration
25
)
26
) AS details
27
FROM
28
public.journal_voucher_headers jvh
29
LEFT JOIN
30
public.journal_voucher_details jvd ON jvh.id = jvd.journal_header_id
31
LEFT JOIN
32
public.chart_of_accounts coa_header ON jvh.account_id = coa_header.id
33
LEFT JOIN
34
public.chart_of_accounts coa_credit ON jvd.credit_account_id = coa_credit.id
35
LEFT JOIN
36
public.chart_of_accounts coa_debit ON jvd.debit_account_id = coa_debit.id
37
WHERE
38
jvh.id = p_header_id
39
AND jvh.is_deleted = FALSE
40
AND (jvd.is_deleted = FALSE OR jvd.is_deleted IS NULL)
41
GROUP BY
42
jvh.id, coa_header.name;
43
END;
44
$function$
|
|||||
| Function | get_sundry_debtors_ledger | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_sundry_debtors_ledger(p_company_id uuid, p_fin_year_id integer, p_start_date date DEFAULT NULL::date, p_end_date date DEFAULT NULL::date)
2
RETURNS TABLE(sl_no integer, ledger text, general_ledger text, opening_balance numeric, debit numeric, credit numeric, closing_balance numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_start_date DATE;
7
v_end_date DATE;
8
BEGIN
9
-- Get financial year start and end date if not provided
10
SELECT fy.start_date, fy.end_date
11
INTO v_start_date, v_end_date
12
FROM public.finance_year fy
13
WHERE fy.id = p_fin_year_id
14
LIMIT 1;
15
16
-- Override with provided values if they are not NULL
17
v_start_date := COALESCE(p_start_date, v_start_date);
18
v_end_date := COALESCE(p_end_date, v_end_date);
19
20
RETURN QUERY
21
SELECT ROW_NUMBER() OVER()::INTEGER AS sl_no,
22
cu.name::TEXT AS ledger,
23
'Sundry Debtors'::TEXT AS general_ledger,
24
0::NUMERIC AS opening_balance,
25
COALESCE(
26
SUM(CASE
27
WHEN je.entry_type = 'D' THEN je.amount -- Debit increases asset balance
28
ELSE 0
29
END), 0
30
) AS debit,
31
COALESCE(
32
SUM(CASE
33
WHEN je.entry_type = 'C' THEN je.amount -- Credit decreases asset balance
34
ELSE 0
35
END), 0
36
) AS credit,
37
COALESCE(
38
SUM(CASE
39
WHEN je.entry_type = 'D' THEN je.amount -- Credit decreases asset balance
40
ELSE 0
41
END), 0
42
) -
43
COALESCE(
44
SUM(CASE
45
WHEN je.entry_type = 'C' THEN je.amount -- Debit increases asset balance
46
ELSE 0
47
END), 0
48
) AS closing_balance
49
FROM public.journal_entries je
50
JOIN public.transaction_headers th ON je.transaction_id = th.id
51
JOIN public.customers cu ON th.customer_id = cu.id
52
WHERE th.company_id = p_company_id
53
AND th.transaction_date BETWEEN v_start_date AND v_end_date
54
GROUP BY cu.name
55
ORDER BY cu.name;
56
END;
57
$function$
|
|||||
| Function | get_dashboard_totals_with_previous_year_comparison | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_dashboard_totals_with_previous_year_comparison(p_company_id uuid, p_finance_year_id integer)
2
RETURNS TABLE(income_total numeric, expense_total numeric, pending_dues_total numeric, pending_payments_total numeric, prev_income_total numeric, prev_expense_total numeric, prev_pending_dues_total numeric, prev_pending_payments_total numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_organization_id UUID;
7
v_financial_year_start DATE;
8
v_financial_year_end DATE;
9
v_prev_financial_year_start DATE;
10
v_prev_financial_year_end DATE;
11
v_finance_year INTEGER;
12
v_today DATE := CURRENT_DATE;
13
BEGIN
14
-- Fetch organization ID
15
SELECT organization_id INTO v_organization_id
16
FROM public.companies
17
WHERE id = p_company_id;
18
19
IF NOT FOUND THEN
20
RAISE EXCEPTION 'Invalid company ID: %', p_company_id;
21
END IF;
22
23
-- Get financial year integer from 'YYYY-YY'
24
SELECT LEFT(year, 4)::INTEGER INTO v_finance_year
25
FROM public.finance_year
26
WHERE id = p_finance_year_id
27
LIMIT 1;
28
29
IF NOT FOUND THEN
30
RAISE EXCEPTION 'Invalid finance year ID: %', p_finance_year_id;
31
END IF;
32
33
-- Define financial year range
34
v_financial_year_start := MAKE_DATE(v_finance_year, 4, 1);
35
v_financial_year_end := MAKE_DATE(v_finance_year + 1, 3, 31);
36
37
-- Define previous financial year range
38
v_prev_financial_year_start := MAKE_DATE(v_finance_year - 1, 4, 1);
39
v_prev_financial_year_end := MAKE_DATE(v_finance_year, 3, 31);
40
41
-- Adjust financial year ends if the year is ongoing
42
IF v_today < v_financial_year_end THEN
43
v_financial_year_end := v_today;
44
END IF;
45
46
IF v_today < v_prev_financial_year_end THEN
47
v_prev_financial_year_end := v_today - INTERVAL '1 year';
48
END IF;
49
50
-- Align both end dates to same day/month by limiting to shorter period
51
v_financial_year_end := LEAST(v_financial_year_end, v_prev_financial_year_end + INTERVAL '1 year');
52
v_prev_financial_year_end := v_financial_year_end - INTERVAL '1 year';
53
54
-- Return calculated data
55
RETURN QUERY
56
SELECT
57
-- Current Year Income
58
ROUND(COALESCE((
59
SELECT SUM(je.amount)
60
FROM journal_entries je
61
JOIN chart_of_accounts ca ON je.account_id = ca.id
62
JOIN account_types at ON ca.account_type_id = at.id
63
JOIN account_categories ac ON at.account_category_id = ac.id
64
JOIN transaction_headers tr ON je.transaction_id = tr.id
65
WHERE ac.id = 4
66
AND tr.company_id = p_company_id
67
AND ca.organization_id = v_organization_id
68
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
69
AND je.is_deleted = FALSE
70
), 0), 2),
71
72
-- Current Year Expense
73
ROUND(COALESCE((
74
SELECT SUM(je.amount)
75
FROM journal_entries je
76
JOIN chart_of_accounts ca ON je.account_id = ca.id
77
JOIN account_types at ON ca.account_type_id = at.id
78
JOIN account_categories ac ON at.account_category_id = ac.id
79
JOIN transaction_headers tr ON je.transaction_id = tr.id
80
WHERE ac.id = 5
81
AND tr.company_id = p_company_id
82
AND ca.organization_id = v_organization_id
83
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
84
AND je.is_deleted = FALSE
85
), 0), 2),
86
87
-- Pending Dues
88
ROUND(COALESCE((
89
SELECT SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END) -
90
SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END)
91
FROM journal_entries je
92
JOIN chart_of_accounts ca ON je.account_id = ca.id
93
JOIN account_types at ON ca.account_type_id = at.id
94
JOIN transaction_headers tr ON je.transaction_id = tr.id
95
WHERE at.name IN ('Accounts Receivable', 'Current Assets')
96
AND tr.company_id = p_company_id
97
AND ca.organization_id = v_organization_id
98
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
99
AND je.is_deleted = FALSE
100
), 0), 2),
101
102
-- Pending Payments
103
ROUND(COALESCE((
104
SELECT SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END) -
105
SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END)
106
FROM journal_entries je
107
JOIN chart_of_accounts ca ON je.account_id = ca.id
108
JOIN account_types at ON ca.account_type_id = at.id
109
JOIN transaction_headers tr ON je.transaction_id = tr.id
110
WHERE ca.account_type_id IN (SELECT id FROM get_account_type_hierarchy(2))
111
AND tr.company_id = p_company_id
112
AND ca.organization_id = v_organization_id
113
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
114
AND je.is_deleted = FALSE
115
), 0), 2),
116
117
-- Previous Year Income
118
ROUND(COALESCE((
119
SELECT SUM(je.amount)
120
FROM journal_entries je
121
JOIN chart_of_accounts ca ON je.account_id = ca.id
122
JOIN account_types at ON ca.account_type_id = at.id
123
JOIN account_categories ac ON at.account_category_id = ac.id
124
JOIN transaction_headers tr ON je.transaction_id = tr.id
125
WHERE ac.id = 4
126
AND tr.company_id = p_company_id
127
AND ca.organization_id = v_organization_id
128
AND je.transaction_date BETWEEN v_prev_financial_year_start AND v_prev_financial_year_end
129
AND je.is_deleted = FALSE
130
), 0), 2),
131
132
-- Previous Year Expense
133
ROUND(COALESCE((
134
SELECT SUM(je.amount)
135
FROM journal_entries je
136
JOIN chart_of_accounts ca ON je.account_id = ca.id
137
JOIN account_types at ON ca.account_type_id = at.id
138
JOIN account_categories ac ON at.account_category_id = ac.id
139
JOIN transaction_headers tr ON je.transaction_id = tr.id
140
WHERE ac.id = 5
141
AND tr.company_id = p_company_id
142
AND ca.organization_id = v_organization_id
143
AND je.transaction_date BETWEEN v_prev_financial_year_start AND v_prev_financial_year_end
144
AND je.is_deleted = FALSE
145
), 0), 2),
146
147
-- Previous Year Pending Dues
148
ROUND(COALESCE((
149
SELECT SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END) -
150
SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END)
151
FROM journal_entries je
152
JOIN chart_of_accounts ca ON je.account_id = ca.id
153
JOIN account_types at ON ca.account_type_id = at.id
154
JOIN transaction_headers tr ON je.transaction_id = tr.id
155
WHERE at.name IN ('Accounts Receivable', 'Current Assets')
156
AND tr.company_id = p_company_id
157
AND ca.organization_id = v_organization_id
158
AND je.transaction_date BETWEEN v_prev_financial_year_start AND v_prev_financial_year_end
159
AND je.is_deleted = FALSE
160
), 0), 2),
161
162
-- Previous Year Pending Payments
163
ROUND(COALESCE((
164
SELECT SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END) -
165
SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END)
166
FROM journal_entries je
167
JOIN chart_of_accounts ca ON je.account_id = ca.id
168
JOIN account_types at ON ca.account_type_id = at.id
169
JOIN transaction_headers tr ON je.transaction_id = tr.id
170
WHERE ca.account_type_id IN (SELECT id FROM get_account_type_hierarchy(2))
171
AND tr.company_id = p_company_id
172
AND ca.organization_id = v_organization_id
173
AND je.transaction_date BETWEEN v_prev_financial_year_start AND v_prev_financial_year_end
174
AND je.is_deleted = FALSE
175
), 0), 2);
176
END;
177
$function$
|
|||||
| Function | get_employee_opening_balance | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_employee_opening_balance(p_employee_id uuid, p_finyear_id integer)
2
RETURNS TABLE(transaction_date timestamp without time zone, employee_id uuid, employee_name text, description text, debit numeric, credit numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_year_text text;
7
BEGIN
8
-- Fetch the year text for the given finance year id
9
SELECT year INTO v_year_text FROM public.finance_year WHERE id = p_finyear_id;
10
IF v_year_text IS NULL THEN
11
RAISE EXCEPTION 'Finance year with id % not found!', p_finyear_id;
12
END IF;
13
14
RETURN QUERY
15
SELECT
16
th.transaction_date,
17
th.employee_id,
18
(e.first_name || ' ' || e.last_name) AS employee_name,
19
th.description,
20
COALESCE(CASE WHEN je.entry_type = 'D' THEN je.amount END, 0) AS debit,
21
COALESCE(CASE WHEN je.entry_type = 'C' THEN je.amount END, 0) AS credit
22
FROM public.transaction_headers th
23
JOIN public.employees e ON th.employee_id = e.id
24
JOIN public.journal_entries je ON th.id = je.transaction_id
25
JOIN public.chart_of_accounts sp ON sp.name ILIKE '%Salary Payable%'
26
WHERE th.employee_id = p_employee_id
27
AND je.account_id = sp.id
28
AND th.transaction_source_type = 13
29
AND th.description LIKE 'Opening Balance for %' || v_year_text || '%'
30
ORDER BY th.transaction_date;
31
END;
32
$function$
|
|||||
| Function | get_expense_monthly_data | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_expense_monthly_data(p_company_id uuid, p_finance_year_id integer)
2
RETURNS TABLE(account_name text, year integer, apr numeric, may numeric, jun numeric, jul numeric, aug numeric, sep numeric, oct numeric, nov numeric, "dec" numeric, jan numeric, feb numeric, mar numeric, total numeric, difference numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_organization_id UUID;
7
v_start_date DATE;
8
v_end_date DATE;
9
v_prev_start_date DATE;
10
v_prev_end_date DATE;
11
BEGIN
12
-- Get the organization_id for the given company_id
13
SELECT organization_id INTO v_organization_id
14
FROM public.companies
15
WHERE id = p_company_id;
16
17
-- Get the start and end dates for the selected financial year
18
SELECT start_date, end_date INTO v_start_date, v_end_date
19
FROM public.finance_year
20
WHERE id = p_finance_year_id;
21
22
-- Get the start and end dates for the previous financial year
23
SELECT start_date, end_date INTO v_prev_start_date, v_prev_end_date
24
FROM public.finance_year
25
WHERE id = (p_finance_year_id - 1); -- Fetch previous year finance data
26
27
RETURN QUERY
28
WITH expense_accounts AS (
29
-- Fetch all expense accounts
30
SELECT coa.id, coa.name
31
FROM chart_of_accounts coa
32
JOIN account_types at ON coa.account_type_id = at.id
33
WHERE at.id IN (SELECT id FROM get_account_type_hierarchy(5))
34
AND coa.organization_id = v_organization_id
35
),
36
monthly_expenses AS (
37
-- Get monthly totals for the selected and previous financial years
38
SELECT
39
je.account_id,
40
CASE
41
WHEN je.transaction_date BETWEEN v_start_date AND v_end_date THEN p_finance_year_id
42
WHEN je.transaction_date BETWEEN v_prev_start_date AND v_prev_end_date THEN (p_finance_year_id - 1)
43
END AS year,
44
EXTRACT(MONTH FROM je.transaction_date) AS month,
45
SUM(je.amount) AS amount
46
FROM journal_entries je
47
JOIN transaction_headers th ON je.transaction_id = th.id
48
WHERE th.company_id = p_company_id
49
AND je.transaction_date BETWEEN v_prev_start_date AND v_end_date -- Filter for both years
50
GROUP BY je.account_id, year, EXTRACT(MONTH FROM je.transaction_date)
51
),
52
pivoted_expenses AS (
53
-- Pivoting data to month-wise columns
54
SELECT
55
ea.name AS account_name,
56
me.year,
57
COALESCE(SUM(CASE WHEN me.month = 4 THEN me.amount ELSE 0 END), 0) AS apr,
58
COALESCE(SUM(CASE WHEN me.month = 5 THEN me.amount ELSE 0 END), 0) AS may,
59
COALESCE(SUM(CASE WHEN me.month = 6 THEN me.amount ELSE 0 END), 0) AS jun,
60
COALESCE(SUM(CASE WHEN me.month = 7 THEN me.amount ELSE 0 END), 0) AS jul,
61
COALESCE(SUM(CASE WHEN me.month = 8 THEN me.amount ELSE 0 END), 0) AS aug,
62
COALESCE(SUM(CASE WHEN me.month = 9 THEN me.amount ELSE 0 END), 0) AS sep,
63
COALESCE(SUM(CASE WHEN me.month = 10 THEN me.amount ELSE 0 END), 0) AS oct,
64
COALESCE(SUM(CASE WHEN me.month = 11 THEN me.amount ELSE 0 END), 0) AS nov,
65
COALESCE(SUM(CASE WHEN me.month = 12 THEN me.amount ELSE 0 END), 0) AS "dec",
66
COALESCE(SUM(CASE WHEN me.month = 1 THEN me.amount ELSE 0 END), 0) AS jan,
67
COALESCE(SUM(CASE WHEN me.month = 2 THEN me.amount ELSE 0 END), 0) AS feb,
68
COALESCE(SUM(CASE WHEN me.month = 3 THEN me.amount ELSE 0 END), 0) AS mar,
69
SUM(me.amount) AS total
70
FROM expense_accounts ea
71
JOIN monthly_expenses me ON ea.id = me.account_id
72
WHERE me.year IN (p_finance_year_id, p_finance_year_id - 1)
73
GROUP BY ea.name, me.year
74
)
75
-- Final table with the difference calculation
76
SELECT
77
pe.account_name,
78
pe.year,
79
pe.apr, pe.may, pe.jun, pe.jul, pe.aug, pe.sep, pe.oct, pe.nov, pe.dec, pe.jan, pe.feb, pe.mar,
80
pe.total,
81
COALESCE(pe.total - LAG(pe.total) OVER (PARTITION BY pe.account_name ORDER BY pe.year), 0) AS difference
82
FROM pivoted_expenses pe
83
ORDER BY pe.account_name, pe.year;
84
END;
85
$function$
|
|||||
| Function | get_expense_overview | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_expense_overview(p_company_id uuid, p_period_type integer, p_finance_id integer)
2
RETURNS TABLE(period text, year numeric, account_name text, total_expense numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_financial_year_start date;
7
v_financial_year_end date;
8
v_finance_year text;
9
v_organization_id uuid;
10
11
-- Define constants for period types
12
CONST_MONTHLY CONSTANT INTEGER := 1;
13
CONST_QUARTERLY CONSTANT INTEGER := 2;
14
CONST_HALF_YEARLY CONSTANT INTEGER := 3;
15
CONST_YEARLY CONSTANT INTEGER := 4;
16
BEGIN
17
-- Step 1: Fetch the organization ID based on the company ID
18
SELECT organization_id INTO v_organization_id
19
FROM public.companies
20
WHERE id = p_company_id;
21
22
-- Step 2: Get financial year boundaries
23
SELECT fy.start_date, fy.end_date, fy.year INTO v_financial_year_start, v_financial_year_end, v_finance_year
24
FROM public.finance_year fy
25
WHERE id = p_finance_id;
26
27
-- Check the period type and execute the appropriate logic
28
IF p_period_type = CONST_MONTHLY THEN
29
RETURN QUERY
30
WITH RECURSIVE AccountHierarchy AS (
31
-- Base case: Select root accounts (expense accounts)
32
SELECT
33
coa.id AS account_id,
34
coa.name AS account_name,
35
coa.account_number,
36
coa.parent_account_id
37
FROM
38
public.chart_of_accounts coa
39
WHERE
40
coa.account_type_id = 5 -- Expense accounts
41
AND coa.organization_id = v_organization_id
42
43
UNION ALL
44
45
-- Recursive case: Select child accounts under the root accounts
46
SELECT
47
coa.id AS account_id,
48
coa.name AS account_name,
49
coa.account_number,
50
coa.parent_account_id
51
FROM
52
public.chart_of_accounts coa
53
INNER JOIN
54
AccountHierarchy ah ON coa.parent_account_id = ah.account_id
55
),
56
months AS (
57
SELECT
58
TO_CHAR(months.m, 'Mon') AS period,
59
EXTRACT(YEAR FROM months.m) AS extracted_year,
60
EXTRACT(MONTH FROM months.m) AS month_num
61
FROM
62
generate_series(v_financial_year_start, v_financial_year_end, '1 month'::interval) AS months(m)
63
)
64
SELECT
65
months.period,
66
months.extracted_year AS year,
67
ah.account_name,
68
COALESCE(SUM(je.amount), 0) AS total_expense
69
FROM
70
months
71
INNER JOIN journal_entries je
72
ON EXTRACT(MONTH FROM je.transaction_date) = months.month_num
73
AND EXTRACT(YEAR FROM je.transaction_date) = months.extracted_year
74
INNER JOIN transaction_headers tr
75
ON je.transaction_id = tr.id
76
AND tr.company_id = p_company_id -- Ensure filtering by company_id
77
INNER JOIN AccountHierarchy ah
78
ON je.account_id = ah.account_id
79
WHERE
80
je.entry_type = 'D' -- Only debits are considered expenses
81
AND je.is_deleted = FALSE -- Filter non-deleted entries
82
GROUP BY
83
months.period, months.extracted_year, months.month_num, ah.account_name
84
ORDER BY
85
months.extracted_year, months.month_num, ah.account_name;
86
87
ELSIF p_period_type = CONST_QUARTERLY THEN
88
RETURN QUERY
89
WITH RECURSIVE AccountHierarchy AS (
90
-- Base case: Select root accounts (expense accounts)
91
SELECT
92
coa.id AS account_id,
93
coa.name AS account_name,
94
coa.account_number,
95
coa.parent_account_id
96
FROM
97
public.chart_of_accounts coa
98
WHERE
99
coa.account_type_id = 5 -- Expense accounts
100
AND coa.organization_id = v_organization_id
101
102
UNION ALL
103
104
-- Recursive case: Select child accounts under the root accounts
105
SELECT
106
coa.id AS account_id,
107
coa.name AS account_name,
108
coa.account_number,
109
coa.parent_account_id
110
FROM
111
public.chart_of_accounts coa
112
INNER JOIN
113
AccountHierarchy ah ON coa.parent_account_id = ah.account_id
114
),
115
quarters AS (
116
SELECT
117
'Q1' AS period, 1 AS quarter_num, EXTRACT(YEAR FROM v_financial_year_start) AS extracted_year,
118
v_financial_year_start AS start_date, v_financial_year_start + interval '2 month' AS end_date
119
UNION ALL
120
SELECT
121
'Q2' AS period, 2 AS quarter_num, EXTRACT(YEAR FROM v_financial_year_start) AS extracted_year,
122
v_financial_year_start + interval '3 month' AS start_date, v_financial_year_start + interval '5 month' AS end_date
123
UNION ALL
124
SELECT
125
'Q3' AS period, 3 AS quarter_num, EXTRACT(YEAR FROM v_financial_year_start) AS extracted_year,
126
v_financial_year_start + interval '6 month' AS start_date, v_financial_year_start + interval '8 month' AS end_date
127
UNION ALL
128
SELECT
129
'Q4' AS period, 4 AS quarter_num, EXTRACT(YEAR FROM v_financial_year_start) + 1 AS extracted_year,
130
v_financial_year_start + interval '9 month' AS start_date, v_financial_year_end AS end_date
131
)
132
SELECT
133
quarters.period,
134
quarters.extracted_year AS year,
135
ah.account_name,
136
COALESCE(SUM(je.amount), 0) AS total_expense
137
FROM
138
quarters
139
LEFT JOIN journal_entries je
140
ON je.transaction_date BETWEEN quarters.start_date AND quarters.end_date
141
LEFT JOIN transaction_headers tr
142
ON je.transaction_id = tr.id
143
AND tr.company_id = p_company_id -- Ensure filtering by company_id
144
INNER JOIN AccountHierarchy ah
145
ON je.account_id = ah.account_id
146
WHERE
147
je.entry_type = 'D' -- Only debits are considered expenses
148
AND je.is_deleted = FALSE -- Filter non-deleted entries
149
GROUP BY
150
quarters.period, quarters.extracted_year, quarters.quarter_num, ah.account_name
151
ORDER BY
152
quarters.extracted_year, quarters.quarter_num, ah.account_name;
153
154
-- Repeat similar adjustments for Half-Yearly and Yearly periods.
155
ELSIF p_period_type = CONST_HALF_YEARLY THEN
156
RETURN QUERY
157
WITH RECURSIVE AccountHierarchy AS (
158
-- Base case: Select root accounts (expense accounts)
159
SELECT
160
coa.id AS account_id,
161
coa.name AS account_name,
162
coa.account_number,
163
coa.parent_account_id
164
FROM
165
public.chart_of_accounts coa
166
WHERE
167
coa.account_type_id = 5 -- Expense accounts
168
AND coa.organization_id = v_organization_id
169
170
UNION ALL
171
172
-- Recursive case: Select child accounts under the root accounts
173
SELECT
174
coa.id AS account_id,
175
coa.name AS account_name,
176
coa.account_number,
177
coa.parent_account_id
178
FROM
179
public.chart_of_accounts coa
180
INNER JOIN
181
AccountHierarchy ah ON coa.parent_account_id = ah.account_id
182
),
183
half_years AS (
184
SELECT
185
'H1' AS period,
186
1 AS half_year_num,
187
EXTRACT(YEAR FROM v_financial_year_start) AS extracted_year,
188
v_financial_year_start AS start_date,
189
v_financial_year_start + interval '5 month' AS end_date
190
UNION ALL
191
SELECT
192
'H2' AS period,
193
2 AS half_year_num,
194
EXTRACT(YEAR FROM v_financial_year_start) AS extracted_year,
195
v_financial_year_start + interval '6 month' AS start_date,
196
v_financial_year_end AS end_date
197
)
198
SELECT
199
half_years.period,
200
half_years.extracted_year AS year,
201
ah.account_name,
202
COALESCE(SUM(je.amount), 0) AS total_expense
203
FROM
204
half_years
205
LEFT JOIN journal_entries je
206
ON je.transaction_date BETWEEN half_years.start_date AND half_years.end_date
207
LEFT JOIN transaction_headers tr
208
ON je.transaction_id = tr.id
209
AND tr.company_id = p_company_id -- Ensure filtering by company_id
210
INNER JOIN AccountHierarchy ah
211
ON je.account_id = ah.account_id
212
WHERE
213
je.entry_type = 'D' -- Only debits are considered expenses
214
AND je.is_deleted = FALSE -- Filter non-deleted entries
215
GROUP BY
216
half_years.period, half_years.extracted_year, half_years.half_year_num, ah.account_name
217
ORDER BY
218
half_years.extracted_year, half_years.half_year_num, ah.account_name;
219
ELSIF p_period_type = CONST_YEARLY THEN
220
RETURN QUERY
221
WITH RECURSIVE AccountHierarchy AS (
222
-- Base case: Select root accounts (expense accounts)
223
SELECT
224
coa.id AS account_id,
225
coa.name AS account_name,
226
coa.account_number,
227
coa.parent_account_id
228
FROM
229
public.chart_of_accounts coa
230
WHERE
231
coa.account_type_id = 5 -- Expense accounts
232
AND coa.organization_id = v_organization_id
233
234
UNION ALL
235
236
-- Recursive case: Select child accounts under the root accounts
237
SELECT
238
coa.id AS account_id,
239
coa.name AS account_name,
240
coa.account_number,
241
coa.parent_account_id
242
FROM
243
public.chart_of_accounts coa
244
INNER JOIN
245
AccountHierarchy ah ON coa.parent_account_id = ah.account_id
246
)
247
SELECT
248
'Year' AS period,
249
EXTRACT(YEAR FROM v_financial_year_start) AS year,
250
ah.account_name,
251
COALESCE(SUM(je.amount), 0) AS total_expense
252
FROM
253
journal_entries je
254
LEFT JOIN transaction_headers tr
255
ON je.transaction_id = tr.id
256
AND tr.company_id = p_company_id -- Ensure filtering by company_id
257
INNER JOIN AccountHierarchy ah
258
ON je.account_id = ah.account_id
259
WHERE
260
je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
261
AND je.entry_type = 'D' -- Only debits are considered expenses
262
AND je.is_deleted = FALSE -- Filter non-deleted entries
263
GROUP BY
264
year, ah.account_name
265
ORDER BY
266
year, ah.account_name;
267
268
END IF;
269
END;
270
$function$
|
|||||
| Function | get_financial_year_dates | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_financial_year_dates(p_fin_year_id integer)
2
RETURNS TABLE(start_date date, end_date date)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT fy.start_date::DATE, fy.end_date::DATE
8
FROM public.finance_year fy
9
WHERE fy.id = p_fin_year_id
10
LIMIT 1;
11
END;
12
$function$
|
|||||
| Function | get_new_voucher_number | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_new_voucher_number(p_company_id uuid, p_date date)
2
RETURNS character varying
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_finance_year_id integer;
7
v_voucher_prefix text;
8
v_last_voucher_id integer;
9
v_new_voucher_number character varying;
10
BEGIN
11
-- Get the financial year ID based on the provided date
12
SELECT id INTO v_finance_year_id
13
FROM finance_year
14
WHERE start_date <= p_date AND end_date >= p_date;
15
16
-- Try updating existing record
17
WITH updated AS (
18
UPDATE public.journal_voucher_header_id
19
SET last_voucher_id = COALESCE(last_voucher_id, 0) + 1
20
WHERE company_id = p_company_id AND fin_year = v_finance_year_id
21
RETURNING last_voucher_id, voucher_prefix
22
),
23
inserted AS (
24
INSERT INTO public.journal_voucher_header_id (
25
company_id, fin_year, voucher_prefix, last_voucher_id
26
)
27
SELECT p_company_id, v_finance_year_id, 'JV', 1
28
WHERE NOT EXISTS (SELECT 1 FROM updated)
29
RETURNING last_voucher_id, voucher_prefix
30
)
31
SELECT
32
COALESCE((SELECT last_voucher_id FROM updated LIMIT 1), (SELECT last_voucher_id FROM inserted LIMIT 1)),
33
COALESCE((SELECT voucher_prefix FROM updated LIMIT 1), (SELECT voucher_prefix FROM inserted LIMIT 1))
34
INTO v_last_voucher_id, v_voucher_prefix;
35
36
-- Generate the voucher number
37
v_new_voucher_number := v_voucher_prefix || LPAD(v_last_voucher_id::text, 5, '0');
38
39
RETURN v_new_voucher_number;
40
END;
41
$function$
|
|||||
| Function | get_full_account_hierarchy_overview | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_full_account_hierarchy_overview(p_company_id uuid, p_fin_year_id integer)
2
RETURNS TABLE(account_id uuid, account_name text, parent_account_id uuid, hierarchy_path text[], financial_year integer, apr numeric, may numeric, jun numeric, jul numeric, aug numeric, sep numeric, oct numeric, nov numeric, "dec" numeric, jan numeric, feb numeric, mar numeric, total_sum numeric, difference numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
#variable_conflict use_column
6
DECLARE
7
v_current_year INTEGER := p_fin_year_id;
8
v_previous_year INTEGER := p_fin_year_id - 1;
9
v_organization_id UUID;
10
BEGIN
11
SELECT c.organization_id INTO v_organization_id
12
FROM public.companies c
13
WHERE c.id = p_company_id;
14
15
RETURN QUERY
16
WITH RECURSIVE account_with_path AS (
17
-- This is a much safer way to build the path
18
SELECT
19
ca.id,
20
ca.name,
21
ca.parent_account_id,
22
-- If parent is missing or is self, start a new path. Otherwise, build the path.
23
COALESCE(p.name, '') || '/' || ca.name AS path_str
24
FROM chart_of_accounts ca
25
LEFT JOIN chart_of_accounts p ON ca.parent_account_id = p.id AND ca.parent_account_id != ca.id
26
WHERE ca.organization_id = v_organization_id
27
),
28
financial_years AS (
29
SELECT id AS fin_year_id, start_date, end_date
30
FROM finance_year
31
WHERE id IN (v_previous_year, v_current_year)
32
),
33
aggregated_data AS (
34
SELECT
35
awp.id, awp.name, awp.parent_account_id,
36
-- Convert the path string to an array for AG Grid
37
string_to_array(ltrim(awp.path_str, '/'), '/') AS hierarchy_path,
38
fy.fin_year_id,
39
COALESCE(SUM(je.amount * CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 4 THEN 1 ELSE 0 END), 0) AS apr,
40
COALESCE(SUM(je.amount * CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 5 THEN 1 ELSE 0 END), 0) AS may,
41
COALESCE(SUM(je.amount * CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 6 THEN 1 ELSE 0 END), 0) AS jun,
42
COALESCE(SUM(je.amount * CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 7 THEN 1 ELSE 0 END), 0) AS jul,
43
COALESCE(SUM(je.amount * CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 8 THEN 1 ELSE 0 END), 0) AS aug,
44
COALESCE(SUM(je.amount * CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 9 THEN 1 ELSE 0 END), 0) AS sep,
45
COALESCE(SUM(je.amount * CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 10 THEN 1 ELSE 0 END), 0) AS oct,
46
COALESCE(SUM(je.amount * CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 11 THEN 1 ELSE 0 END), 0) AS nov,
47
COALESCE(SUM(je.amount * CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 12 THEN 1 ELSE 0 END), 0) AS "dec",
48
COALESCE(SUM(je.amount * CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 1 THEN 1 ELSE 0 END), 0) AS jan,
49
COALESCE(SUM(je.amount * CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 2 THEN 1 ELSE 0 END), 0) AS feb,
50
COALESCE(SUM(je.amount * CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 3 THEN 1 ELSE 0 END), 0) AS mar,
51
COALESCE(SUM(je.amount), 0) AS total_sum
52
FROM account_with_path awp
53
CROSS JOIN financial_years fy
54
LEFT JOIN journal_entries je ON je.account_id = awp.id AND je.transaction_date BETWEEN fy.start_date AND fy.end_date
55
LEFT JOIN transaction_headers th ON th.id = je.transaction_id AND th.company_id = p_company_id
56
WHERE (th.transaction_source_type IS NULL OR th.transaction_source_type != 13)
57
GROUP BY awp.id, awp.name, awp.parent_account_id, awp.path_str, fy.fin_year_id
58
)
59
SELECT
60
a1.id, a1.name, a1.parent_account_id, a1.hierarchy_path, a1.fin_year_id,
61
a1.apr, a1.may, a1.jun, a1.jul, a1.aug, a1.sep, a1.oct, a1.nov, a1."dec",
62
a1.jan, a1.feb, a1.mar,
63
a1.total_sum,
64
(COALESCE(a1.total_sum, 0) - COALESCE(a2.total_sum, 0)) AS difference
65
FROM aggregated_data a1
66
LEFT JOIN aggregated_data a2 ON a1.id = a2.id AND a1.fin_year_id = v_current_year AND a2.fin_year_id = v_previous_year
67
ORDER BY a1.hierarchy_path, a1.fin_year_id;
68
END;
69
$function$
|
|||||
| Function | get_income_expense_monthly_hierarchy | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_income_expense_monthly_hierarchy(p_company_id uuid, p_finance_year_id integer)
2
RETURNS jsonb
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_organization_id UUID;
7
v_start_date DATE;
8
v_end_date DATE;
9
v_prev_start_date DATE;
10
v_prev_end_date DATE;
11
result JSONB;
12
BEGIN
13
-- Get the organization_id for the given company_id
14
SELECT c.organization_id INTO v_organization_id
15
FROM public.companies c
16
WHERE c.id = p_company_id;
17
18
-- Get the start and end dates for the selected financial year
19
SELECT fy.start_date, fy.end_date INTO v_start_date, v_end_date
20
FROM public.finance_year fy
21
WHERE fy.id = p_finance_year_id;
22
23
-- Get the start and end dates for the previous financial year
24
SELECT pfy.start_date, pfy.end_date INTO v_prev_start_date, v_prev_end_date
25
FROM public.finance_year pfy
26
WHERE pfy.id = (p_finance_year_id - 1);
27
28
-- Recursive CTE to build parent-child hierarchy
29
WITH RECURSIVE account_hierarchy AS (
30
-- Base case: Fetch top-level accounts
31
SELECT
32
coa.id,
33
coa.name AS account_name,
34
coa.parent_account_id,
35
0 AS level,
36
at.name AS account_type
37
FROM chart_of_accounts coa
38
JOIN account_types at ON coa.account_type_id = at.id
39
WHERE coa.organization_id = v_organization_id
40
AND at.name IN ('Revenue', 'Expenses')
41
AND (coa.parent_account_id IS NULL OR coa.parent_account_id = '00000000-0000-0000-0000-000000000000')
42
AND coa.is_deleted = FALSE
43
44
UNION ALL
45
46
-- Recursive case: Fetch children accounts
47
SELECT
48
coa.id,
49
coa.name AS account_name,
50
coa.parent_account_id,
51
parent.level + 1 AS level,
52
parent.account_type
53
FROM chart_of_accounts coa
54
JOIN account_hierarchy parent ON parent.id = coa.parent_account_id
55
WHERE coa.organization_id = v_organization_id
56
AND coa.is_deleted = FALSE
57
),
58
monthly_totals AS (
59
-- Get monthly totals
60
SELECT
61
je.account_id AS id,
62
SUM(je.amount) AS total
63
FROM journal_entries je
64
JOIN transaction_headers th ON je.transaction_id = th.id
65
WHERE th.company_id = p_company_id
66
AND je.transaction_date BETWEEN v_prev_start_date AND v_end_date
67
GROUP BY je.account_id
68
),
69
pivoted_data AS (
70
-- Join accounts with totals
71
SELECT
72
ah.id,
73
ah.account_type,
74
ah.account_name,
75
ah.parent_account_id,
76
ah.level,
77
COALESCE(mt.total, 0) AS total
78
FROM account_hierarchy ah
79
LEFT JOIN monthly_totals mt ON ah.id = mt.id
80
),
81
full_hierarchy AS (
82
-- Ensure all accounts are present in JSON format
83
SELECT
84
p.id,
85
p.account_name,
86
p.account_type,
87
p.level,
88
p.total,
89
jsonb_agg(
90
jsonb_build_object(
91
'id', c.id,
92
'account_name', c.account_name,
93
'account_type', c.account_type,
94
'level', c.level,
95
'total', c.total,
96
'children', '[]'::jsonb -- Placeholder for child accounts
97
)
98
) FILTER (WHERE c.id IS NOT NULL) AS children
99
FROM pivoted_data p
100
LEFT JOIN pivoted_data c ON p.id = c.parent_account_id
101
GROUP BY p.id, p.account_name, p.account_type, p.level, p.total
102
)
103
-- Aggregate only the **top-level accounts** into JSON
104
SELECT jsonb_agg(full_hierarchy.*) INTO result FROM full_hierarchy WHERE full_hierarchy.level = 0;
105
106
RETURN result;
107
END;
108
$function$
|
|||||
| Function | get_income_expense_ytd_all_years | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_income_expense_ytd_all_years(p_company_id uuid, p_years_back integer)
2
RETURNS TABLE(period text, year numeric, total_income numeric, total_expense numeric, actual_income numeric, actual_expense numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
WITH target_years AS (
8
SELECT id, start_date, end_date,
9
EXTRACT(YEAR FROM start_date) AS year_start,
10
EXTRACT(YEAR FROM end_date) AS year_end
11
FROM finance_year
12
WHERE start_date <= CURRENT_DATE
13
ORDER BY start_date DESC
14
LIMIT p_years_back
15
),
16
ytd_periods AS (
17
SELECT
18
fy.id AS finance_id,
19
'YTD H1' AS period,
20
make_date(fy.year_start::int, 9, 30) AS period_end,
21
fy.year_start AS year,
22
fy.start_date AS year_start,
23
fy.end_date AS year_end
24
FROM target_years fy
25
26
UNION ALL
27
28
SELECT
29
fy.id,
30
'YTD H2',
31
fy.end_date,
32
fy.year_end,
33
fy.start_date,
34
fy.end_date
35
FROM target_years fy
36
),
37
filtered_je AS (
38
SELECT
39
je.id,
40
je.account_id,
41
je.transaction_id,
42
je.entry_type,
43
je.amount,
44
je.is_deleted,
45
tr.company_id,
46
tr.transaction_date,
47
coa.account_type_id
48
FROM journal_entries je
49
JOIN transaction_headers tr ON je.transaction_id = tr.id
50
JOIN chart_of_accounts coa ON je.account_id = coa.id
51
WHERE je.is_deleted = FALSE
52
)
53
SELECT
54
CONCAT(yp.period, ' ', yp.year) AS period,
55
yp.year,
56
COALESCE(SUM(CASE WHEN fje.entry_type = 'C' AND fje.account_type_id IN (4,14,15,19,20)
57
AND fje.transaction_date BETWEEN yp.year_start AND yp.period_end THEN fje.amount ELSE 0 END), 0),
58
COALESCE(SUM(CASE WHEN fje.entry_type = 'D' AND fje.account_type_id IN (5,16,17,18,21,22)
59
AND fje.transaction_date BETWEEN yp.year_start AND yp.period_end THEN fje.amount ELSE 0 END), 0),
60
COALESCE(SUM(CASE WHEN fje.entry_type = 'D' AND fje.account_type_id IN (8,9)
61
AND fje.transaction_date BETWEEN yp.year_start AND yp.period_end THEN fje.amount ELSE 0 END), 0),
62
COALESCE(SUM(CASE WHEN fje.entry_type = 'C' AND fje.account_type_id IN (8,9)
63
AND fje.transaction_date BETWEEN yp.year_start AND yp.period_end THEN fje.amount ELSE 0 END), 0)
64
FROM ytd_periods yp
65
LEFT JOIN filtered_je fje ON fje.company_id = p_company_id
66
GROUP BY yp.period, yp.year, yp.period_end
67
ORDER BY yp.year, yp.period;
68
END;
69
$function$
|
|||||
| Function | get_total_income | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_total_income(p_company_id uuid, p_finance_id integer)
2
RETURNS numeric
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_financial_year_start DATE;
7
v_financial_year_end DATE;
8
v_total_income NUMERIC := 0;
9
v_organization_id UUID;
10
BEGIN
11
-- Step 1: Get the organization ID for the given company
12
SELECT organization_id
13
INTO v_organization_id
14
FROM public.companies
15
WHERE id = p_company_id;
16
17
-- Step 2: Validate if the organization ID exists
18
IF v_organization_id IS NULL THEN
19
RAISE EXCEPTION 'Organization ID not found for company ID %', p_company_id;
20
END IF;
21
22
-- Step 3: Get the financial year start and end dates
23
SELECT fy.start_date, fy.end_date
24
INTO v_financial_year_start, v_financial_year_end
25
FROM public.finance_year fy
26
WHERE fy.id = p_finance_id;
27
28
-- Step 4: Validate if the financial year exists
29
IF v_financial_year_start IS NULL OR v_financial_year_end IS NULL THEN
30
RAISE EXCEPTION 'Financial year with ID % not found', p_finance_id;
31
END IF;
32
33
-- Step 5: Calculate the total income
34
SELECT COALESCE(SUM(je.amount), 0) INTO v_total_income
35
FROM public.journal_entries je
36
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
37
INNER JOIN public.chart_of_accounts coa ON je.account_id = coa.id
38
WHERE th.company_id = p_company_id
39
AND coa.organization_id = v_organization_id -- Match organization ID
40
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
41
AND je.is_deleted = FALSE
42
AND EXISTS (
43
SELECT 1
44
FROM public.account_types at
45
WHERE at.id = coa.account_type_id
46
AND at.account_category_id = 4 -- Income/Revenue category
47
);
48
49
-- Step 6: Return the total income
50
RETURN v_total_income;
51
END;
52
$function$
|
|||||
| Function | get_top_expense_categories | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_top_expense_categories(p_company_id uuid, p_finance_year_id integer)
2
RETURNS TABLE(account_id uuid, account_name text, account_number text, total_expense numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_financial_year_start DATE;
7
v_financial_year_end DATE;
8
BEGIN
9
-- Fetch financial year start and end dates
10
SELECT start_date, end_date
11
INTO v_financial_year_start, v_financial_year_end
12
FROM public.finance_year
13
WHERE id = p_finance_year_id;
14
15
-- Raise an exception if financial year is not found
16
IF v_financial_year_start IS NULL OR v_financial_year_end IS NULL THEN
17
RAISE EXCEPTION 'Financial year with ID % not found', p_finance_year_id;
18
END IF;
19
20
-- Calculate and return top 10 Expense accounts using `get_account_type_hierarchy(5)`
21
RETURN QUERY
22
SELECT
23
coa.id As account_id,
24
coa.name AS account_name,
25
coa.account_number AS account_number,
26
ROUND(COALESCE(SUM(je.amount), 0), 2) AS total_expense
27
FROM public.journal_entries je
28
INNER JOIN public.chart_of_accounts coa ON je.account_id = coa.id
29
INNER JOIN public.account_types at ON coa.account_type_id = at.id
30
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
31
WHERE at.id IN (SELECT id FROM get_account_type_hierarchy(5)) -- Dynamically fetch all expense types
32
AND th.company_id = p_company_id
33
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
34
AND je.is_deleted = FALSE
35
GROUP BY coa.id, coa.name, coa.account_number
36
ORDER BY total_expense DESC
37
LIMIT 10;
38
39
END;
40
$function$
|
|||||
| Function | get_ledger_report | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_ledger_report(p_company_id uuid, p_fin_year_id integer, p_start_date date DEFAULT NULL::date, p_end_date date DEFAULT NULL::date)
2
RETURNS TABLE(sl_no integer, ledger text, general_ledger text, opening_balance numeric, total_debit numeric, total_credit numeric, closing_balance numeric, parent_ledger text)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_start_date DATE;
7
v_end_date DATE;
8
BEGIN
9
-- Get financial year start and end date if not provided
10
SELECT fy.start_date, fy.end_date
11
INTO v_start_date, v_end_date
12
FROM public.finance_year fy
13
WHERE fy.id = p_fin_year_id
14
LIMIT 1;
15
16
-- Override with provided values if they are not NULL
17
v_start_date := COALESCE(p_start_date, v_start_date);
18
v_end_date := COALESCE(p_end_date, v_end_date);
19
20
RETURN QUERY
21
WITH trial_balance AS (
22
SELECT
23
account_type AS account_category,
24
account_number,
25
account_name,
26
debit,
27
credit
28
FROM public.get_trial_balance_by_date_range(
29
p_company_id, p_fin_year_id, v_start_date, v_end_date
30
)
31
),
32
opening_balances AS (
33
SELECT
34
je.account_id::TEXT AS account_id,
35
SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE -je.amount END) AS opening_balance
36
FROM public.journal_entries je
37
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
38
WHERE th.company_id = p_company_id
39
AND th.transaction_date BETWEEN v_start_date AND v_end_date
40
GROUP BY je.account_id
41
)
42
43
-- Main Trial Balance Rows
44
SELECT
45
CAST(ROW_NUMBER() OVER() AS INTEGER) AS sl_no,
46
tb.account_name AS ledger,
47
tb.account_category AS general_ledger,
48
COALESCE(ob.opening_balance, 0) AS opening_balance,
49
COALESCE(tb.debit, 0) AS total_debit,
50
COALESCE(tb.credit, 0) AS total_credit,
51
COALESCE(ob.opening_balance, 0) + COALESCE(tb.debit, 0) - COALESCE(tb.credit, 0) AS closing_balance,
52
CASE
53
WHEN tb.account_category IN ('Assets', 'Current Assets', 'Non-Current Assets', 'Cash', 'Bank') THEN 'Asset'
54
WHEN tb.account_category IN ('Liabilities', 'Current Liabilities', 'Non-Current Liabilities') THEN 'Liability'
55
WHEN tb.account_category IN ('Revenue', 'Sales Revenue', 'Service Revenue', 'Non Member Income', 'Member Income') THEN 'Income'
56
WHEN tb.account_category IN ('Expenses', 'Direct Expenses', 'Indirect Expenses', 'Operating Expenses', 'Cost of Goods Sold', 'Non-Operating Expense') THEN 'Expense'
57
ELSE NULL
58
END AS parent_ledger
59
FROM trial_balance tb
60
LEFT JOIN opening_balances ob ON tb.account_number::TEXT = ob.account_id
61
62
UNION ALL
63
64
-- Sundry Debtors Parent Row
65
SELECT 9999, 'Sundry Debtors', 'Assets', 0, 0, 0, 0, 'Asset'
66
67
UNION ALL
68
69
-- Sundry Debtors Children
70
SELECT
71
sd.sl_no + 10000,
72
sd.ledger,
73
sd.general_ledger,
74
sd.opening_balance,
75
sd.debit AS total_debit,
76
sd.credit AS total_credit,
77
sd.closing_balance,
78
'Asset'
79
FROM public.get_sundry_debtors_ledger(p_company_id, p_fin_year_id, v_start_date, v_end_date) sd
80
81
UNION ALL
82
83
-- Sundry Creditors Parent Row
84
SELECT 9998, 'Sundry Creditors', 'Liabilities', 0, 0, 0, 0, 'Liability'
85
86
UNION ALL
87
88
-- Sundry Creditors Children
89
SELECT
90
sc.sl_no + 20000,
91
sc.ledger,
92
sc.general_ledger,
93
sc.opening_balance,
94
sc.debit AS total_debit,
95
sc.credit AS total_credit,
96
sc.closing_balance,
97
'Liability'
98
FROM public.get_sundry_creditors_ledger(p_company_id, p_fin_year_id, v_start_date, v_end_date) sc
99
100
ORDER BY parent_ledger NULLS FIRST, ledger;
101
END;
102
$function$
|
|||||
| Function | get_maintenance_collection_status | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_maintenance_collection_status(p_company_id uuid, p_start_date date, p_end_date date)
2
RETURNS TABLE(year numeric, total_income numeric, total_expense numeric)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
EXTRACT(YEAR FROM je.transaction_date) AS year,
9
SUM(CASE
10
WHEN ac.id = 4 THEN je.amount -- 4 corresponds to 'Revenue'
11
ELSE 0
12
END) AS total_income,
13
SUM(CASE
14
WHEN ac.id = 5 THEN je.amount -- 5 corresponds to 'Expenses'
15
ELSE 0
16
END) AS total_expense
17
FROM
18
journal_entries je
19
JOIN
20
chart_of_accounts coa ON je.account_id = coa.id
21
JOIN
22
account_types act ON coa.account_type_id = act.id
23
JOIN
24
account_categories ac ON act.account_category_id = ac.id
25
WHERE
26
je.transaction_date BETWEEN p_start_date AND p_end_date
27
And je.company_id = p_company_id
28
GROUP BY
29
year
30
ORDER BY
31
year ;
32
END;
33
$function$
|
|||||
| Function | get_n_years_expenses | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_n_years_expenses(p_company_id uuid, p_fin_year_ids integer[])
2
RETURNS TABLE(account_id uuid, account_name text, finance_year_id integer, finance_year_label text, month_number integer, month_label text, amount numeric)
3
LANGUAGE plpgsql
4
STABLE PARALLEL SAFE
5
AS $function$
6
DECLARE
7
months_arr TEXT[] := ARRAY['Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec','Jan','Feb','Mar'];
8
yrec RECORD;
9
m INT;
10
month_start DATE;
11
month_end DATE;
12
BEGIN
13
FOR yrec IN
14
SELECT
15
fy.id,
16
fy.start_date,
17
fy.end_date,
18
CONCAT(EXTRACT(YEAR FROM fy.start_date), '-', RIGHT(TO_CHAR(fy.end_date, 'YYYY'), 2)) AS label
19
FROM public.finance_year fy
20
WHERE fy.id = ANY(p_fin_year_ids)
21
ORDER BY fy.start_date
22
LOOP
23
FOR m IN 1..12 LOOP
24
month_start := yrec.start_date + ((m - 1) * INTERVAL '1 month');
25
month_end := (month_start + INTERVAL '1 month' - INTERVAL '1 day')::DATE;
26
27
RETURN QUERY
28
WITH RECURSIVE AccountHierarchy AS (
29
SELECT coa.id, coa.name, coa.parent_account_id
30
FROM public.chart_of_accounts coa
31
WHERE coa.account_type_id = 5
32
UNION ALL
33
SELECT coa.id, coa.name, coa.parent_account_id
34
FROM public.chart_of_accounts coa
35
INNER JOIN AccountHierarchy ah ON coa.parent_account_id = ah.id
36
)
37
SELECT
38
ah.id AS account_id,
39
ah.name AS account_name,
40
yrec.id AS finance_year_id,
41
yrec.label AS finance_year_label,
42
m AS month_number,
43
months_arr[m] AS month_label,
44
COALESCE(SUM(je.amount), 0) AS amount
45
FROM AccountHierarchy ah
46
LEFT JOIN public.journal_entries je
47
ON je.account_id = ah.id
48
AND je.transaction_date BETWEEN month_start AND month_end
49
AND je.is_deleted = FALSE
50
LEFT JOIN public.transaction_headers th
51
ON je.transaction_id = th.id
52
AND th.company_id = p_company_id
53
GROUP BY ah.id, ah.name;
54
END LOOP;
55
END LOOP;
56
END;
57
$function$
|
|||||
| Function | get_user_notifications | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_user_notifications(p_user_id uuid)
2
RETURNS TABLE(id integer, message text, short_desc text, route text, created_time timestamp with time zone, additional_parameter_id text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
nt.id AS iid,
9
nt.message,
10
nt.short_description AS short_desc,
11
nt.route,
12
m.created_time::timestamp with time zone,
13
m.additional_parameter_id
14
FROM
15
public.notification_types nt
16
LEFT JOIN
17
public.messages m ON m.notification_type_id = nt.id
18
WHERE
19
nt.user_id = p_user_id
20
AND nt.created_on_utc >= CURRENT_TIMESTAMP - INTERVAL '24 hours'
21
AND nt.is_deleted = false
22
AND m.is_deleted = false
23
ORDER BY
24
m.created_time DESC;
25
END;
26
$function$
|
|||||
| Function | get_new_transfer_number | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_new_transfer_number(p_company_id uuid, p_date date)
2
RETURNS character varying
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_finance_year_id integer;
7
v_transfer_prefix text;
8
9
v_last_transfer_id integer;
10
v_new_transfer_number character varying;
11
BEGIN
12
-- Determine financial year for the given date
13
SELECT id INTO v_finance_year_id
14
FROM finance_year
15
WHERE start_date <= p_date AND end_date >= p_date;
16
17
-- Try update; if no row, insert a new counter row
18
WITH updated AS (
19
UPDATE public.bank_transfer_ids
20
21
SET last_transfer_id = COALESCE(last_transfer_id, 0) + 1
22
WHERE company_id = p_company_id AND fin_year = v_finance_year_id
23
RETURNING last_transfer_id, transfer_prefix
24
),
25
inserted AS (
26
INSERT INTO public.bank_transfer_ids (
27
company_id, fin_year, transfer_prefix, last_transfer_id
28
)
29
SELECT p_company_id, v_finance_year_id, 'BT', 1
30
WHERE NOT EXISTS (SELECT 1 FROM updated)
31
RETURNING last_transfer_id, transfer_prefix
32
)
33
SELECT
34
COALESCE((SELECT last_transfer_id FROM updated LIMIT 1), (SELECT last_transfer_id FROM inserted LIMIT 1)),
35
COALESCE((SELECT transfer_prefix FROM updated LIMIT 1), (SELECT transfer_prefix FROM inserted LIMIT 1))
36
37
INTO v_last_transfer_id, v_transfer_prefix;
38
39
-- Build the transfer number
40
v_new_transfer_number := v_transfer_prefix || LPAD(v_last_transfer_id::text, 5,'0');
41
42
RETURN v_new_transfer_number;
43
END;
44
$function$
|
|||||
| Function | get_user_by_email | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_user_by_email(p_email text)
2
RETURNS TABLE(id uuid, user_id uuid, third_party_id text, first_name character varying, last_name character varying, email character varying, phone_number character varying, company_id uuid, company_name text, company_description text, company_gstin text, company_is_apartment boolean, company_org_id uuid, organization_id uuid, organization_name text, organization_gstin text, organization_pan text, organization_tan text, organization_short_name text, organization_type_id integer, roles text[], role_name text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
u.id as user_id,
9
u.id as user_id,
10
u.third_party_id,
11
u.first_name,
12
u.last_name,
13
u.email,
14
u.phone_number,
15
resolved_company.id,
16
resolved_company.name,
17
resolved_company.description,
18
resolved_company.gstin,
19
resolved_company.is_apartment,
20
resolved_company.organization_id,
21
o.id,
22
o.name,
23
o.gstin,
24
o.pan,
25
o.tan,
26
o.short_name,
27
o.organization_type_id,
28
ARRAY(
29
SELECT DISTINCT r2.name
30
FROM user_roles ur2
31
JOIN roles r2 ON r2.id = ur2.role_id
32
WHERE ur2.user_id = u.id AND r2.name IS NOT NULL
33
),
34
35
MIN(r.name) -- Primary role
36
FROM users u
37
LEFT JOIN organization_users ou ON ou.user_id = u.id
38
39
-- Resolve company using COALESCE fallback
40
LEFT JOIN LATERAL (
41
SELECT *
42
FROM companies c
43
WHERE c.id = COALESCE(
44
NULLIF(u.company_id, '00000000-0000-0000-0000-000000000000'),
45
(
46
SELECT MIN(c2.id::text)::uuid
47
FROM companies c2
48
JOIN organization_users ou2 ON c2.organization_id = ou2.organization_id
49
WHERE ou2.user_id = u.id
50
)
51
)
52
LIMIT 1
53
) AS resolved_company ON TRUE
54
55
LEFT JOIN organizations o ON resolved_company.organization_id = o.id
56
LEFT JOIN user_roles ur ON ur.user_id = u.id
57
LEFT JOIN roles r ON r.id = ur.role_id
58
WHERE u.email = p_email
59
AND u.is_deleted = false
60
GROUP BY
61
u.id, u.third_party_id, u.first_name, u.last_name, u.email, u.phone_number,
62
resolved_company.id, resolved_company.name, resolved_company.description, resolved_company.gstin,
63
resolved_company.is_apartment, resolved_company.organization_id,
64
o.id, o.name, o.gstin, o.pan, o.tan, o.short_name, o.organization_type_id;
65
END;
66
$function$
|
|||||
| Function | get_user_by_username | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_user_by_username(p_username text)
2
RETURNS TABLE(id uuid, user_id uuid, third_party_id text, first_name character varying, last_name character varying, user_name character varying, email character varying, phone_number character varying, company_id uuid, company_name character varying, company_description character varying, company_gstin text, company_is_apartment boolean, company_org_id uuid, organization_id uuid, organization_name character varying, organization_gstin text, organization_pan text, organization_tan text, organization_short_name text, organization_type_id integer, roles text[], role_name text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
u.id as user_id, -- Ensure this is referring to the 'users' table alias 'u'
9
u.id as user_id,
10
u.third_party_id,
11
u.first_name::character varying, -- Cast first_name to character varying
12
u.last_name::character varying, -- Cast last_name to character varying
13
u.user_name::character varying, -- Cast user_name to character varying
14
u.email::character varying, -- Include email as character varying
15
u.phone_number::character varying, -- Cast phone_number to character varying
16
resolved_company.id,
17
resolved_company.name::character varying, -- Cast company name to character varying
18
resolved_company.description::character varying, -- Cast company description to character varying
19
resolved_company.gstin,
20
resolved_company.is_apartment,
21
resolved_company.organization_id,
22
o.id,
23
o.name::character varying, -- Cast organization name to character varying
24
o.gstin,
25
o.pan,
26
o.tan,
27
o.short_name,
28
o.organization_type_id,
29
ARRAY(
30
SELECT DISTINCT r2.name
31
FROM user_roles ur2
32
JOIN roles r2 ON r2.id = ur2.role_id
33
WHERE ur2.user_id = u.id AND r2.name IS NOT NULL
34
),
35
-- Use COALESCE to handle NULL role_name and return a default value if NULL
36
COALESCE(MIN(r.name), 'No Role Assigned') AS role_name
37
FROM users u -- Ensure 'u' is the correct alias for the 'users' table
38
LEFT JOIN organization_users ou ON ou.user_id = u.id
39
40
-- Resolve company using COALESCE fallback
41
LEFT JOIN LATERAL (
42
SELECT *
43
FROM companies c
44
WHERE c.id = COALESCE(
45
NULLIF(u.company_id, '00000000-0000-0000-0000-000000000000'),
46
(
47
SELECT MIN(c2.id::text)::uuid
48
FROM companies c2
49
JOIN organization_users ou2 ON c2.organization_id = ou2.organization_id
50
WHERE ou2.user_id = u.id
51
)
52
)
53
LIMIT 1
54
) AS resolved_company ON TRUE
55
56
LEFT JOIN organizations o ON resolved_company.organization_id = o.id
57
LEFT JOIN user_roles ur ON ur.user_id = u.id
58
LEFT JOIN roles r ON r.id = ur.role_id
59
WHERE u.user_name = p_username -- Filter by user_name instead of email
60
AND u.is_deleted = false
61
GROUP BY
62
u.id, u.third_party_id, u.first_name, u.last_name, u.user_name, u.email, u.phone_number,
63
resolved_company.id, resolved_company.name, resolved_company.description, resolved_company.gstin,
64
resolved_company.is_apartment, resolved_company.organization_id,
65
o.id, o.name, o.gstin, o.pan, o.tan, o.short_name, o.organization_type_id;
66
END;
67
$function$
|
|||||
| Function | get_vendor_dues | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_vendor_dues(p_company_id uuid)
2
RETURNS TABLE(vendor_name text, coa_name text, amount numeric, due_date date, aging_bucket text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
v.name::text AS vendor_name,
9
coa.name AS coa_name,
10
je.amount,
11
(th.transaction_date + INTERVAL '30 days')::date AS due_date, -- Assuming a 30-day payment term
12
CASE
13
WHEN (th.transaction_date + INTERVAL '30 days') <= CURRENT_DATE THEN 'Current'
14
WHEN (th.transaction_date + INTERVAL '30 days') BETWEEN CURRENT_DATE - INTERVAL '30 days' AND CURRENT_DATE THEN '1-30 Days Past Due'
15
WHEN (th.transaction_date + INTERVAL '60 days') BETWEEN CURRENT_DATE - INTERVAL '60 days' AND CURRENT_DATE - INTERVAL '31 days' THEN '31-60 Days Past Due'
16
ELSE 'Over 60 Days Past Due'
17
END AS aging_bucket
18
FROM
19
public.journal_entries je
20
JOIN
21
public.transaction_headers th ON je.transaction_id = th.id
22
JOIN
23
public.chart_of_accounts coa ON je.account_id = coa.id
24
JOIN
25
public.vendors v ON th.vendor_id = v.id
26
WHERE
27
coa.account_type_id = (SELECT id FROM public.account_types WHERE name = 'Accounts Payable')
28
AND je.is_deleted = false
29
AND (th.transaction_date + INTERVAL '30 days') <= CURRENT_DATE
30
AND th.company_id = p_company_id
31
ORDER BY
32
v.name, (th.transaction_date + INTERVAL '30 days')::date;
33
END;
34
$function$
|
|||||
| Function | get_vendor_ledger | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_vendor_ledger(p_vendor_id uuid, p_company_id uuid, p_organization_id uuid, p_fin_year_id integer, p_start_date date DEFAULT NULL::date, p_end_date date DEFAULT NULL::date)
2
RETURNS TABLE(transaction_date date, account_name text, debit numeric, credit numeric, balance numeric, transaction_source_type integer, document_number text, document_id uuid, sort_order integer)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_opening_balance NUMERIC;
7
v_start_date DATE;
8
v_end_date DATE;
9
BEGIN
10
SELECT start_date, end_date
11
INTO v_start_date, v_end_date
12
FROM public.get_financial_year_dates(p_fin_year_id);
13
14
v_start_date := COALESCE(p_start_date, v_start_date);
15
v_end_date := COALESCE(p_end_date, v_end_date);
16
17
SELECT COALESCE(
18
SUM(
19
CASE
20
WHEN ob.entry_type = 'C' THEN ob.balance -- Debit increases balance
21
ELSE -ob.balance -- Credit reduces balance
22
END
23
), 0)
24
INTO v_opening_balance
25
FROM public.opening_balances ob
26
WHERE ob.organization_id = p_organization_id
27
AND ob.finyear_id = p_fin_year_id
28
AND ob.vendor_id = p_vendor_id
29
AND ob.is_deleted = FALSE;
30
31
RETURN QUERY
32
WITH vendor_transactions AS (
33
SELECT
34
th.transaction_date::date AS transaction_date,
35
coa.name AS account_name,
36
CASE
37
WHEN je.entry_type = 'D' THEN je.amount
38
ELSE 0
39
END AS debit,
40
CASE
41
WHEN je.entry_type = 'C' THEN je.amount
42
ELSE 0
43
END AS credit,
44
th.company_id,
45
th.vendor_id,
46
th.transaction_source_type,
47
th.document_number,
48
th.document_id,
49
2 AS sort_order
50
FROM
51
public.transaction_headers th
52
INNER JOIN
53
public.journal_entries je ON th.id = je.transaction_id
54
INNER JOIN
55
public.chart_of_accounts coa ON je.account_id = coa.id
56
WHERE
57
th.vendor_id = p_vendor_id
58
AND th.company_id = p_company_id
59
AND coa.organization_id = p_organization_id
60
AND th.is_deleted = false
61
AND je.is_deleted = false
62
AND th.transaction_date >= p_start_date
63
AND th.transaction_date <= p_end_date
64
)
65
SELECT
66
v_start_date AS transaction_date,
67
'Opening Balance' AS account_name,
68
0 AS debit,
69
0 AS credit,
70
v_opening_balance AS balance,
71
NULL AS document_number,
72
NULL AS document_id,
73
NULL AS transaction_source_type,
74
1 AS sort_order
75
76
UNION ALL
77
78
SELECT
79
vt.transaction_date,
80
vt.account_name,
81
vt.debit,
82
vt.credit,
83
v_opening_balance +
84
SUM(vt.debit - vt.credit) OVER (PARTITION BY vt.vendor_id ORDER BY vt.transaction_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS balance,
85
vt.transaction_source_type,
86
vt.document_number,
87
vt.document_id,
88
vt.sort_order
89
FROM
90
vendor_transactions vt
91
ORDER BY
92
vt.transaction_date,
93
vt.sort_order ASC;
94
END;
95
$function$
|
|||||
| Function | get_vendor_outstanding | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_vendor_outstanding()
2
RETURNS TABLE(vendor_name text, account_name text, amount numeric, due_date date, aging_bucket text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
v.name AS vendor_name,
9
coa.name AS account_name,
10
je.amount,
11
th.transaction_date + INTERVAL '30 days' AS due_date, -- Assuming a 30-day payment term
12
CASE
13
WHEN th.transaction_date + INTERVAL '30 days' <= CURRENT_DATE THEN 'Current'
14
WHEN th.transaction_date + INTERVAL '30 days' BETWEEN CURRENT_DATE - INTERVAL '30 days' AND CURRENT_DATE THEN '1-30 Days Past Due'
15
WHEN th.transaction_date + INTERVAL '30 days' BETWEEN CURRENT_DATE - INTERVAL '60 days' AND CURRENT_DATE - INTERVAL '31 days' THEN '31-60 Days Past Due'
16
ELSE 'Over 60 Days Past Due'
17
END AS aging_bucket
18
FROM
19
public.journal_entries je
20
JOIN
21
public.transaction_headers th ON je.transaction_id = th.id
22
JOIN
23
public.chart_of_accounts coa ON je.account_id = coa.id
24
JOIN
25
public.vendors v ON th.vendor_id = v.id
26
WHERE
27
coa.account_type_id = (SELECT id FROM account_types WHERE name = 'Accounts Payable')
28
AND je.is_deleted = false
29
ORDER BY
30
v.name, th.transaction_date;
31
END;
32
$function$
|
|||||
| Function | get_year_wise_opening_balance | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_year_wise_opening_balance(p_finyear_id integer)
2
RETURNS TABLE(id bigint, transaction_date timestamp without time zone, entity_id uuid, entity_name text, description text, debit numeric, credit numeric, entity_type text)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_year_text text;
7
BEGIN
8
-- Fetch the year text for the given finance year id
9
SELECT fy.year INTO v_year_text FROM public.finance_year fy WHERE fy.id = p_finyear_id;
10
IF v_year_text IS NULL THEN
11
RAISE EXCEPTION 'Finance year with id % not found!', p_finyear_id;
12
END IF;
13
14
RETURN QUERY
15
16
-- VENDORS
17
SELECT
18
th.id,
19
th.transaction_date,
20
th.vendor_id as entity_id,
21
v.name::text as entity_name,
22
th.description,
23
COALESCE(CASE WHEN je.entry_type = 'D' THEN je.amount END, 0) as debit,
24
COALESCE(CASE WHEN je.entry_type = 'C' THEN je.amount END, 0) as credit,
25
'Vendor' as entity_type
26
FROM public.transaction_headers th
27
JOIN public.vendors v ON th.vendor_id = v.id
28
JOIN public.journal_entries je ON th.id = je.transaction_id
29
JOIN public.chart_of_accounts sc ON je.account_id = sc.id AND sc.name ILIKE '%Sundry Creditors%'
30
WHERE th.transaction_source_type = 13
31
AND th.description LIKE 'Opening Balance for %' || v_year_text || '%'
32
33
UNION ALL
34
35
-- CUSTOMERS
36
SELECT
37
th.id,
38
th.transaction_date,
39
th.customer_id as entity_id,
40
c.name::text as entity_name,
41
th.description,
42
COALESCE(CASE WHEN je.entry_type = 'D' THEN je.amount END, 0) as debit,
43
COALESCE(CASE WHEN je.entry_type = 'C' THEN je.amount END, 0) as credit,
44
'Customer' as entity_type
45
FROM public.transaction_headers th
46
JOIN public.customers c ON th.customer_id = c.id
47
JOIN public.journal_entries je ON th.id = je.transaction_id
48
JOIN public.chart_of_accounts sd ON je.account_id = sd.id AND sd.name ILIKE '%Sundry Debtors%'
49
WHERE th.transaction_source_type = 13
50
AND th.description LIKE 'Opening Balance for %' || v_year_text || '%'
51
52
UNION ALL
53
54
-- EMPLOYEES
55
SELECT
56
th.id,
57
th.transaction_date,
58
th.employee_id as entity_id,
59
e.first_name::text as entity_name,
60
th.description,
61
COALESCE(CASE WHEN je.entry_type = 'D' THEN je.amount END, 0) as debit,
62
COALESCE(CASE WHEN je.entry_type = 'C' THEN je.amount END, 0) as credit,
63
'Employee' as entity_type
64
FROM public.transaction_headers th
65
JOIN public.employees e ON th.employee_id = e.id
66
JOIN public.journal_entries je ON th.id = je.transaction_id
67
JOIN public.chart_of_accounts sp ON je.account_id = sp.id AND sp.name ILIKE '%Salary Payable%'
68
WHERE th.transaction_source_type = 13
69
AND th.description LIKE 'Opening Balance for %' || v_year_text || '%'
70
71
UNION ALL
72
73
-- COA ACCOUNTS (other than above)
74
SELECT
75
th.id,
76
th.transaction_date,
77
th.document_id as entity_id,
78
coa.name::text as entity_name,
79
th.description,
80
COALESCE(CASE WHEN je.entry_type = 'D' THEN je.amount END, 0) as debit,
81
COALESCE(CASE WHEN je.entry_type = 'C' THEN je.amount END, 0) as credit,
82
'COA' as entity_type
83
FROM public.transaction_headers th
84
JOIN public.journal_entries je ON th.id = je.transaction_id
85
JOIN public.chart_of_accounts coa ON th.document_id = coa.id AND je.account_id = th.document_id
86
WHERE th.transaction_source_type = 13
87
AND th.description LIKE 'Opening Balance for %' || v_year_text || '%'
88
89
ORDER BY entity_name;
90
91
END;
92
$function$
|
|||||
| Function | validate_organization_accounts | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.validate_organization_accounts()
2
RETURNS trigger
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
invalid_account_id UUID;
7
BEGIN
8
-- Check if any account ID does not belong to the specified organization
9
SELECT id INTO invalid_account_id
10
FROM chart_of_accounts
11
WHERE id IN (
12
NEW.accounts_receivable_account_id,
13
NEW.accounts_payable_account_id,
14
NEW.sales_revenue_account_id,
15
NEW.cgst_receivable_account_id,
16
NEW.sgst_receivable_account_id,
17
NEW.igst_receivable_account_id,
18
NEW.cgst_payable_account_id,
19
NEW.sgst_payable_account_id,
20
NEW.igst_payable_account_id,
21
NEW.round_off_gain_account_id,
22
NEW.round_off_loss_account_id,
23
NEW.sales_tax_payable_account_id,
24
NEW.purchase_tax_receivable_account_id,
25
NEW.discounts_given_account_id,
26
NEW.discounts_received_account_id,
27
NEW.interest_income_account_id,
28
NEW.interest_expense_account_id,
29
NEW.depreciation_expense_account_id,
30
NEW.bad_debt_expense_account_id,
31
NEW.bank_charges_account_id,
32
NEW.foreign_exchange_gain_loss_account_id,
33
NEW.cost_of_goods_sold_account_id,
34
NEW.inventory_account_id,
35
NEW.salary_expense_account_id,
36
NEW.salary_payable_account_id,
37
NEW.tds_receivable_account_id,
38
NEW.penalty_receivable_account_id,
39
NEW.tds_payable_account_id
40
) AND organization_id != NEW.organization_id
41
LIMIT 1;
42
43
-- If an invalid account ID is found, raise an exception
44
IF invalid_account_id IS NOT NULL THEN
45
RAISE EXCEPTION 'Account ID % does not belong to the specified organization %',
46
invalid_account_id, NEW.organization_id;
47
END IF;
48
49
RETURN NEW;
50
END;
51
$function$
|
|||||
| Function | insert_multiple_transactions_and_journal_entries | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.insert_multiple_transactions_and_journal_entries(p_transactions_data jsonb)
2
RETURNS TABLE(transaction_id bigint, document_id uuid, reference text)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_transaction_id BIGINT;
7
v_transaction_record JSONB;
8
v_journal_entry JSONB;
9
v_narration_id BIGINT;
10
v_reference TEXT;
11
BEGIN
12
-- Loop through each transaction in the input array
13
FOR v_transaction_record IN SELECT * FROM jsonb_array_elements(p_transactions_data)
14
LOOP
15
-- Insert into transaction_headers
16
INSERT INTO public.transaction_headers (
17
company_id,
18
customer_id,
19
vendor_id,
20
employee_id,
21
transaction_date,
22
transaction_source_type,
23
status_id,
24
document_id,
25
document_number,
26
description,
27
created_by,
28
created_on_utc
29
)
30
VALUES(
31
(v_transaction_record->>'company_id')::uuid,
32
NULLIF((v_transaction_record->>'customer_id')::uuid, '00000000-0000-0000-0000-000000000000'),
33
NULLIF((v_transaction_record->>'vendor_id')::uuid, '00000000-0000-0000-0000-000000000000'),
34
NULLIF((v_transaction_record->>'employee_id')::uuid, '00000000-0000-0000-0000-000000000000'),
35
(v_transaction_record->>'transaction_date')::date,
36
(v_transaction_record->>'transaction_source_type')::integer,
37
(v_transaction_record->>'status_id')::integer,
38
(v_transaction_record->>'document_id')::uuid,
39
v_transaction_record->>'document_number',
40
v_transaction_record->>'description',
41
(v_transaction_record->>'user_id')::uuid,
42
CURRENT_TIMESTAMP
43
)
44
RETURNING id INTO v_transaction_id;
45
46
-- ✅ Insert into payment_references if reference exists and not empty
47
v_reference := NULLIF(TRIM(v_transaction_record->>'reference'), '');
48
IF v_reference IS NOT NULL THEN
49
INSERT INTO public.payment_references (
50
transaction_id,
51
reference
52
)
53
VALUES (
54
v_transaction_id,
55
v_reference
56
);
57
END IF;
58
59
-- Loop through each journal entry associated with the transaction
60
FOR v_journal_entry IN SELECT * FROM jsonb_array_elements(v_transaction_record->'journal_entries')
61
LOOP
62
-- Insert narration first
63
INSERT INTO public.journal_narrations (
64
transaction_date,
65
description_template_id,
66
dynamic_data,
67
created_by,
68
created_on_utc
69
)
70
VALUES(
71
(v_journal_entry->>'transaction_date')::timestamp,
72
(v_journal_entry->>'description_template_id')::integer,
73
COALESCE((v_journal_entry->>'dynamic_data'), ''),
74
(v_transaction_record->>'user_id')::uuid,
75
CURRENT_TIMESTAMP
76
)
77
RETURNING id INTO v_narration_id;
78
79
-- Insert journal entry
80
INSERT INTO public.journal_entries (
81
transaction_id,
82
account_id,
83
transaction_date,
84
amount,
85
entry_type,
86
entry_source_id,
87
journal_narration_id
88
)
89
VALUES(
90
v_transaction_id,
91
(v_journal_entry->>'account_id')::uuid,
92
(v_journal_entry->>'transaction_date')::date,
93
(v_journal_entry->>'amount')::numeric,
94
(v_journal_entry->>'entry_type')::char,
95
(v_journal_entry->>'entry_source_id')::integer,
96
v_narration_id
97
);
98
END LOOP;
99
100
-- Return transaction ID, document ID, and reference for this transaction
101
RETURN QUERY SELECT v_transaction_id, (v_transaction_record->>'document_id')::uuid, v_reference;
102
END LOOP;
103
104
EXCEPTION
105
WHEN OTHERS THEN
106
RAISE EXCEPTION 'Error occurred: %', SQLERRM;
107
END;
108
$function$
|
|||||
| Function | assign_user_default_permissions | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.assign_user_default_permissions(p_user_id uuid, p_role_id integer, p_organization_id uuid, p_created_by uuid DEFAULT NULL::uuid)
2
RETURNS void
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_permission_count INT;
7
v_existing_organization_user INT;
8
v_existing_user_role INT;
9
BEGIN
10
RAISE NOTICE 'Assigning default permissions for User=% to Role=%', p_user_id, p_role_id;
11
12
-- 1️⃣ Ensure user exists in organization_users
13
SELECT COUNT(*) INTO v_existing_organization_user
14
FROM organization_users ou
15
WHERE ou.user_id = p_user_id
16
AND ou.organization_id = p_organization_id
17
AND ou.is_deleted = FALSE;
18
19
IF v_existing_organization_user = 0 THEN
20
INSERT INTO organization_users (
21
id, user_id, effective_start_date, created_on_utc,
22
created_by, organization_id, is_deleted
23
)
24
VALUES (
25
uuid_generate_v4(),
26
p_user_id,
27
NOW(),
28
NOW(),
29
COALESCE(p_created_by, p_user_id),
30
p_organization_id,
31
FALSE
32
);
33
RAISE NOTICE '✅ Added user to organization_users';
34
END IF;
35
36
-- 2️⃣ Ensure user has matching role in user_roles
37
SELECT COUNT(*) INTO v_existing_user_role
38
FROM user_roles ur
39
WHERE ur.user_id = p_user_id
40
AND ur.role_id = p_role_id
41
AND ur.organization_id = p_organization_id
42
AND ur.is_deleted = FALSE;
43
44
IF v_existing_user_role = 0 THEN
45
INSERT INTO user_roles (
46
user_id, role_id, created_on_utc, created_by,
47
is_deleted, start_date, organization_id
48
)
49
VALUES (
50
p_user_id,
51
p_role_id,
52
NOW(),
53
COALESCE(p_created_by, p_user_id),
54
FALSE,
55
NOW(),
56
p_organization_id
57
);
58
RAISE NOTICE '✅ Added role assignment for user';
59
END IF;
60
61
-- 3️⃣ Assign permissions from role_permissions (NOT template mappings)
62
INSERT INTO user_permissions (
63
user_id,
64
permission_id,
65
organization_id,
66
created_on_utc,
67
created_by,
68
is_deleted
69
)
70
SELECT
71
p_user_id,
72
rp.permission_id,
73
p_organization_id,
74
NOW(),
75
COALESCE(p_created_by, p_user_id),
76
FALSE
77
FROM role_permissions rp
78
WHERE rp.role_id = p_role_id
79
AND NOT EXISTS (
80
SELECT 1
81
FROM user_permissions up
82
WHERE up.user_id = p_user_id
83
AND up.permission_id = rp.permission_id
84
AND up.organization_id = p_organization_id
85
AND up.is_deleted = FALSE
86
);
87
88
GET DIAGNOSTICS v_permission_count = ROW_COUNT;
89
90
RAISE NOTICE '✅ Assigned % permissions to User=% for Role=%',
91
v_permission_count, p_user_id, p_role_id;
92
93
EXCEPTION
94
WHEN OTHERS THEN
95
RAISE WARNING '⚠️ Error assigning permissions for user %: %', p_user_id, SQLERRM;
96
END;
97
$function$
|
|||||
| Function | upsert_role_permissions | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.upsert_role_permissions(p_role_id integer, p_permission_ids uuid[], p_user_id uuid)
2
RETURNS void
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
-- 1️⃣ Reactivate existing soft-deleted permissions that are now in the list
7
UPDATE public.role_permissions
8
SET is_deleted = FALSE,
9
deleted_on_utc = NULL,
10
modified_on_utc = NOW(),
11
modified_by = p_user_id
12
WHERE role_id = p_role_id
13
AND permission_id = ANY(p_permission_ids)
14
AND is_deleted = TRUE;
15
16
-- 2️⃣ Insert missing (new) permissions
17
INSERT INTO public.role_permissions (
18
role_id,
19
permission_id,
20
created_on_utc,
21
created_by,
22
is_deleted
23
)
24
SELECT
25
p_role_id,
26
pid,
27
NOW(),
28
p_user_id,
29
FALSE
30
FROM UNNEST(p_permission_ids) AS pid
31
WHERE NOT EXISTS (
32
SELECT 1
33
FROM public.role_permissions rp
34
WHERE rp.role_id = p_role_id
35
AND rp.permission_id = pid
36
);
37
38
-- 3️⃣ Soft delete permissions not in the provided list
39
UPDATE public.role_permissions
40
SET is_deleted = TRUE,
41
deleted_on_utc = NOW(),
42
modified_on_utc = NOW(),
43
modified_by = p_user_id
44
WHERE role_id = p_role_id
45
AND permission_id NOT IN (SELECT unnest(p_permission_ids))
46
AND is_deleted = FALSE;
47
END;
48
$function$
|
|||||
| Function | get_outstanding_invoices_by_customer_id | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_outstanding_invoices_by_customer_id(p_company_id uuid, p_customer_id uuid)
2
RETURNS TABLE(customer_name text, coa_name text, amount numeric, due_date date, aging_bucket text)
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
RETURN QUERY
7
SELECT
8
c.name::text AS customer_name,
9
coa.name AS coa_name,
10
je.amount,
11
(th.transaction_date + INTERVAL '30 days')::date AS due_date,
12
CASE
13
WHEN (th.transaction_date + INTERVAL '30 days') <= CURRENT_DATE
14
THEN 'Current'
15
WHEN (th.transaction_date + INTERVAL '30 days') BETWEEN CURRENT_DATE - INTERVAL '30 days' AND CURRENT_DATE
16
THEN '1-30 Days Past Due'
17
WHEN (th.transaction_date + INTERVAL '60 days') BETWEEN CURRENT_DATE - INTERVAL '60 days' AND CURRENT_DATE - INTERVAL '31 days'
18
THEN '31-60 Days Past Due'
19
ELSE 'Over 60 Days Past Due'
20
END AS aging_bucket
21
FROM
22
public.journal_entries je
23
JOIN
24
public.transaction_headers th ON je.transaction_id = th.id
25
JOIN
26
public.chart_of_accounts coa ON je.account_id = coa.id
27
JOIN
28
public.customers c ON th.customer_id = c.id
29
WHERE
30
coa.account_type_id = (SELECT id FROM public.account_types WHERE name = 'Accounts Receivable')
31
AND je.is_deleted = false
32
AND th.company_id = p_company_id
33
AND th.customer_id = p_customer_id
34
AND (th.transaction_date + INTERVAL '30 days') <= CURRENT_DATE
35
ORDER BY
36
(th.transaction_date + INTERVAL '30 days')::date;
37
END;
38
$function$
|
|||||
| Function | get_outstanding_invoices_by_customer | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_outstanding_invoices_by_customer(p_company_id uuid, p_customer_id uuid, p_start_date date DEFAULT NULL::date, p_end_date date DEFAULT NULL::date)
2
RETURNS TABLE(customer_name text, coa_name text, amount numeric, due_date date, aging_bucket text)
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_start_date date := COALESCE(p_start_date, CURRENT_DATE - INTERVAL '365 days');
7
v_end_date date := COALESCE(p_end_date, CURRENT_DATE);
8
BEGIN
9
RETURN QUERY
10
SELECT
11
c.name::text AS customer_name,
12
coa.name AS coa_name,
13
je.amount,
14
(th.transaction_date + INTERVAL '30 days')::date AS due_date,
15
16
/* Aging bucket calculation */
17
CASE
18
WHEN (th.transaction_date + INTERVAL '30 days') <= CURRENT_DATE
19
THEN 'Current'
20
WHEN (th.transaction_date + INTERVAL '30 days') BETWEEN CURRENT_DATE - INTERVAL '30 days' AND CURRENT_DATE
21
THEN '1-30 Days Past Due'
22
WHEN (th.transaction_date + INTERVAL '60 days') BETWEEN CURRENT_DATE - INTERVAL '60 days' AND CURRENT_DATE - INTERVAL '31 days'
23
THEN '31-60 Days Past Due'
24
ELSE 'Over 60 Days Past Due'
25
END AS aging_bucket
26
27
FROM public.journal_entries je
28
JOIN public.transaction_headers th ON je.transaction_id = th.id
29
JOIN public.chart_of_accounts coa ON je.account_id = coa.id
30
JOIN public.customers c ON th.customer_id = c.id
31
32
WHERE
33
th.company_id = p_company_id
34
AND th.customer_id = p_customer_id
35
AND je.is_deleted = false
36
AND coa.account_type_id = (
37
SELECT id FROM public.account_types WHERE name = 'Accounts Receivable'
38
)
39
40
/* Overdue invoices only (due_date <= today) */
41
AND (th.transaction_date + INTERVAL '30 days')::date <= CURRENT_DATE
42
43
/* Date range filtering */
44
AND th.transaction_date::date BETWEEN v_start_date AND v_end_date
45
46
ORDER BY (th.transaction_date + INTERVAL '30 days')::date;
47
END;
48
$function$
|
|||||
| Function | postgres_fdw_handler | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.postgres_fdw_handler()
2
RETURNS fdw_handler
3
LANGUAGE c
4
STRICT
5
AS '$libdir/postgres_fdw', $function$postgres_fdw_handler$function$
|
|||||
| Function | postgres_fdw_validator | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.postgres_fdw_validator(text[], oid)
2
RETURNS void
3
LANGUAGE c
4
STRICT
5
AS '$libdir/postgres_fdw', $function$postgres_fdw_validator$function$
|
|||||
| Function | postgres_fdw_disconnect | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.postgres_fdw_disconnect(text)
2
RETURNS boolean
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/postgres_fdw', $function$postgres_fdw_disconnect$function$
|
|||||
| Function | postgres_fdw_disconnect_all | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.postgres_fdw_disconnect_all()
2
RETURNS boolean
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/postgres_fdw', $function$postgres_fdw_disconnect_all$function$
|
|||||
| Function | postgres_fdw_get_connections | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.postgres_fdw_get_connections(check_conn boolean DEFAULT false, OUT server_name text, OUT user_name text, OUT valid boolean, OUT used_in_xact boolean, OUT closed boolean, OUT remote_backend_pid integer)
2
RETURNS SETOF record
3
LANGUAGE c
4
PARALLEL RESTRICTED STRICT
5
AS '$libdir/postgres_fdw', $function$postgres_fdw_get_connections_1_2$function$
|
|||||
| Function | notify_user_permission_change | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.notify_user_permission_change()
2
RETURNS trigger
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_user_id uuid;
7
v_org_id uuid;
8
payload json;
9
BEGIN
10
-- Handle DELETE separately (NEW is null)
11
IF TG_OP = 'DELETE' THEN
12
v_user_id := OLD.user_id;
13
v_org_id := OLD.organization_id;
14
ELSE
15
v_user_id := NEW.user_id;
16
v_org_id := NEW.organization_id;
17
END IF;
18
19
payload := json_build_object(
20
'entity', 'user_permissions',
21
'operation', TG_OP,
22
'userId', v_user_id,
23
'organizationId', v_org_id,
24
'occurredAt', now()
25
);
26
27
PERFORM pg_notify('permission_user_changed', payload::text);
28
29
RETURN NULL;
30
END;
31
$function$
|
|||||
| Function | get_system_user_id | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.get_system_user_id()
2
RETURNS uuid
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
v_user_id uuid;
7
BEGIN
8
SELECT id
9
INTO v_user_id
10
FROM public.users
11
WHERE first_name = 'System'
12
AND is_deleted = false
13
LIMIT 1;
14
15
16
RETURN COALESCE(
17
v_user_id,
18
'dd4f94f2-0f79-4748-b94b-bf935e3944c7'::uuid
19
);
20
END;
21
$function$
|
|||||
| Function | upsert_user_roles | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.upsert_user_roles(p_user_id uuid, p_role_ids integer[], p_organization_id uuid, p_created_by uuid)
2
RETURNS void
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
INSERT INTO public.user_roles
7
(
8
user_id,
9
role_id,
10
organization_id,
11
created_on_utc,
12
created_by,
13
is_deleted,
14
start_date,
15
end_date
16
)
17
SELECT
18
p_user_id,
19
role_id,
20
p_organization_id,
21
NOW(),
22
p_created_by,
23
false,
24
NOW(),
25
NOW() + INTERVAL '1 year'
26
FROM UNNEST(p_role_ids) AS role_id
27
ON CONFLICT (user_id, role_id, organization_id)
28
DO UPDATE
29
SET
30
is_deleted = false,
31
start_date = NOW(),
32
end_date = NOW() + INTERVAL '1 year',
33
modified_on_utc = NOW(),
34
modified_by = p_created_by;
35
END;
36
$function$
|
|||||
| Function | upsert_user_permissions_from_user_roles | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.upsert_user_permissions_from_user_roles(p_user_id uuid, p_organization_id uuid, p_role_ids integer[], p_created_by uuid)
2
RETURNS void
3
LANGUAGE plpgsql
4
AS $function$
5
BEGIN
6
/*
7
* STEP 1: Insert or revive permissions coming from roles
8
*/
9
INSERT INTO public.user_permissions (
10
user_id,
11
organization_id,
12
permission_id,
13
created_on_utc,
14
created_by,
15
is_deleted,
16
is_explicit
17
)
18
SELECT DISTINCT
19
p_user_id,
20
p_organization_id,
21
rp.permission_id,
22
now(),
23
p_created_by,
24
false,
25
false -- role-derived
26
FROM public.role_permissions rp
27
WHERE rp.role_id = ANY (p_role_ids)
28
AND rp.is_deleted = false
29
30
ON CONFLICT (user_id, organization_id, permission_id)
31
DO UPDATE
32
SET
33
is_deleted = false,
34
deleted_on_utc = NULL,
35
modified_on_utc = now(),
36
modified_by = p_created_by,
37
is_explicit = false;
38
39
/*
40
* STEP 2: Soft-delete role-derived permissions
41
* that are no longer present in current roles
42
*/
43
UPDATE public.user_permissions up
44
SET
45
is_deleted = true,
46
deleted_on_utc = now(),
47
modified_on_utc = now(),
48
modified_by = p_created_by
49
WHERE up.user_id = p_user_id
50
AND up.organization_id = p_organization_id
51
AND up.is_explicit = false
52
AND up.is_deleted = false
53
AND up.permission_id NOT IN (
54
SELECT DISTINCT rp.permission_id
55
FROM public.role_permissions rp
56
WHERE rp.role_id = ANY (p_role_ids)
57
AND rp.is_deleted = false
58
);
59
END;
60
$function$
|
|||||
| Function | sync_user_permissions_from_roles | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE FUNCTION public.sync_user_permissions_from_roles(p_user_id uuid DEFAULT NULL::uuid, p_organization_id uuid DEFAULT NULL::uuid, p_triggered_by text DEFAULT 'manual'::text, p_notes text DEFAULT NULL::text)
2
RETURNS uuid
3
LANGUAGE plpgsql
4
AS $function$
5
DECLARE
6
-- Sync run tracking
7
v_sync_run_id uuid := gen_random_uuid();
8
9
-- Loop variables
10
v_user_id uuid;
11
v_org_id uuid;
12
v_role_id int;
13
v_permission_id uuid;
14
15
-- For UPSERT result
16
v_user_permission_id int;
17
BEGIN
18
/*
19
============================================================
20
START SYNC RUN
21
============================================================
22
*/
23
INSERT INTO public.user_permission_sync_runs (
24
id,
25
started_on_utc,
26
triggered_by,
27
notes
28
)
29
VALUES (
30
v_sync_run_id,
31
NOW(),
32
p_triggered_by,
33
p_notes
34
);
35
36
/*
37
============================================================
38
MAIN SYNC LOOP
39
- Filtered by optional user / organization
40
============================================================
41
*/
42
FOR v_user_id, v_org_id IN
43
SELECT DISTINCT
44
ur.user_id,
45
ur.organization_id
46
FROM public.user_roles ur
47
WHERE ur.is_deleted = false
48
AND (p_user_id IS NULL OR ur.user_id = p_user_id)
49
AND (p_organization_id IS NULL OR ur.organization_id = p_organization_id)
50
LOOP
51
52
/*
53
--------------------------------------------------------
54
LOOP ROLES FOR USER + ORGANIZATION
55
--------------------------------------------------------
56
*/
57
FOR v_role_id IN
58
SELECT ur.role_id
59
FROM public.user_roles ur
60
WHERE ur.user_id = v_user_id
61
AND ur.organization_id = v_org_id
62
AND ur.is_deleted = false
63
LOOP
64
65
/*
66
----------------------------------------------------
67
LOOP PERMISSIONS FOR ROLE
68
----------------------------------------------------
69
*/
70
FOR v_permission_id IN
71
SELECT rp.permission_id
72
FROM public.role_permissions rp
73
WHERE rp.role_id = v_role_id
74
AND rp.is_deleted = false
75
LOOP
76
77
/*
78
------------------------------------------------
79
UPSERT USER PERMISSION (SAFE)
80
------------------------------------------------
81
*/
82
INSERT INTO public.user_permissions (
83
user_id,
84
permission_id,
85
organization_id,
86
created_on_utc,
87
created_by,
88
is_deleted,
89
modified_on_utc,
90
modified_by,
91
deleted_on_utc
92
)
93
VALUES (
94
v_user_id,
95
v_permission_id,
96
v_org_id,
97
NOW(),
98
v_user_id,
99
false,
100
NULL,
101
NULL,
102
NULL
103
)
104
ON CONFLICT (user_id, organization_id, permission_id)
105
DO UPDATE
106
SET
107
is_deleted = false,
108
deleted_on_utc = NULL,
109
modified_on_utc = NOW(),
110
modified_by = v_user_id
111
WHERE public.user_permissions.is_deleted = true
112
RETURNING id
113
INTO v_user_permission_id;
114
115
/*
116
------------------------------------------------
117
BACKUP / DELTA SNAPSHOT
118
- Only when INSERT or REVIVE happened
119
------------------------------------------------
120
*/
121
IF FOUND THEN
122
INSERT INTO public.user_permission_sync_deltas (
123
id,
124
sync_run_id,
125
user_permission_id,
126
user_id,
127
organization_id,
128
permission_id,
129
action,
130
created_on_utc
131
)
132
VALUES (
133
gen_random_uuid(),
134
v_sync_run_id,
135
v_user_permission_id,
136
v_user_id,
137
v_org_id,
138
v_permission_id,
139
'UPSERT',
140
NOW()
141
);
142
END IF;
143
144
END LOOP; -- permissions
145
END LOOP; -- roles
146
END LOOP; -- users + organizations
147
148
/*
149
============================================================
150
COMPLETE SYNC RUN
151
============================================================
152
*/
153
UPDATE public.user_permission_sync_runs
154
SET completed_on_utc = NOW()
155
WHERE id = v_sync_run_id;
156
157
RETURN v_sync_run_id;
158
END;
159
$function$
|
|||||
| Procedure | delete_journal_entries_by_accounts | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.delete_journal_entries_by_accounts(IN p_account_ids uuid[])
2
LANGUAGE plpgsql
3
AS $procedure$
4
BEGIN
5
DELETE FROM ONLY public.journal_entries
6
WHERE account_id = ANY(p_account_ids);
7
8
RAISE NOTICE 'Deleted entries for Account IDs: %', p_account_ids;
9
END;
10
$procedure$
|
|||||
| Procedure | copy_user_permissions | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.copy_user_permissions(IN p_source_user_id uuid, IN p_target_user_id uuid, IN p_created_by uuid, IN p_is_override boolean)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_new_id INTEGER;
6
BEGIN
7
-- If override is true, delete existing permissions for the target user
8
IF p_is_override THEN
9
DELETE FROM public.user_permissions
10
WHERE user_id = p_target_user_id;
11
END IF;
12
13
-- Get the current maximum id from user_permissions
14
SELECT COALESCE(MAX(id), 0) INTO v_new_id FROM public.user_permissions;
15
16
-- Insert permissions from the source user to the target user, with checks to avoid duplicates and child assignments if parent exists
17
INSERT INTO public.user_permissions (
18
id,
19
user_id,
20
organization_id,
21
permission_id,
22
created_on_utc,
23
created_by
24
)
25
SELECT
26
v_new_id + ROW_NUMBER() OVER (), -- generate incremented ids starting from max_id + 1
27
p_target_user_id,
28
up.organization_id,
29
up.permission_id,
30
NOW(),
31
p_created_by
32
FROM
33
public.user_permissions AS up
34
WHERE
35
up.user_id = p_source_user_id
36
AND up.is_deleted = false
37
-- Avoid assigning child permissions if a parent permission is already assigned
38
AND NOT EXISTS (
39
SELECT 1
40
FROM public.user_permissions AS target_up
41
JOIN public.permissions AS parent_perm ON target_up.permission_id = parent_perm.id
42
WHERE target_up.user_id = p_target_user_id
43
AND parent_perm.id = up.permission_id
44
AND parent_perm.parent_permission_id IS NULL -- Checking only for root or parent permissions
45
AND parent_perm.is_deleted = false
46
)
47
-- Avoid duplicate permission entries
48
AND NOT EXISTS (
49
SELECT 1
50
FROM public.user_permissions AS target_up
51
WHERE target_up.user_id = p_target_user_id
52
AND target_up.permission_id = up.permission_id
53
AND target_up.is_deleted = false
54
);
55
END;
56
$procedure$
|
|||||
| Procedure | create_company1 | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.create_company1(IN p_id uuid, IN p_organization_id uuid, IN p_name text, IN p_description text, IN p_account_id uuid, IN p_gstin text, IN p_pan text, IN p_tan text, IN p_currency text, IN p_short_name text, IN p_tag_line text, IN p_proprietor_name text, IN p_outstanding_limit numeric, IN p_is_non_work boolean, IN p_interest_percentage numeric, IN p_created_by uuid, IN p_created_on_utc timestamp without time zone)
2
LANGUAGE plpgsql
3
AS $procedure$
4
BEGIN
5
INSERT INTO public.companies (
6
id, organization_id, name, description, account_id, gstin, pan, tan,
7
currency, short_name, tag_line, proprietor_name, outstanding_limit,
8
is_non_work, interest_percentage, created_on_utc, created_by, modified_by
9
) VALUES (
10
p_id, p_organization_id, p_name, p_description, p_account_id, p_gstin,
11
p_pan, p_tan, p_currency, p_short_name, p_tag_line, p_proprietor_name,
12
p_outstanding_limit, p_is_non_work, p_interest_percentage,
13
p_created_on_utc, p_created_by
14
);
15
END;
16
$procedure$
|
|||||
| Procedure | delete_journal_entries_by_account | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.delete_journal_entries_by_account(IN p_account_id uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
BEGIN
5
DELETE FROM ONLY public.journal_entries
6
WHERE account_id = p_account_id;
7
8
RAISE NOTICE 'Deleted entries for Account ID: %', p_account_id;
9
END;
10
$procedure$
|
|||||
| Procedure | get_all_chart_of_accounts | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.get_all_chart_of_accounts()
2
LANGUAGE plpgsql
3
AS $procedure$
4
5
begin
6
select * from chart_of_accounts;
7
end;
8
$procedure$
|
|||||
| Procedure | assign_default_users_to_organization | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.assign_default_users_to_organization(IN p_organization_id uuid, IN p_permission_ids uuid[], IN p_created_by uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
loop_user_id UUID;
6
BEGIN
7
-- Insert default users into organization_users (only if they don't exist)
8
INSERT INTO public.organization_users (
9
id,
10
user_id,
11
effective_start_date,
12
effective_end_date,
13
created_on_utc,
14
created_by,
15
organization_id
16
)
17
SELECT
18
gen_random_uuid(),
19
d.user_id,
20
NOW(),
21
DATE '9999-12-31',
22
NOW(),
23
p_created_by,
24
p_organization_id
25
FROM
26
public.default_organization_users d
27
WHERE NOT EXISTS (
28
SELECT 1
29
FROM public.organization_users ou
30
WHERE ou.user_id = d.user_id
31
AND ou.organization_id = p_organization_id
32
);
33
34
RAISE NOTICE 'Added % default users to organization %',
35
(SELECT COUNT(*) FROM public.default_organization_users), p_organization_id;
36
37
-- Assign permissions for these users by calling insert_user_permission
38
FOR loop_user_id IN (SELECT d.user_id
39
FROM public.default_organization_users d)
40
LOOP
41
CALL public.insert_user_permission(
42
loop_user_id,
43
p_organization_id,
44
p_permission_ids,
45
p_created_by
46
);
47
END LOOP;
48
RAISE NOTICE 'Attempted to assign permission % to default users in organization %',
49
p_permission_ids,
50
p_organization_id;
51
END;
52
$procedure$
|
|||||
| Procedure | create_organization | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.create_organization(IN p_id uuid, IN p_name text, IN p_description text, IN p_phone_number text, IN p_email text, IN p_address_line text, IN p_gstin text, IN p_tag_line text, IN p_city_id uuid, IN p_short_name text, IN p_pan text, IN p_tan text, IN p_created_by uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
BEGIN
5
-- Step 1: Insert into organizations table
6
INSERT INTO public.organizations (
7
id,
8
name,
9
description,
10
phone_number,
11
email,
12
address_line,
13
gstin,
14
tag_line,
15
city_id,
16
short_name,
17
pan,
18
tan,
19
outstanding_limit,
20
is_non_work,
21
interest_percentage,
22
created_on_utc,
23
created_by
24
) VALUES (
25
p_id, -- Organization ID
26
p_name, -- Organization Name
27
p_description, -- Description
28
p_phone_number, -- Phone Number
29
p_email, -- Email
30
p_address_line, -- Address Line
31
p_gstin, -- GSTIN
32
p_tag_line, -- Tag Line
33
p_city_id, -- City ID
34
p_short_name, -- Short Name
35
p_pan, -- PAN
36
p_tan, -- TAN
37
0, -- Outstanding Limit
38
false, -- Is Non-Work
39
0, -- Interest Percentage
40
NOW(), -- Created On Timestamp
41
p_created_by -- Created By
42
);
43
44
-- Notify the successful creation of the organization
45
RAISE NOTICE 'Organization created successfully with ID: %', p_id;
46
END;
47
$procedure$
|
|||||
| Procedure | delete_journal_vouchers_by_company | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.delete_journal_vouchers_by_company(IN p_company_id uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
BEGIN
5
-- Delete from journal_voucher_details first to maintain referential integrity
6
DELETE FROM public.journal_voucher_details
7
WHERE journal_header_id IN (
8
SELECT id FROM public.journal_voucher_headers WHERE company_id = p_company_id
9
);
10
11
-- Delete from journal_voucher_headers
12
DELETE FROM public.journal_voucher_headers
13
WHERE company_id = p_company_id;
14
15
RAISE NOTICE 'Data deleted successfully for company_id: %', p_company_id;
16
END;
17
$procedure$
|
|||||
| Procedure | delete_transactions_by_company | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.delete_transactions_by_company(IN p_company_id uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
BEGIN
5
-- Delete from journal_entries first to maintain referential integrity
6
DELETE FROM public.journal_entries
7
WHERE transaction_id IN (
8
SELECT id FROM public.transaction_headers WHERE company_id = p_company_id
9
);
10
11
-- Delete from transaction_headers
12
DELETE FROM public.transaction_headers
13
WHERE company_id = p_company_id;
14
15
RAISE NOTICE 'Data deleted successfully for company_id: %', p_company_id;
16
END;
17
$procedure$
|
|||||
| Procedure | dummy_log_procedure | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.dummy_log_procedure(IN msg text)
2
LANGUAGE plpgsql
3
AS $procedure$
4
BEGIN
5
INSERT INTO public.dummy_log_table(message) VALUES (msg);
6
END;
7
$procedure$
|
|||||
| Procedure | hard_delete_org_common | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.hard_delete_org_common(IN p_organization_id uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_company_id uuid;
6
BEGIN
7
-- ========== Delete Common Organization-Level Data ==========
8
DELETE FROM organization_accounts WHERE organization_id = p_organization_id;
9
DELETE FROM email_templates WHERE organization_id = p_organization_id;
10
DELETE FROM user_permissions WHERE organization_id = p_organization_id;
11
DELETE FROM organization_users WHERE organization_id = p_organization_id;
12
13
-- ========== Delete Company-Level Data ==========
14
FOR v_company_id IN
15
SELECT id FROM companies WHERE organization_id = p_organization_id
16
LOOP
17
-- Journal data
18
DELETE FROM journal_voucher_details WHERE journal_header_id IN (
19
SELECT id FROM journal_voucher_headers WHERE company_id = v_company_id
20
);
21
DELETE FROM journal_voucher_headers WHERE company_id = v_company_id;
22
DELETE FROM journal_voucher_header_id WHERE company_id = v_company_id;
23
24
-- General ledgers
25
DELETE FROM general_ledgers WHERE company_id = v_company_id;
26
27
-- Company-specific info
28
DELETE FROM company_bank_accounts WHERE company_id = v_company_id;
29
DELETE FROM company_contacts WHERE company_id = v_company_id;
30
DELETE FROM company_finance_year WHERE company_id = v_company_id;
31
DELETE FROM company_preferences WHERE company_id = v_company_id;
32
DELETE FROM company_upis WHERE company_id = v_company_id;
33
DELETE FROM company_users WHERE company_id = v_company_id;
34
35
-- Delete journal entries by company-linked transactions
36
DELETE FROM journal_entries WHERE transaction_id IN (
37
SELECT id FROM transaction_headers WHERE company_id = v_company_id
38
);
39
40
-- Delete transaction headers (must be before customers/vendors)
41
DELETE FROM transaction_headers WHERE company_id = v_company_id;
42
43
-- Linked business entities (now safe to delete)
44
DELETE FROM customers WHERE company_id = v_company_id;
45
DELETE FROM vendors WHERE company_id = v_company_id;
46
DELETE FROM warehouses WHERE company_id = v_company_id;
47
48
-- Remove user_roles BEFORE users
49
DELETE FROM user_roles WHERE user_id IN (
50
SELECT id FROM users WHERE company_id = v_company_id
51
) OR created_by IN (
52
SELECT id FROM users WHERE company_id = v_company_id
53
);
54
55
-- Now it's safe to delete users
56
DELETE FROM users WHERE company_id = v_company_id;
57
END LOOP;
58
59
-- Chart of accounts and account-linked entries
60
DELETE FROM general_ledgers
61
WHERE credit_account_id IN (
62
SELECT id FROM chart_of_accounts WHERE organization_id = p_organization_id
63
) OR debit_account_id IN (
64
SELECT id FROM chart_of_accounts WHERE organization_id = p_organization_id
65
);
66
67
DELETE FROM journal_entries
68
WHERE account_id IN (
69
SELECT id FROM chart_of_accounts WHERE organization_id = p_organization_id
70
);
71
72
DELETE FROM chart_of_accounts WHERE organization_id = p_organization_id;
73
74
-- Delete all companies under the organization
75
DELETE FROM companies WHERE organization_id = p_organization_id;
76
77
-- Delete the organization itself
78
DELETE FROM organizations WHERE id = p_organization_id;
79
80
RAISE NOTICE 'Organization with ID % and related data from all common tables has been deleted.', p_organization_id;
81
END;
82
$procedure$
|
|||||
| Procedure | hard_delete_organization | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.hard_delete_organization(IN p_organization_id uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
BEGIN
5
-- Delete from user_permissions (cascading through organization and users)
6
DELETE FROM user_permissions
7
WHERE organization_id = p_organization_id;
8
9
-- Delete from organization_users (cascading through organization)
10
DELETE FROM organization_users
11
WHERE organization_id = p_organization_id;
12
13
-- Delete from company_finance_year (cascading through companies)
14
DELETE FROM company_finance_year
15
WHERE company_id IN (
16
SELECT id FROM companies WHERE organization_id = p_organization_id
17
);
18
19
-- Delete from company_users (cascading through companies)
20
DELETE FROM company_users
21
WHERE company_id IN (
22
SELECT id FROM companies WHERE organization_id = p_organization_id
23
);
24
25
-- Delete from company_contacts (cascading through companies)
26
DELETE FROM company_contacts
27
WHERE company_id IN (
28
SELECT id FROM companies WHERE organization_id = p_organization_id
29
);
30
31
-- Delete from contacts (cascading through companies)
32
DELETE FROM contacts
33
WHERE id IN (
34
SELECT contact_id FROM company_contacts
35
WHERE company_id IN (
36
SELECT id FROM companies WHERE organization_id = p_organization_id
37
)
38
);
39
40
--delete from company_preference
41
DELETE FROM public.company_preferences
42
WHERE company_id in (SELECT id from public.companies where organization_id = p_organization_id);
43
44
-- Delete from companies (cascading through organization)
45
DELETE FROM companies
46
WHERE organization_id = p_organization_id;
47
48
DELETE FROM organization_accounts
49
WHERE organization_id = p_organization_id;
50
51
-- Delete from chart_of_accounts (cascading through organization)
52
DELETE FROM chart_of_accounts
53
WHERE organization_id = p_organization_id;
54
55
-- Finally, delete the organization itself
56
DELETE FROM organizations
57
WHERE id = p_organization_id;
58
59
-- Optional: Log the deletion action
60
RAISE NOTICE 'Organization with ID % and all related data have been hard deleted.',
61
p_organization_id;
62
63
END;
64
$procedure$
|
|||||
| Procedure | initialize_company | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.initialize_company(IN p_id uuid, IN p_organization_id uuid, IN p_name text, IN p_description text, IN p_gstin text, IN p_pan text, IN p_tan text, IN p_currency text, IN p_short_name text, IN p_tag_line text, IN p_proprietor_name text, IN p_phone_number character varying, IN p_email character varying, IN p_created_by uuid, IN p_user_id uuid, IN p_default_company_id uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_contact_id uuid := gen_random_uuid();
6
v_company_contact_id uuid := gen_random_uuid();
7
v_finance_year_id int;
8
v_prev_finance_year_id INTEGER;
9
v_next_finance_year_id INTEGER;
10
DEFAULT_USER_ID CONSTANT uuid := '50fd5012-940b-4fec-ad0a-2ac900239c8b';
11
v_company_exists boolean;
12
v_contact_exists boolean;
13
v_company_contact_exists boolean;
14
v_user_company_exists boolean;
15
v_default_user_company_exists boolean;
16
BEGIN
17
-- Check if company already exists
18
SELECT EXISTS (
19
SELECT 1 FROM public.companies
20
WHERE id = p_id
21
OR (organization_id = p_organization_id AND name = p_name)
22
) INTO v_company_exists;
23
24
IF NOT v_company_exists THEN
25
-- Insert into companies table
26
INSERT INTO public.companies (
27
id, organization_id, name, description, gstin, pan, tan,
28
currency, short_name, tag_line, proprietor_name,
29
outstanding_limit, is_non_work, is_apartment, interest_percentage,
30
created_on_utc, created_by
31
) VALUES (
32
p_id, p_organization_id, p_name, p_description, p_gstin, p_pan, p_tan,
33
p_currency, p_short_name, p_tag_line, p_proprietor_name,
34
0, false, true, 0, NOW(), p_created_by
35
);
36
RAISE NOTICE 'Company % created successfully.', p_name;
37
ELSE
38
RAISE NOTICE 'Company with ID %, name %, or GSTIN/PAN % already exists. Skipping company creation.',
39
p_id, p_name, p_gstin;
40
END IF;
41
42
-- Check if contact already exists for this email/phone
43
SELECT EXISTS (
44
SELECT 1 FROM contacts
45
WHERE email = p_email OR phone_number = p_phone_number
46
) INTO v_contact_exists;
47
48
IF NOT v_contact_exists THEN
49
-- Insert contact
50
INSERT INTO contacts (
51
id, salutation, first_name, last_name, email,
52
phone_number, mobile_number, is_primary,
53
created_on_utc, created_by
54
) VALUES (
55
v_contact_id, 'Mr.', p_proprietor_name, NULL, p_email,
56
p_phone_number, p_phone_number, TRUE,
57
NOW(), p_created_by
58
);
59
RAISE NOTICE 'Contact created for company % with email %.', p_name, p_email;
60
ELSE
61
-- Get existing contact ID
62
SELECT id INTO v_contact_id
63
FROM contacts
64
WHERE email = p_email OR phone_number = p_phone_number
65
LIMIT 1;
66
RAISE NOTICE 'Contact with email % or phone % already exists. Using existing contact.', p_email, p_phone_number;
67
END IF;
68
69
-- Check if company-contact relationship exists
70
SELECT EXISTS (
71
SELECT 1 FROM company_contacts
72
WHERE company_id = p_id AND contact_id = v_contact_id
73
) INTO v_company_contact_exists;
74
75
IF NOT v_company_contact_exists THEN
76
-- Link company and contact
77
INSERT INTO company_contacts (
78
id, company_id, contact_id, created_on_utc, created_by
79
) VALUES (
80
v_company_contact_id, p_id, v_contact_id, NOW(), p_created_by
81
);
82
END IF;
83
84
-- Check if user-company relationship exists for main user
85
SELECT EXISTS (
86
SELECT 1 FROM company_users
87
WHERE company_id = p_id AND user_id = p_user_id
88
) INTO v_user_company_exists;
89
90
IF NOT v_user_company_exists THEN
91
-- Link main user to company
92
INSERT INTO company_users (
93
id, company_id, user_id, effective_start_date,
94
effective_end_date, created_on_utc, created_by
95
) VALUES (
96
gen_random_uuid(), p_id, p_user_id, NOW(),
97
DATE '9999-12-31', NOW(), p_created_by
98
);
99
END IF;
100
101
-- Check if user-company relationship exists for default user
102
SELECT EXISTS (
103
SELECT 1 FROM company_users
104
WHERE company_id = p_id AND user_id = DEFAULT_USER_ID
105
) INTO v_default_user_company_exists;
106
107
IF NOT v_default_user_company_exists THEN
108
-- Link default user to company
109
INSERT INTO company_users (
110
id, company_id, user_id, effective_start_date,
111
effective_end_date, created_on_utc, created_by
112
) VALUES (
113
gen_random_uuid(), p_id, DEFAULT_USER_ID, NOW(),
114
DATE '9999-12-31', NOW(), p_created_by
115
);
116
END IF;
117
118
-- Initialize finance years if company was newly created
119
IF NOT v_company_exists THEN
120
CALL public.insert_company_finance_years(p_id);
121
CALL public.initialize_journal_voucher_header_ids(p_default_company_id, p_id);
122
END IF;
123
124
RAISE NOTICE 'Company initialization completed for % (ID: %)', p_name, p_id;
125
END;
126
$procedure$
|
|||||
| Procedure | initialize_journal_voucher_header_ids | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.initialize_journal_voucher_header_ids(IN old_company_id uuid, IN new_company_id uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
BEGIN
5
-- Insert new records for the new company, skipping duplicates
6
INSERT INTO journal_voucher_header_id (
7
company_id,
8
fin_year,
9
voucher_prefix,
10
voucher_length,
11
last_voucher_id
12
)
13
SELECT
14
new_company_id,
15
fin_year,
16
voucher_prefix,
17
voucher_length,
18
last_voucher_id
19
FROM journal_voucher_header_id AS j
20
WHERE company_id = old_company_id
21
AND NOT EXISTS (
22
SELECT 1
23
FROM journal_voucher_header_id
24
WHERE company_id = new_company_id
25
AND fin_year = j.fin_year
26
);
27
28
RAISE NOTICE 'Journal voucher header IDs initialized successfully for new company ID: %', new_company_id;
29
END;
30
$procedure$
|
|||||
| Procedure | initialize_org_coa | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.initialize_org_coa(IN old_org_id uuid, IN new_org_id uuid, IN new_company_ids text, IN p_created_by uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
account_mapping RECORD;
6
old_organization_account RECORD; -- To store the old organization_accounts record
7
defualt_company_preference RECORD; -- to store default company preference record
8
v_company_id_array uuid[];
9
v_company_id uuid;
10
v_coa_exists boolean;
11
DEFAULT_COMPANY_ID CONSTANT uuid := '048000ba-e62c-474b-b5eb-fe629a4dc863';
12
BEGIN
13
SELECT EXISTS (
14
SELECT 1 FROM chart_of_accounts WHERE organization_id = new_org_id
15
) INTO v_coa_exists;
16
17
IF v_coa_exists THEN
18
RAISE NOTICE 'Chart of accounts already exists for organization %. Skipping initialization.', new_org_id;
19
RETURN;
20
END IF;
21
-- Parse the comma-separated company IDs into an array
22
v_company_id_array := string_to_array(new_company_ids, ',');
23
24
-- Create a temporary table for mapping old account IDs to new account IDs
25
CREATE TEMP TABLE temp_account_mapping (
26
old_id UUID,
27
new_id UUID
28
);
29
30
-- Populate the temporary mapping table with old IDs and their new UUIDs
31
INSERT INTO temp_account_mapping (old_id, new_id)
32
SELECT id, gen_random_uuid()
33
FROM chart_of_accounts
34
WHERE organization_id = old_org_id;
35
36
-- Insert new records into chart_of_accounts for the new organization
37
INSERT INTO chart_of_accounts (
38
id,
39
account_number,
40
name,
41
organization_id,
42
parent_account_id,
43
description,
44
account_type_id,
45
account_group_code,
46
second_group_code,
47
alternative_name,
48
is_ledger_total,
49
is_show_outs,
50
is_tds_tcs,
51
opening_balance,
52
current_balance,
53
created_by,
54
created_on_utc,
55
is_default_account
56
)
57
SELECT
58
tm.new_id, -- Use the new ID from the mapping table
59
coa.account_number,
60
coa.name,
61
new_org_id, -- Assign to the new organization
62
(SELECT tm2.new_id FROM temp_account_mapping tm2 WHERE tm2.old_id = coa.parent_account_id), -- Map the parent ID
63
coa.description,
64
coa.account_type_id,
65
coa.account_group_code,
66
coa.second_group_code,
67
coa.alternative_name,
68
coa.is_ledger_total,
69
coa.is_show_outs,
70
coa.is_tds_tcs,
71
0,
72
0,
73
p_created_by,
74
NOW(),
75
coa.is_default_account
76
FROM chart_of_accounts coa
77
INNER JOIN temp_account_mapping tm ON coa.id = tm.old_id;
78
79
-- Fetch the old organization_accounts record
80
SELECT * INTO old_organization_account
81
FROM organization_accounts
82
WHERE organization_id = old_org_id;
83
84
-- Insert a single entry into organization_accounts for the new organization
85
INSERT INTO organization_accounts (
86
id,
87
organization_id,
88
accounts_receivable_account_id,
89
accounts_payable_account_id,
90
sales_revenue_account_id,
91
cgst_receivable_account_id,
92
sgst_receivable_account_id,
93
igst_receivable_account_id,
94
cgst_payable_account_id,
95
sgst_payable_account_id,
96
igst_payable_account_id,
97
round_off_gain_account_id,
98
round_off_loss_account_id,
99
sales_tax_payable_account_id,
100
purchase_tax_receivable_account_id,
101
discounts_given_account_id,
102
discounts_received_account_id,
103
interest_income_account_id,
104
interest_expense_account_id,
105
depreciation_expense_account_id,
106
bad_debt_expense_account_id,
107
bank_charges_account_id,
108
foreign_exchange_gain_loss_account_id,
109
created_on_utc,
110
created_by,
111
cost_of_goods_sold_account_id,
112
inventory_account_id,
113
salary_expense_account_id,
114
salary_payable_account_id,
115
tds_receivable_account_id,
116
penalty_receivable_account_id,
117
tds_payable_account_id
118
)
119
VALUES (
120
nextval('organization_accounts_id_seq'), -- Generate a new ID for organization_accounts
121
new_org_id, -- Assign to the new organization
122
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.accounts_receivable_account_id),
123
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.accounts_payable_account_id),
124
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.sales_revenue_account_id),
125
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.cgst_receivable_account_id),
126
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.sgst_receivable_account_id),
127
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.igst_receivable_account_id),
128
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.cgst_payable_account_id),
129
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.sgst_payable_account_id),
130
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.igst_payable_account_id),
131
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.round_off_gain_account_id),
132
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.round_off_loss_account_id),
133
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.sales_tax_payable_account_id),
134
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.purchase_tax_receivable_account_id),
135
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.discounts_given_account_id),
136
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.discounts_received_account_id),
137
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.interest_income_account_id),
138
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.interest_expense_account_id),
139
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.depreciation_expense_account_id),
140
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.bad_debt_expense_account_id),
141
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.bank_charges_account_id),
142
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.foreign_exchange_gain_loss_account_id),
143
NOW(),
144
p_created_by,
145
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.cost_of_goods_sold_account_id),
146
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.inventory_account_id),
147
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.salary_expense_account_id),
148
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.salary_payable_account_id),
149
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.tds_receivable_account_id),
150
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.penalty_receivable_account_id),
151
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.tds_payable_account_id)
152
);
153
154
--fetch old company_preference data
155
SELECT * INTO defualt_company_preference
156
FROM public.company_preferences
157
WHERE company_id = DEFAULT_COMPANY_ID;
158
159
-- Loop over the company IDs to insert records into company_preferences
160
FOREACH v_company_id IN ARRAY v_company_id_array LOOP
161
INSERT INTO public.company_preferences (
162
id,
163
company_id,
164
default_sales_account_id,
165
default_purchase_account_id,
166
default_cash_account_id,
167
default_bank_account_id,
168
created_on_utc,
169
created_by
170
)
171
VALUES (
172
gen_random_uuid(),
173
v_company_id,
174
(SELECT new_id FROM temp_account_mapping WHERE old_id = defualt_company_preference.default_sales_account_id),
175
(SELECT new_id FROM temp_account_mapping WHERE old_id = defualt_company_preference.default_purchase_account_id),
176
(SELECT new_id FROM temp_account_mapping WHERE old_id = defualt_company_preference.default_cash_account_id),
177
(SELECT new_id FROM temp_account_mapping WHERE old_id = defualt_company_preference.default_bank_account_id),
178
NOW(),
179
p_created_by
180
);
181
END LOOP;
182
183
-- Drop the temporary mapping table
184
DROP TABLE temp_account_mapping;
185
186
UPDATE public.chart_of_accounts
187
set parent_account_id = '00000000-0000-0000-0000-000000000000'
188
where organization_id = new_org_id
189
And account_number in ('1000000','2000000','3000000','4000000','5000000');
190
191
-- Log theparent_account_id completion of the procedure
192
RAISE NOTICE 'Chart of accounts and organization accounts successfully copied from organization % to %.', old_org_id, new_org_id;
193
END;
194
$procedure$
|
|||||
| Procedure | initilize_organization | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.initilize_organization(IN p_id uuid, IN p_name text, IN p_company_guids text, IN p_company_names text, IN p_user_id uuid, IN p_user_first_name text, IN p_user_last_name text, IN p_phone_number character varying, IN p_email character varying, IN p_gstin text, IN p_description text, IN p_tag_line text, IN p_short_name text, IN p_pan text, IN p_tan text, IN p_address_line1 text, IN p_address_line2 text, IN p_country_id uuid, IN p_state_id uuid, IN p_city_name text, IN p_zip_code text, IN p_created_by uuid, IN p_default_company_id uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_city_id uuid;
6
v_address_id uuid; -- Generate a unique address ID
7
v_user_id uuid; -- Generate a unique user ID
8
v_permission_ids uuid[];
9
v_company_names text[]; -- Array to hold parsed company names
10
v_company_guids uuid[]; -- Array to hold parsed company GUIDs
11
v_company_name text; -- Variable for iterating through company names
12
v_company_guid uuid; -- Variable for iterating through company GUIDs
13
v_user_company_id uuid;
14
15
-- Constants for default organization and company IDs
16
DEFAULT_ORGANIZATION_ID CONSTANT uuid := '68929d37-b647-4d7b-8c91-7dfe2396c93b';
17
--DEFAULT_COMPANY_ID CONSTANT uuid := '048000ba-e62c-474b-b5eb-fe629a4dc863';
18
ADMIN_PERMISSION CONSTANT text := 'Dhanman.Admin';
19
BEGIN
20
21
22
-- Parse company names and GUIDs into arrays
23
v_company_names := string_to_array(p_company_names, ',');
24
v_company_guids := string_to_array(p_company_guids, ',');
25
26
-- Get city_id
27
v_city_id := get_city_id(p_city_name, p_zip_code, p_state_id, p_created_by);
28
29
-- Get address_id
30
v_address_id := get_address_id(p_country_id, p_state_id , v_city_id , p_address_line1, p_zip_code, p_created_by, p_address_line2);
31
32
v_user_company_id := v_company_guids[1]; -- Pick the first company ID
33
34
-- Create and get user_id
35
v_user_id := create_user(p_user_id, v_user_company_id, p_email, p_phone_number, p_user_first_name, p_user_last_name, v_address_id, p_created_by);
36
RAISE NOTICE 'Generated User ID: %',v_user_company_id;
37
38
-- Print the generated address_id and fetched state_id and country_id
39
RAISE NOTICE 'Generated address_id: %, City ID: %, Country ID: %, User ID: %', v_address_id, v_city_id, p_country_id, v_user_id;
40
41
-- Check if organization exists by any unique identifier
42
IF NOT EXISTS (SELECT 1 FROM organizations WHERE id = p_id) THEN
43
-- Only create organization if no matching record exists
44
CALL public.create_organization(
45
p_id,
46
p_name,
47
p_description,
48
p_phone_number,
49
p_email,
50
p_address_line1,
51
p_gstin,
52
p_tag_line,
53
v_city_id,
54
p_short_name,
55
p_pan,
56
p_tan,
57
p_created_by
58
);
59
60
RAISE NOTICE 'Organization ID % successfully inserted.', p_id;
61
ELSE
62
RAISE NOTICE 'Organization with ID % already exists. Skipping organization creation.', p_id;
63
END IF;
64
65
-- Insert into organization_users table to link the generated user to the organization
66
CALL public.insert_organization_user(v_user_id, p_created_by, p_id);
67
68
-- Fetch the permission_id for 'Dhanman.Admin'
69
SELECT array_agg(id) INTO v_permission_ids
70
FROM permissions
71
WHERE name = ADMIN_PERMISSION;
72
73
CALL public.insert_user_permission(
74
v_user_id ,
75
p_id ,
76
v_permission_ids,
77
p_created_by
78
);
79
80
-- Assign all default users and default permissions as admin
81
CALL public.assign_default_users_to_organization(p_id, v_permission_ids, p_created_by);
82
83
-- Iterate through the company names and GUIDs
84
FOR i IN 1..array_length(v_company_names, 1) LOOP
85
v_company_name := v_company_names[i];
86
v_company_guid := v_company_guids[i];
87
88
-- Call initialize_company for each company
89
CALL public.initialize_company(
90
v_company_guid,
91
p_id,
92
v_company_name,
93
p_description,
94
p_gstin,
95
p_pan,
96
p_tan,
97
'INR',
98
p_short_name,
99
p_tag_line,
100
p_name,
101
p_phone_number,
102
p_email,
103
p_created_by,
104
v_user_id,
105
p_default_company_id
106
);
107
108
RAISE NOTICE 'Company % initialized with GUID %.', v_company_name, v_company_guid;
109
END LOOP;
110
111
-- Call initialize_org_coa to copy chart_of_accounts from the old organization to the new organization
112
CALL public.initialize_org_coa(
113
DEFAULT_ORGANIZATION_ID, -- Old organization ID
114
p_id, -- New organization ID
115
p_company_guids, -- New Company Id
116
p_created_by
117
);
118
119
-- Print a confirmation after the chart_of_accounts is copied
120
RAISE NOTICE 'Chart of accounts successfully copied from organization % to %.', DEFAULT_ORGANIZATION_ID, p_id;
121
122
END;
123
$procedure$
|
|||||
| Procedure | insert_bank_statement_by_excel | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.insert_bank_statement_by_excel(IN p_company_id uuid, IN p_txn_date date, IN p_cheque_number text, IN p_description text, IN p_value_date date, IN p_branch_code text, IN p_debit_amount numeric, IN p_credit_amount numeric, IN p_balance numeric, IN p_bank_id integer, IN p_created_by uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
existing_id INT;
6
BEGIN
7
-- Check if a record with same txn_date and description already exists for the company and bank
8
SELECT id INTO existing_id
9
FROM public.bank_statements
10
WHERE company_id = p_company_id
11
AND bank_id = p_bank_id
12
AND txn_date = p_txn_date
13
AND description = p_description
14
AND is_deleted = false
15
LIMIT 1;
16
17
IF existing_id IS NOT NULL THEN
18
-- Update the existing record
19
UPDATE public.bank_statements
20
SET
21
cheque_number = p_cheque_number,
22
value_date = p_value_date,
23
branch_code = p_branch_code,
24
debit_amount = p_debit_amount,
25
credit_amount = p_credit_amount,
26
balance = p_balance,
27
modified_by = p_created_by,
28
modified_on_utc = (CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Kolkata')::timestamp
29
WHERE id = existing_id;
30
31
RAISE NOTICE 'Updated existing bank statement with ID: %', existing_id;
32
ELSE
33
-- Insert a new record
34
INSERT INTO public.bank_statements (
35
company_id,
36
txn_date,
37
cheque_number,
38
description,
39
value_date,
40
branch_code,
41
debit_amount,
42
credit_amount,
43
balance,
44
bank_id,
45
created_by,
46
created_on_utc,
47
is_deleted
48
) VALUES (
49
p_company_id,
50
p_txn_date,
51
p_cheque_number,
52
p_description,
53
p_value_date,
54
p_branch_code,
55
p_debit_amount,
56
p_credit_amount,
57
p_balance,
58
p_bank_id,
59
p_created_by,
60
(CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Kolkata')::timestamp,
61
false
62
);
63
64
RAISE NOTICE 'Inserted new bank statement';
65
END IF;
66
EXCEPTION
67
WHEN OTHERS THEN
68
RAISE EXCEPTION 'Bank Statement insert/update failed: %', SQLERRM;
69
END;
70
$procedure$
|
|||||
| Procedure | insert_company_finance_years | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.insert_company_finance_years(IN p_company_id uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_finance_year_id INTEGER;
6
v_prev_finance_year_id INTEGER;
7
v_next_finance_year_id INTEGER;
8
BEGIN
9
-- Get the financial year ID based on the current date
10
SELECT id INTO v_finance_year_id
11
FROM finance_year
12
WHERE start_date <= NOW() AND end_date >= NOW();
13
14
-- Get the previous and next year IDs
15
SELECT id INTO v_prev_finance_year_id FROM finance_year WHERE id = v_finance_year_id - 1;
16
SELECT id INTO v_next_finance_year_id FROM finance_year WHERE id = v_finance_year_id + 1;
17
18
-- Insert for the current, previous, and next years (if not already inserted)
19
IF v_finance_year_id IS NOT NULL AND NOT EXISTS (
20
SELECT 1 FROM company_finance_year WHERE company_id = p_company_id AND finance_year_id = v_finance_year_id
21
) THEN
22
INSERT INTO company_finance_year (company_id, finance_year_id)
23
VALUES (p_company_id, v_finance_year_id);
24
END IF;
25
26
IF v_prev_finance_year_id IS NOT NULL AND NOT EXISTS (
27
SELECT 1 FROM company_finance_year WHERE company_id = p_company_id AND finance_year_id = v_prev_finance_year_id
28
) THEN
29
INSERT INTO company_finance_year (company_id, finance_year_id)
30
VALUES (p_company_id, v_prev_finance_year_id);
31
END IF;
32
33
IF v_next_finance_year_id IS NOT NULL AND NOT EXISTS (
34
SELECT 1 FROM company_finance_year WHERE company_id = p_company_id AND finance_year_id = v_next_finance_year_id
35
) THEN
36
INSERT INTO company_finance_year (company_id, finance_year_id)
37
VALUES (p_company_id, v_next_finance_year_id);
38
END IF;
39
40
RAISE NOTICE 'Data inserted for company_id: %', p_company_id;
41
END;
42
$procedure$
|
|||||
| Procedure | insert_company_user | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.insert_company_user(IN p_user_id uuid, IN p_created_by uuid, IN p_company_id uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_existing_id uuid;
6
v_is_deleted boolean;
7
BEGIN
8
-- Check if user-company relationship exists (including soft-deleted)
9
SELECT id, is_deleted
10
INTO v_existing_id, v_is_deleted
11
FROM public.company_users
12
WHERE user_id = p_user_id
13
AND company_id = p_company_id
14
LIMIT 1;
15
16
-- If exists and active (not deleted), do nothing
17
IF FOUND AND v_is_deleted = false THEN
18
RAISE NOTICE 'User % already exists in company % (active), skipping insert.', p_user_id, p_company_id;
19
20
-- If exists but soft-deleted, reactivate the record
21
ELSIF FOUND AND v_is_deleted = true THEN
22
UPDATE public.company_users
23
SET is_deleted = false,
24
modified_on_utc = NOW(),
25
modified_by = p_created_by,
26
deleted_on_utc = NULL
27
WHERE id = v_existing_id;
28
29
RAISE NOTICE 'User % re-activated in company % (id=%)', p_user_id, p_company_id, v_existing_id;
30
31
-- If no record found, insert new
32
ELSE
33
INSERT INTO public.company_users (
34
id,
35
user_id,
36
company_id,
37
effective_start_date,
38
effective_end_date,
39
created_on_utc,
40
created_by,
41
is_deleted
42
) VALUES (
43
gen_random_uuid(),
44
p_user_id,
45
p_company_id,
46
NOW(),
47
DATE '9999-12-31',
48
NOW(),
49
p_created_by,
50
false
51
);
52
53
RAISE NOTICE 'Company-user relationship created for user % in company %', p_user_id, p_company_id;
54
END IF;
55
END;
56
$procedure$
|
|||||
| Procedure | insert_into_manual_journal | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.insert_into_manual_journal(IN p_company_id uuid, IN p_journal_header_id uuid, IN p_date date, IN p_amount numeric, IN p_note text, IN p_description text, IN p_is_debit boolean, IN p_account_id uuid, IN p_created_by uuid, IN p_journal_details_data jsonb, IN p_transaction_header_data jsonb, IN journal_entries_data jsonb)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_voucher_number character varying;
6
v_voucher_detail JSONB;
7
v_transaction_record JSONB;
8
v_transaction_result RECORD;
9
v_transactions_data JSONB;
10
BEGIN
11
v_voucher_number := get_new_voucher_number(p_company_id, p_date);
12
RAISE NOTICE 'new v_voucher_number: %', v_voucher_number;
13
14
-- Insert into journal_voucher_headers
15
INSERT INTO public.journal_voucher_headers(
16
id,
17
company_id,
18
date,
19
amount,
20
note,
21
is_debit,
22
account_id,
23
voucher_number,
24
created_by,
25
created_on_utc,
26
is_deleted
27
)
28
VALUES (
29
p_journal_header_id,
30
p_company_id,
31
p_date,
32
p_amount,
33
p_note,
34
p_is_debit,
35
p_account_id,
36
v_voucher_number,
37
p_created_by,
38
CURRENT_TIMESTAMP,
39
FALSE
40
);
41
42
RAISE NOTICE 'Inserted into journal_voucher_headers: %', p_journal_header_id;
43
44
-- Insert into journal_voucher_details
45
FOR v_voucher_detail IN SELECT * FROM jsonb_array_elements(p_journal_details_data)
46
LOOP
47
INSERT INTO public.journal_voucher_details(
48
id,
49
journal_header_id,
50
credit_account_id,
51
debit_account_id,
52
amount,
53
tds_tcs,
54
narration,
55
created_by,
56
created_on_utc,
57
is_deleted
58
)
59
VALUES (
60
(v_voucher_detail->>'detail_id')::UUID,
61
p_journal_header_id,
62
(v_voucher_detail->>'credit_account_id')::UUID,
63
(v_voucher_detail->>'debit_account_id')::UUID,
64
(v_voucher_detail->>'amount')::NUMERIC,
65
v_voucher_detail->>'tds_tcs',
66
v_voucher_detail->>'narration',
67
p_created_by,
68
CURRENT_TIMESTAMP,
69
FALSE
70
);
71
RAISE NOTICE 'Inserted into journal_voucher_details';
72
END LOOP;
73
74
-- Prepare single transaction record matching insert_multiple_transactions_and_journal_entries input
75
v_transaction_record := jsonb_build_object(
76
'company_id', p_transaction_header_data->>'company_id',
77
'customer_id', p_transaction_header_data->>'customer_id',
78
'vendor_id', p_transaction_header_data->>'vendor_id',
79
'employee_id', p_transaction_header_data->>'employee_id',
80
'transaction_date', p_transaction_header_data->>'transaction_date',
81
'transaction_source_type', p_transaction_header_data->>'transaction_source_type',
82
'status_id', p_transaction_header_data->>'status_id',
83
'document_id', p_transaction_header_data->>'document_id',
84
'document_number', v_voucher_number,
85
'description', p_transaction_header_data->>'description',
86
'user_id', p_created_by,
87
'journal_entries', journal_entries_data
88
);
89
90
v_transactions_data := jsonb_build_array(v_transaction_record);
91
92
-- Call the batch function to insert into transaction_headers and journal_entries (and narration)
93
FOR v_transaction_result IN
94
SELECT * FROM public.insert_multiple_transactions_and_journal_entries(v_transactions_data)
95
LOOP
96
RAISE NOTICE 'Inserted transaction_id: %, document_id: %',
97
v_transaction_result.transaction_id, v_transaction_result.document_id;
98
-- You can use v_transaction_result.transaction_id if you need to reference it for other inserts
99
END LOOP;
100
101
EXCEPTION
102
WHEN OTHERS THEN
103
RAISE EXCEPTION 'Error occurred: %', SQLERRM;
104
END;
105
$procedure$
|
|||||
| Procedure | insert_organization_company | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.insert_organization_company(IN p_organization_id uuid, IN p_organization_name text, IN p_description text, IN p_phone_number character varying, IN p_email character varying, IN p_address_line text, IN p_gstin text, IN p_tag_line text, IN p_city_id uuid, IN p_pin text, IN p_short_name text, IN p_pan text, IN p_tan text, IN p_outstanding_limit numeric, IN p_is_non_work boolean, IN p_interest_percentage numeric, IN p_created_by uuid, IN p_companies jsonb)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
company RECORD;
6
v_created_on_utc timestamp without time zone := CURRENT_TIMESTAMP;
7
BEGIN
8
-- Insert into the organizations table
9
CALL public.create_organization(
10
p_organization_id,
11
p_organization_name,
12
p_description,
13
p_phone_number,
14
p_email,
15
p_address_line,
16
p_gstin,
17
p_tag_line,
18
p_city_id,
19
p_pin,
20
p_short_name,
21
p_pan,
22
p_tan,
23
p_outstanding_limit,
24
p_is_non_work,
25
p_interest_percentage,
26
p_created_by,
27
v_created_on_utc
28
);
29
30
RAISE NOTICE 'Organization inserted with ID: %', p_organization_id;
31
32
-- Loop through the JSONB array of companies
33
FOR company IN
34
SELECT * FROM jsonb_to_recordset(p_companies) AS (
35
id uuid,
36
name text,
37
description text,
38
gstin text,
39
pan text,
40
tan text,
41
currency text,
42
short_name text,
43
tag_line text,
44
proprietor_name text,
45
outstanding_limit numeric,
46
is_non_work boolean,
47
interest_percentage numeric,
48
created_by uuid
49
)
50
LOOP
51
-- Insert into the companies table
52
CALL public.create_company(
53
company.id,
54
p_organization_id,
55
company.name,
56
company.description,
57
company.gstin,
58
company.pan,
59
company.tan,
60
company.currency,
61
company.short_name,
62
company.tag_line,
63
company.proprietor_name,
64
company.outstanding_limit,
65
company.is_non_work,
66
company.interest_percentage,
67
company.created_by,
68
v_created_on_utc
69
);
70
71
RAISE NOTICE 'Company inserted with ID: %', company.id;
72
END LOOP;
73
74
RAISE NOTICE 'Transaction completed successfully';
75
EXCEPTION
76
WHEN OTHERS THEN
77
RAISE NOTICE 'An error occurred: %, transaction rolled back', SQLERRM;
78
ROLLBACK;
79
END;
80
$procedure$
|
|||||
| Procedure | insert_organization_user | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.insert_organization_user(IN p_user_id uuid, IN p_created_by uuid, IN p_organization_id uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_existing_id uuid;
6
v_is_deleted boolean;
7
BEGIN
8
-- Check if user-organization relationship exists (including soft-deleted)
9
SELECT id, is_deleted
10
INTO v_existing_id, v_is_deleted
11
FROM public.organization_users
12
WHERE user_id = p_user_id
13
AND organization_id = p_organization_id
14
LIMIT 1;
15
16
-- If exists and active (not deleted), do nothing
17
-- If exists and active (not deleted), do nothing
18
-- If exists and active (not deleted), do nothing
19
-- If exists and active (not deleted), do nothing
20
-- If exists and active (not deleted), do nothing
21
IF FOUND AND v_is_deleted = false THEN
22
RAISE NOTICE 'User % already exists in organization % (active), skipping insert.', p_user_id, p_organization_id;
23
24
-- If exists but soft-deleted, reactivate the record
25
ELSIF FOUND AND v_is_deleted = true THEN
26
UPDATE public.organization_users
27
SET is_deleted = false,
28
modified_on_utc = NOW(),
29
modified_by = p_created_by,
30
deleted_on_utc = NULL
31
WHERE id = v_existing_id;
32
33
RAISE NOTICE 'User % re-activated in organization % (id=%)', p_user_id, p_organization_id, v_existing_id;
34
35
-- If no record found, insert new
36
ELSE
37
INSERT INTO public.organization_users(
38
id,
39
user_id,
40
organization_id,
41
effective_start_date,
42
effective_end_date,
43
created_on_utc,
44
created_by,
45
is_deleted
46
)
47
VALUES (
48
gen_random_uuid(),
49
p_user_id,
50
p_organization_id,
51
NOW(),
52
DATE '9999-12-31',
53
NOW(),
54
p_created_by,
55
false
56
);
57
58
RAISE NOTICE 'Organization-user relationship created for user % in organization %', p_user_id, p_organization_id;
59
END IF;
60
END;
61
$procedure$
|
|||||
| Procedure | insert_user_role | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.insert_user_role(IN p_user_id uuid, IN p_role_id integer, IN p_created_by uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_existing_id INT;
6
v_is_deleted BOOLEAN;
7
BEGIN
8
-- Look for existing role assignment for user
9
SELECT id, is_deleted
10
INTO v_existing_id, v_is_deleted
11
FROM public.user_roles
12
WHERE user_id = p_user_id
13
AND role_id = p_role_id
14
LIMIT 1;
15
16
-- Case 1: Record exists and active -> do nothing
17
IF FOUND AND v_is_deleted = false THEN
18
RAISE NOTICE 'Role % already active for user %, skipping insert', p_role_id, p_user_id;
19
20
-- Case 2: Record exists but soft-deleted -> reactivate
21
ELSIF FOUND AND v_is_deleted = true THEN
22
UPDATE public.user_roles
23
SET is_deleted = false,
24
modified_on_utc = NOW(),
25
modified_by = p_created_by,
26
deleted_on_utc = NULL
27
WHERE id = v_existing_id;
28
29
RAISE NOTICE 'Role % re-activated for user % (id=%)', p_role_id, p_user_id, v_existing_id;
30
31
-- Case 3: No record - insert new
32
ELSE
33
INSERT INTO public.user_roles (
34
user_id,
35
role_id,
36
created_on_utc,
37
created_by,
38
is_deleted,
39
start_date,
40
end_date
41
)
42
VALUES (
43
p_user_id,
44
p_role_id,
45
NOW(),
46
p_created_by,
47
false,
48
NOW(), -- Fixed start_date as now()
49
'9999-12-31 00:00:00'::timestamp -- Fixed end_date as default far future
50
);
51
52
RAISE NOTICE 'Role % assigned to user % (new)', p_role_id, p_user_id;
53
END IF;
54
END;
55
$procedure$
|
|||||
| Procedure | insert_user_roles | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.insert_user_roles(IN p_user_id uuid, IN p_role_ids integer[], IN p_organization_id uuid, IN p_created_by uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_role_id integer;
6
v_existing_id INT;
7
v_is_deleted BOOLEAN;
8
BEGIN
9
FOREACH v_role_id IN ARRAY p_role_ids
10
LOOP
11
-- Find matching user-role-org record
12
SELECT id, is_deleted
13
INTO v_existing_id, v_is_deleted
14
FROM public.user_roles
15
WHERE user_id = p_user_id
16
AND role_id = v_role_id
17
AND organization_id = p_organization_id
18
LIMIT 1;
19
20
-- If active record exists -> do nothing
21
IF FOUND AND v_is_deleted = false THEN
22
RAISE NOTICE 'Role % already active for user % in organization %, skipping insert', v_role_id, p_user_id, p_organization_id;
23
24
-- If found and soft-deleted -> reactivate
25
ELSIF FOUND AND v_is_deleted = true THEN
26
UPDATE public.user_roles
27
SET is_deleted = false,
28
modified_on_utc = NOW(),
29
modified_by = p_created_by,
30
deleted_on_utc = NULL
31
WHERE id = v_existing_id;
32
RAISE NOTICE 'Role % re-activated for user % in organization % (id=%)', v_role_id, p_user_id, p_organization_id, v_existing_id;
33
34
-- No record exists, insert new
35
ELSE
36
INSERT INTO public.user_roles (
37
user_id,
38
role_id,
39
organization_id,
40
created_on_utc,
41
created_by,
42
is_deleted,
43
start_date,
44
end_date
45
)
46
VALUES (
47
p_user_id,
48
v_role_id,
49
p_organization_id,
50
NOW(),
51
p_created_by,
52
false,
53
NOW(),
54
'9999-12-31 00:00:00'::timestamp
55
);
56
RAISE NOTICE 'Role % assigned to user % in organization % (new)', v_role_id, p_user_id, p_organization_id;
57
END IF;
58
END LOOP;
59
END;
60
$procedure$
|
|||||
| Procedure | migrate_opening_balances_for_organization | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.migrate_opening_balances_for_organization(IN p_organization_id uuid, IN p_finyear_id integer, IN p_created_by uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_status_id integer := 1;
6
v_transaction_source_type_id integer := 18; -- as per your instruction
7
v_description_template_id integer := 1;
8
v_entry_source_id integer := 1;
9
v_opening_equity uuid;
10
v_sundry_debtors uuid;
11
v_sundry_creditors uuid;
12
v_salary_payable uuid;
13
14
rec_company RECORD;
15
ob_rec RECORD;
16
v_transaction_id bigint;
17
v_document_number text;
18
v_transaction_date timestamp;
19
v_entry_type text;
20
v_entity_type text;
21
v_entity_id uuid;
22
v_main_account uuid;
23
v_should_insert boolean;
24
v_name text;
25
v_customer_id uuid;
26
v_vendor_id uuid;
27
v_employee_id uuid;
28
v_account_id uuid;
29
v_year_label text;
30
BEGIN
31
-- COA account IDs
32
SELECT id INTO v_opening_equity
33
FROM public.chart_of_accounts
34
WHERE account_type_id = (SELECT account_type_id FROM public.chart_of_accounts WHERE name = 'Opening Balance Equity' LIMIT 1)
35
AND name = 'Opening Balance Equity'
36
LIMIT 1;
37
IF v_opening_equity IS NULL THEN
38
RAISE EXCEPTION 'Chart of Accounts "Opening Balance Equity" not found!';
39
END IF;
40
41
SELECT accounts_receivable_account_id, accounts_payable_account_id, salary_payable_account_id INTO v_sundry_debtors, v_sundry_creditors, v_salary_payable
42
FROM public.organization_accounts
43
WHERE organization_id = p_organization_id;
44
45
IF v_sundry_debtors IS NULL THEN
46
RAISE EXCEPTION 'Chart of Accounts "Sundry Debtors" not found!';
47
END IF;
48
49
IF v_sundry_creditors IS NULL THEN
50
RAISE EXCEPTION 'Chart of Accounts "Sundry Creditors" not found!';
51
END IF;
52
53
IF v_salary_payable IS NULL THEN
54
RAISE EXCEPTION 'Chart of Accounts "Salary Payable" not found!';
55
END IF;
56
57
-- Set transaction_date and year label from the finance year
58
SELECT start_date, "year" INTO v_transaction_date, v_year_label FROM public.finance_year WHERE id = p_finyear_id;
59
IF v_transaction_date IS NULL OR v_year_label IS NULL THEN
60
RAISE EXCEPTION 'Finance year with id % not found or year column is NULL!', p_finyear_id;
61
END IF;
62
63
-- Create a temporary table to track inserted COA accounts
64
CREATE TEMP TABLE IF NOT EXISTS temp_inserted_coa_accounts (account_id uuid PRIMARY KEY);
65
66
-- For each company in the organization, with the selected finance year
67
FOR rec_company IN
68
SELECT c.id AS company_id
69
FROM public.companies c
70
JOIN public.company_finance_year cfy ON c.id = cfy.company_id
71
WHERE c.organization_id = p_organization_id
72
AND cfy.finance_year_id = p_finyear_id
73
LOOP
74
-- For each opening balance for this organization and year
75
FOR ob_rec IN
76
SELECT *
77
FROM public.opening_balances
78
WHERE organization_id = p_organization_id
79
AND finyear_id = p_finyear_id
80
LOOP
81
v_should_insert := false;
82
v_name := null;
83
v_customer_id := null;
84
v_vendor_id := null;
85
v_employee_id := null;
86
v_account_id := null;
87
88
-- Determine main account and entity type, and check mapping
89
IF ob_rec.customer_id IS NOT NULL THEN
90
v_entity_type := 'Customer';
91
v_entity_id := ob_rec.customer_id;
92
v_main_account := v_sundry_debtors;
93
94
-- Check customers.company_id = rec_company.company_id
95
SELECT c.name INTO v_name
96
FROM public.customers c
97
WHERE c.id = ob_rec.customer_id AND c.company_id = rec_company.company_id;
98
IF v_name IS NOT NULL THEN
99
v_should_insert := true;
100
v_document_number := v_name; -- Only the name, not 'OPENBAL-' || v_name
101
v_customer_id := ob_rec.customer_id;
102
END IF;
103
104
ELSIF ob_rec.vendor_id IS NOT NULL THEN
105
v_entity_type := 'Vendor';
106
v_entity_id := ob_rec.vendor_id;
107
v_main_account := v_sundry_creditors;
108
109
SELECT v.name INTO v_name
110
FROM public.vendors v
111
WHERE v.id = ob_rec.vendor_id AND v.company_id = rec_company.company_id;
112
IF v_name IS NOT NULL THEN
113
v_should_insert := true;
114
v_document_number := v_name; -- Only the name, not 'OPENBAL-' || v_name
115
v_vendor_id := ob_rec.vendor_id;
116
END IF;
117
118
ELSIF ob_rec.employee_id IS NOT NULL THEN
119
v_entity_type := 'Employee';
120
v_entity_id := ob_rec.employee_id;
121
v_main_account := v_salary_payable;
122
123
SELECT e.first_name INTO v_name
124
FROM public.employees e
125
WHERE e.id = ob_rec.employee_id AND e.company_id = rec_company.company_id;
126
IF v_name IS NOT NULL THEN
127
v_should_insert := true;
128
v_document_number := v_name; -- Only the name, not 'OPENBAL-' || v_name
129
v_employee_id := ob_rec.employee_id;
130
END IF;
131
132
ELSE
133
-- COA-only
134
v_entity_type := 'COA';
135
v_entity_id := ob_rec.account_id;
136
v_main_account := ob_rec.account_id;
137
138
-- Get COA name
139
SELECT c.name INTO v_name
140
FROM public.chart_of_accounts c
141
WHERE c.id = ob_rec.account_id;
142
IF v_name IS NOT NULL THEN
143
v_document_number := v_name; -- Only the name, not 'OPENBAL-' || v_name
144
v_account_id := ob_rec.account_id;
145
-- insert only if this coa id is not already present
146
IF NOT EXISTS (SELECT 1 FROM temp_inserted_coa_accounts WHERE account_id = v_account_id) THEN
147
v_should_insert := true;
148
INSERT INTO temp_inserted_coa_accounts(account_id) VALUES (v_account_id);
149
END IF;
150
END IF;
151
END IF;
152
153
IF v_should_insert THEN
154
-- Insert into transaction_headers
155
INSERT INTO public.transaction_headers (
156
company_id, transaction_date, status_id, document_number, description, created_on_utc, created_by, transaction_source_type,
157
customer_id, vendor_id, employee_id, document_id
158
)
159
VALUES (
160
rec_company.company_id,
161
v_transaction_date,
162
v_status_id,
163
v_document_number,
164
'Opening Balance for ' || v_year_label, -- Only the year label from finance_year
165
v_transaction_date,
166
p_created_by,
167
v_transaction_source_type_id,
168
v_customer_id,
169
v_vendor_id,
170
v_employee_id,
171
v_account_id
172
)
173
RETURNING id INTO v_transaction_id;
174
175
-- Insert into journal_entries (double-entry)
176
INSERT INTO public.journal_entries (
177
transaction_id, transaction_date, amount, entry_type, description_template_id, dynamic_data, entry_source_id, account_id
178
) VALUES (
179
v_transaction_id, v_transaction_date, ob_rec.balance, ob_rec.entry_type, v_description_template_id, '{}'::jsonb, v_entry_source_id, v_main_account
180
);
181
182
-- Opening Balance Equity
183
IF ob_rec.entry_type = 'D' THEN
184
v_entry_type := 'C';
185
ELSE
186
v_entry_type := 'D';
187
END IF;
188
INSERT INTO public.journal_entries (
189
transaction_id, transaction_date, amount, entry_type, description_template_id, dynamic_data, entry_source_id, account_id
190
) VALUES (
191
v_transaction_id, v_transaction_date, ob_rec.balance, v_entry_type, v_description_template_id, '{}'::jsonb, v_entry_source_id, v_opening_equity
192
);
193
END IF;
194
END LOOP;
195
END LOOP;
196
197
DROP TABLE IF EXISTS temp_inserted_coa_accounts;
198
END;
199
$procedure$
|
|||||
| Procedure | update_bank_transfers | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.update_bank_transfers(IN p_company_id uuid, IN p_bank_transfer_id uuid, IN p_transfer_date timestamp without time zone, IN p_amount numeric, IN p_description text, IN p_source_account_id uuid, IN p_target_account_id uuid, IN p_mode_of_payment integer, IN p_reference text, IN p_modified_by uuid, IN p_transaction_header_data jsonb, IN p_journal_entries_data jsonb)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_now_utc timestamp := (now() at time zone 'utc');
6
v_header_id bigint;
7
v_txn_ts timestamp;
8
v_txn_date date;
9
v_desc text;
10
v_status integer;
11
v_source_type integer := 12; -- default BankTransfer source type
12
v_credit_count integer;
13
v_debit_count integer;
14
v_credit_id bigint;
15
v_debit_id bigint;
16
v_credit_rec jsonb;
17
v_debit_rec jsonb;
18
BEGIN
19
-- 0) Validate the bank transfer exists & active
20
IF NOT EXISTS (
21
SELECT 1
22
FROM public.bank_transfers bt
23
WHERE bt.id = p_bank_transfer_id
24
AND bt.company_id = p_company_id
25
AND bt.is_deleted = FALSE
26
) THEN
27
RAISE EXCEPTION 'Bank transfer % not found for company %', p_bank_transfer_id, p_company_id
28
USING ERRCODE = 'P0002';
29
END IF;
30
31
-- 1) Update bank_transfers (note: description keeps existing if empty passed)
32
UPDATE public.bank_transfers
33
SET transfer_date = p_transfer_date::timestamp,
34
amount = p_amount,
35
source_account_id = p_source_account_id,
36
target_account_id = p_target_account_id,
37
mode_of_payment = p_mode_of_payment,
38
reference = p_reference,
39
description = COALESCE(NULLIF(p_description, ''), description),
40
modified_by = p_modified_by,
41
modified_on_utc = v_now_utc
42
WHERE id = p_bank_transfer_id
43
AND company_id = p_company_id
44
AND is_deleted = FALSE;
45
46
-- 2) Resolve header data from JSON (with safe fallbacks)
47
v_txn_ts := COALESCE(
48
NULLIF(p_transaction_header_data ->> 'transaction_date','')::timestamp,
49
p_transfer_date::timestamp
50
);
51
v_txn_date := v_txn_ts::date;
52
53
v_desc := COALESCE(
54
NULLIF(p_transaction_header_data ->> 'description',''),
55
(SELECT description FROM public.bank_transfers WHERE id = p_bank_transfer_id)
56
);
57
58
v_status := NULLIF(p_transaction_header_data ->> 'status_id','')::int;
59
v_source_type := COALESCE(NULLIF(p_transaction_header_data ->> 'transaction_source_type','')::int, v_source_type);
60
61
-- 3) Locate transaction header
62
SELECT th.id
63
INTO v_header_id
64
FROM public.transaction_headers th
65
WHERE th.company_id = p_company_id
66
AND th.document_id = p_bank_transfer_id
67
AND th.transaction_source_type = v_source_type
68
AND th.is_deleted = FALSE
69
LIMIT 1;
70
71
IF v_header_id IS NULL THEN
72
RAISE EXCEPTION 'Transaction header not found for bank transfer %', p_bank_transfer_id
73
USING ERRCODE = 'P0002';
74
END IF;
75
76
-- 4) Update header (partition key is transaction_date (timestamp))
77
UPDATE public.transaction_headers
78
SET transaction_date = v_txn_ts,
79
description = v_desc,
80
status_id = COALESCE(v_status, status_id),
81
modified_by = p_modified_by,
82
modified_on_utc = v_now_utc
83
WHERE id = v_header_id;
84
85
-- 5) Journal shape check (exactly 1C + 1D among active entries)
86
SELECT COUNT(*) FILTER (WHERE entry_type = 'C'),
87
COUNT(*) FILTER (WHERE entry_type = 'D')
88
INTO v_credit_count, v_debit_count
89
FROM public.journal_entries je
90
WHERE je.transaction_id = v_header_id
91
AND je.is_deleted = FALSE;
92
93
IF v_credit_count <> 1 OR v_debit_count <> 1 THEN
94
-- Soft-delete all active and re-insert from JSON payload
95
UPDATE public.journal_entries
96
SET is_deleted = TRUE,
97
deleted_on_utc = v_now_utc
98
WHERE transaction_id = v_header_id
99
AND is_deleted = FALSE;
100
101
-- Insert CREDIT entries
102
FOR v_credit_rec IN
103
SELECT elem
104
FROM jsonb_array_elements(p_journal_entries_data) AS elem
105
WHERE UPPER(elem ->> 'entry_type') = 'C'
106
LOOP
107
INSERT INTO public.journal_entries
108
(transaction_id, transaction_date, amount, entry_type,
109
description_template_id, dynamic_data, entry_source_id, account_id, is_deleted)
110
VALUES
111
(v_header_id,
112
COALESCE(NULLIF(v_credit_rec ->> 'transaction_date','')::date, v_txn_date),
113
(v_credit_rec ->> 'amount')::numeric,
114
'C',
115
COALESCE(NULLIF(v_credit_rec ->> 'description_template_id','')::int, 1),
116
COALESCE((v_credit_rec ->> 'dynamic_data')::jsonb, '{}'::jsonb),
117
COALESCE(NULLIF(v_credit_rec ->> 'entry_source_id','')::int, v_source_type),
118
(v_credit_rec ->> 'account_id')::uuid,
119
FALSE);
120
END LOOP;
121
122
-- Insert DEBIT entries
123
FOR v_debit_rec IN
124
SELECT elem
125
FROM jsonb_array_elements(p_journal_entries_data) AS elem
126
WHERE UPPER(elem ->> 'entry_type') = 'D'
127
LOOP
128
INSERT INTO public.journal_entries
129
(transaction_id, transaction_date, amount, entry_type,
130
description_template_id, dynamic_data, entry_source_id, account_id, is_deleted)
131
VALUES
132
(v_header_id,
133
COALESCE(NULLIF(v_debit_rec ->> 'transaction_date','')::date, v_txn_date),
134
(v_debit_rec ->> 'amount')::numeric,
135
'D',
136
COALESCE(NULLIF(v_debit_rec ->> 'description_template_id','')::int, 1),
137
COALESCE((v_debit_rec ->> 'dynamic_data')::jsonb, '{}'::jsonb),
138
COALESCE(NULLIF(v_debit_rec ->> 'entry_source_id','')::int, v_source_type),
139
(v_debit_rec ->> 'account_id')::uuid,
140
FALSE);
141
END LOOP;
142
143
ELSE
144
-- Update-in-place path
145
SELECT je.id INTO v_credit_id
146
FROM public.journal_entries je
147
WHERE je.transaction_id = v_header_id
148
AND je.entry_type = 'C'
149
AND je.is_deleted = FALSE
150
LIMIT 1;
151
152
SELECT je.id INTO v_debit_id
153
FROM public.journal_entries je
154
WHERE je.transaction_id = v_header_id
155
AND je.entry_type = 'D'
156
AND je.is_deleted = FALSE
157
LIMIT 1;
158
159
SELECT elem INTO v_credit_rec
160
FROM jsonb_array_elements(p_journal_entries_data) AS elem
161
WHERE UPPER(elem ->> 'entry_type') = 'C'
162
LIMIT 1;
163
164
SELECT elem INTO v_debit_rec
165
FROM jsonb_array_elements(p_journal_entries_data) AS elem
166
WHERE UPPER(elem ->> 'entry_type') = 'D'
167
LIMIT 1;
168
169
-- Fallbacks if payload is missing either side
170
IF v_credit_rec IS NULL THEN
171
v_credit_rec := jsonb_build_object(
172
'account_id', p_source_account_id::text,
173
'transaction_date', v_txn_date::text,
174
'amount', p_amount::text,
175
'entry_type', 'C',
176
'entry_source_id', v_source_type,
177
'description_template_id', 1,
178
'dynamic_data', '{"type":"bank_transfer"}'
179
);
180
END IF;
181
182
IF v_debit_rec IS NULL THEN
183
v_debit_rec := jsonb_build_object(
184
'account_id', p_target_account_id::text,
185
'transaction_date', v_txn_date::text,
186
'amount', p_amount::text,
187
'entry_type', 'D',
188
'entry_source_id', v_source_type,
189
'description_template_id', 1,
190
'dynamic_data', '{"type":"bank_transfer"}'
191
);
192
END IF;
193
194
UPDATE public.journal_entries
195
SET account_id = (v_credit_rec ->> 'account_id')::uuid,
196
transaction_date = COALESCE(NULLIF(v_credit_rec ->> 'transaction_date','')::date, v_txn_date),
197
amount = (v_credit_rec ->> 'amount')::numeric,
198
entry_source_id = COALESCE(NULLIF(v_credit_rec ->> 'entry_source_id','')::int, v_source_type),
199
description_template_id = COALESCE(NULLIF(v_credit_rec ->> 'description_template_id','')::int, 1),
200
dynamic_data = COALESCE((v_credit_rec ->> 'dynamic_data')::jsonb, '{}'::jsonb)
201
WHERE id = v_credit_id;
202
203
UPDATE public.journal_entries
204
SET account_id = (v_debit_rec ->> 'account_id')::uuid,
205
transaction_date = COALESCE(NULLIF(v_debit_rec ->> 'transaction_date','')::date, v_txn_date),
206
amount = (v_debit_rec ->> 'amount')::numeric,
207
entry_source_id = COALESCE(NULLIF(v_debit_rec ->> 'entry_source_id','')::int, v_source_type),
208
description_template_id = COALESCE(NULLIF(v_debit_rec ->> 'description_template_id','')::int, 1),
209
dynamic_data = COALESCE((v_debit_rec ->> 'dynamic_data')::jsonb, '{}'::jsonb)
210
WHERE id = v_debit_id;
211
END IF;
212
END;
213
$procedure$
|
|||||
| Procedure | upsert_user_permissions | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.upsert_user_permissions(IN p_user_id uuid, IN p_permission_ids uuid[], IN p_organization_id uuid, IN p_created_by uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_permission_id uuid;
6
v_existing_id int;
7
v_existing_deleted bool;
8
v_inserted_id int;
9
BEGIN
10
FOR v_permission_id IN SELECT unnest(p_permission_ids) LOOP
11
SELECT id, is_deleted INTO v_existing_id, v_existing_deleted
12
FROM public.user_permissions
13
WHERE user_id = p_user_id
14
AND permission_id = v_permission_id
15
AND organization_id = p_organization_id
16
FOR UPDATE;
17
18
IF v_existing_id IS NOT NULL AND NOT v_existing_deleted THEN
19
CONTINUE;
20
ELSIF v_existing_id IS NOT NULL AND v_existing_deleted THEN
21
UPDATE public.user_permissions
22
SET is_deleted = false,
23
deleted_on_utc = NULL,
24
modified_by = p_created_by,
25
modified_on_utc = NOW()
26
WHERE id = v_existing_id;
27
ELSE
28
INSERT INTO public.user_permissions (
29
user_id,
30
permission_id,
31
organization_id,
32
created_on_utc,
33
created_by,
34
is_deleted
35
) VALUES (
36
p_user_id,
37
v_permission_id,
38
p_organization_id,
39
NOW(),
40
p_created_by,
41
false
42
) RETURNING id INTO v_inserted_id;
43
END IF;
44
END LOOP;
45
END;
46
$procedure$
|
|||||
| Procedure | purge_common_organization_data | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.purge_common_organization_data(IN p_organization_ids uuid[] DEFAULT NULL::uuid[])
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_orgs uuid[];
6
v_company_ids uuid[];
7
v_user_ids uuid[];
8
v_contact_ids uuid[];
9
v_bank_account_ids uuid[];
10
v_bank_ids int[]; -- integer PK
11
v_address_ids uuid[];
12
BEGIN
13
-- Determine which organizations to clean
14
IF p_organization_ids IS NULL OR array_length(p_organization_ids, 1) IS NULL THEN
15
SELECT array_agg(id)
16
INTO v_orgs
17
FROM organizations
18
WHERE created_on_utc > '2025-05-23'
19
AND created_on_utc < (NOW() - interval '24 hours');
20
ELSE
21
v_orgs := p_organization_ids;
22
END IF;
23
24
IF v_orgs IS NULL OR array_length(v_orgs, 1) IS NULL THEN
25
RAISE NOTICE 'No organizations found for cleanup.';
26
RETURN;
27
END IF;
28
29
-- Log organizations to be cleaned up
30
RAISE NOTICE 'Organizations to be deleted: %', v_orgs;
31
32
33
-- Find all company_ids under these orgs
34
SELECT array_agg(id) INTO v_company_ids FROM companies WHERE organization_id = ANY(v_orgs);
35
36
-- Log companies to be cleaned up
37
RAISE NOTICE 'Companies to be deleted: %', v_company_ids;
38
39
-- Find all users under these companies
40
SELECT array_agg(id) INTO v_user_ids FROM users WHERE company_id = ANY(v_company_ids)
41
AND created_on_utc > '2025-05-23'
42
AND created_on_utc < (NOW() - interval '24 hours');
43
44
-- Find all contact_ids for company_contacts
45
SELECT array_agg(contact_id) INTO v_contact_ids FROM company_contacts WHERE company_id = ANY(v_company_ids);
46
47
-- Find all bank_account_ids for company_bank_accounts
48
SELECT array_agg(bank_account_id) INTO v_bank_account_ids FROM company_bank_accounts WHERE company_id = ANY(v_company_ids);
49
50
-- Find all bank_ids for bank_accounts
51
SELECT array_agg(bank_id) INTO v_bank_ids FROM bank_accounts WHERE id = ANY(v_bank_account_ids);
52
53
-- Find all addresses for warehouses (address_id)
54
SELECT array_agg(address_id) INTO v_address_ids FROM warehouses WHERE company_id = ANY(v_company_ids);
55
56
-------------------------------------------------------------------
57
-- CHILD TABLES: Delete by IDs collected above, in safe order
58
-------------------------------------------------------------------
59
60
-- 1. user_roles (must go before users)
61
DELETE FROM user_roles WHERE user_id = ANY(v_user_ids);
62
63
-- 2. company_contacts (before contacts)
64
DELETE FROM company_contacts WHERE company_id = ANY(v_company_ids);
65
66
-- 3. contacts (after company_contacts)
67
DELETE FROM contacts WHERE id = ANY(v_contact_ids);
68
69
-- 4. company_bank_accounts (before bank_accounts)
70
DELETE FROM company_bank_accounts WHERE company_id = ANY(v_company_ids);
71
72
-- 5. bank_accounts (before banks)
73
DELETE FROM bank_accounts WHERE id = ANY(v_bank_account_ids);
74
75
-- 6. banks (if no more bank_accounts reference them, safe to delete)
76
DELETE FROM banks WHERE id = ANY(v_bank_ids);
77
78
-- 7. warehouses (before addresses)
79
DELETE FROM warehouses WHERE company_id = ANY(v_company_ids);
80
81
-- 8. addresses (only addresses used by warehouses)
82
DELETE FROM addresses WHERE id = ANY(v_address_ids);
83
84
-------------------------------------------------------------------
85
-- Your original cleanup (core business, already comprehensive)
86
-------------------------------------------------------------------
87
88
-- Vendors & related
89
DELETE FROM journal_entries
90
WHERE (transaction_id, transaction_date) IN (
91
SELECT th.id, th.transaction_date
92
FROM transaction_headers th
93
JOIN vendors v ON th.vendor_id = v.id
94
WHERE v.company_id = ANY(v_company_ids)
95
);
96
97
DELETE FROM opening_balances
98
WHERE vendor_id IN (
99
SELECT id FROM vendors WHERE company_id = ANY(v_company_ids)
100
);
101
102
DELETE FROM transaction_headers
103
WHERE vendor_id IN (
104
SELECT id FROM vendors WHERE company_id = ANY(v_company_ids)
105
);
106
107
DELETE FROM vendors
108
WHERE company_id = ANY(v_company_ids);
109
110
-- Customers & related
111
DELETE FROM journal_entries
112
WHERE (transaction_id, transaction_date) IN (
113
SELECT th.id, th.transaction_date
114
FROM transaction_headers th
115
JOIN customers c ON th.customer_id = c.id
116
WHERE c.company_id = ANY(v_company_ids)
117
);
118
119
DELETE FROM opening_balances
120
WHERE customer_id IN (
121
SELECT id FROM customers WHERE company_id = ANY(v_company_ids)
122
);
123
124
DELETE FROM transaction_headers
125
WHERE customer_id IN (
126
SELECT id FROM customers WHERE company_id = ANY(v_company_ids)
127
);
128
129
DELETE FROM customers
130
WHERE company_id = ANY(v_company_ids);
131
132
-- Journal vouchers
133
DELETE FROM journal_voucher_details
134
WHERE journal_header_id IN (
135
SELECT id FROM journal_voucher_headers
136
WHERE company_id = ANY(v_company_ids)
137
);
138
139
DELETE FROM journal_voucher_headers
140
WHERE company_id = ANY(v_company_ids);
141
142
-- Transactions directly linked to company
143
DELETE FROM journal_entries
144
WHERE (transaction_id, transaction_date) IN (
145
SELECT id, transaction_date
146
FROM transaction_headers
147
WHERE company_id = ANY(v_company_ids)
148
);
149
150
DELETE FROM transaction_headers
151
WHERE company_id = ANY(v_company_ids);
152
153
-- Company-related cleanup
154
DELETE FROM general_ledgers WHERE company_id = ANY(v_company_ids);
155
DELETE FROM company_finance_year WHERE company_id = ANY(v_company_ids);
156
DELETE FROM company_preferences WHERE company_id = ANY(v_company_ids);
157
DELETE FROM company_upis WHERE company_id = ANY(v_company_ids);
158
DELETE FROM company_users WHERE company_id = ANY(v_company_ids);
159
160
-- Chart of accounts (org level)
161
DELETE FROM journal_entries
162
WHERE account_id IN (
163
SELECT id FROM chart_of_accounts WHERE organization_id = ANY(v_orgs)
164
);
165
166
DELETE FROM journal_voucher_header_id WHERE company_id = ANY(v_company_ids);
167
168
-- Org-level children
169
DELETE FROM organization_accounts WHERE organization_id = ANY(v_orgs);
170
DELETE FROM chart_of_accounts WHERE organization_id = ANY(v_orgs);
171
DELETE FROM email_templates WHERE organization_id = ANY(v_orgs);
172
DELETE FROM user_permissions WHERE organization_id = ANY(v_orgs);
173
DELETE FROM organization_users WHERE organization_id = ANY(v_orgs);
174
DELETE FROM account_groups WHERE organization_id = ANY(v_orgs);
175
176
-- Finally, companies and organizations
177
DELETE FROM companies WHERE id = ANY(v_company_ids);
178
DELETE FROM organizations WHERE id = ANY(v_orgs);
179
180
DELETE FROM users
181
WHERE company_id = ANY(v_company_ids)
182
AND created_on_utc > '2025-05-23'
183
AND created_on_utc < (NOW() - interval '24 hours');
184
185
186
RAISE NOTICE 'Deleted all common data for organizations: %', v_orgs;
187
END;
188
$procedure$
|
|||||
| Procedure | insert_user_permission | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.insert_user_permission(IN p_user_id uuid, IN p_organization_id uuid, IN p_permission_ids uuid[], IN p_created_by uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_permission_id uuid;
6
v_existing_id INT;
7
v_is_deleted BOOLEAN;
8
BEGIN
9
RAISE NOTICE '▶️ START insert_user_permission for user_id=%, org_id=%, created_by=%, permissions=%',
10
p_user_id, p_organization_id, p_created_by, p_permission_ids;
11
12
IF p_user_id IS NULL OR p_organization_id IS NULL OR p_created_by IS NULL THEN
13
RAISE EXCEPTION '❌ One of the input parameters is NULL (user_id=%, org_id=%, created_by=%)',
14
p_user_id, p_organization_id, p_created_by;
15
END IF;
16
17
FOREACH v_permission_id IN ARRAY p_permission_ids
18
LOOP
19
RAISE NOTICE '🔍 Checking permission_id=% for user_id=% in org_id=%',
20
v_permission_id, p_user_id, p_organization_id;
21
22
-- Check if the record exists
23
SELECT id, is_deleted
24
INTO v_existing_id, v_is_deleted
25
FROM public.user_permissions
26
WHERE user_id = p_user_id
27
AND organization_id = p_organization_id
28
AND permission_id = v_permission_id
29
LIMIT 1;
30
31
IF FOUND THEN
32
RAISE NOTICE '✅ Found existing record with id=%, is_deleted=%', v_existing_id, v_is_deleted;
33
ELSE
34
RAISE NOTICE '❌ No existing record found for this combination';
35
END IF;
36
37
IF FOUND AND v_is_deleted = false THEN
38
RAISE NOTICE '⚠️ Permission % already active for user %, skipping insert',
39
v_permission_id, p_user_id;
40
41
ELSIF FOUND AND v_is_deleted = true THEN
42
UPDATE public.user_permissions
43
SET is_deleted = false,
44
modified_on_utc = NOW(),
45
modified_by = p_created_by,
46
deleted_on_utc = NULL
47
WHERE id = v_existing_id;
48
49
RAISE NOTICE '♻️ Permission % re-activated for user % (id=%)',
50
v_permission_id, p_user_id, v_existing_id;
51
52
ELSE
53
RAISE NOTICE '✅ Inserting Permission % assigned to user % (new) org: %', v_permission_id, p_user_id, p_organization_id;
54
INSERT INTO public.user_permissions (
55
user_id,
56
organization_id,
57
permission_id,
58
created_on_utc,
59
created_by,
60
is_deleted
61
)
62
VALUES (
63
p_user_id,
64
p_organization_id,
65
v_permission_id,
66
NOW(),
67
p_created_by,
68
false
69
);
70
71
72
END IF;
73
END LOOP;
74
75
RAISE NOTICE '✅ Completed insert_user_permission for user_id=%', p_user_id;
76
END;
77
$procedure$
|
|||||
| Procedure | insert_multiple_bank_statements_by_excel | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.insert_multiple_bank_statements_by_excel(IN p_statements jsonb)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
stmt RECORD;
6
v_created_by UUID;
7
BEGIN
8
-- Get 'System' user or fallback
9
SELECT id INTO v_created_by
10
FROM public.users
11
WHERE first_name = 'System'
12
LIMIT 1;
13
14
IF v_created_by IS NULL THEN
15
RAISE NOTICE '⚠️ No System user found. Using NULL created_by.';
16
END IF;
17
18
-- Loop through input JSON
19
FOR stmt IN
20
SELECT * FROM jsonb_to_recordset(p_statements) AS (
21
company_id UUID,
22
txn_date TIMESTAMP,
23
cheque_number TEXT,
24
description TEXT,
25
value_date TIMESTAMP,
26
branch_code TEXT,
27
debit_amount NUMERIC,
28
credit_amount NUMERIC,
29
balance NUMERIC,
30
bank_id UUID,
31
created_by UUID
32
)
33
LOOP
34
BEGIN
35
-- Check for existing record using composite natural key
36
IF NOT EXISTS (
37
SELECT 1
38
FROM public.bank_statements bs
39
WHERE bs.company_id = stmt.company_id
40
AND bs.bank_id = stmt.bank_id
41
AND bs.is_deleted = FALSE
42
AND (
43
(stmt.txn_date::time <> '00:00:00'::time AND bs.txn_date = stmt.txn_date)
44
OR
45
(stmt.txn_date::time = '00:00:00'::time AND bs.txn_date::date = stmt.txn_date::date)
46
)
47
AND COALESCE(bs.debit_amount, 0) = COALESCE(stmt.debit_amount, 0)
48
AND COALESCE(bs.credit_amount, 0) = COALESCE(stmt.credit_amount, 0)
49
AND COALESCE(bs.cheque_number, '') = COALESCE(stmt.cheque_number, '')
50
AND COALESCE(bs.description, '') = COALESCE(stmt.description, '') -- Ensure description is checked
51
) THEN
52
-- Insert new record if not exists
53
INSERT INTO public.bank_statements (
54
company_id,
55
txn_date,
56
cheque_number,
57
description,
58
value_date,
59
branch_code,
60
debit_amount,
61
credit_amount,
62
balance,
63
bank_id,
64
created_by,
65
created_on_utc,
66
is_deleted,
67
has_reconciled
68
)
69
VALUES (
70
stmt.company_id,
71
stmt.txn_date,
72
stmt.cheque_number,
73
stmt.description,
74
stmt.value_date,
75
stmt.branch_code,
76
stmt.debit_amount,
77
stmt.credit_amount,
78
stmt.balance,
79
stmt.bank_id,
80
COALESCE(stmt.created_by, v_created_by),
81
(CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Kolkata')::timestamp,
82
FALSE,
83
FALSE
84
);
85
86
RAISE NOTICE '✅ Inserted: %, %', stmt.txn_date, stmt.description;
87
ELSE
88
-- Update existing record if found
89
UPDATE public.bank_statements
90
SET
91
cheque_number = stmt.cheque_number,
92
value_date = stmt.value_date,
93
branch_code = stmt.branch_code,
94
balance = stmt.balance,
95
modified_by = COALESCE(stmt.created_by, v_created_by),
96
modified_on_utc = (CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Kolkata')::timestamp
97
WHERE company_id = stmt.company_id
98
AND bank_id = stmt.bank_id
99
AND is_deleted = FALSE
100
AND (
101
(stmt.txn_date::time <> '00:00:00'::time AND txn_date = stmt.txn_date)
102
OR
103
(stmt.txn_date::time = '00:00:00'::time AND txn_date::date = stmt.txn_date::date)
104
)
105
AND COALESCE(debit_amount, 0) = COALESCE(stmt.debit_amount, 0)
106
AND COALESCE(credit_amount, 0) = COALESCE(stmt.credit_amount, 0)
107
AND COALESCE(cheque_number, '') = COALESCE(stmt.cheque_number, '')
108
AND COALESCE(description, '') = COALESCE(stmt.description, ''); -- Ensure description is checked
109
110
RAISE NOTICE '🔄 Updated: %, %', stmt.txn_date, stmt.description;
111
END IF;
112
113
EXCEPTION
114
WHEN OTHERS THEN
115
RAISE EXCEPTION '❌ Error for txn_date=%, desc=%, Error: %',
116
stmt.txn_date, stmt.description, SQLERRM;
117
END;
118
END LOOP;
119
120
RAISE NOTICE '🎉 All bank statements processed successfully.';
121
END;
122
$procedure$
|
|||||
| Procedure | upsert_user_permissions | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.upsert_user_permissions(IN p_user_id uuid, IN p_permission_ids uuid[], IN p_role_ids integer[], IN p_organization_id uuid, IN p_created_by uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_permission_id uuid;
6
v_role_id int;
7
v_existing_id int;
8
v_existing_deleted bool;
9
v_existing_role_id int;
10
BEGIN
11
-- 0️⃣ Soft-delete permissions NOT in the new list
12
UPDATE public.user_permissions
13
SET is_deleted = TRUE,
14
deleted_on_utc = NOW(),
15
modified_by = p_created_by,
16
modified_on_utc = NOW()
17
WHERE user_id = p_user_id
18
AND organization_id = p_organization_id
19
AND (p_permission_ids IS NULL OR permission_id <> ALL(p_permission_ids))
20
AND is_deleted = FALSE;
21
22
-- 1️⃣ Upsert (reactivate or insert) listed permissions
23
FOR v_permission_id IN SELECT unnest(p_permission_ids) LOOP
24
SELECT id, is_deleted
25
INTO v_existing_id, v_existing_deleted
26
FROM public.user_permissions
27
WHERE user_id = p_user_id
28
AND permission_id = v_permission_id
29
AND organization_id = p_organization_id
30
FOR UPDATE;
31
32
IF v_existing_id IS NOT NULL AND NOT v_existing_deleted THEN
33
CONTINUE;
34
ELSIF v_existing_id IS NOT NULL AND v_existing_deleted THEN
35
UPDATE public.user_permissions
36
SET is_deleted = FALSE,
37
deleted_on_utc = NULL,
38
modified_by = p_created_by,
39
modified_on_utc = NOW()
40
WHERE id = v_existing_id;
41
ELSE
42
INSERT INTO public.user_permissions (
43
user_id, permission_id, organization_id,
44
created_on_utc, created_by, is_deleted
45
) VALUES (
46
p_user_id, v_permission_id, p_organization_id,
47
NOW(), p_created_by, false
48
);
49
END IF;
50
END LOOP;
51
52
-- 2️⃣ Soft-delete roles NOT in the new list
53
UPDATE public.user_roles
54
SET is_deleted = TRUE,
55
deleted_on_utc = NOW(),
56
modified_by = p_created_by,
57
modified_on_utc = NOW()
58
WHERE user_id = p_user_id
59
AND organization_id = p_organization_id
60
AND (p_role_ids IS NULL OR role_id <> ALL(p_role_ids))
61
AND is_deleted = FALSE;
62
63
-- 3️⃣ Upsert (reactivate or insert) listed roles
64
FOR v_role_id IN SELECT unnest(p_role_ids) LOOP
65
SELECT id
66
INTO v_existing_role_id
67
FROM public.user_roles
68
WHERE user_id = p_user_id
69
AND role_id = v_role_id
70
AND organization_id = p_organization_id;
71
72
IF v_existing_role_id IS NULL THEN
73
INSERT INTO public.user_roles (
74
user_id, role_id, organization_id,
75
created_on_utc, created_by, start_date, is_deleted
76
) VALUES (
77
p_user_id, v_role_id, p_organization_id,
78
NOW(), p_created_by, NOW(), FALSE
79
);
80
ELSE
81
UPDATE public.user_roles
82
SET is_deleted = FALSE,
83
deleted_on_utc = NULL,
84
modified_by = p_created_by,
85
modified_on_utc = NOW()
86
WHERE id = v_existing_role_id
87
AND is_deleted = TRUE;
88
END IF;
89
END LOOP;
90
END;
91
$procedure$
|
|||||
| Procedure | upsert_user_permission_template | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.upsert_user_permission_template(IN p_user_id uuid, IN p_permission_template_ids integer[], IN p_created_by uuid, IN p_organization_id uuid DEFAULT NULL::uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_permission_id uuid;
6
v_existing_up_id int;
7
v_existing_up_deleted boolean;
8
v_inserted_up_id int;
9
v_org_user_id uuid;
10
BEGIN
11
-- nothing to do if no templates provided
12
IF p_permission_template_ids IS NULL OR array_length(p_permission_template_ids, 1) IS NULL THEN
13
RETURN;
14
END IF;
15
16
----------------------------------------------------------------
17
-- 1) Ensure organization_users row exists for this user + org
18
-- (Do this before touching user_permissions)
19
----------------------------------------------------------------
20
SELECT id
21
INTO v_org_user_id
22
FROM public.organization_users
23
WHERE user_id = p_user_id
24
AND organization_id IS NOT DISTINCT FROM p_organization_id
25
FOR UPDATE;
26
27
IF v_org_user_id IS NULL THEN
28
INSERT INTO public.organization_users (
29
id,
30
user_id,
31
effective_start_date,
32
effective_end_date,
33
created_on_utc,
34
is_deleted,
35
created_by,
36
organization_id
37
) VALUES (
38
gen_random_uuid(), -- requires pgcrypto (or replace with uuid_generate_v4())
39
p_user_id,
40
NOW(), -- effective_start_date
41
NOW() + INTERVAL '1 year', -- effective_end_date
42
NOW(), -- created_on_utc
43
false, -- is_deleted
44
p_created_by,
45
COALESCE(p_organization_id, '00000000-0000-0000-0000-000000000000'::uuid)
46
)
47
RETURNING id INTO v_org_user_id;
48
END IF;
49
50
----------------------------------------------------------------
51
-- 2) Upsert user_permissions for permission_ids from templates
52
----------------------------------------------------------------
53
FOR v_permission_id IN
54
SELECT DISTINCT ptm.permission_id
55
FROM public.permission_template_mappings ptm
56
WHERE ptm.permission_template_id = ANY (p_permission_template_ids)
57
LOOP
58
-- Look for an existing user_permissions row for this user, permission and organization.
59
SELECT id, is_deleted
60
INTO v_existing_up_id, v_existing_up_deleted
61
FROM public.user_permissions
62
WHERE user_id = p_user_id
63
AND permission_id = v_permission_id
64
AND organization_id IS NOT DISTINCT FROM p_organization_id
65
FOR UPDATE;
66
67
-- If it exists and is active, nothing to do.
68
IF v_existing_up_id IS NOT NULL AND NOT v_existing_up_deleted THEN
69
CONTINUE;
70
71
-- If it exists and is marked deleted, reactivate it.
72
ELSIF v_existing_up_id IS NOT NULL AND v_existing_up_deleted THEN
73
UPDATE public.user_permissions
74
SET is_deleted = false,
75
deleted_on_utc = NULL,
76
modified_by = p_created_by,
77
modified_on_utc = NOW()
78
WHERE id = v_existing_up_id;
79
80
-- Otherwise insert a new row.
81
ELSE
82
INSERT INTO public.user_permissions (
83
user_id,
84
permission_id,
85
organization_id,
86
created_on_utc,
87
created_by,
88
is_deleted
89
) VALUES (
90
p_user_id,
91
v_permission_id,
92
p_organization_id,
93
NOW(),
94
p_created_by,
95
false
96
) RETURNING id INTO v_inserted_up_id;
97
END IF;
98
END LOOP;
99
END;
100
$procedure$
|
|||||
| Procedure | insert_into_bank_transfers | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.insert_into_bank_transfers(IN p_company_id uuid, IN p_bank_transfer_id uuid, IN p_transfer_date date, IN p_amount numeric, IN p_description text, IN p_source_account_id uuid, IN p_target_account_id uuid, IN p_mode_of_payment integer, IN p_reference text, IN p_created_by uuid)
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_transfer_number text;
6
v_transaction_id bigint;
7
v_narration_id bigint;
8
BEGIN
9
-- 1️⃣ Generate transfer number
10
v_transfer_number :=
11
get_new_transfer_number(p_company_id, p_transfer_date);
12
13
-- 2️⃣ Insert bank transfer
14
INSERT INTO public.bank_transfers (
15
id,
16
company_id,
17
source_account_id,
18
target_account_id,
19
amount,
20
transfer_date,
21
mode_of_payment,
22
reference,
23
description,
24
is_bulk,
25
created_by,
26
created_on_utc,
27
is_deleted,
28
transfer_number
29
)
30
VALUES (
31
p_bank_transfer_id,
32
p_company_id,
33
p_source_account_id,
34
p_target_account_id,
35
p_amount,
36
p_transfer_date,
37
COALESCE(p_mode_of_payment, 0),
38
NULLIF(p_reference, ''),
39
p_description,
40
FALSE,
41
p_created_by,
42
CURRENT_TIMESTAMP,
43
FALSE,
44
v_transfer_number
45
);
46
47
-- 3️⃣ Insert transaction header
48
INSERT INTO public.transaction_headers (
49
company_id,
50
transaction_date,
51
transaction_source_type,
52
status_id,
53
document_id,
54
document_number,
55
description,
56
created_by,
57
created_on_utc
58
)
59
VALUES (
60
p_company_id,
61
p_transfer_date,
62
19, -- BANK_TRANSFER
63
1,
64
p_bank_transfer_id,
65
v_transfer_number,
66
p_description,
67
p_created_by,
68
CURRENT_TIMESTAMP
69
)
70
RETURNING id INTO v_transaction_id;
71
72
-- 4️⃣ Create narration ONCE
73
INSERT INTO public.journal_narrations (
74
transaction_date,
75
description_template_id,
76
dynamic_data,
77
created_on_utc,
78
created_by
79
)
80
VALUES (
81
p_transfer_date,
82
49, -- BANK_TRANSFER narration template
83
jsonb_build_object(
84
'type', 'bank_transfer',
85
'reference', p_reference
86
)::text,
87
CURRENT_TIMESTAMP,
88
p_created_by
89
)
90
RETURNING id INTO v_narration_id;
91
92
-- 5️⃣ Credit source account
93
INSERT INTO public.journal_entries (
94
transaction_id,
95
transaction_date,
96
account_id,
97
amount,
98
entry_type,
99
entry_source_id,
100
journal_narration_id
101
)
102
VALUES (
103
v_transaction_id,
104
p_transfer_date,
105
p_source_account_id,
106
p_amount,
107
'C',
108
2,
109
v_narration_id
110
);
111
112
-- 6️⃣ Debit target account
113
INSERT INTO public.journal_entries (
114
transaction_id,
115
transaction_date,
116
account_id,
117
amount,
118
entry_type,
119
entry_source_id,
120
journal_narration_id
121
)
122
VALUES (
123
v_transaction_id,
124
p_transfer_date,
125
p_target_account_id,
126
p_amount,
127
'D',
128
2,
129
v_narration_id
130
);
131
132
END;
133
$procedure$
|
|||||
| Procedure | update_existing_user_role_permissions | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.update_existing_user_role_permissions(IN p_user_id uuid, IN p_created_by uuid, IN p_organization_id uuid DEFAULT NULL::uuid, IN p_company_id uuid DEFAULT NULL::uuid, IN p_permission_ids uuid[] DEFAULT NULL::uuid[], IN p_role_ids integer[] DEFAULT NULL::integer[])
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_role_permission_ids uuid[];
6
v_final_permission_ids uuid[];
7
BEGIN
8
RAISE NOTICE 'Procedure started for user_id=%', p_user_id;
9
10
-- Insert user to organization
11
IF p_organization_id IS NOT NULL THEN
12
RAISE NOTICE 'Inserting user into organization: %', p_organization_id;
13
14
CALL public.insert_organization_user(
15
p_user_id,
16
p_created_by,
17
p_organization_id
18
);
19
END IF;
20
21
-- Insert user to company
22
IF p_company_id IS NOT NULL THEN
23
RAISE NOTICE 'Inserting user into company: %', p_company_id;
24
25
CALL public.insert_company_user(
26
p_user_id,
27
p_created_by,
28
p_company_id
29
);
30
END IF;
31
32
/* ---------------------------------------------
33
1. Fetch permission_ids from role_permissions
34
--------------------------------------------- */
35
IF p_role_ids IS NOT NULL THEN
36
RAISE NOTICE 'Fetching permissions for roles: %', p_role_ids;
37
38
SELECT ARRAY_AGG(DISTINCT rp.permission_id)
39
INTO v_role_permission_ids
40
FROM public.role_permissions rp
41
WHERE rp.role_id = ANY (p_role_ids)
42
AND rp.is_deleted = false;
43
44
RAISE NOTICE 'Permissions from roles: %', v_role_permission_ids;
45
END IF;
46
47
/* ---------------------------------------------
48
2. Merge direct permissions + role permissions
49
--------------------------------------------- */
50
v_final_permission_ids :=
51
ARRAY(
52
SELECT DISTINCT unnest(
53
COALESCE(p_permission_ids, '{}') ||
54
COALESCE(v_role_permission_ids, '{}')
55
)
56
);
57
58
RAISE NOTICE 'Final permission list: %', v_final_permission_ids;
59
60
/* ---------------------------------------------
61
3. Insert permissions to user
62
--------------------------------------------- */
63
IF v_final_permission_ids IS NOT NULL
64
AND array_length(v_final_permission_ids, 1) > 0
65
AND p_organization_id IS NOT NULL THEN
66
67
RAISE NOTICE 'Inserting permissions for user into organization %', p_organization_id;
68
69
CALL public.insert_user_permission(
70
p_user_id,
71
p_organization_id,
72
v_final_permission_ids,
73
p_created_by
74
);
75
ELSE
76
RAISE NOTICE 'No permissions inserted (empty list or organization missing)';
77
END IF;
78
79
/* ---------------------------------------------
80
4. Insert user roles
81
--------------------------------------------- */
82
IF p_role_ids IS NOT NULL THEN
83
RAISE NOTICE 'Assigning roles to user: %', p_role_ids;
84
85
CALL public.insert_user_roles(
86
p_user_id,
87
p_role_ids,
88
p_organization_id,
89
p_created_by
90
);
91
END IF;
92
93
RAISE NOTICE 'Procedure completed successfully for user_id=%', p_user_id;
94
END;
95
$procedure$
|
|||||
| Procedure | update_existing_user_role_permissions | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.update_existing_user_role_permissions(IN p_user_id uuid, IN p_created_by uuid, IN p_organization_id uuid DEFAULT NULL::uuid, IN p_company_id uuid DEFAULT NULL::uuid, IN p_role_ids integer[] DEFAULT NULL::integer[])
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_role_permission_ids uuid[];
6
BEGIN
7
RAISE NOTICE 'UPDATE procedure started for user_id=%', p_user_id;
8
9
/* ---------------------------------------------
10
Ensure user-organization & company mapping
11
--------------------------------------------- */
12
IF p_organization_id IS NOT NULL THEN
13
RAISE NOTICE 'Ensuring user exists in organization: %', p_organization_id;
14
15
CALL public.insert_organization_user(
16
p_user_id,
17
p_created_by,
18
p_organization_id
19
);
20
END IF;
21
22
IF p_company_id IS NOT NULL THEN
23
RAISE NOTICE 'Ensuring user exists in company: %', p_company_id;
24
25
CALL public.insert_company_user(
26
p_user_id,
27
p_created_by,
28
p_company_id
29
);
30
END IF;
31
32
/* ---------------------------------------------
33
1. Fetch permissions ONLY from roles
34
--------------------------------------------- */
35
IF p_role_ids IS NOT NULL THEN
36
RAISE NOTICE 'Roles received for update: %', p_role_ids;
37
38
SELECT ARRAY_AGG(DISTINCT rp.permission_id)
39
INTO v_role_permission_ids
40
FROM public.role_permissions rp
41
WHERE rp.role_id = ANY (p_role_ids)
42
AND rp.is_deleted = false;
43
44
RAISE NOTICE 'Permissions derived from roles: %', v_role_permission_ids;
45
ELSE
46
RAISE NOTICE 'No roles provided, skipping permission derivation';
47
END IF;
48
49
/* ---------------------------------------------
50
2. Update user permissions (role-based only)
51
--------------------------------------------- */
52
IF v_role_permission_ids IS NOT NULL
53
AND array_length(v_role_permission_ids, 1) > 0
54
AND p_organization_id IS NOT NULL THEN
55
56
RAISE NOTICE 'Updating user permissions using role-based permissions';
57
58
CALL public.insert_user_permission(
59
p_user_id,
60
p_organization_id,
61
v_role_permission_ids,
62
p_created_by
63
);
64
ELSE
65
RAISE NOTICE 'No role-based permissions to apply';
66
END IF;
67
68
/* ---------------------------------------------
69
3. Update user roles
70
--------------------------------------------- */
71
IF p_role_ids IS NOT NULL THEN
72
RAISE NOTICE 'Updating user roles: %', p_role_ids;
73
74
CALL public.insert_user_roles(
75
p_user_id,
76
p_role_ids,
77
p_organization_id,
78
p_created_by
79
);
80
END IF;
81
82
RAISE NOTICE 'UPDATE procedure completed successfully for user_id=%', p_user_id;
83
END;
84
$procedure$
|
|||||
| Procedure | update_existing_users_role_permissions | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.update_existing_users_role_permissions(IN p_user_ids uuid[], IN p_created_by uuid, IN p_organization_id uuid DEFAULT NULL::uuid, IN p_company_id uuid DEFAULT NULL::uuid, IN p_role_ids integer[] DEFAULT NULL::integer[])
2
LANGUAGE plpgsql
3
AS $procedure$
4
DECLARE
5
v_user_id uuid;
6
v_role_permission_ids uuid[];
7
BEGIN
8
RAISE NOTICE 'UPDATE procedure started for users=%', p_user_ids;
9
10
/* ---------------------------------------------
11
1. Fetch permissions ONLY from roles (once)
12
--------------------------------------------- */
13
IF p_role_ids IS NOT NULL THEN
14
RAISE NOTICE 'Roles received: %', p_role_ids;
15
16
SELECT ARRAY_AGG(DISTINCT rp.permission_id)
17
INTO v_role_permission_ids
18
FROM public.role_permissions rp
19
WHERE rp.role_id = ANY (p_role_ids)
20
AND rp.is_deleted = false;
21
22
RAISE NOTICE 'Permissions derived from roles: %', v_role_permission_ids;
23
ELSE
24
RAISE NOTICE 'No roles provided, skipping permission derivation';
25
END IF;
26
27
/* ---------------------------------------------
28
2. Loop through each user
29
--------------------------------------------- */
30
FOREACH v_user_id IN ARRAY p_user_ids
31
LOOP
32
RAISE NOTICE 'Processing user_id=%', v_user_id;
33
34
-- Ensure user-organization mapping
35
IF p_organization_id IS NOT NULL THEN
36
CALL public.insert_organization_user(
37
v_user_id,
38
p_created_by,
39
p_organization_id
40
);
41
END IF;
42
43
-- Ensure user-company mapping
44
IF p_company_id IS NOT NULL THEN
45
CALL public.insert_company_user(
46
v_user_id,
47
p_created_by,
48
p_company_id
49
);
50
END IF;
51
52
-- Assign role-based permissions
53
IF v_role_permission_ids IS NOT NULL
54
AND array_length(v_role_permission_ids, 1) > 0
55
AND p_organization_id IS NOT NULL THEN
56
57
CALL public.insert_user_permission(
58
v_user_id,
59
p_organization_id,
60
v_role_permission_ids,
61
p_created_by
62
);
63
END IF;
64
65
-- Assign roles
66
IF p_role_ids IS NOT NULL THEN
67
CALL public.insert_user_roles(
68
v_user_id,
69
p_role_ids,
70
p_organization_id,
71
p_created_by
72
);
73
END IF;
74
75
RAISE NOTICE 'Completed user_id=%', v_user_id;
76
END LOOP;
77
78
RAISE NOTICE 'UPDATE procedure completed for all users';
79
END;
80
$procedure$
|
|||||
| Procedure | add_existing_user_role_permissions | Missing in Target |
Source Script
Target Script
1
CREATE OR REPLACE PROCEDURE public.add_existing_user_role_permissions(IN p_user_id uuid, IN p_created_by uuid, IN p_organization_id uuid DEFAULT NULL::uuid, IN p_company_id uuid DEFAULT NULL::uuid, IN p_permission_ids uuid[] DEFAULT NULL::uuid[], IN p_role_ids integer[] DEFAULT NULL::integer[])
2
LANGUAGE plpgsql
3
AS $procedure$
4
BEGIN
5
-- Insert user to organization if organization_id is provided
6
IF p_organization_id IS NOT NULL THEN
7
CALL public.insert_organization_user(p_user_id, p_created_by, p_organization_id);
8
END IF;
9
10
-- Insert user to company if company_id is provided
11
IF p_company_id IS NOT NULL THEN
12
CALL public.insert_company_user(p_user_id, p_created_by, p_company_id);
13
END IF;
14
15
-- Insert permissions if both permission_ids array and organization_id are provided
16
IF p_permission_ids IS NOT NULL AND p_organization_id IS NOT NULL THEN
17
CALL public.insert_user_permission(p_user_id, p_organization_id, p_permission_ids, p_created_by);
18
END IF;
19
20
-- Insert roles if role_ids array is provided
21
IF p_role_ids IS NOT NULL THEN
22
CALL public.insert_user_roles(p_user_id, p_role_ids, p_organization_id, p_created_by);
23
END IF;
24
END;
25
$procedure$
|
|||||
| View | vw_permission_hierarchy | Missing in Target |
Source Script
Target Script
1
WITH RECURSIVE permission_tree AS (
2
SELECT p.id,
3
p.name,
4
p.parent_permission_id,
5
p.description,
6
1 AS level,
7
p.id AS root_permission_id
8
FROM permissions p
9
UNION ALL
10
SELECT c.id,
11
c.name,
12
c.parent_permission_id,
13
c.description,
14
(pt.level + 1) AS level,
15
pt.root_permission_id
16
FROM (permissions c
17
JOIN permission_tree pt ON ((c.parent_permission_id = pt.id)))
18
)
19
SELECT root_permission_id,
20
id AS permission_id,
21
name AS permission_name,
22
parent_permission_id,
23
description,
24
level
25
FROM permission_tree;
|
|||||
| View | vw_organization_summary | Missing in Target |
Source Script
Target Script
1
SELECT DISTINCT o.id AS organization_id,
2
o.name AS organization_name,
3
c.id AS company_id,
4
c.name AS company_name,
5
u.id AS user_id,
6
concat(u.first_name, ' ', u.last_name) AS user_full_name,
7
u.email AS user_email,
8
u.phone_number AS user_phone,
9
'Company User'::text AS user_role_source,
10
o.created_on_utc AS organization_created_on
11
FROM (((organizations o
12
JOIN companies c ON ((c.organization_id = o.id)))
13
JOIN company_users cu ON ((cu.company_id = c.id)))
14
JOIN users u ON ((u.id = cu.user_id)))
15
UNION
16
SELECT DISTINCT o.id AS organization_id,
17
o.name AS organization_name,
18
NULL::uuid AS company_id,
19
NULL::text AS company_name,
20
u.id AS user_id,
21
concat(u.first_name, ' ', u.last_name) AS user_full_name,
22
u.email AS user_email,
23
u.phone_number AS user_phone,
24
'Organization User'::text AS user_role_source,
25
o.created_on_utc AS organization_created_on
26
FROM ((organizations o
27
JOIN organization_users ou ON ((ou.organization_id = o.id)))
28
JOIN users u ON ((u.id = ou.user_id)));
|
|||||
| View | transaction_sums_by_party | Missing in Target |
Source Script
Target Script
1
SELECT th.customer_id,
2
th.vendor_id,
3
th.employee_id,
4
c.name AS customer_name,
5
v.name AS vendor_name,
6
sum(
7
CASE
8
WHEN ((je.entry_type = 'C'::bpchar) AND (coa.account_type_id = ANY (ARRAY[4, 14, 15, 19, 20]))) THEN je.amount
9
ELSE (0)::numeric
10
END) AS total_invoiced,
11
sum(
12
CASE
13
WHEN ((je.entry_type = 'D'::bpchar) AND (coa.account_type_id = ANY (ARRAY[5, 16, 17, 18, 21, 22]))) THEN je.amount
14
ELSE (0)::numeric
15
END) AS total_billed,
16
sum(
17
CASE
18
WHEN ((je.entry_type = 'C'::bpchar) AND (coa.account_type_id = ANY (ARRAY[8, 9]))) THEN je.amount
19
ELSE (0)::numeric
20
END) AS total_received,
21
sum(
22
CASE
23
WHEN ((je.entry_type = 'D'::bpchar) AND (coa.account_type_id = ANY (ARRAY[8, 9]))) THEN je.amount
24
ELSE (0)::numeric
25
END) AS total_paid
26
FROM ((((transaction_headers th
27
LEFT JOIN customers c ON ((th.customer_id = c.id)))
28
LEFT JOIN vendors v ON ((th.vendor_id = v.id)))
29
JOIN journal_entries je ON ((je.transaction_id = th.id)))
30
JOIN chart_of_accounts coa ON ((je.account_id = coa.id)))
31
WHERE ((th.is_deleted = false) AND (je.is_deleted = false) AND (th.company_id = '9891a03a-d80a-430e-ab33-c419f99cffa0'::uuid))
32
GROUP BY th.customer_id, th.vendor_id, th.employee_id, c.name, v.name
33
ORDER BY COALESCE(c.name, v.name);
|
|||||
| View | user_permission_view | Missing in Target |
Source Script
Target Script
1
SELECT ou.user_id,
2
(((u.first_name)::text || ' '::text) || (u.last_name)::text) AS user_name,
3
ou.organization_id,
4
o.name AS organization_name,
5
up.permission_id,
6
p.name AS permission_name,
7
p.description AS permission_description
8
FROM ((((organization_users ou
9
JOIN user_permissions up ON (((ou.user_id = up.user_id) AND (ou.organization_id = up.organization_id))))
10
JOIN permissions p ON ((up.permission_id = p.id)))
11
JOIN users u ON ((ou.user_id = u.id)))
12
JOIN organizations o ON ((ou.organization_id = o.id)))
13
WHERE ((ou.is_deleted = false) AND (up.is_deleted = false) AND (p.is_deleted = false) AND (u.is_deleted = false) AND (o.is_deleted = false));
|
|||||
| View | vw_user_permissions_details | Missing in Target |
Source Script
Target Script
1
SELECT up.id AS user_permission_id,
2
u.id AS user_id,
3
u.first_name AS user_name,
4
p.id AS permission_id,
5
p.name AS permission_name,
6
o.id AS organization_id,
7
o.name AS organization_name,
8
o.short_name AS organization_short_name,
9
o.phone_number AS organization_phone_number,
10
o.email AS organization_email,
11
o.gstin AS organization_gstin,
12
up.created_on_utc,
13
up.created_by,
14
up.modified_on_utc,
15
up.modified_by,
16
up.is_deleted,
17
p.deleted_on_utc AS permission_deleted_on_utc,
18
up.deleted_on_utc AS user_permission_deleted_on_utc
19
FROM (((user_permissions up
20
JOIN permissions p ON ((up.permission_id = p.id)))
21
JOIN users u ON ((up.user_id = u.id)))
22
JOIN organizations o ON ((up.organization_id = o.id)))
23
WHERE ((up.is_deleted = false) AND (p.is_deleted = false) AND (o.is_deleted = false));
|
|||||
| View | transaction_party_view | Missing in Target |
Source Script
Target Script
1
SELECT th.id AS transaction_id,
2
th.company_id,
3
th.transaction_date,
4
th.document_number,
5
th.description,
6
th.customer_id,
7
c.name AS customer_name,
8
th.vendor_id,
9
v.name AS vendor_name,
10
th.employee_id,
11
je.entry_type,
12
coa.account_type_id,
13
coa.name AS account_name,
14
je.amount,
15
CASE
16
WHEN ((je.entry_type = 'C'::bpchar) AND (coa.account_type_id = ANY (ARRAY[4, 14, 15, 19, 20]))) THEN je.amount
17
ELSE (0)::numeric
18
END AS invoiced,
19
CASE
20
WHEN ((je.entry_type = 'C'::bpchar) AND (coa.account_type_id = ANY (ARRAY[8, 9]))) THEN je.amount
21
ELSE (0)::numeric
22
END AS received,
23
CASE
24
WHEN ((je.entry_type = 'D'::bpchar) AND (coa.account_type_id = ANY (ARRAY[5, 16, 17, 18, 21, 22]))) THEN je.amount
25
ELSE (0)::numeric
26
END AS billed,
27
CASE
28
WHEN ((je.entry_type = 'D'::bpchar) AND (coa.account_type_id = ANY (ARRAY[8, 9]))) THEN je.amount
29
ELSE (0)::numeric
30
END AS paid
31
FROM ((((transaction_headers th
32
LEFT JOIN customers c ON ((th.customer_id = c.id)))
33
LEFT JOIN vendors v ON ((th.vendor_id = v.id)))
34
JOIN journal_entries je ON ((je.transaction_id = th.id)))
35
JOIN chart_of_accounts coa ON ((je.account_id = coa.id)))
36
WHERE ((th.is_deleted = false) AND (je.is_deleted = false));
|
|||||
| View | view_permissions | Missing in Target |
Source Script
Target Script
1
SELECT id,
2
name,
3
parent_permission_id,
4
tree_name,
5
description
6
FROM get_permissions() get_permissions(id, name, parent_permission_id, tree_name, description);
|
|||||
| Trigger | trg_user_permissions_notify | Missing in Target |
Source Script
Target Script
1
CREATE TRIGGER trg_user_permissions_notify AFTER INSERT OR DELETE OR UPDATE ON user_permissions FOR EACH ROW EXECUTE FUNCTION notify_user_permission_change()
|
-- Table: module_permissions
Exists in source, missing in target
-- Table: contacts
Exists in source, missing in target
-- Table: countries
Exists in source, missing in target
-- Table: currencies
Exists in source, missing in target
-- Table: __EFMigrationsHistory
Exists in source, missing in target
-- Table: company_upis
Exists in source, missing in target
-- Table: company_users
Exists in source, missing in target
-- Table: customers
Exists in source, missing in target
-- Table: default_organization_users
Exists in source, missing in target
-- Table: departments
Exists in source, missing in target
-- Table: user_roles
Exists in source, missing in target
-- Table: description_templates
Exists in source, missing in target
-- Table: role_permissions
Exists in source, missing in target
-- Table: chart_of_accounts
Exists in source, missing in target
-- Table: companies
Exists in source, missing in target
-- Table: users
Exists in source, missing in target
-- Table: users_bk
Exists in source, missing in target
-- Table: v_finance_year_id
Exists in source, missing in target
-- Table: v_is_second_last_leaf
Exists in source, missing in target
-- Table: v_permission_id
Exists in source, missing in target
-- Table: user_permissions
Exists in source, missing in target
-- Table: account_categories
Exists in source, missing in target
-- Table: account_groups
Exists in source, missing in target
-- Table: document_meta_datas
Exists in source, missing in target
-- Table: account_opening_balances
Exists in source, missing in target
-- Table: bank_reconciliation_links
Exists in source, missing in target
-- Table: bank_transfers
Exists in source, missing in target
-- Table: journal_voucher_details
Exists in source, missing in target
-- Table: schema_versions
Exists in source, missing in target
-- Table: states
Exists in source, missing in target
-- Table: temp_organization_logs
Exists in source, missing in target
-- Table: temp_paymnet_data
Exists in source, missing in target
-- Table: bank_reconciliation_stagings
Exists in source, missing in target
-- Table: payment_references
Exists in source, missing in target
-- Table: journal_narrations
Exists in source, missing in target
-- Table: organization_users
Exists in source, missing in target
-- Table: transaction_header_2024_2025
Exists in source, missing in target
-- Table: bank_transfer_ids
Exists in source, missing in target
-- Table: banks
Exists in source, missing in target
-- Table: budget_lines
Exists in source, missing in target
-- Table: budget_statuses
Exists in source, missing in target
-- Table: budget_workflow
Exists in source, missing in target
-- Table: budgets
Exists in source, missing in target
-- Table: cities
Exists in source, missing in target
-- Table: cron_dummy_log
Exists in source, missing in target
-- Table: permission_groups
Exists in source, missing in target
-- Table: company_bank_accounts
Exists in source, missing in target
-- Table: company_contacts
Exists in source, missing in target
-- Table: company_finance_year
Exists in source, missing in target
-- Table: journal_entries
Exists in source, missing in target
-- Table: warehouses
Exists in source, missing in target
-- Table: transaction_header_2022_2023
Exists in source, missing in target
-- Table: transaction_header_2023_2024
Exists in source, missing in target
-- Table: bank_reconciliations
Exists in source, missing in target
-- Table: feature_toggles
Exists in source, missing in target
-- Table: user_permissions_backup
Exists in source, missing in target
-- Table: journal_entries_2022_2023
Exists in source, missing in target
-- Table: feature_toggle_overrides
Exists in source, missing in target
-- Table: user_permission_sync_runs
Exists in source, missing in target
-- Table: user_permission_sync_deltas
Exists in source, missing in target
-- Table: scope_types
Exists in source, missing in target
-- Table: journal_entries_2023_2024
Exists in source, missing in target
-- Table: journal_entries_2024_2025
Exists in source, missing in target
-- Table: journal_entries_2025_2026
Exists in source, missing in target
-- Table: journal_voucher_header_id
Exists in source, missing in target
-- Table: journal_voucher_headers
Exists in source, missing in target
-- Table: actions
Exists in source, missing in target
-- Table: feature_toggle_audits
Exists in source, missing in target
-- Table: roles
Exists in source, missing in target
-- Table: opening_balances
Exists in source, missing in target
-- Table: organization_accounts
Exists in source, missing in target
-- Table: bank_statements
Exists in source, missing in target
-- Table: account_types
Exists in source, missing in target
-- Table: addresses
Exists in source, missing in target
-- Table: audit_queries
Exists in source, missing in target
-- Table: audit_query_responses
Exists in source, missing in target
-- Table: audit_query_statuses
Exists in source, missing in target
-- Table: bank_accounts
Exists in source, missing in target
-- Table: transaction_header_2025_2026
Exists in source, missing in target
-- Table: transaction_headers
Exists in source, missing in target
-- Table: logs
Exists in source, missing in target
-- Table: messages
Exists in source, missing in target
-- Table: notification_types
Exists in source, missing in target
-- Table: notifications
Exists in source, missing in target
-- Table: organization_types
Exists in source, missing in target
-- Table: organizations
Exists in source, missing in target
-- Table: permissions
Exists in source, missing in target
-- Table: reconciliation_statuses
Exists in source, missing in target
-- Table: transaction_headers_19_05_25
Exists in source, missing in target
-- Table: transaction_headers_bk_20_5_25
Exists in source, missing in target
-- Table: vendors
Exists in source, missing in target
-- Table: transaction_source_types
Exists in source, missing in target
-- Table: upis
Exists in source, missing in target
-- Table: user_deletion_request_statuses
Exists in source, missing in target
-- Table: user_deletion_requests
Exists in source, missing in target
-- Table: user_global_permissions
Exists in source, missing in target
-- Table: company_preferences
Exists in source, missing in target
-- Table: designations
Exists in source, missing in target
-- Table: dummy_log_table
Exists in source, missing in target
-- Table: dummy_test_table
Exists in source, missing in target
-- Table: email_templates
Exists in source, missing in target
-- Table: employees
Exists in source, missing in target
-- Table: entry_sources
Exists in source, missing in target
-- Table: finance_year
Exists in source, missing in target
-- Table: fixed_deposits
Exists in source, missing in target
-- Table: general_ledgers
Exists in source, missing in target
-- Table: images
Exists in source, missing in target
-- Function: pgp_sym_decrypt_bytea
CREATE OR REPLACE FUNCTION public.pgp_sym_decrypt_bytea(bytea, text, text)
RETURNS bytea
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_sym_decrypt_bytea$function$
-- Function: pgp_pub_encrypt
CREATE OR REPLACE FUNCTION public.pgp_pub_encrypt(text, bytea)
RETURNS bytea
LANGUAGE c
PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_pub_encrypt_text$function$
-- Function: pgp_pub_encrypt_bytea
CREATE OR REPLACE FUNCTION public.pgp_pub_encrypt_bytea(bytea, bytea)
RETURNS bytea
LANGUAGE c
PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_pub_encrypt_bytea$function$
-- Function: pgp_pub_encrypt
CREATE OR REPLACE FUNCTION public.pgp_pub_encrypt(text, bytea, text)
RETURNS bytea
LANGUAGE c
PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_pub_encrypt_text$function$
-- Function: pgp_pub_decrypt
CREATE OR REPLACE FUNCTION public.pgp_pub_decrypt(bytea, bytea, text)
RETURNS text
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_pub_decrypt_text$function$
-- Function: pgp_pub_decrypt_bytea
CREATE OR REPLACE FUNCTION public.pgp_pub_decrypt_bytea(bytea, bytea, text)
RETURNS bytea
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_pub_decrypt_bytea$function$
-- Function: pgp_pub_decrypt
CREATE OR REPLACE FUNCTION public.pgp_pub_decrypt(bytea, bytea, text, text)
RETURNS text
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_pub_decrypt_text$function$
-- Function: pgp_pub_decrypt_bytea
CREATE OR REPLACE FUNCTION public.pgp_pub_decrypt_bytea(bytea, bytea, text, text)
RETURNS bytea
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_pub_decrypt_bytea$function$
-- Function: pgp_key_id
CREATE OR REPLACE FUNCTION public.pgp_key_id(bytea)
RETURNS text
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_key_id_w$function$
-- Function: armor
CREATE OR REPLACE FUNCTION public.armor(bytea)
RETURNS text
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pg_armor$function$
-- Function: armor
CREATE OR REPLACE FUNCTION public.armor(bytea, text[], text[])
RETURNS text
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pg_armor$function$
-- Function: dearmor
CREATE OR REPLACE FUNCTION public.dearmor(text)
RETURNS bytea
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pg_dearmor$function$
-- Function: pgp_armor_headers
CREATE OR REPLACE FUNCTION public.pgp_armor_headers(text, OUT key text, OUT value text)
RETURNS SETOF record
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_armor_headers$function$
-- Function: fips_mode
CREATE OR REPLACE FUNCTION public.fips_mode()
RETURNS boolean
LANGUAGE c
PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pg_check_fipsmode$function$
-- Function: uuid_nil
CREATE OR REPLACE FUNCTION public.uuid_nil()
RETURNS uuid
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/uuid-ossp', $function$uuid_nil$function$
-- Function: create_user
CREATE OR REPLACE FUNCTION public.create_user(p_user_id uuid, p_company_id uuid, p_email text, p_phone_number text, p_first_name text, p_last_name text, p_address_id uuid, p_created_by uuid)
RETURNS uuid
LANGUAGE plpgsql
AS $function$
DECLARE
v_existing_user_id UUID; -- Variable to store the user ID
BEGIN
-- Step 1: Check if the user already exists based on email
SELECT id INTO v_existing_user_id
FROM public.users
WHERE email = p_email;
-- If user already exists, return the existing user_id
IF v_existing_user_id IS NOT NULL THEN
RETURN v_existing_user_id;
END IF;
-- Step 2: Insert into users table
INSERT INTO public.users (
id,
company_id,
email,
phone_number,
first_name,
last_name,
password_hash,
address_id,
created_on_utc,
created_by
) VALUES (
p_user_id, -- Generate a new UUID for the user ID
p_company_id,
p_email, -- User email
p_phone_number, -- User phone number
p_first_name, -- User first name
p_last_name, -- Default empty last name
'', -- Default empty password hash
p_address_id, -- Provided Address ID
NOW(), -- Current timestamp
p_created_by -- Created by
);
-- Return the new user ID
RETURN p_user_id;
END;
$function$
-- Function: uuid_ns_dns
CREATE OR REPLACE FUNCTION public.uuid_ns_dns()
RETURNS uuid
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/uuid-ossp', $function$uuid_ns_dns$function$
-- Function: uuid_ns_url
CREATE OR REPLACE FUNCTION public.uuid_ns_url()
RETURNS uuid
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/uuid-ossp', $function$uuid_ns_url$function$
-- Function: uuid_ns_oid
CREATE OR REPLACE FUNCTION public.uuid_ns_oid()
RETURNS uuid
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/uuid-ossp', $function$uuid_ns_oid$function$
-- Function: uuid_ns_x500
CREATE OR REPLACE FUNCTION public.uuid_ns_x500()
RETURNS uuid
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/uuid-ossp', $function$uuid_ns_x500$function$
-- Function: uuid_generate_v1
CREATE OR REPLACE FUNCTION public.uuid_generate_v1()
RETURNS uuid
LANGUAGE c
PARALLEL SAFE STRICT
AS '$libdir/uuid-ossp', $function$uuid_generate_v1$function$
-- Function: uuid_generate_v1mc
CREATE OR REPLACE FUNCTION public.uuid_generate_v1mc()
RETURNS uuid
LANGUAGE c
PARALLEL SAFE STRICT
AS '$libdir/uuid-ossp', $function$uuid_generate_v1mc$function$
-- Function: uuid_generate_v3
CREATE OR REPLACE FUNCTION public.uuid_generate_v3(namespace uuid, name text)
RETURNS uuid
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/uuid-ossp', $function$uuid_generate_v3$function$
-- Function: uuid_generate_v4
CREATE OR REPLACE FUNCTION public.uuid_generate_v4()
RETURNS uuid
LANGUAGE c
PARALLEL SAFE STRICT
AS '$libdir/uuid-ossp', $function$uuid_generate_v4$function$
-- Function: uuid_generate_v5
CREATE OR REPLACE FUNCTION public.uuid_generate_v5(namespace uuid, name text)
RETURNS uuid
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/uuid-ossp', $function$uuid_generate_v5$function$
-- Function: generate_permission_description
CREATE OR REPLACE FUNCTION public.generate_permission_description(p_name text)
RETURNS text
LANGUAGE plpgsql
AS $function$
DECLARE
parts text[];
module text;
tail text[];
action text;
object_tokens text[];
object_raw text;
object_phrase text;
is_myspace boolean := false;
-- scratch vars for “humanize” and “pluralize”
t text;
obj_lc text;
BEGIN
-- Expect Dhanman.<Module>...
parts := regexp_split_to_array(p_name, '\.');
IF array_length(parts, 1) IS NULL OR parts[1] <> 'Dhanman' THEN
RETURN NULL; -- not our namespace
END IF;
module := parts[2];
is_myspace := (module = 'MySpace');
IF array_length(parts, 1) < 3 THEN
-- e.g., Dhanman.Read / Dhanman.Write / Dhanman.Delete
RETURN trim(both ' ' FROM regexp_replace(module, '([a-z])([A-Z])', '\1 \2', 'g')) || ' permission';
END IF;
tail := parts[3:array_length(parts,1)]; -- everything after module
-- detect multi-word actions first
IF tail[array_length(tail,1)] = 'SendForApproval' THEN
action := 'SendForApproval';
IF array_length(tail,1) > 1 THEN
object_tokens := tail[1:array_length(tail,1)-1];
ELSE
object_tokens := ARRAY[]::text[];
END IF;
ELSIF tail[array_length(tail,1)] IN ('IdRead','TokenRead') THEN
action := 'Read';
IF array_length(tail,1) > 1 THEN
object_tokens := tail[1:array_length(tail,1)-1];
ELSE
object_tokens := ARRAY[]::text[];
END IF;
ELSE
-- simple actions (last token)
IF tail[array_length(tail,1)] IN ('Read','Write','Delete','Approve','Cancel','Reject','Resolve','Assign','Copy','Pay','Import') THEN
action := tail[array_length(tail,1)];
IF array_length(tail,1) > 1 THEN
object_tokens := tail[1:array_length(tail,1)-1];
ELSE
object_tokens := ARRAY[]::text[];
END IF;
ELSE
-- No explicit action => treat the last token as (maybe) object and assume base Read on module
action := NULL;
object_tokens := tail;
END IF;
END IF;
-- join tokens with '.' to get a raw object handle
object_raw := NULL;
IF object_tokens IS NOT NULL AND array_length(object_tokens,1) IS NOT NULL THEN
SELECT string_agg(x, '.' ORDER BY ord)
INTO object_raw
FROM (
SELECT object_tokens[i] AS x, i AS ord
FROM generate_subscripts(object_tokens,1) AS i
) s;
END IF;
-- If action is NULL, try to interpret tail as a “Read/Write/Delete” on module (rare fallback)
IF action IS NULL THEN
IF object_raw ILIKE 'Read' THEN
t := trim(both ' ' FROM regexp_replace(module, '([a-z])([A-Z])', '\1 \2', 'g'));
RETURN 'Read ' || lower(t) || ' data';
ELSIF object_raw ILIKE 'Write' THEN
t := trim(both ' ' FROM regexp_replace(module, '([a-z])([A-Z])', '\1 \2', 'g'));
RETURN 'Add/Update ' || lower(t) || ' data';
ELSIF object_raw ILIKE 'Delete' THEN
RETURN 'Delete general data';
END IF;
-- else continue with generic handling below
END IF;
-- ===== normalize object to a nicer phrase =====
object_phrase := NULL;
IF object_raw IS NOT NULL AND object_raw <> '' THEN
obj_lc := lower(object_raw);
-- specific aliases
IF obj_lc ~* '^basic(\.read|\.write|\.delete)?$' THEN
object_phrase := 'Basic Info';
ELSIF obj_lc LIKE 'ticketstatus%' THEN
object_phrase := 'Ticket Status';
ELSIF obj_lc = 'invoicetemplate' THEN
object_phrase := 'Invoice Template';
ELSIF obj_lc = 'groupedinvoice' THEN
object_phrase := 'Grouped Invoice';
ELSIF obj_lc = 'recurringinvoice' THEN
object_phrase := 'Recurring Invoice';
ELSIF obj_lc = 'customeraccountconfig' THEN
object_phrase := 'Customer Account Configuration';
ELSIF obj_lc = 'vendoraccountconfig' THEN
object_phrase := 'Vendor Account Configuration';
ELSIF obj_lc = 'bankstatement' THEN
object_phrase := 'Bank Statement';
ELSIF obj_lc = 'banktransfer' THEN
object_phrase := 'Bank Transfer';
ELSIF obj_lc = 'fixeddeposit' THEN
object_phrase := 'Fixed Deposit';
ELSE
-- generic humanize: add spaces before capitals
object_phrase := trim(both ' ' FROM regexp_replace(object_raw, '([a-z])([A-Z])', '\1 \2', 'g'));
END IF;
END IF;
-- quick helpers inlined:
-- humanize(module)
t := trim(both ' ' FROM regexp_replace(module, '([a-z])([A-Z])', '\1 \2', 'g'));
-- module phrase
t := t || ' module';
-- MySpace special-casing (root objects)
IF is_myspace AND (object_phrase IS NULL OR object_phrase = '') THEN
IF action = 'Read' THEN
RETURN 'View own My Space data. (myspace)';
ELSIF action = 'Write' THEN
RETURN 'Create or update own data. (myspace)';
ELSIF action = 'Delete' THEN
RETURN 'Delete own entries. (myspace)';
END IF;
END IF;
-- pluralize (very naive) if we need a plural in messages
-- compute plural into obj_lc (reuse var)
IF object_phrase IS NOT NULL AND object_phrase <> '' THEN
obj_lc := lower(object_phrase);
-- don’t pluralize these:
IF obj_lc NOT IN ('info','data','ledger','cash flow','journal','basicinfo','basic info') THEN
IF obj_lc !~ '(s|x|z|ch|sh)$' THEN
object_phrase := object_phrase || 's';
END IF;
END IF;
END IF;
-- Render phrases by action
IF is_myspace THEN
-- MySpace + specific objects
IF action = 'Read' AND object_phrase IS NOT NULL THEN
IF lower(object_phrase) IN ('user', 'users') THEN
RETURN 'View user (own) details. (myspace)';
ELSIF lower(object_phrase) IN ('due','dues') THEN
RETURN 'View dues summary. (myspace)';
ELSE
RETURN 'View own ' || lower(object_phrase) || '. (myspace)';
END IF;
END IF;
-- For other MySpace actions on specific objects, fall through to generic wording below
END IF;
-- rebuild a fresh humanized module text for generic lines
t := trim(both ' ' FROM regexp_replace(module, '([a-z])([A-Z])', '\1 \2', 'g'));
t := t || ' module';
CASE action
WHEN 'Read' THEN
IF object_phrase IS NULL OR object_phrase = '' THEN
RETURN 'Read ' || lower(trim(both ' ' FROM regexp_replace(module, '([a-z])([A-Z])', '\1 \2', 'g'))) || ' data';
ELSE
RETURN 'Read ' || lower(object_phrase) || ' in ' || t;
END IF;
WHEN 'Write' THEN
IF object_phrase IS NULL OR object_phrase = '' THEN
RETURN 'Add/Update ' || lower(trim(both ' ' FROM regexp_replace(module, '([a-z])([A-Z])', '\1 \2', 'g'))) || ' data';
ELSE
RETURN 'Add/Update ' || lower(object_phrase) || ' in ' || t;
END IF;
WHEN 'Delete' THEN
IF object_phrase IS NULL OR object_phrase = '' THEN
RETURN 'Delete general data';
ELSE
RETURN 'Delete ' || lower(object_phrase) || ' in ' || t;
END IF;
WHEN 'Approve' THEN
IF object_phrase IS NULL OR object_phrase = '' THEN
RETURN 'Approve ' || lower(trim(both ' ' FROM regexp_replace(module, '([a-z])([A-Z])', '\1 \2', 'g'))) || ' actions';
ELSE
RETURN 'Approve ' || lower(object_phrase) || ' in the ' || t;
END IF;
WHEN 'Cancel' THEN
RETURN 'Cancel ' || lower(COALESCE(object_phrase,'items')) || ' in ' || t;
WHEN 'Reject' THEN
RETURN 'Reject ' || lower(COALESCE(object_phrase,'items')) || ' in ' || t;
WHEN 'Resolve' THEN
RETURN 'Mark ' || lower(COALESCE(object_phrase,'items')) || ' as resolved in the ' || t;
WHEN 'Assign' THEN
RETURN 'Assign ' || lower(COALESCE(object_phrase,'items')) || ' in the ' || t;
WHEN 'Copy' THEN
RETURN 'Copy ' || lower(COALESCE(object_phrase,'items')) || ' in ' || t;
WHEN 'Pay' THEN
RETURN 'Make payments for ' || lower(COALESCE(object_phrase,'items'));
WHEN 'SendForApproval' THEN
RETURN 'Send ' || lower(COALESCE(object_phrase,'items')) || ' for approval';
WHEN 'Import' THEN
RETURN 'Import ' || lower(COALESCE(object_phrase,'items')) || ' data';
ELSE
-- Fallback
IF object_phrase IS NULL OR object_phrase = '' THEN
RETURN trim(both ' ' FROM regexp_replace(module, '([a-z])([A-Z])', '\1 \2', 'g')) || ' permission';
ELSE
RETURN trim(both ' ' FROM regexp_replace(module, '([a-z])([A-Z])', '\1 \2', 'g'))
|| ' ' || trim(both ' ' FROM regexp_replace(object_phrase, '([a-z])([A-Z])', '\1 \2', 'g'))
|| ' permission';
END IF;
END CASE;
END;
$function$
-- Function: get_account_expense_monthly_breakdown
CREATE OR REPLACE FUNCTION public.get_account_expense_monthly_breakdown(p_company_id uuid, p_finance_year_id integer)
RETURNS TABLE(account_id uuid, account_name text, year integer, apr numeric, apr_diff numeric, may numeric, may_diff numeric, jun numeric, jun_diff numeric, jul numeric, jul_diff numeric, aug numeric, aug_diff numeric, sep numeric, sep_diff numeric, oct numeric, oct_diff numeric, nov numeric, nov_diff numeric, "dec" numeric, dec_diff numeric, jan numeric, jan_diff numeric, feb numeric, feb_diff numeric, mar numeric, mar_diff numeric, total numeric, difference numeric)
LANGUAGE plpgsql
AS $function$
DECLARE
v_organization_id UUID;
v_start_date DATE;
v_end_date DATE;
v_prev_start_date DATE;
v_prev_end_date DATE;
BEGIN
-- Get organization and financial year details
SELECT c.organization_id INTO v_organization_id FROM public.companies c WHERE c.id = p_company_id;
SELECT fy.start_date, fy.end_date INTO v_start_date, v_end_date FROM public.finance_year fy WHERE fy.id = p_finance_year_id;
SELECT pfy.start_date, pfy.end_date INTO v_prev_start_date, v_prev_end_date FROM public.finance_year pfy WHERE pfy.id = (p_finance_year_id - 1);
RETURN QUERY
WITH expense_accounts AS (
SELECT DISTINCT coa.id AS account_id, coa.name
FROM chart_of_accounts coa
JOIN account_types at ON coa.account_type_id = at.id
WHERE at.id IN (SELECT id FROM get_account_type_hierarchy(5)) AND coa.organization_id = v_organization_id
),
years AS (
SELECT p_finance_year_id AS year UNION ALL SELECT p_finance_year_id - 1
),
all_accounts AS (
SELECT ea.account_id, ea.name, y.year
FROM expense_accounts ea CROSS JOIN years y
),
monthly_expenses AS (
-- Get monthly totals, excluding opening balance entries
SELECT
je.account_id,
CASE
WHEN je.transaction_date BETWEEN v_start_date AND v_end_date THEN p_finance_year_id
WHEN je.transaction_date BETWEEN v_prev_start_date AND v_prev_end_date THEN (p_finance_year_id - 1)
END AS year,
EXTRACT(MONTH FROM je.transaction_date) AS month,
SUM(je.amount) AS amount
FROM journal_entries je
JOIN transaction_headers th ON je.transaction_id = th.id
WHERE th.company_id = p_company_id
AND je.transaction_date BETWEEN v_prev_start_date AND v_end_date
AND th.transaction_source_type != 18
AND th.is_deleted = FALSE
AND je.is_deleted = FALSE
GROUP BY je.account_id, year, EXTRACT(MONTH FROM je.transaction_date)
),
pivoted_expenses AS (
-- Pivot data to month-wise columns
SELECT
aa.account_id, aa.name AS account_name, aa.year,
COALESCE(SUM(CASE WHEN me.month = 4 THEN me.amount ELSE 0 END), 0) AS apr,
COALESCE(SUM(CASE WHEN me.month = 5 THEN me.amount ELSE 0 END), 0) AS may,
COALESCE(SUM(CASE WHEN me.month = 6 THEN me.amount ELSE 0 END), 0) AS jun,
COALESCE(SUM(CASE WHEN me.month = 7 THEN me.amount ELSE 0 END), 0) AS jul,
COALESCE(SUM(CASE WHEN me.month = 8 THEN me.amount ELSE 0 END), 0) AS aug,
COALESCE(SUM(CASE WHEN me.month = 9 THEN me.amount ELSE 0 END), 0) AS sep,
COALESCE(SUM(CASE WHEN me.month = 10 THEN me.amount ELSE 0 END), 0) AS oct,
COALESCE(SUM(CASE WHEN me.month = 11 THEN me.amount ELSE 0 END), 0) AS nov,
COALESCE(SUM(CASE WHEN me.month = 12 THEN me.amount ELSE 0 END), 0) AS "dec",
COALESCE(SUM(CASE WHEN me.month = 1 THEN me.amount ELSE 0 END), 0) AS jan,
COALESCE(SUM(CASE WHEN me.month = 2 THEN me.amount ELSE 0 END), 0) AS feb,
COALESCE(SUM(CASE WHEN me.month = 3 THEN me.amount ELSE 0 END), 0) AS mar,
COALESCE(SUM(me.amount), 0) AS total
FROM all_accounts aa
LEFT JOIN monthly_expenses me ON aa.account_id = me.account_id AND aa.year = me.year
GROUP BY aa.account_id, aa.name, aa.year
)
-- Final table with difference calculation
SELECT
pe.account_id, pe.account_name, pe.year,
pe.apr, COALESCE(pe.apr - LAG(pe.apr) OVER (PARTITION BY pe.account_name ORDER BY pe.year), 0) AS apr_diff,
pe.may, COALESCE(pe.may - LAG(pe.may) OVER (PARTITION BY pe.account_name ORDER BY pe.year), 0) AS may_diff,
pe.jun, COALESCE(pe.jun - LAG(pe.jun) OVER (PARTITION BY pe.account_name ORDER BY pe.year), 0) AS jun_diff,
pe.jul, COALESCE(pe.jul - LAG(pe.jul) OVER (PARTITION BY pe.account_name ORDER BY pe.year), 0) AS jul_diff,
pe.aug, COALESCE(pe.aug - LAG(pe.aug) OVER (PARTITION BY pe.account_name ORDER BY pe.year), 0) AS aug_diff,
pe.sep, COALESCE(pe.sep - LAG(pe.sep) OVER (PARTITION BY pe.account_name ORDER BY pe.year), 0) AS sep_diff,
pe.oct, COALESCE(pe.oct - LAG(pe.oct) OVER (PARTITION BY pe.account_name ORDER BY pe.year), 0) AS oct_diff,
pe.nov, COALESCE(pe.nov - LAG(pe.nov) OVER (PARTITION BY pe.account_name ORDER BY pe.year), 0) AS nov_diff,
pe.dec, COALESCE(pe.dec - LAG(pe.dec) OVER (PARTITION BY pe.account_name ORDER BY pe.year), 0) AS dec_diff,
pe.jan, COALESCE(pe.jan - LAG(pe.jan) OVER (PARTITION BY pe.account_name ORDER BY pe.year), 0) AS jan_diff,
pe.feb, COALESCE(pe.feb - LAG(pe.feb) OVER (PARTITION BY pe.account_name ORDER BY pe.year), 0) AS feb_diff,
pe.mar, COALESCE(pe.mar - LAG(pe.mar) OVER (PARTITION BY pe.account_name ORDER BY pe.year), 0) AS mar_diff,
pe.total,
COALESCE(pe.total - LAG(pe.total) OVER (PARTITION BY pe.account_name ORDER BY pe.year), 0) AS difference
FROM pivoted_expenses pe
ORDER BY pe.account_name, pe.year;
END;
$function$
-- Function: get_permissions
CREATE OR REPLACE FUNCTION public.get_permissions()
RETURNS TABLE(id uuid, name text, parent_permission_id uuid, tree_name text, description text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
WITH RECURSIVE permissions_cte AS (
SELECT
p.id,
p.name,
p.parent_permission_id,
0 AS level,
p.name::text AS tree_name,
p.name::text AS sort_key,
p.description
FROM
public.permissions p
WHERE
p.parent_permission_id = '00000000-0000-0000-0000-000000000000'
UNION ALL
SELECT
p.id,
p.name,
p.parent_permission_id,
cte.level + 1 AS level,
(LPAD('', (cte.level + 1) * 4, ' ') || p.name::text)::text AS tree_name,
(cte.sort_key || ' > ' || p.name::text)::text AS sort_key,
p.description
FROM
public.permissions p
INNER JOIN
permissions_cte cte ON cte.id = p.parent_permission_id
)
SELECT
cte.id,
cte.name,
cte.parent_permission_id,
cte.tree_name::text,
cte.description
FROM
permissions_cte cte
ORDER BY
cte.sort_key;
END;
$function$
-- Function: get_five_years_expenses
CREATE OR REPLACE FUNCTION public.get_five_years_expenses(p_company_id uuid, p_finance_year_id integer)
RETURNS TABLE(id uuid, financial_year_range text, account_name text, y1_apr numeric, y1_may numeric, y1_jun numeric, y1_jul numeric, y1_aug numeric, y1_sep numeric, y1_oct numeric, y1_nov numeric, y1_dec numeric, y1_jan numeric, y1_feb numeric, y1_mar numeric, y2_apr numeric, y2_may numeric, y2_jun numeric, y2_jul numeric, y2_aug numeric, y2_sep numeric, y2_oct numeric, y2_nov numeric, y2_dec numeric, y2_jan numeric, y2_feb numeric, y2_mar numeric, y3_apr numeric, y3_may numeric, y3_jun numeric, y3_jul numeric, y3_aug numeric, y3_sep numeric, y3_oct numeric, y3_nov numeric, y3_dec numeric, y3_jan numeric, y3_feb numeric, y3_mar numeric, y4_apr numeric, y4_may numeric, y4_jun numeric, y4_jul numeric, y4_aug numeric, y4_sep numeric, y4_oct numeric, y4_nov numeric, y4_dec numeric, y4_jan numeric, y4_feb numeric, y4_mar numeric, y5_apr numeric, y5_may numeric, y5_jun numeric, y5_jul numeric, y5_aug numeric, y5_sep numeric, y5_oct numeric, y5_nov numeric, y5_dec numeric, y5_jan numeric, y5_feb numeric, y5_mar numeric)
LANGUAGE plpgsql
STABLE PARALLEL SAFE
AS $function$
DECLARE
current_year_start DATE;
current_year_end DATE;
previous_year1_start DATE;
previous_year1_end DATE;
previous_year2_start DATE;
previous_year2_end DATE;
previous_year3_start DATE;
previous_year3_end DATE;
previous_year4_start DATE;
previous_year4_end DATE;
CONST_EXPENSE_TYPE_ID INTEGER := 5;
BEGIN
-- Get start/end of current year
SELECT fy.start_date, fy.end_date
INTO current_year_start, current_year_end
FROM public.finance_year fy
WHERE fy.id = p_finance_year_id;
IF current_year_start IS NULL OR current_year_end IS NULL THEN
RAISE EXCEPTION 'Financial year with ID % not found', p_finance_year_id;
END IF;
-- Previous year 1
SELECT fy.start_date, fy.end_date
INTO previous_year1_start, previous_year1_end
FROM public.finance_year fy
WHERE fy.end_date < current_year_start
ORDER BY fy.end_date DESC
LIMIT 1;
-- Previous year 2
SELECT fy.start_date, fy.end_date
INTO previous_year2_start, previous_year2_end
FROM public.finance_year fy
WHERE fy.end_date < previous_year1_start
ORDER BY fy.end_date DESC
LIMIT 1;
-- Previous year 3
SELECT fy.start_date, fy.end_date
INTO previous_year3_start, previous_year3_end
FROM public.finance_year fy
WHERE fy.end_date < previous_year2_start
ORDER BY fy.end_date DESC
LIMIT 1;
-- Previous year 4
SELECT fy.start_date, fy.end_date
INTO previous_year4_start, previous_year4_end
FROM public.finance_year fy
WHERE fy.end_date < previous_year3_start
ORDER BY fy.end_date DESC
LIMIT 1;
RETURN QUERY
WITH RECURSIVE AccountHierarchy AS (
SELECT coa.id AS account_id, coa.name, coa.parent_account_id
FROM public.chart_of_accounts coa
WHERE coa.account_type_id = CONST_EXPENSE_TYPE_ID
UNION ALL
SELECT coa.id, coa.name, coa.parent_account_id
FROM public.chart_of_accounts coa
INNER JOIN AccountHierarchy ah ON coa.parent_account_id = ah.account_id
),
MonthlyExpenses AS (
SELECT
ah.name AS account_name,
TO_CHAR(je.transaction_date, 'MM') AS transaction_month,
CASE
WHEN je.transaction_date BETWEEN previous_year4_start AND previous_year3_start - INTERVAL '1 day' THEN 'y1'
WHEN je.transaction_date BETWEEN previous_year3_start AND previous_year2_start - INTERVAL '1 day' THEN 'y2'
WHEN je.transaction_date BETWEEN previous_year2_start AND previous_year1_start - INTERVAL '1 day' THEN 'y3'
WHEN je.transaction_date BETWEEN previous_year1_start AND current_year_start - INTERVAL '1 day' THEN 'y4'
WHEN je.transaction_date BETWEEN current_year_start AND current_year_end THEN 'y5'
END AS year_group,
SUM(je.amount) AS monthly_amount
FROM AccountHierarchy ah
LEFT JOIN public.journal_entries je ON je.account_id = ah.account_id
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
WHERE th.company_id = p_company_id
AND je.transaction_date BETWEEN previous_year4_start AND current_year_end
AND je.is_deleted = FALSE
GROUP BY ah.name, year_group, TO_CHAR(je.transaction_date, 'MM')
),
AggregatedExpenses AS (
SELECT
me.account_name,
-- y1
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '04' THEN me.monthly_amount ELSE 0 END) AS y1_apr,
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '05' THEN me.monthly_amount ELSE 0 END) AS y1_may,
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '06' THEN me.monthly_amount ELSE 0 END) AS y1_jun,
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '07' THEN me.monthly_amount ELSE 0 END) AS y1_jul,
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '08' THEN me.monthly_amount ELSE 0 END) AS y1_aug,
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '09' THEN me.monthly_amount ELSE 0 END) AS y1_sep,
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '10' THEN me.monthly_amount ELSE 0 END) AS y1_oct,
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '11' THEN me.monthly_amount ELSE 0 END) AS y1_nov,
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '12' THEN me.monthly_amount ELSE 0 END) AS y1_dec,
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '01' THEN me.monthly_amount ELSE 0 END) AS y1_jan,
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '02' THEN me.monthly_amount ELSE 0 END) AS y1_feb,
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '03' THEN me.monthly_amount ELSE 0 END) AS y1_mar,
-- y2
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '04' THEN me.monthly_amount ELSE 0 END) AS y2_apr,
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '05' THEN me.monthly_amount ELSE 0 END) AS y2_may,
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '06' THEN me.monthly_amount ELSE 0 END) AS y2_jun,
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '07' THEN me.monthly_amount ELSE 0 END) AS y2_jul,
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '08' THEN me.monthly_amount ELSE 0 END) AS y2_aug,
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '09' THEN me.monthly_amount ELSE 0 END) AS y2_sep,
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '10' THEN me.monthly_amount ELSE 0 END) AS y2_oct,
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '11' THEN me.monthly_amount ELSE 0 END) AS y2_nov,
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '12' THEN me.monthly_amount ELSE 0 END) AS y2_dec,
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '01' THEN me.monthly_amount ELSE 0 END) AS y2_jan,
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '02' THEN me.monthly_amount ELSE 0 END) AS y2_feb,
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '03' THEN me.monthly_amount ELSE 0 END) AS y2_mar,
-- y3
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '04' THEN me.monthly_amount ELSE 0 END) AS y3_apr,
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '05' THEN me.monthly_amount ELSE 0 END) AS y3_may,
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '06' THEN me.monthly_amount ELSE 0 END) AS y3_jun,
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '07' THEN me.monthly_amount ELSE 0 END) AS y3_jul,
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '08' THEN me.monthly_amount ELSE 0 END) AS y3_aug,
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '09' THEN me.monthly_amount ELSE 0 END) AS y3_sep,
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '10' THEN me.monthly_amount ELSE 0 END) AS y3_oct,
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '11' THEN me.monthly_amount ELSE 0 END) AS y3_nov,
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '12' THEN me.monthly_amount ELSE 0 END) AS y3_dec,
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '01' THEN me.monthly_amount ELSE 0 END) AS y3_jan,
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '02' THEN me.monthly_amount ELSE 0 END) AS y3_feb,
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '03' THEN me.monthly_amount ELSE 0 END) AS y3_mar,
-- y4
SUM(CASE WHEN me.year_group = 'y4' AND me.transaction_month = '04' THEN me.monthly_amount ELSE 0 END) AS y4_apr,
SUM(CASE WHEN me.year_group = 'y4' AND me.transaction_month = '05' THEN me.monthly_amount ELSE 0 END) AS y4_may,
SUM(CASE WHEN me.year_group = 'y4' AND me.transaction_month = '06' THEN me.monthly_amount ELSE 0 END) AS y4_jun,
SUM(CASE WHEN me.year_group = 'y4' AND me.transaction_month = '07' THEN me.monthly_amount ELSE 0 END) AS y4_jul,
SUM(CASE WHEN me.year_group = 'y4' AND me.transaction_month = '08' THEN me.monthly_amount ELSE 0 END) AS y4_aug,
SUM(CASE WHEN me.year_group = 'y4' AND me.transaction_month = '09' THEN me.monthly_amount ELSE 0 END) AS y4_sep,
SUM(CASE WHEN me.year_group = 'y4' AND me.transaction_month = '10' THEN me.monthly_amount ELSE 0 END) AS y4_oct,
SUM(CASE WHEN me.year_group = 'y4' AND me.transaction_month = '11' THEN me.monthly_amount ELSE 0 END) AS y4_nov,
SUM(CASE WHEN me.year_group = 'y4' AND me.transaction_month = '12' THEN me.monthly_amount ELSE 0 END) AS y4_dec,
SUM(CASE WHEN me.year_group = 'y4' AND me.transaction_month = '01' THEN me.monthly_amount ELSE 0 END) AS y4_jan,
SUM(CASE WHEN me.year_group = 'y4' AND me.transaction_month = '02' THEN me.monthly_amount ELSE 0 END) AS y4_feb,
SUM(CASE WHEN me.year_group = 'y4' AND me.transaction_month = '03' THEN me.monthly_amount ELSE 0 END) AS y4_mar,
-- y5
SUM(CASE WHEN me.year_group = 'y5' AND me.transaction_month = '04' THEN me.monthly_amount ELSE 0 END) AS y5_apr,
SUM(CASE WHEN me.year_group = 'y5' AND me.transaction_month = '05' THEN me.monthly_amount ELSE 0 END) AS y5_may,
SUM(CASE WHEN me.year_group = 'y5' AND me.transaction_month = '06' THEN me.monthly_amount ELSE 0 END) AS y5_jun,
SUM(CASE WHEN me.year_group = 'y5' AND me.transaction_month = '07' THEN me.monthly_amount ELSE 0 END) AS y5_jul,
SUM(CASE WHEN me.year_group = 'y5' AND me.transaction_month = '08' THEN me.monthly_amount ELSE 0 END) AS y5_aug,
SUM(CASE WHEN me.year_group = 'y5' AND me.transaction_month = '09' THEN me.monthly_amount ELSE 0 END) AS y5_sep,
SUM(CASE WHEN me.year_group = 'y5' AND me.transaction_month = '10' THEN me.monthly_amount ELSE 0 END) AS y5_oct,
SUM(CASE WHEN me.year_group = 'y5' AND me.transaction_month = '11' THEN me.monthly_amount ELSE 0 END) AS y5_nov,
SUM(CASE WHEN me.year_group = 'y5' AND me.transaction_month = '12' THEN me.monthly_amount ELSE 0 END) AS y5_dec,
SUM(CASE WHEN me.year_group = 'y5' AND me.transaction_month = '01' THEN me.monthly_amount ELSE 0 END) AS y5_jan,
SUM(CASE WHEN me.year_group = 'y5' AND me.transaction_month = '02' THEN me.monthly_amount ELSE 0 END) AS y5_feb,
SUM(CASE WHEN me.year_group = 'y5' AND me.transaction_month = '03' THEN me.monthly_amount ELSE 0 END) AS y5_mar
FROM MonthlyExpenses me
GROUP BY me.account_name
)
SELECT
gen_random_uuid() AS id,
CONCAT(EXTRACT(YEAR FROM previous_year4_start), '-', EXTRACT(YEAR FROM current_year_end)) AS financial_year_range,
ae.account_name,
-- y1 months
ae.y1_apr, ae.y1_may, ae.y1_jun, ae.y1_jul, ae.y1_aug, ae.y1_sep, ae.y1_oct, ae.y1_nov, ae.y1_dec, ae.y1_jan, ae.y1_feb, ae.y1_mar,
-- y2 months
ae.y2_apr, ae.y2_may, ae.y2_jun, ae.y2_jul, ae.y2_aug, ae.y2_sep, ae.y2_oct, ae.y2_nov, ae.y2_dec, ae.y2_jan, ae.y2_feb, ae.y2_mar,
-- y3 months
ae.y3_apr, ae.y3_may, ae.y3_jun, ae.y3_jul, ae.y3_aug, ae.y3_sep, ae.y3_oct, ae.y3_nov, ae.y3_dec, ae.y3_jan, ae.y3_feb, ae.y3_mar,
-- y4 months
ae.y4_apr, ae.y4_may, ae.y4_jun, ae.y4_jul, ae.y4_aug, ae.y4_sep, ae.y4_oct, ae.y4_nov, ae.y4_dec, ae.y4_jan, ae.y4_feb, ae.y4_mar,
-- y5 months
ae.y5_apr, ae.y5_may, ae.y5_jun, ae.y5_jul, ae.y5_aug, ae.y5_sep, ae.y5_oct, ae.y5_nov, ae.y5_dec, ae.y5_jan, ae.y5_feb, ae.y5_mar
FROM AggregatedExpenses ae
ORDER BY
COALESCE(ae.y1_apr,0)+COALESCE(ae.y1_may,0)+COALESCE(ae.y1_jun,0)+COALESCE(ae.y1_jul,0)+COALESCE(ae.y1_aug,0)+COALESCE(ae.y1_sep,0)+COALESCE(ae.y1_oct,0)+COALESCE(ae.y1_nov,0)+COALESCE(ae.y1_dec,0)+COALESCE(ae.y1_jan,0)+COALESCE(ae.y1_feb,0)+COALESCE(ae.y1_mar,0)
+COALESCE(ae.y2_apr,0)+COALESCE(ae.y2_may,0)+COALESCE(ae.y2_jun,0)+COALESCE(ae.y2_jul,0)+COALESCE(ae.y2_aug,0)+COALESCE(ae.y2_sep,0)+COALESCE(ae.y2_oct,0)+COALESCE(ae.y2_nov,0)+COALESCE(ae.y2_dec,0)+COALESCE(ae.y2_jan,0)+COALESCE(ae.y2_feb,0)+COALESCE(ae.y2_mar,0)
+COALESCE(ae.y3_apr,0)+COALESCE(ae.y3_may,0)+COALESCE(ae.y3_jun,0)+COALESCE(ae.y3_jul,0)+COALESCE(ae.y3_aug,0)+COALESCE(ae.y3_sep,0)+COALESCE(ae.y3_oct,0)+COALESCE(ae.y3_nov,0)+COALESCE(ae.y3_dec,0)+COALESCE(ae.y3_jan,0)+COALESCE(ae.y3_feb,0)+COALESCE(ae.y3_mar,0)
+COALESCE(ae.y4_apr,0)+COALESCE(ae.y4_may,0)+COALESCE(ae.y4_jun,0)+COALESCE(ae.y4_jul,0)+COALESCE(ae.y4_aug,0)+COALESCE(ae.y4_sep,0)+COALESCE(ae.y4_oct,0)+COALESCE(ae.y4_nov,0)+COALESCE(ae.y4_dec,0)+COALESCE(ae.y4_jan,0)+COALESCE(ae.y4_feb,0)+COALESCE(ae.y4_mar,0)
+COALESCE(ae.y5_apr,0)+COALESCE(ae.y5_may,0)+COALESCE(ae.y5_jun,0)+COALESCE(ae.y5_jul,0)+COALESCE(ae.y5_aug,0)+COALESCE(ae.y5_sep,0)+COALESCE(ae.y5_oct,0)+COALESCE(ae.y5_nov,0)+COALESCE(ae.y5_dec,0)+COALESCE(ae.y5_jan,0)+COALESCE(ae.y5_feb,0)+COALESCE(ae.y5_mar,0)
DESC
LIMIT 12;
END;
$function$
-- Function: get_three_years_expenses
CREATE OR REPLACE FUNCTION public.get_three_years_expenses(p_company_id uuid, p_finance_year_id integer)
RETURNS TABLE(id uuid, financial_year_range text, account_name text, y1_apr numeric, y1_may numeric, y1_jun numeric, y1_jul numeric, y1_aug numeric, y1_sep numeric, y1_oct numeric, y1_nov numeric, y1_dec numeric, y1_jan numeric, y1_feb numeric, y1_mar numeric, y2_apr numeric, y2_may numeric, y2_jun numeric, y2_jul numeric, y2_aug numeric, y2_sep numeric, y2_oct numeric, y2_nov numeric, y2_dec numeric, y2_jan numeric, y2_feb numeric, y2_mar numeric, y3_apr numeric, y3_may numeric, y3_jun numeric, y3_jul numeric, y3_aug numeric, y3_sep numeric, y3_oct numeric, y3_nov numeric, y3_dec numeric, y3_jan numeric, y3_feb numeric, y3_mar numeric)
LANGUAGE plpgsql
STABLE PARALLEL SAFE
AS $function$
DECLARE
current_year_start DATE;
current_year_end DATE;
previous_year1_start DATE;
previous_year1_end DATE;
previous_year2_start DATE;
previous_year2_end DATE;
CONST_EXPENSE_TYPE_ID INTEGER := 5;
BEGIN
-- Fetch the start and end dates for the current financial year
SELECT fy.start_date, fy.end_date
INTO current_year_start, current_year_end
FROM public.finance_year fy
WHERE fy.id = p_finance_year_id;
-- Validate financial year existence
IF current_year_start IS NULL OR current_year_end IS NULL THEN
RAISE EXCEPTION 'Financial year with ID % not found', p_finance_year_id;
END IF;
-- Fetch the previous financial year's start and end dates
SELECT fy.start_date, fy.end_date
INTO previous_year1_start, previous_year1_end
FROM public.finance_year fy
WHERE fy.end_date < current_year_start
ORDER BY fy.end_date DESC
LIMIT 1;
-- Fetch the year before the previous financial year
SELECT fy.start_date, fy.end_date
INTO previous_year2_start, previous_year2_end
FROM public.finance_year fy
WHERE fy.end_date < previous_year1_start
ORDER BY fy.end_date DESC
LIMIT 1;
-- Generate financial year range dynamically
RETURN QUERY
WITH RECURSIVE AccountHierarchy AS (
-- Base case: Select root accounts (account_type_id = 5 indicates expense accounts)
SELECT
coa.id AS account_id,
coa.name,
coa.parent_account_id
FROM public.chart_of_accounts coa
WHERE coa.account_type_id = CONST_EXPENSE_TYPE_ID
UNION ALL
-- Select all child accounts recursively under the root accounts
SELECT
coa.id AS account_id,
coa.name,
coa.parent_account_id
FROM public.chart_of_accounts coa
INNER JOIN AccountHierarchy ah ON coa.parent_account_id = ah.account_id
),
MonthlyExpenses AS (
SELECT
ah.name AS account_name,
TO_CHAR(je.transaction_date, 'MM') AS transaction_month,
CASE
WHEN je.transaction_date BETWEEN previous_year2_start AND previous_year1_start - INTERVAL '1 day' THEN 'y1'
WHEN je.transaction_date BETWEEN previous_year1_start AND current_year_start - INTERVAL '1 day' THEN 'y2'
WHEN je.transaction_date BETWEEN current_year_start AND current_year_end THEN 'y3'
END AS year_group,
SUM(je.amount) AS monthly_amount
FROM AccountHierarchy ah
LEFT JOIN public.journal_entries je ON je.account_id = ah.account_id
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
WHERE th.company_id = p_company_id
AND je.transaction_date BETWEEN previous_year2_start AND current_year_end
AND je.is_deleted = FALSE
GROUP BY ah.name, year_group, TO_CHAR(je.transaction_date, 'MM')
),
AggregatedExpenses AS (
SELECT
me.account_name,
-- Populate y1 values
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '04' THEN me.monthly_amount ELSE 0 END) AS y1_apr,
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '05' THEN me.monthly_amount ELSE 0 END) AS y1_may,
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '06' THEN me.monthly_amount ELSE 0 END) AS y1_jun,
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '07' THEN me.monthly_amount ELSE 0 END) AS y1_jul,
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '08' THEN me.monthly_amount ELSE 0 END) AS y1_aug,
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '09' THEN me.monthly_amount ELSE 0 END) AS y1_sep,
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '10' THEN me.monthly_amount ELSE 0 END) AS y1_oct,
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '11' THEN me.monthly_amount ELSE 0 END) AS y1_nov,
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '12' THEN me.monthly_amount ELSE 0 END) AS y1_dec,
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '01' THEN me.monthly_amount ELSE 0 END) AS y1_jan,
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '02' THEN me.monthly_amount ELSE 0 END) AS y1_feb,
SUM(CASE WHEN me.year_group = 'y1' AND me.transaction_month = '03' THEN me.monthly_amount ELSE 0 END) AS y1_mar,
-- Similarly populate y2 and y3 values
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '04' THEN me.monthly_amount ELSE 0 END) AS y2_apr,
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '05' THEN me.monthly_amount ELSE 0 END) AS y2_may,
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '06' THEN me.monthly_amount ELSE 0 END) AS y2_jun,
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '07' THEN me.monthly_amount ELSE 0 END) AS y2_jul,
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '08' THEN me.monthly_amount ELSE 0 END) AS y2_aug,
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '09' THEN me.monthly_amount ELSE 0 END) AS y2_sep,
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '10' THEN me.monthly_amount ELSE 0 END) AS y2_oct,
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '11' THEN me.monthly_amount ELSE 0 END) AS y2_nov,
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '12' THEN me.monthly_amount ELSE 0 END) AS y2_dec,
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '01' THEN me.monthly_amount ELSE 0 END) AS y2_jan,
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '02' THEN me.monthly_amount ELSE 0 END) AS y2_feb,
SUM(CASE WHEN me.year_group = 'y2' AND me.transaction_month = '03' THEN me.monthly_amount ELSE 0 END) AS y2_mar,
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '04' THEN me.monthly_amount ELSE 0 END) AS y3_apr,
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '05' THEN me.monthly_amount ELSE 0 END) AS y3_may,
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '06' THEN me.monthly_amount ELSE 0 END) AS y3_jun,
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '07' THEN me.monthly_amount ELSE 0 END) AS y3_jul,
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '08' THEN me.monthly_amount ELSE 0 END) AS y3_aug,
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '09' THEN me.monthly_amount ELSE 0 END) AS y3_sep,
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '10' THEN me.monthly_amount ELSE 0 END) AS y3_oct,
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '11' THEN me.monthly_amount ELSE 0 END) AS y3_nov,
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '12' THEN me.monthly_amount ELSE 0 END) AS y3_dec,
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '01' THEN me.monthly_amount ELSE 0 END) AS y3_jan,
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '02' THEN me.monthly_amount ELSE 0 END) AS y3_feb,
SUM(CASE WHEN me.year_group = 'y3' AND me.transaction_month = '03' THEN me.monthly_amount ELSE 0 END) AS y3_mar
FROM MonthlyExpenses me
GROUP BY me.account_name
)
SELECT
gen_random_uuid() AS id,
CONCAT(EXTRACT(YEAR FROM previous_year2_start), '-', EXTRACT(YEAR FROM current_year_end)) AS financial_year_range,
ae.account_name,
ae.y1_apr, ae.y1_may, ae.y1_jun, ae.y1_jul, ae.y1_aug, ae.y1_sep, ae.y1_oct, ae.y1_nov, ae.y1_dec, ae.y1_jan, ae.y1_feb, ae.y1_mar,
ae.y2_apr, ae.y2_may, ae.y2_jun, ae.y2_jul, ae.y2_aug, ae.y2_sep, ae.y2_oct, ae.y2_nov, ae.y2_dec, ae.y2_jan, ae.y2_feb, ae.y2_mar,
ae.y3_apr, ae.y3_may, ae.y3_jun, ae.y3_jul, ae.y3_aug, ae.y3_sep, ae.y3_oct, ae.y3_nov, ae.y3_dec, ae.y3_jan, ae.y3_feb, ae.y3_mar
FROM AggregatedExpenses ae
ORDER BY
COALESCE(ae.y1_apr, 0) + COALESCE(ae.y1_may, 0) + COALESCE(ae.y1_jun, 0) + COALESCE(ae.y1_jul, 0) +
COALESCE(ae.y1_aug, 0) + COALESCE(ae.y1_sep, 0) + COALESCE(ae.y1_oct, 0) + COALESCE(ae.y1_nov, 0) +
COALESCE(ae.y1_dec, 0) + COALESCE(ae.y1_jan, 0) + COALESCE(ae.y1_feb, 0) + COALESCE(ae.y1_mar, 0) +
COALESCE(ae.y2_apr, 0) + COALESCE(ae.y2_may, 0) + COALESCE(ae.y2_jun, 0) + COALESCE(ae.y2_jul, 0) +
COALESCE(ae.y2_aug, 0) + COALESCE(ae.y2_sep, 0) + COALESCE(ae.y2_oct, 0) + COALESCE(ae.y2_nov, 0) +
COALESCE(ae.y2_dec, 0) + COALESCE(ae.y2_jan, 0) + COALESCE(ae.y2_feb, 0) + COALESCE(ae.y2_mar, 0) +
COALESCE(ae.y3_apr, 0) + COALESCE(ae.y3_may, 0) + COALESCE(ae.y3_jun, 0) + COALESCE(ae.y3_jul, 0) +
COALESCE(ae.y3_aug, 0) + COALESCE(ae.y3_sep, 0) + COALESCE(ae.y3_oct, 0) + COALESCE(ae.y3_nov, 0) +
COALESCE(ae.y3_dec, 0) + COALESCE(ae.y3_jan, 0) + COALESCE(ae.y3_feb, 0) + COALESCE(ae.y3_mar, 0) DESC
LIMIT 12;
END;
$function$
-- Function: get_all_coa
CREATE OR REPLACE FUNCTION public.get_all_coa(p_organization_id uuid)
RETURNS TABLE(id uuid, account_type text, account_number integer, name text, opening_balance numeric, level integer, order_sequence character varying, is_default_account boolean, schedule text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN query
WITH RECURSIVE coa_view AS (
SELECT
coa.id,
act.name as account_type,
coa.account_number::integer,
coa.name,
coa.parent_account_id,
0 AS level,
coa.account_number::text AS order_sequence,
coa.is_default_account, -- Include is_default_account,
NULL::text AS schedule
FROM
public.chart_of_accounts coa
INNER JOIN
public.account_types act ON coa.account_type_id = act.id
WHERE
coa.organization_id = p_organization_id
AND (coa.parent_account_id = '00000000-0000-0000-0000-000000000000' or coa.parent_account_id is null)
AND coa.is_deleted = FALSE
UNION ALL
SELECT
coa.id,
act.name as account_type,
coa.account_number::integer,
coa.name,
coa.parent_account_id,
view.level + 1 AS level,
view.order_sequence || '.' || coa.account_number::text AS order_sequence,
coa.is_default_account, -- Include is_default_account,
NULL::text AS schedule
FROM
public.chart_of_accounts coa
INNER JOIN
coa_view view ON view.id = coa.parent_account_id
INNER JOIN
public.account_types act ON coa.account_type_id = act.id
WHERE
coa.organization_id = p_organization_id
AND coa.is_deleted = FALSE
)
SELECT
view.id,
view.account_type,
view.account_number,
LPAD('', view.level * 4, ' ') || view.name AS name,
0::numeric AS opening_balance,
view.level,
view.order_sequence::character varying AS order_sequence,
view.is_default_account, -- Include is_default_account in final output
view.schedule
FROM
coa_view view
ORDER BY
view.order_sequence;
END;
$function$
-- Function: pg_get_tabledef
CREATE OR REPLACE FUNCTION public.pg_get_tabledef(p_table_name text)
RETURNS text
LANGUAGE plpgsql
AS $function$
DECLARE
col RECORD;
col_defs TEXT := '';
pk_cols TEXT := '';
result TEXT;
BEGIN
FOR col IN
SELECT
column_name,
data_type,
character_maximum_length,
numeric_precision,
numeric_scale,
is_nullable,
column_default
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = p_table_name
ORDER BY ordinal_position
LOOP
col_defs := col_defs ||
format('"%s" %s%s%s%s, ',
col.column_name,
CASE
WHEN col.data_type = 'character varying' THEN format('varchar(%s)', col.character_maximum_length)
WHEN col.data_type = 'numeric' THEN format('numeric(%s,%s)', col.numeric_precision, col.numeric_scale)
ELSE col.data_type
END,
CASE WHEN col.column_default IS NOT NULL THEN ' DEFAULT ' || col.column_default ELSE '' END,
CASE WHEN col.is_nullable = 'NO' THEN ' NOT NULL' ELSE '' END,
''
);
END LOOP;
-- Get primary key columns
SELECT string_agg(format('"%s"', kcu.column_name), ', ')
INTO pk_cols
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE tc.table_schema = 'public'
AND tc.table_name = p_table_name
AND tc.constraint_type = 'PRIMARY KEY';
IF pk_cols IS NOT NULL THEN
col_defs := col_defs || format('PRIMARY KEY (%s), ', pk_cols);
END IF;
col_defs := left(col_defs, length(col_defs) - 2);
result := format('CREATE TABLE "%s" (%s);', p_table_name, col_defs);
RETURN result;
END;
$function$
-- Function: get_cash_flow_statement
CREATE OR REPLACE FUNCTION public.get_cash_flow_statement(p_company_id uuid, p_finance_year_id integer, p_is_get_for_organization boolean)
RETURNS TABLE(account_id uuid, account_category text, account_type_id integer, account_type text, account_name text, net_cash_flow numeric)
LANGUAGE plpgsql
AS $function$
DECLARE
v_financial_year_start DATE;
v_financial_year_end DATE;
v_company_ids UUID[];
v_organization_id UUID;
BEGIN
-- Fetch the Organization ID for the given Company ID
SELECT organization_id INTO v_organization_id
FROM public.companies
WHERE id = p_company_id;
IF v_organization_id IS NULL THEN
RAISE EXCEPTION 'Organization ID not found for company ID %', p_company_id;
END IF;
-- Fetch the start and end dates of the financial year
SELECT fy.start_date, fy.end_date INTO v_financial_year_start, v_financial_year_end
FROM public.finance_year fy
WHERE fy.id = p_finance_year_id;
IF v_financial_year_start IS NULL OR v_financial_year_end IS NULL THEN
RAISE EXCEPTION 'Financial year with ID % not found', p_finance_year_id;
END IF;
-- Determine the company IDs to include (for all companies in the same organization or a single company)
IF p_is_get_for_organization THEN
SELECT ARRAY_AGG(id) INTO v_company_ids
FROM public.companies
WHERE organization_id = v_organization_id;
ELSE
v_company_ids := ARRAY[p_company_id];
END IF;
-- Combining Operating, Investing, and Financing Activities into a single result
RETURN QUERY
WITH cash_flow_activities AS (
-- Operating Activities: Assets, Liabilities, Revenue, and Expenses
SELECT
coa.id AS account_id,
'Operating' AS account_category,
at.id as account_type_id,
at.name as account_type,
coa.name AS account_name,
SUM(CASE
-- Assets
WHEN je.entry_type = 'D' AND coa.account_type_id IN (6, 8, 9) THEN je.amount -- Debit for Assets increases balance
WHEN je.entry_type = 'C' AND coa.account_type_id IN (6, 8, 9) THEN -je.amount -- Credit for Assets decreases balance
-- Liabilities
WHEN je.entry_type = 'C' AND coa.account_type_id = 10 THEN je.amount -- Credit for Liabilities increases balance
WHEN je.entry_type = 'D' AND coa.account_type_id = 10 THEN -je.amount -- Debit for Liabilities decreases balance
-- Revenue
WHEN je.entry_type = 'C' AND coa.account_type_id IN (14, 15, 19, 20) THEN je.amount -- Credit for Revenue increases balance
WHEN je.entry_type = 'D' AND coa.account_type_id IN (14, 15, 19, 20) THEN -je.amount -- Debit for Revenue decreases balance
-- Expenses
WHEN je.entry_type = 'D' AND coa.account_type_id IN (16, 17, 18, 21, 22) THEN je.amount -- Debit for Expenses increases balance
WHEN je.entry_type = 'C' AND coa.account_type_id IN (16, 17, 18, 21, 22) THEN -je.amount -- Credit for Expenses decreases balance
ELSE 0
END) AS net_cash_flow
FROM
public.chart_of_accounts coa
INNER JOIN public.account_types at ON coa.account_type_id = at.id
INNER JOIN public.journal_entries je ON je.account_id = coa.id
INNER JOIN public.transaction_headers th ON th.id = je.transaction_id
WHERE
th.company_id = ANY(v_company_ids)
AND th.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND NOT th.is_deleted
AND NOT je.is_deleted
AND coa.account_type_id IN (6, 8, 9, 10, 14, 15, 16, 17, 18, 19, 20, 21, 22)
GROUP BY
coa.id, at.id, at.name, coa.name
UNION ALL
-- Investing Activities: Non-Current Assets
SELECT
coa.id AS account_id,
'Investing' AS account_category,
at.id as account_type_id,
at.name as account_type,
coa.name AS account_name,
SUM(CASE
WHEN je.entry_type = 'D' AND coa.account_type_id = 7 THEN je.amount -- Debit for Non-Current Assets increases balance
WHEN je.entry_type = 'C' AND coa.account_type_id = 7 THEN -je.amount -- Credit for Non-Current Assets decreases balance
ELSE 0
END) AS net_cash_flow
FROM
public.chart_of_accounts coa
INNER JOIN public.account_types at ON coa.account_type_id = at.id
INNER JOIN public.journal_entries je ON je.account_id = coa.id
INNER JOIN public.transaction_headers th ON th.id = je.transaction_id
WHERE
th.company_id = ANY(v_company_ids)
AND th.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND NOT th.is_deleted
AND NOT je.is_deleted
AND coa.account_type_id = 7
GROUP BY
coa.id, at.id, at.name, coa.name
UNION ALL
-- Financing Activities: Equity and Long-Term Liabilities
SELECT
coa.id AS account_id,
'Financing' AS account_category,
at.id as account_type_id,
at.name as account_type,
coa.name AS account_name,
SUM(CASE
-- Liabilities
WHEN je.entry_type = 'C' AND coa.account_type_id = 11 THEN je.amount -- Credit for Liabilities increases balance
WHEN je.entry_type = 'D' AND coa.account_type_id = 11 THEN -je.amount -- Debit for Liabilities decreases balance
-- Equity
WHEN je.entry_type = 'C' AND coa.account_type_id IN (12, 13) THEN je.amount -- Credit for Equity increases balance
WHEN je.entry_type = 'D' AND coa.account_type_id IN (12, 13) THEN -je.amount -- Debit for Equity decreases balance
ELSE 0
END) AS net_cash_flow
FROM
public.chart_of_accounts coa
INNER JOIN public.account_types at ON coa.account_type_id = at.id
INNER JOIN public.journal_entries je ON je.account_id = coa.id
INNER JOIN public.transaction_headers th ON th.id = je.transaction_id
WHERE
th.company_id = ANY(v_company_ids)
AND th.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND NOT th.is_deleted
AND NOT je.is_deleted
AND coa.account_type_id IN (11, 12, 13)
GROUP BY
coa.id, at.id, at.name, coa.name
)
SELECT
cfa.account_id,
cfa.account_category,
cfa.account_type_id,
cfa.account_type,
cfa.account_name,
cfa.net_cash_flow
FROM cash_flow_activities cfa
WHERE cfa.net_cash_flow != 0 -- Exclude records with zero net cash flow
ORDER BY cfa.account_category, cfa.account_type_id, cfa.account_type, cfa.account_name;
END;
$function$
-- Function: get_bank_statements
CREATE OR REPLACE FUNCTION public.get_bank_statements(p_company_id uuid, p_finyear_id integer, p_date_from timestamp without time zone DEFAULT NULL::timestamp without time zone, p_date_to timestamp without time zone DEFAULT NULL::timestamp without time zone)
RETURNS TABLE(id integer, company_id uuid, bank_id uuid, bank_name text, txn_date timestamp without time zone, cheque_number character varying, description text, value_date timestamp without time zone, branch_code character varying, debit_amount numeric, credit_amount numeric, balance numeric, has_reconciled boolean, created_by uuid, created_on_utc timestamp without time zone, modified_by uuid, modified_on_utc timestamp without time zone, deleted_on_utc timestamp without time zone, is_deleted boolean, rec_status_id integer, rec_status text, rec_confidence_score numeric, rec_strategy_name text, rec_matched_party_name text, rec_party_id uuid, rec_party_type text, rec_reviewed_by uuid, rec_reviewed_on_utc timestamp without time zone, rec_matched_amount numeric)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
bs.id,
bs.company_id,
bs.bank_id,
coa.name AS bank_name,
bs.txn_date::timestamp,
bs.cheque_number,
bs.description,
bs.value_date::timestamp,
bs.branch_code,
bs.debit_amount,
bs.credit_amount,
bs.balance,
bs.has_reconciled,
bs.created_by,
bs.created_on_utc,
bs.modified_by,
bs.modified_on_utc,
bs.deleted_on_utc,
bs.is_deleted,
-- ✅ Finalized Reconciliation Status
COALESCE(br.reconciliation_status_id, 0) AS rec_status_id,
-- Determine rec_status based on staging status and confidence score
CASE
WHEN br.reconciliation_status_id IS NOT NULL THEN
COALESCE(rs.status::text, 'unmatched')
WHEN brs.confidence_score >= 70.99 THEN
'matched' -- Auto-match based on high confidence score
WHEN brs.status = 'pending' THEN
'pending' -- Pending review status
ELSE
'unmatched'
END AS rec_status,
-- Latest staging details (AI / manual reconciliation suggestion)
COALESCE(brs.confidence_score, 0) AS rec_confidence_score,
COALESCE(brs.strategy_name::text, 'No Match Found') AS rec_strategy_name,
brs.matched_party_name::text AS rec_matched_party_name,
brs.party_id AS rec_party_id,
brs.party_type::text AS rec_party_type,
brs.reviewed_by AS rec_reviewed_by,
brs.reviewed_on_utc AS rec_reviewed_on_utc,
COALESCE(brs.matched_amount, 0) AS rec_matched_amount
FROM public.bank_statements bs
INNER JOIN public.chart_of_accounts coa
ON coa.id = bs.bank_id
AND coa.account_type_id = 9
AND coa.is_deleted = FALSE
INNER JOIN public.finance_year fy
ON fy.id = p_finyear_id
-- 🟢 Latest staging entry (AI / manual reconciliation suggestion)
LEFT JOIN LATERAL (
SELECT brs_inner.*
FROM public.bank_reconciliation_stagings brs_inner
WHERE brs_inner.company_id = bs.company_id
AND brs_inner.bank_statement_id = bs.id
AND brs_inner.is_deleted = FALSE
ORDER BY brs_inner.created_on_utc DESC
LIMIT 1
) brs ON TRUE
-- 🟢 Finalized reconciliations
LEFT JOIN public.bank_reconciliations br
ON br.bank_statement_id = bs.id
AND br.company_id = bs.company_id
-- 🟢 Status lookup
LEFT JOIN public.reconciliation_statuses rs
ON rs.id = br.reconciliation_status_id
WHERE bs.company_id = p_company_id
AND bs.txn_date BETWEEN fy.start_date AND fy.end_date
AND (p_date_from IS NULL OR bs.txn_date >= p_date_from)
AND (p_date_to IS NULL OR bs.txn_date <= p_date_to)
AND bs.is_deleted = FALSE
ORDER BY bs.txn_date, bs.id;
END;
$function$
-- Function: get_comparative_accounts_overview
CREATE OR REPLACE FUNCTION public.get_comparative_accounts_overview(p_company_id uuid, p_fin_year_id integer)
RETURNS TABLE(account_id uuid, account_name text, parent_account_id uuid, hierarchy_path text[], order_sequence text, financial_year integer, apr numeric, may numeric, jun numeric, jul numeric, aug numeric, sep numeric, oct numeric, nov numeric, "dec" numeric, jan numeric, feb numeric, mar numeric, total_sum numeric, difference numeric)
LANGUAGE plpgsql
AS $function$
DECLARE
v_current_year_id INTEGER := p_fin_year_id;
v_previous_year_id INTEGER;
v_organization_id UUID;
BEGIN
-- Get organization
SELECT c.organization_id INTO v_organization_id
FROM public.companies c
WHERE c.id = p_company_id;
-- Find previous financial year (based on start_date)
SELECT fy.id
INTO v_previous_year_id
FROM finance_year fy
WHERE fy.start_date < (
SELECT start_date FROM finance_year WHERE id = v_current_year_id
)
ORDER BY fy.start_date DESC
LIMIT 1;
RETURN QUERY
WITH RECURSIVE account_with_path AS (
-- Root accounts
SELECT
coa.id,
coa.name,
coa.parent_account_id,
ARRAY[coa.name]::text[] AS path,
coa.account_number::text AS order_sequence
FROM chart_of_accounts coa
WHERE coa.organization_id = v_organization_id
AND (coa.parent_account_id IS NULL OR coa.parent_account_id = '00000000-0000-0000-0000-000000000000')
AND coa.is_deleted = FALSE
UNION ALL
-- Children
SELECT
c.id,
c.name,
c.parent_account_id,
awp.path || c.name,
awp.order_sequence || '.' || c.account_number::text
FROM chart_of_accounts c
JOIN account_with_path awp ON c.parent_account_id = awp.id
WHERE c.organization_id = v_organization_id
AND c.is_deleted = FALSE
),
leaf_accounts AS (
SELECT a.id
FROM account_with_path a
WHERE NOT EXISTS (
SELECT 1 FROM chart_of_accounts c
WHERE c.parent_account_id = a.id
AND c.organization_id = v_organization_id
AND c.is_deleted = FALSE
)
),
financial_years AS (
SELECT id AS fin_year_id, start_date, end_date
FROM finance_year
WHERE id IN (v_current_year_id, v_previous_year_id)
),
aggregated_data AS (
SELECT
awp.id,
awp.name,
awp.parent_account_id,
awp.path AS hierarchy_path,
awp.order_sequence,
fy.fin_year_id,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 4 THEN je.amount END), 0) AS apr,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 5 THEN je.amount END), 0) AS may,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 6 THEN je.amount END), 0) AS jun,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 7 THEN je.amount END), 0) AS jul,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 8 THEN je.amount END), 0) AS aug,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 9 THEN je.amount END), 0) AS sep,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 10 THEN je.amount END), 0) AS oct,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 11 THEN je.amount END), 0) AS nov,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 12 THEN je.amount END), 0) AS "dec",
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 1 THEN je.amount END), 0) AS jan,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 2 THEN je.amount END), 0) AS feb,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 3 THEN je.amount END), 0) AS mar
FROM account_with_path awp
JOIN leaf_accounts la ON la.id = awp.id -- ✅ only leaf accounts get financial years
CROSS JOIN financial_years fy
LEFT JOIN journal_entries je
ON je.account_id = awp.id
AND je.transaction_date BETWEEN fy.start_date AND fy.end_date
LEFT JOIN transaction_headers th
ON th.id = je.transaction_id
AND th.company_id = p_company_id
WHERE (th.transaction_source_type IS NULL OR th.transaction_source_type != 18)
GROUP BY awp.id, awp.name, awp.parent_account_id, awp.path, awp.order_sequence, fy.fin_year_id
)
SELECT
a1.id AS account_id,
a1.name AS account_name,
a1.parent_account_id,
a1.hierarchy_path,
a1.order_sequence,
a1.fin_year_id AS financial_year,
a1.apr, a1.may, a1.jun, a1.jul, a1.aug, a1.sep, a1.oct, a1.nov, a1."dec",
a1.jan, a1.feb, a1.mar,
(COALESCE(a1.apr,0) + COALESCE(a1.may,0) + COALESCE(a1.jun,0) +
COALESCE(a1.jul,0) + COALESCE(a1.aug,0) + COALESCE(a1.sep,0) +
COALESCE(a1.oct,0) + COALESCE(a1.nov,0) + COALESCE(a1."dec",0) +
COALESCE(a1.jan,0) + COALESCE(a1.feb,0) + COALESCE(a1.mar,0)) AS total_sum,
((COALESCE(a1.apr,0) + COALESCE(a1.may,0) + COALESCE(a1.jun,0) +
COALESCE(a1.jul,0) + COALESCE(a1.aug,0) + COALESCE(a1.sep,0) +
COALESCE(a1.oct,0) + COALESCE(a1.nov,0) + COALESCE(a1."dec",0) +
COALESCE(a1.jan,0) + COALESCE(a1.feb,0) + COALESCE(a1.mar,0))
-
(COALESCE(a2.apr,0) + COALESCE(a2.may,0) + COALESCE(a2.jun,0) +
COALESCE(a2.jul,0) + COALESCE(a2.aug,0) + COALESCE(a2.sep,0) +
COALESCE(a2.oct,0) + COALESCE(a2.nov,0) + COALESCE(a2."dec",0) +
COALESCE(a2.jan,0) + COALESCE(a2.feb,0) + COALESCE(a2.mar,0))
) AS difference
FROM aggregated_data a1
LEFT JOIN aggregated_data a2
ON a1.id = a2.id
AND a1.fin_year_id = v_current_year_id
AND a2.fin_year_id = v_previous_year_id
ORDER BY a1.order_sequence, a1.fin_year_id;
END;
$function$
-- Function: get_expense_categorization
CREATE OR REPLACE FUNCTION public.get_expense_categorization(p_company_id uuid, p_period_type integer, p_finance_id integer)
RETURNS TABLE(period text, year numeric, account_id uuid, account_name text, total_expense numeric)
LANGUAGE plpgsql
AS $function$
DECLARE
v_financial_year_start DATE;
v_financial_year_end DATE;
v_organization_id uuid;
CONST_MONTHLY CONSTANT INTEGER := 1;
CONST_QUARTERLY CONSTANT INTEGER := 2;
CONST_HALF_YEARLY CONSTANT INTEGER := 3;
CONST_YEARLY CONSTANT INTEGER := 4;
CONST_YTD CONSTANT INTEGER := 5;
BEGIN
-- Get financial year boundaries
SELECT start_date, end_date INTO v_financial_year_start, v_financial_year_end
FROM public.finance_year
WHERE id = p_finance_id;
-- Get organization_id for the given company_id
SELECT organization_id INTO v_organization_id
FROM public.companies
WHERE id = p_company_id;
IF v_financial_year_start IS NULL OR v_financial_year_end IS NULL THEN
RAISE EXCEPTION 'Financial year with ID % not found', p_finance_id;
END IF;
IF v_organization_id IS NULL THEN
RAISE EXCEPTION 'Company % not found or missing organization_id', p_company_id;
END IF;
IF p_period_type = CONST_MONTHLY THEN
RETURN QUERY
WITH months AS (
SELECT
TO_CHAR(m, 'Mon') AS period,
EXTRACT(YEAR FROM m) AS year,
EXTRACT(MONTH FROM m) AS month_num
FROM generate_series(v_financial_year_start, v_financial_year_end, '1 month'::interval) AS m
)
SELECT
months.period,
months.year,
coa.id AS account_id,
coa.name AS account_name,
COALESCE(SUM(je.amount), 0) AS total_expense
FROM months
LEFT JOIN public.journal_entries je
ON EXTRACT(MONTH FROM je.transaction_date) = months.month_num
AND EXTRACT(YEAR FROM je.transaction_date) = months.year
AND je.is_deleted = FALSE
INNER JOIN public.transaction_headers th
ON je.transaction_id = th.id
AND th.company_id = p_company_id
INNER JOIN public.chart_of_accounts coa
ON je.account_id = coa.id
AND coa.organization_id = v_organization_id
AND coa.is_deleted = FALSE
WHERE coa.account_type_id IN (SELECT id FROM get_account_type_hierarchy(5))
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND je.entry_type = 'D'
GROUP BY months.year, months.month_num, months.period, coa.id, coa.name
ORDER BY months.year, months.month_num, coa.name;
ELSIF p_period_type = CONST_QUARTERLY THEN
RETURN QUERY
WITH quarters AS (
SELECT 'Q1' AS period, v_financial_year_start AS start_date, v_financial_year_start + interval '2 month' AS end_date
UNION ALL
SELECT 'Q2', v_financial_year_start + interval '3 month', v_financial_year_start + interval '5 month'
UNION ALL
SELECT 'Q3', v_financial_year_start + interval '6 month', v_financial_year_start + interval '8 month'
UNION ALL
SELECT 'Q4', v_financial_year_start + interval '9 month', v_financial_year_end
)
SELECT
quarters.period,
EXTRACT(YEAR FROM quarters.start_date) AS year,
coa.id AS account_id,
coa.name AS account_name,
COALESCE(SUM(je.amount), 0) AS total_expense
FROM quarters
LEFT JOIN public.journal_entries je
ON je.transaction_date BETWEEN quarters.start_date AND quarters.end_date
AND je.is_deleted = FALSE
INNER JOIN public.transaction_headers th
ON je.transaction_id = th.id
AND th.company_id = p_company_id
INNER JOIN public.chart_of_accounts coa
ON je.account_id = coa.id
AND coa.organization_id = v_organization_id
AND coa.is_deleted = FALSE
WHERE coa.account_type_id IN (SELECT id FROM get_account_type_hierarchy(5))
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND je.entry_type = 'D'
GROUP BY quarters.period, year, coa.id, coa.name
ORDER BY year, quarters.period, coa.name;
ELSIF p_period_type = CONST_HALF_YEARLY THEN
RETURN QUERY
WITH half_years AS (
SELECT 'H1' AS period, v_financial_year_start AS start_date, v_financial_year_start + interval '5 month' AS end_date
UNION ALL
SELECT 'H2', v_financial_year_start + interval '6 month', v_financial_year_end
)
SELECT
half_years.period,
EXTRACT(YEAR FROM half_years.start_date) AS year,
coa.id AS account_id,
coa.name AS account_name,
COALESCE(SUM(je.amount), 0) AS total_expense
FROM half_years
LEFT JOIN public.journal_entries je
ON je.transaction_date BETWEEN half_years.start_date AND half_years.end_date
AND je.is_deleted = FALSE
INNER JOIN public.transaction_headers th
ON je.transaction_id = th.id
AND th.company_id = p_company_id
INNER JOIN public.chart_of_accounts coa
ON je.account_id = coa.id
AND coa.organization_id = v_organization_id
AND coa.is_deleted = FALSE
WHERE coa.account_type_id IN (SELECT id FROM get_account_type_hierarchy(5))
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND je.entry_type = 'D'
GROUP BY half_years.period, year, coa.id, coa.name
ORDER BY year, half_years.period, coa.name;
ELSIF p_period_type = CONST_YEARLY THEN
RETURN QUERY
SELECT
'Year' AS period,
EXTRACT(YEAR FROM je.transaction_date) AS year,
coa.id AS account_id,
coa.name AS account_name,
COALESCE(SUM(je.amount), 0) AS total_expense
FROM public.journal_entries je
INNER JOIN public.transaction_headers th
ON je.transaction_id = th.id
AND th.company_id = p_company_id
INNER JOIN public.chart_of_accounts coa
ON je.account_id = coa.id
AND coa.organization_id = v_organization_id
AND coa.is_deleted = FALSE
WHERE coa.account_type_id IN (SELECT id FROM get_account_type_hierarchy(5))
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND je.entry_type = 'D'
AND je.is_deleted = FALSE
GROUP BY year, coa.id, coa.name
ORDER BY year, coa.name;
ELSIF p_period_type = CONST_YTD THEN
RETURN QUERY
WITH ytd_fin_years AS (
SELECT fy.start_date, fy.end_date
FROM public.finance_year fy
WHERE EXTRACT(YEAR FROM fy.start_date) BETWEEN EXTRACT(YEAR FROM CURRENT_DATE) - 4 AND EXTRACT(YEAR FROM CURRENT_DATE)
ORDER BY fy.start_date DESC
LIMIT 5
)
SELECT
'YTD' AS period,
NULL::numeric AS year,
coa.id AS account_id,
coa.name AS account_name,
COALESCE(SUM(je.amount), 0) AS total_expense
FROM ytd_fin_years
LEFT JOIN public.journal_entries je
ON je.transaction_date BETWEEN ytd_fin_years.start_date AND LEAST(
ytd_fin_years.end_date,
CASE WHEN CURRENT_DATE BETWEEN ytd_fin_years.start_date AND ytd_fin_years.end_date
THEN CURRENT_DATE ELSE ytd_fin_years.end_date END
)
AND je.is_deleted = FALSE
LEFT JOIN public.transaction_headers th ON je.transaction_id = th.id
AND th.company_id = p_company_id
LEFT JOIN public.chart_of_accounts coa ON je.account_id = coa.id
AND coa.organization_id = v_organization_id
AND coa.is_deleted = FALSE
WHERE coa.account_type_id IN (SELECT id FROM get_account_type_hierarchy(5))
AND (coa.name IS NULL OR coa.name NOT IN ('Rounding Gain'))
GROUP BY coa.id, coa.name
ORDER BY total_expense DESC;
END IF;
END;
$function$
-- Function: get_income_expense_monthly_breakdown
CREATE OR REPLACE FUNCTION public.get_income_expense_monthly_breakdown(p_company_id uuid, p_finance_year_id integer)
RETURNS TABLE(id uuid, account_type text, account_name text, parent_account_id uuid, level integer, year integer, apr numeric, apr_diff numeric, may numeric, may_diff numeric, jun numeric, jun_diff numeric, jul numeric, jul_diff numeric, aug numeric, aug_diff numeric, sep numeric, sep_diff numeric, oct numeric, oct_diff numeric, nov numeric, nov_diff numeric, "dec" numeric, dec_diff numeric, jan numeric, jan_diff numeric, feb numeric, feb_diff numeric, mar numeric, mar_diff numeric, total numeric, difference numeric, order_sequence character varying)
LANGUAGE plpgsql
AS $function$
DECLARE
v_organization_id UUID;
v_start_date DATE;
v_end_date DATE;
v_prev_start_date DATE;
v_prev_end_date DATE;
BEGIN
-- Get the organization_id for the given company_id
SELECT c.organization_id INTO v_organization_id
FROM public.companies c
WHERE c.id = p_company_id;
-- Get the start and end dates for the selected financial year
SELECT fy.start_date, fy.end_date INTO v_start_date, v_end_date
FROM public.finance_year fy
WHERE fy.id = p_finance_year_id;
-- Get the start and end dates for the previous financial year
SELECT pfy.start_date, pfy.end_date INTO v_prev_start_date, v_prev_end_date
FROM public.finance_year pfy
WHERE pfy.id = (p_finance_year_id - 1);
RETURN QUERY
WITH RECURSIVE account_hierarchy AS (
-- Get all income and expense accounts in a hierarchical order
SELECT
coa.id AS id,
act.name AS account_type,
coa.name AS account_name,
coa.parent_account_id,
0 AS level,
coa.account_number::TEXT::CHARACTER VARYING AS order_sequence -- ✅ Cast explicitly
FROM chart_of_accounts coa
JOIN account_types act ON coa.account_type_id = act.id
WHERE coa.organization_id = v_organization_id
AND act.id IN (4, 5)
AND (coa.parent_account_id IS NULL OR coa.parent_account_id = '00000000-0000-0000-0000-000000000000')
AND coa.is_deleted = FALSE
UNION ALL
SELECT
coa.id AS id,
act.name AS account_type,
coa.name AS account_name,
coa.parent_account_id,
parent.level + 1 AS level,
(parent.order_sequence || '.' || coa.account_number::TEXT)::CHARACTER VARYING -- ✅ Cast explicitly
FROM chart_of_accounts coa
JOIN account_hierarchy parent ON parent.id = coa.parent_account_id
JOIN account_types act ON coa.account_type_id = act.id
WHERE coa.organization_id = v_organization_id
AND coa.is_deleted = FALSE
),
years AS (
-- Ensure two years are always present
SELECT p_finance_year_id AS year
UNION ALL
SELECT p_finance_year_id - 1
),
all_accounts AS (
-- Ensure all accounts have both years
SELECT ah.id, ah.account_name, ah.account_type, ah.parent_account_id, ah.level, ah.order_sequence, y.year
FROM account_hierarchy ah
CROSS JOIN years y
),
monthly_totals AS (
-- Get monthly totals for the selected and previous financial years
SELECT
je.account_id AS id,
CASE
WHEN je.transaction_date BETWEEN v_start_date AND v_end_date THEN p_finance_year_id
WHEN je.transaction_date BETWEEN v_prev_start_date AND v_prev_end_date THEN (p_finance_year_id - 1)
END AS year,
EXTRACT(MONTH FROM je.transaction_date) AS month,
SUM(je.amount) AS amount
FROM journal_entries je
JOIN transaction_headers th ON je.transaction_id = th.id
WHERE th.company_id = p_company_id
AND je.transaction_date BETWEEN v_prev_start_date AND v_end_date
GROUP BY je.account_id, year, EXTRACT(MONTH FROM je.transaction_date)
),
pivoted_data AS (
-- Pivoting data to month-wise columns
SELECT
aa.id,
aa.account_type,
aa.account_name,
aa.parent_account_id,
aa.level,
aa.year,
aa.order_sequence,
COALESCE(SUM(CASE WHEN mt.month = 4 THEN mt.amount ELSE 0 END), 0) AS apr,
COALESCE(SUM(CASE WHEN mt.month = 5 THEN mt.amount ELSE 0 END), 0) AS may,
COALESCE(SUM(CASE WHEN mt.month = 6 THEN mt.amount ELSE 0 END), 0) AS jun,
COALESCE(SUM(CASE WHEN mt.month = 7 THEN mt.amount ELSE 0 END), 0) AS jul,
COALESCE(SUM(CASE WHEN mt.month = 8 THEN mt.amount ELSE 0 END), 0) AS aug,
COALESCE(SUM(CASE WHEN mt.month = 9 THEN mt.amount ELSE 0 END), 0) AS sep,
COALESCE(SUM(CASE WHEN mt.month = 10 THEN mt.amount ELSE 0 END), 0) AS oct,
COALESCE(SUM(CASE WHEN mt.month = 11 THEN mt.amount ELSE 0 END), 0) AS nov,
COALESCE(SUM(CASE WHEN mt.month = 12 THEN mt.amount ELSE 0 END), 0) AS "dec",
COALESCE(SUM(CASE WHEN mt.month = 1 THEN mt.amount ELSE 0 END), 0) AS jan,
COALESCE(SUM(CASE WHEN mt.month = 2 THEN mt.amount ELSE 0 END), 0) AS feb,
COALESCE(SUM(CASE WHEN mt.month = 3 THEN mt.amount ELSE 0 END), 0) AS mar,
COALESCE(SUM(mt.amount), 0) AS total
FROM all_accounts aa
LEFT JOIN monthly_totals mt ON aa.id = mt.id AND aa.year = mt.year
GROUP BY aa.id, aa.account_type, aa.account_name, aa.parent_account_id, aa.level, aa.order_sequence, aa.year
)
-- Final table with the difference calculation
SELECT
pd.id,
pd.account_type,
LPAD('', pd.level * 4, ' ') || pd.account_name,
pd.parent_account_id,
pd.level,
pd.year,
pd.apr, COALESCE(pd.apr - LAG(pd.apr) OVER (PARTITION BY pd.id ORDER BY pd.year), 0) AS apr_diff,
pd.may, COALESCE(pd.may - LAG(pd.may) OVER (PARTITION BY pd.id ORDER BY pd.year), 0) AS may_diff,
pd.jun, COALESCE(pd.jun - LAG(pd.jun) OVER (PARTITION BY pd.id ORDER BY pd.year), 0) AS jun_diff,
pd.jul, COALESCE(pd.jul - LAG(pd.jul) OVER (PARTITION BY pd.id ORDER BY pd.year), 0) AS jul_diff,
pd.aug, COALESCE(pd.aug - LAG(pd.aug) OVER (PARTITION BY pd.id ORDER BY pd.year), 0) AS aug_diff,
pd.sep, COALESCE(pd.sep - LAG(pd.sep) OVER (PARTITION BY pd.id ORDER BY pd.year), 0) AS sep_diff,
pd.oct, COALESCE(pd.oct - LAG(pd.oct) OVER (PARTITION BY pd.id ORDER BY pd.year), 0) AS oct_diff,
pd.nov, COALESCE(pd.nov - LAG(pd.nov) OVER (PARTITION BY pd.id ORDER BY pd.year), 0) AS nov_diff,
pd.dec, COALESCE(pd.dec - LAG(pd.dec) OVER (PARTITION BY pd.id ORDER BY pd.year), 0) AS dec_diff,
pd.jan, COALESCE(pd.jan - LAG(pd.jan) OVER (PARTITION BY pd.id ORDER BY pd.year), 0) AS jan_diff,
pd.feb, COALESCE(pd.feb - LAG(pd.feb) OVER (PARTITION BY pd.id ORDER BY pd.year), 0) AS feb_diff,
pd.mar, COALESCE(pd.mar - LAG(pd.mar) OVER (PARTITION BY pd.id ORDER BY pd.year), 0) AS mar_diff,
pd.total,
COALESCE(pd.total - LAG(pd.total) OVER (PARTITION BY pd.id ORDER BY pd.year), 0) AS difference,
pd.order_sequence::CHARACTER VARYING -- ✅ Ensures type consistency
FROM pivoted_data pd
ORDER BY pd.order_sequence;
END;
$function$
-- Function: fn_check_account_id_exists
CREATE OR REPLACE FUNCTION public.fn_check_account_id_exists(account_id uuid)
RETURNS boolean
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN EXISTS (
SELECT 1 FROM public.vendors WHERE id = account_id
) OR EXISTS (
SELECT 1 FROM public.customers WHERE id = account_id
) OR EXISTS (
SELECT 1 FROM public.chart_of_accounts WHERE id = account_id
);
END;
$function$
-- Function: create_notification_with_message
CREATE OR REPLACE FUNCTION public.create_notification_with_message(p_user_id uuid, p_message text, p_route text, p_short_desc text, p_created_by uuid, p_created_time timestamp without time zone, p_additional_parameter_id text)
RETURNS integer
LANGUAGE plpgsql
AS $function$
DECLARE
v_notification_id INT;
BEGIN
-- Insert notification and retrieve generated ID
INSERT INTO notifications (
user_id, message, route, short_description, additional_parameter_id,
created_on_utc, created_by, is_deleted
) VALUES (
p_user_id, p_message, p_route, p_short_desc,p_additional_parameter_id,
p_created_time, p_created_by, FALSE
)
RETURNING id INTO v_notification_id;
RETURN v_notification_id;
END;
$function$
-- Function: approve_visitor_for_unit
CREATE OR REPLACE FUNCTION public.approve_visitor_for_unit(p_visitor_log_id integer, p_unit_id integer, p_resident_user uuid, p_resident_id integer)
RETURNS TABLE(visitor_log_id integer, unit_id integer, approver_resident_id integer, final_status_id integer)
LANGUAGE plpgsql
AS $function$
DECLARE
local_total_visits INT;
local_approved_count INT;
local_rejected_count INT;
local_new_visitor_status INT;
BEGIN
-- Step 1: Approve for this unit only
UPDATE public.multi_unit_visits
SET visitor_statuses = 2, -- Approved
modified_on_utc = NOW(),
modified_by = p_resident_user
WHERE visitor_log_id = p_visitor_log_id
AND unit_id = p_unit_id
AND is_deleted = FALSE;
IF NOT FOUND THEN
-- No update happened (invalid visitor_log_id or unit_id)
RETURN;
END IF;
-- Step 2: Recalculate parent visitor log status
SELECT
COUNT(*),
COUNT(*) FILTER (WHERE muv.visitor_statuses = 2), -- Approved
COUNT(*) FILTER (WHERE muv.visitor_statuses = 6) -- Rejected
INTO
local_total_visits,
local_approved_count,
local_rejected_count
FROM public.multi_unit_visits muv
WHERE muv.visitor_log_id = p_visitor_log_id
AND muv.is_deleted = FALSE;
-- Step 3: Decide new parent status
IF local_approved_count = local_total_visits AND local_total_visits > 0 THEN
local_new_visitor_status := 2; -- All Approved
ELSIF local_approved_count > 0 THEN
local_new_visitor_status := 7; -- Partial Approved
ELSIF local_rejected_count > 0 THEN
local_new_visitor_status := 6; -- Rejected
ELSE
local_new_visitor_status := 1; -- Pending
END IF;
UPDATE public.visitor_logs
SET visitor_status_id = local_new_visitor_status,
modified_on_utc = NOW(),
modified_by = p_resident_user
WHERE id = p_visitor_log_id;
-- Step 4: Return minimal information for the service layer
RETURN QUERY
SELECT
p_visitor_log_id,
p_unit_id,
p_resident_id, -- approver resident
local_new_visitor_status;
END;
$function$
-- Function: get_address_id
CREATE OR REPLACE FUNCTION public.get_address_id(p_country_id uuid, p_state_id uuid, p_city_id uuid, p_address_line1 text, p_zip_code text, p_created_by uuid, p_address_line2 text DEFAULT NULL::text)
RETURNS uuid
LANGUAGE plpgsql
AS $function$
DECLARE
v_address_id UUID;
BEGIN
-- Insert into addresses table
INSERT INTO public.addresses (
id,
country_id,
state_id,
city_id,
address_line1,
address_line2,
zip_code,
created_on_utc,
created_by
) VALUES (
gen_random_uuid(), -- Generated address ID
p_country_id, -- Fetched country ID
p_state_id, -- Fetched state ID
p_city_id, -- City ID
p_address_line1, -- Address Line 1
p_address_line2,
p_zip_code, -- Zip Code
NOW(), -- Created on timestamp
p_created_by -- Created by
)RETURNING id INTO v_address_id;
RETURN v_address_id;
END;
$function$
-- Function: get_all_child_accounts
CREATE OR REPLACE FUNCTION public.get_all_child_accounts(p_organization_id uuid, p_group_id uuid)
RETURNS TABLE(id uuid)
LANGUAGE plpgsql
AS $function$
BEGIN
IF p_organization_id IS NULL OR p_group_id IS NULL THEN
RAISE EXCEPTION 'Organization ID and COA ID cannot be null';
END IF;
RETURN QUERY
WITH RECURSIVE account_group_view AS (
SELECT
ag.code,
ag.group_code,
ag.top_group_code,
ag.parent_group_code
FROM
public.account_groups ag
LEFT OUTER JOIN public.account_groups tgc
ON tgc.code = ag.top_group_code
WHERE
ag.organization_id = p_organization_id AND
ag.id = p_group_id
UNION ALL
SELECT
parent.code,
parent.group_code,
parent.top_group_code,
parent.parent_group_code
FROM
public.account_groups parent
LEFT OUTER JOIN public.account_groups tgc
ON tgc.code = parent.top_group_code
JOIN
account_group_view ag
ON parent.parent_group_code = ag.code
AND parent.organization_id = p_organization_id
)
SELECT
coa.id
FROM
public.chart_of_accounts coa
INNER JOIN account_group_view agv on coa.account_group_code = agv.code;
END;
$function$
-- Function: get_opening_balances
CREATE OR REPLACE FUNCTION public.get_opening_balances(p_organization_id uuid, p_fin_year_id integer)
RETURNS TABLE(account_id uuid, balance numeric)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
ob.account_id,
SUM(
CASE
WHEN ob.entry_type = 'D' THEN ob.balance
ELSE -ob.balance
END
) AS balance
FROM public.opening_balances ob
WHERE ob.organization_id = p_organization_id
AND ob.finyear_id = p_fin_year_id
AND ob.is_deleted = false
group by ob.account_id;
END;
$function$
-- Function: get_trial_balance_of_company_fy
CREATE OR REPLACE FUNCTION public.get_trial_balance_of_company_fy(p_company_id uuid, p_finance_year_id integer)
RETURNS TABLE(account_id uuid, account_type text, account_category text, account_number text, account_name text, total_debits numeric, total_credits numeric)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
WITH RECURSIVE coa_hierarchy AS (
-- Base case: Fetch the top-level (root) accounts with no parent (e.g., Assets, Liabilities)
SELECT
coa.id,
act.name AS account_type,
coa.account_number::integer,
coa.name,
coa.parent_account_id,
0 AS level,
coa.account_number::text AS order_sequence
FROM
public.chart_of_accounts coa
INNER JOIN
public.account_types act ON coa.account_type_id = act.id
WHERE
coa.is_deleted = FALSE
AND coa.parent_account_id = '00000000-0000-0000-0000-000000000000' -- Root-level accounts
UNION ALL
-- Recursive case: Fetch all child accounts for each parent
SELECT
coa.id,
act.name AS account_type,
coa.account_number::integer,
coa.name,
coa.parent_account_id,
ch.level + 1 AS level,
ch.order_sequence || '.' || coa.account_number::text AS order_sequence
FROM
public.chart_of_accounts coa
INNER JOIN
public.account_types act ON coa.account_type_id = act.id
INNER JOIN
coa_hierarchy ch ON ch.id = coa.parent_account_id
WHERE
coa.is_deleted = FALSE
),
coa_with_balances AS (
-- Fetch the current financial year start and end date from the finance_year table
SELECT
fy.start_date::DATE, -- Cast start_date to DATE
fy.end_date::DATE, -- Cast end_date to DATE
fy.year
FROM
public.finance_year fy
WHERE
fy.id = p_finance_year_id
LIMIT 1
),
journal_entries_filtered AS (
-- Fetch accounts that have non-zero balances within the financial year range
SELECT
je.account_id,
SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END) AS total_debits,
SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END) AS total_credits
FROM
public.journal_entries je
JOIN
public.transaction_headers th ON je.transaction_id = th.id
JOIN
coa_with_balances fy ON th.transaction_date BETWEEN fy.start_date AND fy.end_date -- Filter by financial year
WHERE
th.company_id = p_company_id
AND je.is_deleted = FALSE
GROUP BY
je.account_id
HAVING
SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END) > 0 OR
SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END) > 0
),
relevant_accounts AS (
-- Select accounts that have balances directly
SELECT
ch.id,
ch.account_type,
ch.account_number,
ch.name,
ch.level,
ch.order_sequence,
ch.parent_account_id,
wb.total_debits,
wb.total_credits
FROM
coa_hierarchy ch
LEFT JOIN
journal_entries_filtered wb ON ch.id = wb.account_id
WHERE
wb.account_id IS NOT NULL -- Only accounts with actual balances
UNION ALL
-- Recursively select parent accounts, without recalculating balances
SELECT
ch.id,
ch.account_type,
ch.account_number,
ch.name,
ch.level,
ch.order_sequence,
ch.parent_account_id,
NULL AS total_debits,
NULL AS total_credits
FROM
coa_hierarchy ch
INNER JOIN
relevant_accounts ra ON ch.id = ra.parent_account_id -- Propagate balance up to parent accounts
)
-- Final output
SELECT DISTINCT ON (ra.order_sequence)
ra.id::uuid AS account_id, -- Chart of Accounts ID
ra.account_type::TEXT AS account_type, -- Account type
ac.name::TEXT AS account_category, -- Account category
ra.account_number::TEXT AS account_number, -- Account number
LPAD('', ra.level * 4, ' ') || ra.name::TEXT AS account_name, -- Indented account name
COALESCE(ra.total_debits, 0) AS total_debits, -- Sum of debits
COALESCE(ra.total_credits, 0) AS total_credits -- Sum of credits
-- Cast end date of the financial year to DATE
FROM
relevant_accounts ra
LEFT JOIN
public.chart_of_accounts coa ON ra.id = coa.id -- Join chart of accounts
LEFT JOIN
public.account_types at ON coa.account_type_id = at.id -- Join account types
LEFT JOIN
public.account_categories ac ON at.account_category_id = ac.id -- Join account categories
CROSS JOIN
coa_with_balances fy -- Include the financial year details
ORDER BY
ra.order_sequence; -- Maintain hierarchy in the output
END;
$function$
-- Function: pgp_pub_encrypt_bytea
CREATE OR REPLACE FUNCTION public.pgp_pub_encrypt_bytea(bytea, bytea, text)
RETURNS bytea
LANGUAGE c
PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_pub_encrypt_bytea$function$
-- Function: pgp_pub_decrypt
CREATE OR REPLACE FUNCTION public.pgp_pub_decrypt(bytea, bytea)
RETURNS text
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_pub_decrypt_text$function$
-- Function: pgp_pub_decrypt_bytea
CREATE OR REPLACE FUNCTION public.pgp_pub_decrypt_bytea(bytea, bytea)
RETURNS bytea
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_pub_decrypt_bytea$function$
-- Function: search_function_with_paramter
CREATE OR REPLACE FUNCTION public.search_function_with_paramter(keyword text)
RETURNS TABLE(function_name text, function_definition text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
proname AS function_name,
prosrc AS function_definition
FROM pg_proc
WHERE prosrc ILIKE '%' || keyword || '%';
END;
$function$
-- Function: get_balance_sheet
CREATE OR REPLACE FUNCTION public.get_balance_sheet(p_company_id uuid, p_finance_year_id integer, p_as_on_date date, p_is_get_all_for_organizations boolean)
RETURNS TABLE(account_id uuid, account_type text, account_name text, amount numeric, category text)
LANGUAGE plpgsql
AS $function$
DECLARE
v_start_date DATE;
v_end_date DATE;
v_company_ids UUID[];
v_organization_id UUID;
asset_account_ids integer[];
liability_account_ids integer[];
equity_account_ids integer[];
BEGIN
-- Fetch financial year start and end dates
SELECT fy.start_date, fy.end_date
INTO v_start_date, v_end_date
FROM public.finance_year fy
WHERE fy.id = p_finance_year_id;
-- Cap the end date by the given as_on_date
v_end_date := LEAST(v_end_date, p_as_on_date);
-- Get organization id of company
SELECT c.organization_id INTO v_organization_id
FROM public.companies c
WHERE c.id = p_company_id;
-- Get account type hierarchies
asset_account_ids := ARRAY(SELECT id FROM get_account_type_hierarchy(1)); -- Assets
liability_account_ids := ARRAY(SELECT id FROM get_account_type_hierarchy(2)); -- Liabilities
equity_account_ids := ARRAY(SELECT id FROM get_account_type_hierarchy(3)); -- Equity
-- Company IDs based on flag
IF p_is_get_all_for_organizations THEN
SELECT ARRAY_AGG(id)
INTO v_company_ids
FROM public.companies
WHERE organization_id = v_organization_id;
ELSE
v_company_ids := ARRAY[p_company_id];
END IF;
-- Assets
RETURN QUERY
SELECT
coa.id,
at.name,
coa.name,
COALESCE(SUM(
CASE
WHEN je.entry_type = 'D' THEN je.amount
WHEN je.entry_type = 'C' THEN -je.amount
ELSE 0
END
),0) AS amount,
'Assets'
FROM public.chart_of_accounts coa
JOIN public.account_types at ON coa.account_type_id = at.id
JOIN public.journal_entries je ON je.account_id = coa.id
JOIN public.transaction_headers th ON th.id = je.transaction_id
WHERE th.company_id = ANY(v_company_ids)
AND th.transaction_date BETWEEN v_start_date AND v_end_date
AND NOT th.is_deleted
AND NOT je.is_deleted
AND coa.account_type_id = ANY(asset_account_ids)
GROUP BY coa.id, at.name, coa.name, coa.account_number, at.id
ORDER BY coa.account_number, at.id;
-- Liabilities
RETURN QUERY
SELECT
coa.id,
at.name,
coa.name,
COALESCE(SUM(
CASE
WHEN je.entry_type = 'C' THEN je.amount
WHEN je.entry_type = 'D' THEN -je.amount
ELSE 0
END
),0) AS amount,
'Liabilities'
FROM public.chart_of_accounts coa
JOIN public.account_types at ON coa.account_type_id = at.id
JOIN public.journal_entries je ON je.account_id = coa.id
JOIN public.transaction_headers th ON th.id = je.transaction_id
WHERE th.company_id = ANY(v_company_ids)
AND th.transaction_date BETWEEN v_start_date AND v_end_date
AND NOT th.is_deleted
AND NOT je.is_deleted
AND coa.account_type_id = ANY(liability_account_ids)
GROUP BY coa.id, at.name, coa.name, coa.account_number, at.id
ORDER BY coa.account_number, at.id;
-- Equity
RETURN QUERY
SELECT
coa.id,
at.name,
coa.name,
COALESCE(SUM(
CASE
WHEN je.entry_type = 'C' THEN je.amount
WHEN je.entry_type = 'D' THEN -je.amount
ELSE 0
END
),0) AS amount,
'Equity'
FROM public.chart_of_accounts coa
JOIN public.account_types at ON coa.account_type_id = at.id
JOIN public.journal_entries je ON je.account_id = coa.id
JOIN public.transaction_headers th ON th.id = je.transaction_id
WHERE th.company_id = ANY(v_company_ids)
AND th.transaction_date BETWEEN v_start_date AND v_end_date
AND NOT th.is_deleted
AND NOT je.is_deleted
AND coa.account_type_id = ANY(equity_account_ids)
GROUP BY coa.id, at.name, coa.name, coa.account_number, at.id
ORDER BY coa.account_number, at.id;
END;
$function$
-- Function: get_all_account_groups
CREATE OR REPLACE FUNCTION public.get_all_account_groups(p_organization_id uuid)
RETURNS TABLE(id uuid, organization_id uuid, code integer, group_code integer, top_group_code integer, parent_group_code integer, name text, level integer, order_sequence character varying, schedule text, is_main_group boolean)
LANGUAGE plpgsql
AS $function$
#variable_conflict use_column
begin
RETURN query
WITH RECURSIVE account_group_view AS (
SELECT
id,
organization_id,
code,
group_code,
top_group_code,
parent_group_code,
name,
0 AS level,
CAST(code AS varchar(50)) AS order_sequence,
schedule,
is_main_group
FROM
public.account_groups
WHERE
organization_id = p_organization_id AND
parent_group_code is null
UNION ALL
SELECT
parent.id,
parent.organization_id,
parent.code,
parent.group_code,
parent.top_group_code,
parent.parent_group_code,
parent.name,
level + 1 AS level,
CAST(order_sequence || '_' || CAST(parent.code AS VARCHAR (50)) AS VARCHAR(50)) AS order_sequence,
parent.schedule,
parent.is_main_group
FROM
public.account_groups parent
JOIN
account_group_view ag
ON parent.parent_group_code = ag.code AND parent.organization_id = p_organization_id
)
SELECT
id,
organization_id,
code,
group_code,
top_group_code,
parent_group_code,
RIGHT(' ',level*3) || name AS name,
level,
order_sequence,
schedule,
is_main_group
FROM
account_group_view
ORDER BY
order_sequence;
end
$function$
-- Function: get_user_notifications_with_message
CREATE OR REPLACE FUNCTION public.get_user_notifications_with_message(p_user_id uuid)
RETURNS TABLE(notification_id integer, message text, short_desc text, route text, message_created_time timestamp without time zone, additional_parameter_id text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
nt.id,
nt.message,
nt.short_description,
nt.route,
msg.created_time,
msg.additional_parameter_id
FROM notification_types nt
LEFT JOIN LATERAL (
SELECT m.*
FROM messages m
WHERE m.notification_type_id = nt.id AND m.is_deleted = FALSE
ORDER BY m.created_time DESC
LIMIT 1
) msg ON true
WHERE nt.user_id = p_user_id
--AND nt.created_on_utc >= NOW() - INTERVAL '24 hours'
AND nt.is_deleted = FALSE;
END;
$function$
-- Function: get_city_id
CREATE OR REPLACE FUNCTION public.get_city_id(p_city_name text, p_zip_code text, p_state_id uuid, p_created_by uuid)
RETURNS uuid
LANGUAGE plpgsql
AS $function$
DECLARE
v_city_id UUID;
BEGIN
SELECT id INTO v_city_id
FROM public.cities
WHERE LOWER(name) = LOWER(p_city_name) AND zip_code = p_zip_code;
IF v_city_id IS NULL THEN
INSERT INTO public.cities (id, name, state_id, zip_code, created_on_utc, created_by)
VALUES (gen_random_uuid(), p_city_name, p_state_id, p_zip_code, NOW(), p_created_by)
RETURNING id INTO v_city_id;
END IF;
RETURN v_city_id;
END;
$function$
-- Function: grant_full_schema_access
CREATE OR REPLACE FUNCTION public.grant_full_schema_access(p_schema text, p_user text)
RETURNS void
LANGUAGE plpgsql
AS $function$
DECLARE
obj RECORD;
BEGIN
-- Grant on tables
FOR obj IN
SELECT table_name
FROM information_schema.tables
WHERE table_schema = p_schema
AND table_type = 'BASE TABLE'
LOOP
EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE %I.%I TO %I;', p_schema, obj.table_name, p_user);
RAISE NOTICE 'GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE %.% TO %;', p_schema, obj.table_name, p_user;
END LOOP;
-- Grant on sequences (USAGE + SELECT + UPDATE: full coverage)
FOR obj IN
SELECT c.relname AS sequence_name
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'S'
AND n.nspname = p_schema
LOOP
EXECUTE format('GRANT USAGE, SELECT, UPDATE ON SEQUENCE %I.%I TO %I;', p_schema, obj.sequence_name, p_user);
RAISE NOTICE 'GRANT USAGE, SELECT, UPDATE ON SEQUENCE %.% TO %;', p_schema, obj.sequence_name, p_user;
END LOOP;
-- Grant on all functions (handles all argument types)
FOR obj IN
SELECT
p.proname AS function_name,
pg_get_function_identity_arguments(p.oid) AS args
FROM
pg_proc p
JOIN pg_namespace n ON p.pronamespace = n.oid
WHERE
n.nspname = p_schema
AND p.prokind = 'f' -- f = function
LOOP
EXECUTE format(
'GRANT EXECUTE ON FUNCTION %I.%I(%s) TO %I;',
p_schema, obj.function_name, obj.args, p_user
);
RAISE NOTICE 'GRANT EXECUTE ON FUNCTION %.%(%) TO %;', p_schema, obj.function_name, obj.args, p_user;
END LOOP;
-- Grant on all procedures (Postgres 11+)
FOR obj IN
SELECT
p.proname AS procedure_name,
pg_get_function_identity_arguments(p.oid) AS args
FROM
pg_proc p
JOIN pg_namespace n ON p.pronamespace = n.oid
WHERE
n.nspname = p_schema
AND p.prokind = 'p' -- p = procedure
LOOP
EXECUTE format(
'GRANT EXECUTE ON PROCEDURE %I.%I(%s) TO %I;',
p_schema, obj.procedure_name, obj.args, p_user
);
RAISE NOTICE 'GRANT EXECUTE ON PROCEDURE %.%(%) TO %;', p_schema, obj.procedure_name, obj.args, p_user;
END LOOP;
END;
$function$
-- Function: create_budget
CREATE OR REPLACE FUNCTION public.create_budget(p_company_id uuid, p_finance_year_id integer, p_name text, p_status_id integer, p_created_by uuid, p_budget_lines_json jsonb)
RETURNS uuid
LANGUAGE plpgsql
AS $function$
DECLARE
v_budget_id uuid;
v_now timestamp := now();
line jsonb;
v_line_id uuid;
BEGIN
RAISE NOTICE '---- create_budget() START ----';
RAISE NOTICE '---- create_budget() START 123----';
RAISE NOTICE 'Company: %, FinanceYear: %, Name: %', p_company_id, p_finance_year_id, p_name;
RAISE NOTICE 'Input budget lines JSON: %', p_budget_lines_json;
-- Step 1: Check if a budget already exists
SELECT id INTO v_budget_id
FROM budgets
WHERE company_id = p_company_id
AND finance_year_id = p_finance_year_id
AND is_deleted = false
LIMIT 1;
IF v_budget_id IS NOT NULL THEN
RAISE NOTICE 'Existing budget found: %', v_budget_id;
UPDATE budgets
SET name = p_name,
status_id = p_status_id,
modified_by = p_created_by,
modified_on_utc = v_now
WHERE id = v_budget_id;
ELSE
v_budget_id := gen_random_uuid();
RAISE NOTICE 'Inserting new budget: %', v_budget_id;
INSERT INTO budgets (
id, company_id, finance_year_id, name, status_id,
created_by, created_on_utc, is_deleted
)
VALUES (
v_budget_id, p_company_id, p_finance_year_id, p_name, p_status_id,
p_created_by, v_now, false
);
END IF;
-- Step 2: Loop through each line
FOR line IN SELECT * FROM jsonb_array_elements(p_budget_lines_json)
LOOP
RAISE NOTICE 'Processing line: %', line;
v_line_id := NULLIF(line->>'id', '')::uuid;
IF v_line_id IS NULL OR v_line_id = '00000000-0000-0000-0000-000000000000'::uuid THEN
v_line_id := gen_random_uuid();
RAISE NOTICE 'Generated new line ID: %', v_line_id;
ELSE
RAISE NOTICE 'Using provided line ID: %', v_line_id;
END IF;
IF EXISTS (
SELECT 1 FROM budget_lines
WHERE id = v_line_id AND budget_id = v_budget_id AND is_deleted = false
) THEN
RAISE NOTICE 'Updating existing budget line: %', v_line_id;
UPDATE budget_lines
SET account_id = (line->>'accountId')::uuid,
period_start = (line->>'periodStart')::timestamp::date,
period_end = (line->>'periodEnd')::timestamp::date,
amount = (line->>'amount')::numeric,
modified_by = p_created_by,
modified_on_utc = v_now,
is_deleted = false
WHERE id = v_line_id AND budget_id = v_budget_id;
ELSE
RAISE NOTICE 'Inserting new budget line: %', v_line_id;
INSERT INTO budget_lines (
id, budget_id, account_id, period_start, period_end,
amount, created_by, created_on_utc, is_deleted
)
VALUES (
v_line_id,
v_budget_id,
(line->>'accountId')::uuid,
(line->>'periodStart')::timestamp::date,
(line->>'periodEnd')::timestamp::date,
(line->>'amount')::numeric,
p_created_by,
v_now,
false
);
END IF;
END LOOP;
RAISE NOTICE '---- create_budget() END ----';
RETURN v_budget_id;
END;
$function$
-- Function: dummy_hello_function
CREATE OR REPLACE FUNCTION public.dummy_hello_function(name text)
RETURNS text
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN 'Hello, ' || name || '!';
END;
$function$
-- Function: get_account_total_amount_by_date_range
CREATE OR REPLACE FUNCTION public.get_account_total_amount_by_date_range(p_company_id uuid, p_finance_id integer, p_account_id uuid, p_start_date date DEFAULT NULL::date, p_end_date date DEFAULT NULL::date)
RETURNS numeric
LANGUAGE plpgsql
AS $function$
DECLARE
v_financial_year_start DATE;
v_financial_year_end DATE;
v_total_amount NUMERIC := 0;
v_organization_id UUID;
BEGIN
-- Step 1: Get the organization ID for the given company
SELECT organization_id
INTO v_organization_id
FROM public.companies
WHERE id = p_company_id;
-- Step 2: Validate if the organization ID exists
IF v_organization_id IS NULL THEN
RAISE EXCEPTION 'Organization ID not found for company ID %', p_company_id;
END IF;
-- Step 3: Get the financial year start and end dates
SELECT fy.start_date, fy.end_date
INTO v_financial_year_start, v_financial_year_end
FROM public.finance_year fy
WHERE fy.id = p_finance_id;
-- Step 4: Check if the financial year exists
IF v_financial_year_start IS NULL OR v_financial_year_end IS NULL THEN
RAISE EXCEPTION 'Financial year with ID % not found', p_finance_id;
END IF;
-- Step 5: Calculate the total amount for the specific account within the given date range
SELECT COALESCE(SUM(je.amount), 0) INTO v_total_amount
FROM public.journal_entries je
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
INNER JOIN public.chart_of_accounts coa ON je.account_id = coa.id
WHERE th.company_id = p_company_id
AND je.account_id = p_account_id
AND coa.organization_id = v_organization_id -- Match organization ID
AND je.transaction_date BETWEEN COALESCE(p_start_date, v_financial_year_start) -- Use provided start date or financial year start
AND COALESCE(p_end_date, v_financial_year_end) -- Use provided end date or financial year end
AND je.is_deleted = FALSE;
-- Step 6: Return the total amount
RETURN v_total_amount;
END;
$function$
-- Function: get_all_users
CREATE OR REPLACE FUNCTION public.get_all_users()
RETURNS TABLE(id uuid, first_name character varying, last_name character varying, email character varying, phone_number character varying, user_name text)
LANGUAGE sql
AS $function$
SELECT id, first_name, last_name, email, phone_number, user_name
FROM public.users;
$function$
-- Function: get_balance_sheet_3
CREATE OR REPLACE FUNCTION public.get_balance_sheet_3(p_company_id uuid, p_finance_year_id integer, p_is_get_all_for_organizations boolean)
RETURNS TABLE(account_type text, account_name text, amount numeric, category text)
LANGUAGE plpgsql
AS $function$
DECLARE
v_start_date DATE;
v_end_date DATE;
v_company_ids UUID[];
v_organization_id UUID;
asset_account_ids integer[];
liability_account_ids integer[];
equity_account_ids integer[];
BEGIN
-- Fetch financial year start and end dates
SELECT fy.start_date, fy.end_date
INTO v_start_date, v_end_date
FROM public.finance_year fy
WHERE fy.id = p_finance_year_id;
select c.organization_id into v_organization_id
from public.companies c
where c.id = p_company_id;
-- Get account type hierarchies for assets, liabilities, and equity
asset_account_ids := ARRAY(SELECT id FROM get_account_type_hierarchy(1)); -- Assets
liability_account_ids := ARRAY(SELECT id FROM get_account_type_hierarchy(2)); -- Liabilities
equity_account_ids := ARRAY(SELECT id FROM get_account_type_hierarchy(3)); -- Equity
-- Determine company IDs to fetch data for (single or all organizations)
IF p_is_get_all_for_organizations THEN
SELECT ARRAY_AGG(id)
INTO v_company_ids
FROM public.companies
WHERE organization_id = (SELECT organization_id FROM public.companies WHERE id = p_company_id);
ELSE
v_company_ids := ARRAY[p_company_id];
END IF;
-- Return Assets
RETURN QUERY
SELECT
at.name AS account_type,
coa.name AS account_name,
COALESCE(
SUM(
CASE
WHEN ob.entry_type = 'D' THEN ob.balance
ELSE -ob.balance
END
), 0) +
COALESCE(
SUM(CASE
WHEN je.entry_type = 'D' THEN je.amount -- Debit increases asset balance
WHEN je.entry_type = 'C' THEN -je.amount -- Credit decreases asset balance
ELSE 0
END), 0
) AS amount,
'Assets' AS category
FROM public.chart_of_accounts coa
LEFT JOIN public.journal_entries je ON je.account_id = coa.id
LEFT JOIN public.transaction_headers th ON je.transaction_id = th.id
LEFT JOIN public.opening_balances ob ON ob.account_id = coa.id
AND ob.organization_id = v_organization_id
AND ob.finyear_id = p_finance_year_id
AND ob.is_deleted = FALSE
INNER JOIN public.account_types at ON coa.account_type_id = at.id
WHERE th.company_id = ANY(v_company_ids)
AND th.transaction_date BETWEEN v_start_date AND v_end_date
AND NOT th.is_deleted
AND NOT je.is_deleted
AND coa.account_type_id = ANY(asset_account_ids)
GROUP BY at.name, coa.name, coa.account_number, at.id
ORDER BY coa.account_number, at.id;
-- Return Liabilities
RETURN QUERY
SELECT
at.name AS account_type,
coa.name AS account_name,
COALESCE(
SUM(
CASE
WHEN ob.entry_type = 'C' THEN ob.balance
ELSE -ob.balance
END
), 0) +
COALESCE(
SUM(CASE
WHEN je.entry_type = 'C' THEN je.amount
WHEN je.entry_type = 'D' THEN -je.amount
ELSE 0
END), 0
) AS amount,
'Liabilities' AS category
FROM public.chart_of_accounts coa
LEFT JOIN public.journal_entries je ON je.account_id = coa.id
LEFT JOIN public.transaction_headers th ON je.transaction_id = th.id
LEFT JOIN public.opening_balances ob ON ob.account_id = coa.id
AND ob.organization_id = v_organization_id
AND ob.finyear_id = p_finance_year_id
AND ob.is_deleted = FALSE
INNER JOIN public.account_types at ON coa.account_type_id = at.id
WHERE th.company_id = ANY(v_company_ids)
AND th.transaction_date BETWEEN v_start_date AND v_end_date
AND NOT th.is_deleted
AND NOT je.is_deleted
AND coa.account_type_id = ANY(liability_account_ids)
GROUP BY at.name, coa.name, coa.account_number, at.id
ORDER BY coa.account_number, at.id;
-- Return Equity
RETURN QUERY
SELECT
at.name AS account_type,
coa.name AS account_name,
COALESCE(
SUM(
CASE
WHEN ob.entry_type = 'C' THEN ob.balance
ELSE -ob.balance
END
), 0) +
COALESCE(
SUM(CASE
WHEN je.entry_type = 'C' THEN je.amount
WHEN je.entry_type = 'D' THEN -je.amount
ELSE 0
END), 0
) AS amount,
'Equity' AS category
FROM public.chart_of_accounts coa
LEFT JOIN public.journal_entries je ON je.account_id = coa.id
LEFT JOIN public.transaction_headers th ON je.transaction_id = th.id
LEFT JOIN public.opening_balances ob ON ob.account_id = coa.id
AND ob.organization_id = v_organization_id
AND ob.finyear_id = p_finance_year_id
AND ob.is_deleted = FALSE
INNER JOIN public.account_types at ON coa.account_type_id = at.id
WHERE th.company_id = ANY(v_company_ids)
AND th.transaction_date BETWEEN v_start_date AND v_end_date
AND NOT th.is_deleted
AND NOT je.is_deleted
AND coa.account_type_id = ANY(equity_account_ids)
GROUP BY at.name, coa.name, coa.account_number, at.id
ORDER BY coa.account_number, at.id;
END;
$function$
-- Function: get_customer_dues
CREATE OR REPLACE FUNCTION public.get_customer_dues(p_company_id uuid)
RETURNS TABLE(customer_name text, coa_name text, amount numeric, due_date date, aging_bucket text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
c.name::text AS customer_name,
coa.name AS coa_name,
je.amount,
(th.transaction_date + INTERVAL '30 days')::date AS due_date, -- Assuming a 30-day payment term
CASE
WHEN (th.transaction_date + INTERVAL '30 days') <= CURRENT_DATE THEN 'Current'
WHEN (th.transaction_date + INTERVAL '30 days') BETWEEN CURRENT_DATE - INTERVAL '30 days' AND CURRENT_DATE THEN '1-30 Days Past Due'
WHEN (th.transaction_date + INTERVAL '60 days') BETWEEN CURRENT_DATE - INTERVAL '60 days' AND CURRENT_DATE - INTERVAL '31 days' THEN '31-60 Days Past Due'
ELSE 'Over 60 Days Past Due'
END AS aging_bucket
FROM
public.journal_entries je
JOIN
public.transaction_headers th ON je.transaction_id = th.id
JOIN
public.chart_of_accounts coa ON je.account_id = coa.id
JOIN
public.customers c ON th.customer_id = c.id
WHERE
coa.account_type_id = (SELECT id FROM public.account_types WHERE name = 'Accounts Receivable')
AND je.is_deleted = false
AND (th.transaction_date + INTERVAL '30 days') <= CURRENT_DATE
AND th.company_id = p_company_id
ORDER BY
(th.transaction_date + INTERVAL '30 days')::date;
END;
$function$
-- Function: get_total_expense
CREATE OR REPLACE FUNCTION public.get_total_expense(p_company_id uuid, p_finance_id integer)
RETURNS numeric
LANGUAGE plpgsql
AS $function$
DECLARE
v_financial_year_start DATE;
v_financial_year_end DATE;
v_total_expense NUMERIC := 0;
v_organization_id UUID;
BEGIN
-- Step 1: Get the organization ID for the given company
SELECT organization_id
INTO v_organization_id
FROM public.companies
WHERE id = p_company_id;
-- Step 2: Validate if the organization ID exists
IF v_organization_id IS NULL THEN
RAISE EXCEPTION 'Organization ID not found for company ID %', p_company_id;
END IF;
-- Step 3: Get the financial year start and end dates
SELECT fy.start_date, fy.end_date
INTO v_financial_year_start, v_financial_year_end
FROM public.finance_year fy
WHERE fy.id = p_finance_id;
-- Step 4: Validate if the financial year exists
IF v_financial_year_start IS NULL OR v_financial_year_end IS NULL THEN
RAISE EXCEPTION 'Financial year with ID % not found', p_finance_id;
END IF;
-- Step 5: Calculate the total expense
SELECT COALESCE(SUM(je.amount), 0) INTO v_total_expense
FROM public.journal_entries je
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
INNER JOIN public.chart_of_accounts coa ON je.account_id = coa.id
WHERE th.company_id = p_company_id
AND coa.organization_id = v_organization_id -- Match organization ID
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND je.is_deleted = FALSE
AND EXISTS (
SELECT 1
FROM public.account_types at
WHERE at.id = coa.account_type_id
AND at.account_category_id = 5 -- Expense category
);
-- Step 6: Return the total expense
RETURN v_total_expense;
END;
$function$
-- Function: get_income_expense_overview
CREATE OR REPLACE FUNCTION public.get_income_expense_overview(p_company_id uuid, p_period_type integer, p_finance_id integer)
RETURNS TABLE(period text, year numeric, total_income numeric, total_expense numeric, actual_income numeric, actual_expense numeric)
LANGUAGE plpgsql
AS $function$
DECLARE
v_financial_year_start DATE;
v_financial_year_end DATE;
v_finance_year_start NUMERIC;
v_finance_year_end NUMERIC;
CONST_MONTHLY CONSTANT INTEGER := 1;
CONST_QUARTERLY CONSTANT INTEGER := 2;
CONST_HALF_YEARLY CONSTANT INTEGER := 3;
CONST_YEARLY CONSTANT INTEGER := 4;
CONST_YTD CONSTANT INTEGER := 5;
BEGIN
IF p_period_type != CONST_YTD THEN
SELECT start_date, end_date,
EXTRACT(YEAR FROM start_date),
EXTRACT(YEAR FROM end_date)
INTO v_financial_year_start, v_financial_year_end,
v_finance_year_start, v_finance_year_end
FROM public.finance_year
WHERE id = p_finance_id;
END IF;
IF p_period_type = CONST_YTD THEN
-- Returns last 5 years, fiscal quarters (Apr–Jun, Jul–Sep, Oct–Dec, Jan–Mar)
RETURN QUERY
WITH recent_years AS (
SELECT fy.id, fy.start_date, fy.end_date,
EXTRACT(YEAR FROM fy.start_date) AS fiscal_year
FROM public.finance_year fy
WHERE EXTRACT(YEAR FROM fy.start_date) >= EXTRACT(YEAR FROM CURRENT_DATE) - 4
AND EXTRACT(YEAR FROM fy.start_date) <= EXTRACT(YEAR FROM CURRENT_DATE)
ORDER BY fy.start_date DESC
LIMIT 5
),
quarters AS (
SELECT id AS finance_id, fiscal_year, 'Q1' AS period,
make_date(fiscal_year::int, 4, 1) AS period_start,
make_date(fiscal_year::int, 6, 30) AS period_end
FROM recent_years
UNION ALL
SELECT id, fiscal_year, 'Q2',
make_date(fiscal_year::int, 7, 1),
make_date(fiscal_year::int, 9, 30)
FROM recent_years
UNION ALL
SELECT id, fiscal_year, 'Q3',
make_date(fiscal_year::int, 10, 1),
make_date(fiscal_year::int, 12, 31)
FROM recent_years
UNION ALL
-- For Q4, fiscal_year stays the same, but period_start/period_end move to the next calendar year
SELECT id, fiscal_year, 'Q4',
make_date((fiscal_year::int) + 1, 1, 1),
make_date((fiscal_year::int) + 1, 3, 31)
FROM recent_years
),
filtered_je AS (
SELECT
je.amount,
je.entry_type,
tr.company_id,
je.transaction_date,
coa.account_type_id
FROM journal_entries je
JOIN transaction_headers tr ON je.transaction_id = tr.id
JOIN chart_of_accounts coa ON je.account_id = coa.id
WHERE je.is_deleted = FALSE
AND tr.company_id = p_company_id
)
SELECT
CONCAT(q.period, ' ', q.fiscal_year) AS period,
q.fiscal_year,
COALESCE(SUM(CASE WHEN je.entry_type = 'C' AND je.account_type_id IN (4,14,15,19,20)
AND je.transaction_date BETWEEN q.period_start AND q.period_end THEN je.amount ELSE 0 END), 0) AS total_income,
COALESCE(SUM(CASE WHEN je.entry_type = 'D' AND je.account_type_id IN (5,16,17,18,21,22)
AND je.transaction_date BETWEEN q.period_start AND q.period_end THEN je.amount ELSE 0 END), 0) AS total_expense,
COALESCE(SUM(CASE WHEN je.entry_type = 'D' AND je.account_type_id IN (8,9)
AND je.transaction_date BETWEEN q.period_start AND q.period_end THEN je.amount ELSE 0 END), 0) AS actual_income,
COALESCE(SUM(CASE WHEN je.entry_type = 'C' AND je.account_type_id IN (8,9)
AND je.transaction_date BETWEEN q.period_start AND q.period_end THEN je.amount ELSE 0 END), 0) AS actual_expense
FROM quarters q
LEFT JOIN filtered_je je
ON je.transaction_date BETWEEN q.period_start AND q.period_end
GROUP BY q.period, q.fiscal_year
HAVING
SUM(CASE WHEN je.entry_type = 'C' AND je.account_type_id IN (4,14,15,19,20)
AND je.transaction_date BETWEEN q.period_start AND q.period_end THEN je.amount ELSE 0 END) > 0
OR SUM(CASE WHEN je.entry_type = 'D' AND je.account_type_id IN (5,16,17,18,21,22)
AND je.transaction_date BETWEEN q.period_start AND q.period_end THEN je.amount ELSE 0 END) > 0
OR SUM(CASE WHEN je.entry_type = 'D' AND je.account_type_id IN (8,9)
AND je.transaction_date BETWEEN q.period_start AND q.period_end THEN je.amount ELSE 0 END) > 0
OR SUM(CASE WHEN je.entry_type = 'C' AND je.account_type_id IN (8,9)
AND je.transaction_date BETWEEN q.period_start AND q.period_end THEN je.amount ELSE 0 END) > 0
ORDER BY q.fiscal_year, q.period;
ELSIF p_period_type = CONST_MONTHLY THEN
RETURN QUERY
WITH months AS (
SELECT
generate_series::date AS month_start,
(generate_series + interval '1 month')::date AS month_end
FROM generate_series(
v_financial_year_start,
v_financial_year_end,
interval '1 month'
)
),
filtered_je AS (
SELECT
je.amount,
je.entry_type,
je.transaction_date,
coa.account_type_id
FROM journal_entries je
JOIN transaction_headers tr ON je.transaction_id = tr.id
JOIN chart_of_accounts coa ON je.account_id = coa.id
WHERE tr.company_id = p_company_id
AND je.is_deleted = FALSE
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
)
SELECT
to_char(m.month_start, 'Mon YYYY') AS period,
EXTRACT(YEAR FROM m.month_start) AS year,
COALESCE(SUM(CASE WHEN je.entry_type = 'C' AND je.account_type_id IN (4,14,15,19,20)
AND je.transaction_date >= m.month_start AND je.transaction_date < m.month_end THEN je.amount ELSE 0 END), 0) AS total_income,
COALESCE(SUM(CASE WHEN je.entry_type = 'D' AND je.account_type_id IN (5,16,17,18,21,22)
AND je.transaction_date >= m.month_start AND je.transaction_date < m.month_end THEN je.amount ELSE 0 END), 0) AS total_expense,
COALESCE(SUM(CASE WHEN je.entry_type = 'D' AND je.account_type_id IN (8,9)
AND je.transaction_date >= m.month_start AND je.transaction_date < m.month_end THEN je.amount ELSE 0 END), 0) AS actual_income,
COALESCE(SUM(CASE WHEN je.entry_type = 'C' AND je.account_type_id IN (8,9)
AND je.transaction_date >= m.month_start AND je.transaction_date < m.month_end THEN je.amount ELSE 0 END), 0) AS actual_expense
FROM months m
LEFT JOIN filtered_je je
ON je.transaction_date >= m.month_start AND je.transaction_date < m.month_end
GROUP BY m.month_start
ORDER BY m.month_start;
ELSIF p_period_type = CONST_QUARTERLY THEN
-- Single year, use explicit fiscal quarters
RETURN QUERY
WITH fiscal_quarters AS (
SELECT 'Q1' AS period, v_financial_year_start AS period_start, make_date(v_finance_year_start::int, 6, 30) AS period_end, v_finance_year_start AS year
UNION ALL
SELECT 'Q2', make_date(v_finance_year_start::int, 7, 1), make_date(v_finance_year_start::int, 9, 30), v_finance_year_start
UNION ALL
SELECT 'Q3', make_date(v_finance_year_start::int, 10, 1), make_date(v_finance_year_start::int, 12, 31), v_finance_year_start
UNION ALL
SELECT 'Q4', make_date((v_finance_year_start::int) + 1, 1, 1), v_financial_year_end, v_finance_year_start
),
filtered_je AS (
SELECT
je.amount,
je.entry_type,
je.transaction_date,
coa.account_type_id
FROM journal_entries je
JOIN transaction_headers tr ON je.transaction_id = tr.id
JOIN chart_of_accounts coa ON je.account_id = coa.id
WHERE tr.company_id = p_company_id
AND je.is_deleted = FALSE
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
)
SELECT
CONCAT(fq.period, ' ', fq.year) AS period,
fq.year,
COALESCE(SUM(CASE WHEN je.entry_type = 'C' AND je.account_type_id IN (4,14,15,19,20)
AND je.transaction_date BETWEEN fq.period_start AND fq.period_end THEN je.amount ELSE 0 END), 0) AS total_income,
COALESCE(SUM(CASE WHEN je.entry_type = 'D' AND je.account_type_id IN (5,16,17,18,21,22)
AND je.transaction_date BETWEEN fq.period_start AND fq.period_end THEN je.amount ELSE 0 END), 0) AS total_expense,
COALESCE(SUM(CASE WHEN je.entry_type = 'D' AND je.account_type_id IN (8,9)
AND je.transaction_date BETWEEN fq.period_start AND fq.period_end THEN je.amount ELSE 0 END), 0) AS actual_income,
COALESCE(SUM(CASE WHEN je.entry_type = 'C' AND je.account_type_id IN (8,9)
AND je.transaction_date BETWEEN fq.period_start AND fq.period_end THEN je.amount ELSE 0 END), 0) AS actual_expense
FROM fiscal_quarters fq
LEFT JOIN filtered_je je
ON je.transaction_date BETWEEN fq.period_start AND fq.period_end
GROUP BY fq.period, fq.year
ORDER BY fq.year, fq.period;
ELSIF p_period_type = CONST_HALF_YEARLY THEN
RETURN QUERY
WITH half_years AS (
SELECT 'H1' AS period, v_financial_year_start AS period_start, make_date(v_finance_year_start::int, 9, 30) AS period_end, v_finance_year_start AS year
UNION ALL
SELECT 'H2', make_date(v_finance_year_start::int, 10, 1), v_financial_year_end, v_finance_year_start
),
filtered_je AS (
SELECT
je.amount,
je.entry_type,
je.transaction_date,
coa.account_type_id
FROM journal_entries je
JOIN transaction_headers tr ON je.transaction_id = tr.id
JOIN chart_of_accounts coa ON je.account_id = coa.id
WHERE tr.company_id = p_company_id
AND je.is_deleted = FALSE
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
)
SELECT
CONCAT(h.period, ' ', h.year) AS period,
h.year,
COALESCE(SUM(CASE WHEN je.entry_type = 'C' AND je.account_type_id IN (4,14,15,19,20)
AND je.transaction_date BETWEEN h.period_start AND h.period_end THEN je.amount ELSE 0 END), 0) AS total_income,
COALESCE(SUM(CASE WHEN je.entry_type = 'D' AND je.account_type_id IN (5,16,17,18,21,22)
AND je.transaction_date BETWEEN h.period_start AND h.period_end THEN je.amount ELSE 0 END), 0) AS total_expense,
COALESCE(SUM(CASE WHEN je.entry_type = 'D' AND je.account_type_id IN (8,9)
AND je.transaction_date BETWEEN h.period_start AND h.period_end THEN je.amount ELSE 0 END), 0) AS actual_income,
COALESCE(SUM(CASE WHEN je.entry_type = 'C' AND je.account_type_id IN (8,9)
AND je.transaction_date BETWEEN h.period_start AND h.period_end THEN je.amount ELSE 0 END), 0) AS actual_expense
FROM half_years h
JOIN filtered_je je
ON je.transaction_date BETWEEN h.period_start AND h.period_end
GROUP BY h.period, h.year
ORDER BY h.year, h.period;
ELSIF p_period_type = CONST_YEARLY THEN
RETURN QUERY
WITH filtered_je AS (
SELECT
je.amount,
je.entry_type,
je.transaction_date,
coa.account_type_id
FROM journal_entries je
JOIN transaction_headers tr ON je.transaction_id = tr.id
JOIN chart_of_accounts coa ON je.account_id = coa.id
WHERE tr.company_id = p_company_id
AND je.is_deleted = FALSE
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
)
SELECT
CONCAT('Year ', v_finance_year_start) AS period,
v_finance_year_start AS year,
COALESCE(SUM(CASE WHEN je.entry_type = 'C' AND je.account_type_id IN (4,14,15,19,20) THEN je.amount ELSE 0 END), 0) AS total_income,
COALESCE(SUM(CASE WHEN je.entry_type = 'D' AND je.account_type_id IN (5,16,17,18,21,22) THEN je.amount ELSE 0 END), 0) AS total_expense,
COALESCE(SUM(CASE WHEN je.entry_type = 'D' AND je.account_type_id IN (8,9) THEN je.amount ELSE 0 END), 0) AS actual_income,
COALESCE(SUM(CASE WHEN je.entry_type = 'C' AND je.account_type_id IN (8,9) THEN je.amount ELSE 0 END), 0) AS actual_expense
FROM filtered_je je;
ELSE
RAISE EXCEPTION 'Unsupported period type %', p_period_type;
END IF;
END;
$function$
-- Function: get_vendor_opening_balance
CREATE OR REPLACE FUNCTION public.get_vendor_opening_balance(p_vendor_id uuid, p_finyear_id integer)
RETURNS TABLE(transaction_date timestamp without time zone, vendor_id uuid, vendor_name text, description text, credit numeric, debit numeric)
LANGUAGE plpgsql
AS $function$
DECLARE
v_year_text text;
BEGIN
-- Fetch the year text for the given finance year id
SELECT year INTO v_year_text FROM public.finance_year WHERE id = p_finyear_id;
IF v_year_text IS NULL THEN
RAISE EXCEPTION 'Finance year with id % not found!', p_finyear_id;
END IF;
RETURN QUERY
SELECT
th.transaction_date,
th.vendor_id,
v.name::text AS vendor_name, -- Explicit cast to match function signature
th.description,
COALESCE(CASE WHEN je.entry_type = 'C' THEN je.amount END, 0) AS credit,
COALESCE(CASE WHEN je.entry_type = 'D' THEN je.amount END, 0) AS debit
FROM public.transaction_headers th
JOIN public.vendors v ON th.vendor_id = v.id
JOIN public.journal_entries je ON th.id = je.transaction_id
JOIN public.chart_of_accounts sc ON sc.name ILIKE '%Sundry Creditors%'
WHERE th.vendor_id = p_vendor_id
AND je.account_id = sc.id
AND th.transaction_source_type = 13
AND th.description LIKE 'Opening Balance for %' || v_year_text || '%'
ORDER BY th.transaction_date;
END;
$function$
-- Function: test
CREATE OR REPLACE FUNCTION public.test(p_company_id uuid, p_financial_year integer, p_chart_of_account_id uuid)
RETURNS TABLE(account_id uuid, account_name text, parent_account_id uuid, account_type text, apr numeric, may numeric, jun numeric, jul numeric, aug numeric, sep numeric, oct numeric, nov numeric, "dec" numeric, jan numeric, feb numeric, mar numeric, total_current_year numeric, total_last_year numeric, difference numeric)
LANGUAGE plpgsql
AS $function$
DECLARE
v_start_date TIMESTAMP DEFAULT NULL;
v_end_date TIMESTAMP DEFAULT NULL;
v_prev_start_date TIMESTAMP DEFAULT NULL;
v_prev_end_date TIMESTAMP DEFAULT NULL;
BEGIN
-- Get start and end date for the given financial year
SELECT start_date, end_date
INTO v_start_date, v_end_date
FROM finance_year
WHERE EXTRACT(YEAR FROM start_date) = p_financial_year
LIMIT 1;
-- If no result, raise an error
IF v_start_date IS NULL OR v_end_date IS NULL THEN
RAISE EXCEPTION 'Financial year % not found in finance_year table', p_financial_year;
END IF;
-- Get the previous financial year's start and end date
SELECT start_date, end_date
INTO v_prev_start_date, v_prev_end_date
FROM finance_year
WHERE EXTRACT(YEAR FROM start_date) = (p_financial_year - 1)
LIMIT 1;
-- If previous year is missing, use the current year as a fallback
IF v_prev_start_date IS NULL OR v_prev_end_date IS NULL THEN
v_prev_start_date := v_start_date;
v_prev_end_date := v_end_date;
END IF;
-- Debugging: Print financial year start and end dates
RAISE NOTICE 'Start Date: %, End Date: %, Prev Start: %, Prev End: %', v_start_date, v_end_date, v_prev_start_date, v_prev_end_date;
-- Return the final query
RETURN QUERY
WITH direct_children AS (
-- Step 1: Get Direct Child Accounts
SELECT coa.id AS child_id, coa.name AS child_name, coa.parent_account_id AS direct_parent_id, coa.account_type_id
FROM chart_of_accounts coa
WHERE coa.parent_account_id = p_chart_of_account_id
),
all_descendants AS (
-- Step 2: Get All Descendants of Each Direct Child
WITH RECURSIVE hierarchy AS (
SELECT ca.id AS descendant_id, ca.parent_account_id AS ancestor_parent_id
FROM chart_of_accounts ca
WHERE ca.parent_account_id IN (SELECT child_id FROM direct_children)
UNION ALL
SELECT coa.id AS descendant_id, coa.parent_account_id AS ancestor_parent_id
FROM chart_of_accounts coa
INNER JOIN hierarchy h ON coa.parent_account_id = h.descendant_id
)
SELECT descendant_id, ancestor_parent_id FROM hierarchy
)
SELECT
dc.child_id AS account_id,
dc.child_name AS account_name,
dc.direct_parent_id AS parent_account_id,
at.name AS account_type,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 4 THEN je.amount ELSE 0 END), 0) AS apr,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 5 THEN je.amount ELSE 0 END), 0) AS may,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 6 THEN je.amount ELSE 0 END), 0) AS jun,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 7 THEN je.amount ELSE 0 END), 0) AS jul,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 8 THEN je.amount ELSE 0 END), 0) AS aug,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 9 THEN je.amount ELSE 0 END), 0) AS sep,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 10 THEN je.amount ELSE 0 END), 0) AS oct,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 11 THEN je.amount ELSE 0 END), 0) AS nov,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 12 THEN je.amount ELSE 0 END), 0) AS "dec",
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 1 THEN je.amount ELSE 0 END), 0) AS jan,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 2 THEN je.amount ELSE 0 END), 0) AS feb,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 3 THEN je.amount ELSE 0 END), 0) AS mar,
-- Total current year transactions
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date THEN je.amount ELSE 0 END), 0) AS total_current_year,
-- Total last year transactions
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_prev_start_date AND v_prev_end_date THEN je.amount ELSE 0 END), 0) AS total_last_year,
-- Difference between the two years
(COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date THEN je.amount ELSE 0 END), 0) -
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_prev_start_date AND v_prev_end_date THEN je.amount ELSE 0 END), 0))
AS difference
FROM direct_children dc
LEFT JOIN all_descendants ad ON dc.child_id = ad.ancestor_parent_id OR dc.child_id = ad.descendant_id
LEFT JOIN journal_entries je ON (je.account_id = ad.descendant_id OR je.account_id = dc.child_id)
LEFT JOIN account_types at ON at.id = dc.account_type_id
GROUP BY dc.child_id, dc.child_name, dc.direct_parent_id, at.name;
END;
$function$
-- Function: get_all_companies
CREATE OR REPLACE FUNCTION public.get_all_companies(p_organization_id uuid)
RETURNS TABLE(id uuid, organization_id uuid, name character varying, contact_name character varying, contact_count integer, mobile_number character varying, gst_in character varying, email character varying, city_name character varying, is_apartment boolean, created_by uuid, created_by_name character varying, modified_by uuid, modified_by_name character varying, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
cmp.id,
cmp.organization_id,
cmp.name::character varying,
COALESCE(
(SELECT con.first_name || ' ' || con.last_name
FROM public.contacts con
JOIN public.company_contacts cc
ON con.id = cc.contact_id
WHERE cc.company_id = cmp.id AND con.is_primary = true
LIMIT 1), ''
)::character varying AS primary_contact_name,
(SELECT COUNT(*)
FROM public.contacts con
JOIN public.company_contacts cc
ON con.id = cc.contact_id
WHERE cc.company_id = cmp.id)::integer AS contact_count,
COALESCE(
(SELECT con.mobile_number
FROM public.contacts con
JOIN public.company_contacts cc
ON con.id = cc.contact_id
WHERE cc.company_id = cmp.id AND con.is_primary = true
LIMIT 1), ''
)::character varying AS primary_contact_mobile,
cmp.gstin::character varying AS gst_in,
COALESCE(
(SELECT con.email
FROM public.contacts con
JOIN public.company_contacts cc
ON con.id = cc.contact_id
WHERE cc.company_id = cmp.id AND con.is_primary = true
LIMIT 1), ''
)::character varying AS primary_contact_email,
COALESCE(
(SELECT ct.name
FROM public.cities ct
WHERE ct.id = (
SELECT ad.city_id
FROM public.addresses ad
WHERE ad.id = cmp.billing_address_id
LIMIT 1)
LIMIT 1), ''
)::character varying AS city_name,
cmp.is_apartment,
cmp.created_by,
COALESCE(
(SELECT u.first_name || ' ' || u.last_name
FROM public.users u
WHERE u.id = cmp.created_by), ''
)::character varying AS created_by_name,
cmp.modified_by,
COALESCE(
(SELECT u.first_name || ' ' || u.last_name
FROM public.users u
WHERE u.id = cmp.modified_by), ''
)::character varying AS modified_by_name,
cmp.created_on_utc,
cmp.modified_on_utc
FROM
public.companies cmp
WHERE
cmp.organization_id = p_organization_id
AND cmp.is_deleted = false;
END;
$function$
-- Function: get_customer_ledger
CREATE OR REPLACE FUNCTION public.get_customer_ledger(p_company_id uuid, p_organization_id uuid, p_customer_id uuid, p_start_date date, p_end_date date)
RETURNS TABLE(transaction_date date, account_id uuid, account_name text, debit numeric, credit numeric, balance numeric, document_number text, document_id uuid, transaction_source_type integer)
LANGUAGE plpgsql
AS $function$
DECLARE
v_opening_equity_id UUID;
v_sundary_detors_account_id UUID;
v_opening_balance_equity_account_id UUID;
BEGIN
SELECT accounts_receivable_account_id, opening_balance_equity_account_id
INTO v_sundary_detors_account_id, v_opening_balance_equity_account_id
FROM public.organization_accounts
WHERE organization_id = p_organization_id
LIMIT 1;
RETURN QUERY
WITH all_transactions AS (
SELECT
th.transaction_date::date,
coa.name AS account_name,
coa.id AS account_id, -- ✅ Include account_id
CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END AS debit,
CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END AS credit,
th.document_number,
th.document_id,
th.transaction_source_type,
tst.sort_order,
th.id AS transaction_id,
je.id AS journal_entry_id
FROM
public.transaction_headers th
JOIN public.journal_entries je ON th.id = je.transaction_id
JOIN public.transaction_source_types tst ON th.transaction_source_type = tst.id
JOIN public.chart_of_accounts coa ON je.account_id = coa.id
WHERE
th.company_id = p_company_id
AND th.customer_id = p_customer_id
AND je.account_id = v_sundary_detors_account_id
AND th.transaction_date BETWEEN p_start_date AND p_end_date
AND th.is_deleted = false
AND je.is_deleted = false
)
SELECT
at.transaction_date,
at.account_id, -- ✅ Added here
at.account_name,
at.debit,
at.credit,
SUM(at.debit - at.credit) OVER (
ORDER BY
at.transaction_date,
at.sort_order,
at.transaction_id,
at.journal_entry_id
) AS balance,
at.document_number,
at.document_id,
at.transaction_source_type
FROM all_transactions AS at
ORDER BY
at.transaction_date,
at.sort_order,
at.transaction_id,
at.journal_entry_id;
END;
$function$
-- Function: get_general_ledger
CREATE OR REPLACE FUNCTION public.get_general_ledger(p_company_id uuid, p_organization_id uuid, p_start_date date, p_end_date date)
RETURNS TABLE(transaction_date date, account_name text, debit numeric, credit numeric, balance numeric, transaction_source_type integer, document_number text, document_id uuid, customer_id uuid, customer_name text, employee_id uuid, employee_name text, vendor_id uuid, vendor_name text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
WITH general_transactions AS (
SELECT
th.transaction_date::date AS transaction_date,
coa.name AS account_name,
CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END AS debit,
CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END AS credit,
th.company_id,
je.account_id,
th.transaction_source_type,
tst.sort_order,
th.document_number,
th.document_id,
th.customer_id,
th.employee_id,
th.vendor_id,
th.id AS transaction_id,
je.id AS journal_entry_id
FROM
public.transaction_headers th
INNER JOIN
public.journal_entries je ON th.id = je.transaction_id
INNER JOIN
public.chart_of_accounts coa ON je.account_id = coa.id
INNER JOIN
public.transaction_source_types tst ON th.transaction_source_type = tst.id
WHERE
th.company_id = p_company_id
AND coa.organization_id = p_organization_id
AND th.is_deleted = false
AND je.is_deleted = false
AND th.transaction_date BETWEEN p_start_date AND p_end_date
)
SELECT
gt.transaction_date,
gt.account_name,
gt.debit,
gt.credit,
SUM(gt.debit - gt.credit) OVER (
ORDER BY
gt.transaction_date,
gt.sort_order,
gt.transaction_id,
gt.journal_entry_id
) AS balance,
gt.transaction_source_type,
gt.document_number,
gt.document_id,
c.id AS customer_id,
c.name::text AS customer_name,
e.id AS employee_id,
CASE
WHEN e.id IS NOT NULL THEN (e.first_name || ' ' || e.last_name)::text
ELSE NULL
END AS employee_name,
v.id AS vendor_id,
v.name::text AS vendor_name
FROM
general_transactions gt
LEFT JOIN public.customers c ON gt.customer_id = c.id AND c.company_id = p_company_id
LEFT JOIN public.employees e ON gt.employee_id = e.id AND e.company_id = p_company_id
LEFT JOIN public.vendors v ON gt.vendor_id = v.id AND v.company_id = p_company_id
WHERE
NOT (gt.debit = 0 AND gt.credit = 0)
ORDER BY
gt.transaction_date,
gt.sort_order,
gt.transaction_id,
gt.journal_entry_id;
END;
$function$
-- Function: get_vendor_ledger
CREATE OR REPLACE FUNCTION public.get_vendor_ledger(p_vendor_id uuid, p_company_id uuid, p_organization_id uuid, p_start_date date, p_end_date date)
RETURNS TABLE(transaction_date date, account_id uuid, account_name text, debit numeric, credit numeric, balance numeric, transaction_source_type integer, document_number text, document_id uuid)
LANGUAGE plpgsql
AS $function$
DECLARE
v_sundary_creditors_account_id UUID;
v_vendor_advance_account_id UUID;
BEGIN
SELECT
accounts_payable_account_id,
vendor_advance_account_id
INTO
v_sundary_creditors_account_id,
v_vendor_advance_account_id
FROM public.organization_accounts
WHERE organization_id = p_organization_id
LIMIT 1;
RETURN QUERY
WITH all_transactions AS (
SELECT
th.transaction_date::date,
coa.id AS account_id,
coa.name AS account_name,
CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END AS debit,
CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END AS credit,
th.document_number,
th.document_id,
th.transaction_source_type,
tst.sort_order,
th.id AS transaction_id,
je.id AS journal_entry_id
FROM public.transaction_headers th
JOIN public.journal_entries je ON th.id = je.transaction_id
JOIN public.transaction_source_types tst ON th.transaction_source_type = tst.id
JOIN public.chart_of_accounts coa ON je.account_id = coa.id
WHERE
th.company_id = p_company_id
AND th.vendor_id = p_vendor_id
AND je.account_id IN (v_sundary_creditors_account_id, v_vendor_advance_account_id)
AND th.transaction_date BETWEEN p_start_date AND p_end_date
AND th.is_deleted = false
AND je.is_deleted = false
)
SELECT
at.transaction_date,
at.account_id,
at.account_name,
at.debit,
at.credit,
SUM(at.debit - at.credit) OVER (
ORDER BY
at.transaction_date,
at.sort_order,
at.transaction_id,
at.journal_entry_id
) AS balance,
at.transaction_source_type,
at.document_number,
at.document_id
FROM all_transactions AS at
ORDER BY
at.transaction_date,
at.sort_order,
at.transaction_id,
at.journal_entry_id;
END;
$function$
-- Function: get_party_totals
CREATE OR REPLACE FUNCTION public.get_party_totals(p_organization_id uuid, p_company_id uuid, p_start_date date, p_end_date date, p_party_type integer)
RETURNS TABLE(id uuid, party_name text, company_id uuid, company_name text, total_invoiced numeric, total_received numeric, total_billed numeric, total_paid numeric)
LANGUAGE plpgsql
AS $function$
DECLARE
CUSTOMER CONSTANT int := 1;
VENDOR CONSTANT int := 2;
BEGIN
-- Customers
IF p_party_type = CUSTOMER THEN
RETURN QUERY
SELECT
th.customer_id AS id,
MAX(cust.name) AS party_name,
th.company_id,
MAX(comp.name) AS company_name,
COALESCE(SUM(CASE WHEN th.transaction_source_type = 1 AND je.entry_type = 'D' THEN je.amount ELSE 0 END), 0) AS total_invoiced,
COALESCE(SUM(CASE WHEN th.transaction_source_type = 3 AND je.entry_type = 'D' THEN je.amount ELSE 0 END), 0) AS total_received,
0::numeric AS total_billed,
0::numeric AS total_paid
FROM public.transaction_headers th
JOIN public.journal_entries je ON je.transaction_id = th.id
JOIN public.companies comp ON comp.id = th.company_id
LEFT JOIN public.customers cust ON cust.id = th.customer_id
WHERE comp.organization_id = p_organization_id
AND (p_company_id IS NULL OR th.company_id = p_company_id)
AND (p_start_date IS NULL OR th.transaction_date >= p_start_date)
AND (p_end_date IS NULL OR th.transaction_date <= p_end_date)
AND th.customer_id IS NOT NULL
GROUP BY th.customer_id, th.company_id;
-- Vendors
ELSIF p_party_type = VENDOR THEN
RETURN QUERY
SELECT
th.vendor_id AS id,
MAX(vend.name) AS party_name,
th.company_id,
MAX(comp.name) AS company_name,
0::numeric AS total_invoiced,
0::numeric AS total_received,
COALESCE(SUM(CASE WHEN th.transaction_source_type = 2 AND je.entry_type = 'D' THEN je.amount ELSE 0 END), 0) AS total_billed,
COALESCE(SUM(CASE WHEN th.transaction_source_type = 4 AND je.entry_type = 'D' THEN je.amount ELSE 0 END), 0) AS total_paid
FROM public.transaction_headers th
JOIN public.journal_entries je ON je.transaction_id = th.id
JOIN public.companies comp ON comp.id = th.company_id
LEFT JOIN public.vendors vend ON vend.id = th.vendor_id
WHERE comp.organization_id = p_organization_id
AND (p_company_id IS NULL OR th.company_id = p_company_id)
AND (p_start_date IS NULL OR th.transaction_date >= p_start_date)
AND (p_end_date IS NULL OR th.transaction_date <= p_end_date)
AND th.vendor_id IS NOT NULL
GROUP BY th.vendor_id, th.company_id;
END IF;
END;
$function$
-- Function: get_accounts_by_classification
CREATE OR REPLACE FUNCTION public.get_accounts_by_classification(classification_type text)
RETURNS TABLE(account_id uuid, account_name text, account_type_name text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
coa.id AS account_id,
coa.name AS account_name,
at.name AS account_type_name
FROM
chart_of_accounts coa
INNER JOIN
account_types at
ON
coa.account_type_id = at.id
WHERE
at.classification = classification_type; -- Filter by classification parameter
END;
$function$
-- Function: get_comparative_accounts_overview
CREATE OR REPLACE FUNCTION public.get_comparative_accounts_overview(p_company_id uuid, p_fin_year_id integer, p_chart_of_account_id uuid)
RETURNS TABLE(account_id uuid, parent_account_name text, parent_account_id uuid, financial_year integer, apr numeric, may numeric, jun numeric, jul numeric, aug numeric, sep numeric, oct numeric, nov numeric, "dec" numeric, jan numeric, feb numeric, mar numeric, total_sum numeric, difference numeric)
LANGUAGE plpgsql
AS $function$
DECLARE
v_current_year INTEGER := p_fin_year_id;
v_previous_year INTEGER := p_fin_year_id - 1;
v_organization_id UUID;
v_is_second_last_leaf BOOLEAN;
BEGIN
-- Get the organization_id for the given company_id
SELECT c.organization_id
INTO v_organization_id
FROM public.companies c
WHERE c.id = p_company_id;
-- Search for 2nd last leaf account
SELECT NOT EXISTS (
SELECT 1 FROM chart_of_accounts ca1
WHERE ca1.parent_account_id = p_chart_of_account_id
)
INTO v_is_second_last_leaf;
RAISE notice 'it is leaf account %', v_is_second_last_leaf;
-- If it is a LEAF account, fetch its transactions directly
IF v_is_second_last_leaf THEN
RETURN QUERY
WITH RECURSIVE child_accounts AS (
-- Get the given parent account and all its child accounts
SELECT id, parent_account_id, name
FROM chart_of_accounts
WHERE parent_account_id = p_chart_of_account_id
UNION ALL
-- Recursively get all child accounts
SELECT ca.id, ca.parent_account_id, ca.name
FROM chart_of_accounts ca
INNER JOIN child_accounts c ON ca.parent_account_id = c.id
),
financial_years AS (
-- Get previous and current financial years
SELECT fy.id AS fin_year_id, fy.start_date, fy.end_date
FROM finance_year fy
WHERE fy.id IN (v_previous_year, v_current_year)
)
SELECT
je.id AS transaction_id,
je.account_id,
ca.name AS account_name,
ca.parent_account_id,
fy.fin_year_id AS financial_year,
je.transaction_date,
EXTRACT(MONTH FROM je.transaction_date) AS calendar_month,
-- Convert Calendar Month to Financial Month (April - March Cycle)
CASE
WHEN EXTRACT(MONTH FROM je.transaction_date) = 4 THEN 'Apr'
WHEN EXTRACT(MONTH FROM je.transaction_date) = 5 THEN 'May'
WHEN EXTRACT(MONTH FROM je.transaction_date) = 6 THEN 'Jun'
WHEN EXTRACT(MONTH FROM je.transaction_date) = 7 THEN 'Jul'
WHEN EXTRACT(MONTH FROM je.transaction_date) = 8 THEN 'Aug'
WHEN EXTRACT(MONTH FROM je.transaction_date) = 9 THEN 'Sep'
WHEN EXTRACT(MONTH FROM je.transaction_date) = 10 THEN 'Oct'
WHEN EXTRACT(MONTH FROM je.transaction_date) = 11 THEN 'Nov'
WHEN EXTRACT(MONTH FROM je.transaction_date) = 12 THEN 'Dec'
WHEN EXTRACT(MONTH FROM je.transaction_date) = 1 THEN 'Jan'
WHEN EXTRACT(MONTH FROM je.transaction_date) = 2 THEN 'Feb'
WHEN EXTRACT(MONTH FROM je.transaction_date) = 3 THEN 'Mar'
END AS financial_month,
je.amount,
je.description
FROM journal_entries je
INNER JOIN child_accounts ca ON je.account_id = ca.id
INNER JOIN financial_years fy ON je.transaction_date BETWEEN fy.start_date AND fy.end_date
ORDER BY fy.fin_year_id, financial_month, ca.name, je.transaction_date;
ELSE
-- If it is NOT a leaf account, use the ORIGINAL FUNCTIONALITY (recursive aggregation)
RETURN QUERY
WITH direct_children AS (
-- Step 1: Get Direct Children of Given Parent
SELECT ca.id AS account_id, ca.parent_account_id, ca.name
FROM chart_of_accounts ca
WHERE ca.parent_account_id = p_chart_of_account_id
AND ca.organization_id = v_organization_id
),
descendant_accounts AS (
-- Step 2: Recursively Get All Descendants of Each Direct Child
WITH RECURSIVE hierarchy AS (
-- Start with direct children
SELECT dc.account_id, dc.parent_account_id, dc.name
FROM direct_children dc
UNION ALL
-- Then, recursively get all their descendants
SELECT coa.id AS account_id, coa.parent_account_id, coa.name
FROM chart_of_accounts coa
INNER JOIN hierarchy h ON coa.parent_account_id = h.account_id
)
SELECT * FROM hierarchy
),
financial_years AS (
-- Step 3: Get Start and End Dates for Given Financial Years
SELECT id AS fin_year_id, start_date, end_date
FROM finance_year
WHERE id IN (v_previous_year, v_current_year)
),
aggregated_data AS (
-- Step 4: Ensure Each Account Appears for Both Financial Years
SELECT
dc.account_id, -- Include Account ID
dc.name AS parent_account_name,
dc.parent_account_id, -- Ensure Parent Account ID is properly referenced
fy.fin_year_id AS financial_year,
-- Monthly Values (April to March)
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 4 THEN je.amount ELSE 0 END), 0) AS apr,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 5 THEN je.amount ELSE 0 END), 0) AS may,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 6 THEN je.amount ELSE 0 END), 0) AS jun,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 7 THEN je.amount ELSE 0 END), 0) AS jul,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 8 THEN je.amount ELSE 0 END), 0) AS aug,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 9 THEN je.amount ELSE 0 END), 0) AS sep,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 10 THEN je.amount ELSE 0 END), 0) AS oct,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 11 THEN je.amount ELSE 0 END), 0) AS nov,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 12 THEN je.amount ELSE 0 END), 0) AS "dec",
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 1 THEN je.amount ELSE 0 END), 0) AS jan,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 2 THEN je.amount ELSE 0 END), 0) AS feb,
COALESCE(SUM(CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 3 THEN je.amount ELSE 0 END), 0) AS mar,
-- Total Sum of All Months
COALESCE(SUM(je.amount), 0) AS total_sum
FROM direct_children dc
CROSS JOIN financial_years fy -- Ensures each account appears for both years
LEFT JOIN descendant_accounts da ON da.parent_account_id = dc.account_id
LEFT JOIN journal_entries je
ON je.account_id = da.account_id
AND je.transaction_date BETWEEN fy.start_date AND fy.end_date -- Ensure correct year filtering
-- AND je.company_id = p_company_id -- Filter by Company ID
GROUP BY dc.account_id, dc.parent_account_id, dc.name, fy.fin_year_id
)
-- Step 5: Compute Difference Between Two Financial Years
SELECT
a1.account_id,
a1.parent_account_name,
a1.parent_account_id,
a1.financial_year,
-- Monthly Values
a1.apr, a1.may, a1.jun, a1.jul, a1.aug, a1.sep, a1.oct, a1.nov, a1."dec",
a1.jan, a1.feb, a1.mar,
-- Total Sum for Financial Year
a1.total_sum,
-- Difference (Current Year - Previous Year)
(COALESCE(a1.total_sum, 0) - COALESCE(a2.total_sum, 0)) AS difference
FROM aggregated_data a1
LEFT JOIN aggregated_data a2
ON a1.account_id = a2.account_id
AND a1.financial_year = v_current_year -- Current Year
AND a2.financial_year = v_previous_year -- Previous Year
ORDER BY a1.parent_account_name, a1.financial_year;
END IF;
END;
$function$
-- Function: get_collection
CREATE OR REPLACE FUNCTION public.get_collection(p_company_id uuid)
RETURNS TABLE(year numeric, total_income numeric, total_expense numeric)
LANGUAGE plpgsql
AS $function$
DECLARE
financial_year_start date;
financial_year_end date;
current_year integer;
BEGIN
-- Determine the current year
current_year := EXTRACT(YEAR FROM CURRENT_DATE);
-- Determine the financial year start and end dates
IF EXTRACT(MONTH FROM CURRENT_DATE) >= 4 THEN
-- If the current month is April or later, financial year starts from April 1 of the current year
financial_year_start := TO_DATE(current_year || '-04-01', 'YYYY-MM-DD');
financial_year_end := TO_DATE((current_year + 1) || '-03-31', 'YYYY-MM-DD');
ELSE
-- If the current month is before April, financial year starts from April 1 of the previous year
financial_year_start := TO_DATE((current_year - 1) || '-04-01', 'YYYY-MM-DD');
financial_year_end := TO_DATE(current_year || '-03-31', 'YYYY-MM-DD');
END IF;
-- Return the yearly income and expense overview
RETURN QUERY
SELECT
EXTRACT(YEAR FROM je.transaction_date) AS year,
SUM(CASE
WHEN ac.id = 4 THEN je.amount -- Revenue
ELSE 0
END) AS total_income,
SUM(CASE
WHEN ac.id = 5 THEN je.amount -- Expenses
ELSE 0
END) AS total_expense
FROM
journal_entries je
JOIN
chart_of_accounts coa ON je.account_id = coa.id
JOIN
account_types act ON coa.account_type_id = act.id
JOIN
account_categories ac ON act.account_category_id = ac.id
WHERE
je.transaction_date BETWEEN financial_year_start AND financial_year_end
AND je.company_id = p_company_id
GROUP BY
year
ORDER BY
year;
END;
$function$
-- Function: update_basic_company
CREATE OR REPLACE FUNCTION public.update_basic_company(p_company_id uuid, p_organization_id uuid, p_name text, p_gst_in text, p_short_name text, p_currency text, p_pan text, p_tan text, p_outstanding_limit numeric, p_is_apartment boolean, p_is_non_work boolean, p_interest_percentage numeric, p_has_gst_in boolean, p_email text, p_mobile_number text)
RETURNS void
LANGUAGE plpgsql
AS $function$
DECLARE
v_contact_id UUID;
BEGIN
-- 1️⃣ Get contact_id from company_contacts
SELECT contact_id INTO v_contact_id
FROM company_contacts
WHERE company_id = p_company_id
LIMIT 1;
-- 2️⃣ Update company table
UPDATE companies
SET
organization_id = p_organization_id,
name = p_name,
gst_in = p_gst_in,
short_name = p_short_name,
currency = p_currency,
pan = p_pan,
tan = p_tan,
outstanding_limit = p_outstanding_limit,
is_apartment = p_is_apartment,
is_non_work = p_is_non_work,
interest_percentage = p_interest_percentage,
has_gst_in = p_has_gst_in,
modified_on_utc = NOW()
WHERE id = p_company_id;
-- 3️⃣ Update contact table if mapping exists
IF v_contact_id IS NOT NULL THEN
UPDATE contacts
SET
email = p_email,
mobile_number = p_mobile_number,
modified_on_utc = NOW()
WHERE id = v_contact_id;
END IF;
END;
$function$
-- Function: create_notification_with_userid
CREATE OR REPLACE FUNCTION public.create_notification_with_userid(p_user_id uuid, p_message text, p_route text, p_short_desc text, p_created_by uuid, p_created_time timestamp without time zone, p_additional_parameter_id text)
RETURNS integer
LANGUAGE plpgsql
AS $function$
DECLARE
v_notification_id INT;
BEGIN
-- Insert notification and retrieve generated ID
INSERT INTO public.notification_types (
user_id, message, route, short_description, additional_parameter_id,
created_on_utc, created_by, is_deleted
) VALUES (
p_user_id, p_message, p_route, p_short_desc, p_additional_parameter_id,
NOW()::timestamp without time zone, p_created_by, FALSE -- Cast NOW() to match parameter type
)
RETURNING id INTO v_notification_id;
-- Return the inserted notification id
RETURN v_notification_id;
END;
$function$
-- Function: get_expense_account_types
CREATE OR REPLACE FUNCTION public.get_expense_account_types()
RETURNS TABLE(account_id uuid, account_name text, account_type_name text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
coa.id AS account_id,
coa.name AS account_name,
at.name AS account_type_name
FROM
chart_of_accounts coa
INNER JOIN
account_types at
ON
coa.account_type_id = at.id
WHERE
at.classification = 'Expense'; -- Filter for expense account types
END;
$function$
-- Function: pgp_sym_decrypt
CREATE OR REPLACE FUNCTION public.pgp_sym_decrypt(bytea, text)
RETURNS text
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_sym_decrypt_text$function$
-- Function: pgp_sym_decrypt_bytea
CREATE OR REPLACE FUNCTION public.pgp_sym_decrypt_bytea(bytea, text)
RETURNS bytea
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_sym_decrypt_bytea$function$
-- Function: get_module_permissions
CREATE OR REPLACE FUNCTION public.get_module_permissions()
RETURNS TABLE(id integer, feature_name text, read_permission_id uuid, create_permission_id uuid, update_permission_id uuid, delete_permission_id uuid, full_access_permission_id uuid, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone, deleted_on_utc timestamp without time zone, is_deleted boolean, created_by uuid, modified_by uuid)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
mp.id,
mp.feature_name,
mp.read_permission_id,
mp.create_permission_id,
mp.update_permission_id,
mp.delete_permission_id,
mp.full_access_permission_id,
mp.created_on_utc,
mp.modified_on_utc,
mp.deleted_on_utc,
mp.is_deleted,
mp.created_by,
mp.modified_by
FROM module_permissions mp
WHERE mp.is_deleted = false
ORDER BY mp.feature_name;
END;
$function$
-- Function: get_profit_loss_statement
CREATE OR REPLACE FUNCTION public.get_profit_loss_statement(p_company_id uuid, p_fin_year_id integer, p_start_date date, p_end_date date, p_is_get_for_organization boolean)
RETURNS TABLE(account_id uuid, account_type text, account_name text, amount numeric, category text)
LANGUAGE plpgsql
AS $function$
DECLARE
v_start_date date;
v_end_date date;
v_fy_start_date date;
v_fy_end_date date;
v_company_ids UUID[];
v_organization_id UUID;
income_account_ids integer[];
expense_account_ids integer[];
BEGIN
-- Get the financial year dates
SELECT start_date::date, end_date::date
INTO v_fy_start_date, v_fy_end_date
FROM public.finance_year
WHERE id = p_fin_year_id
LIMIT 1;
-- Apply filter logic for new date params
v_start_date := COALESCE(p_start_date, v_fy_start_date);
v_end_date := COALESCE(p_end_date, v_fy_end_date);
-- Get organization id
SELECT organization_id INTO v_organization_id
FROM public.companies
WHERE id = p_company_id;
-- Get account type hierarchies
income_account_ids := ARRAY(SELECT id FROM get_account_type_hierarchy(4));
expense_account_ids := ARRAY(SELECT id FROM get_account_type_hierarchy(5));
-- Populate company IDs based on org flag
IF p_is_get_for_organization THEN
SELECT ARRAY(
SELECT id FROM companies
WHERE organization_id = v_organization_id
)
INTO v_company_ids;
ELSE
v_company_ids := ARRAY[p_company_id];
END IF;
-- Return Income
RETURN QUERY
SELECT
coa.id as account_id, -- GUID column
at.name AS account_type,
coa.name AS account_name,
COALESCE(
SUM(
CASE
WHEN ob.entry_type = 'C' THEN ob.balance
ELSE -ob.balance
END
),0
) +
COALESCE(
SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE -je.amount END), 0
) AS amount,
'Income' AS category
FROM public.chart_of_accounts coa
INNER JOIN public.journal_entries je ON je.account_id = coa.id
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
INNER JOIN public.account_types at ON coa.account_type_id = at.id
LEFT JOIN public.opening_balances ob ON ob.account_id = coa.id
AND ob.organization_id = v_organization_id
AND ob.finyear_id = p_fin_year_id
AND ob.is_deleted = FALSE
WHERE th.company_id = ANY(v_company_ids)
AND th.transaction_date BETWEEN v_start_date AND v_end_date
AND NOT th.is_deleted
AND NOT je.is_deleted
AND coa.account_type_id = ANY(income_account_ids)
GROUP BY coa.id, at.name, coa.name, coa.account_number, at.id
ORDER BY coa.account_number, at.id;
-- Return Expenses
RETURN QUERY
SELECT
coa.id as account_id, -- GUID column
at.name AS account_type,
coa.name AS account_name,
COALESCE(
SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE -je.amount END), 0
) AS amount,
'Expenses' AS category
FROM public.chart_of_accounts coa
INNER JOIN public.journal_entries je ON je.account_id = coa.id
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
INNER JOIN public.account_types at ON coa.account_type_id = at.id
WHERE th.company_id = ANY(v_company_ids)
AND th.transaction_date BETWEEN v_start_date AND v_end_date
AND NOT th.is_deleted
AND NOT je.is_deleted
AND coa.account_type_id = ANY(expense_account_ids)
GROUP BY coa.id, at.name, coa.name, coa.account_number, at.id
ORDER BY coa.account_number, at.id;
END;
$function$
-- Function: get_user_notifications_with_timespan
CREATE OR REPLACE FUNCTION public.get_user_notifications_with_timespan(p_user_id uuid, p_start_date timestamp with time zone, p_end_date timestamp with time zone)
RETURNS TABLE(id integer, message text, short_desc text, route text, created_time timestamp with time zone, additional_parameter_id text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
ntf.id AS id,
ntf.message,
ntf.short_description AS short_desc,
ntf.route,
ntf.created_on_utc::timestamp with time zone,
ntf.additional_parameter_id
FROM
public.notifications ntf
WHERE
ntf.user_id = p_user_id
AND ntf.created_on_utc BETWEEN p_start_date AND p_end_date -- Use the time span here
AND ntf.is_deleted = false
AND ntf.is_deleted = false
ORDER BY
ntf.created_on_utc DESC;
END;
$function$
-- Function: get_trial_balance_by_date_range
CREATE OR REPLACE FUNCTION public.get_trial_balance_by_date_range(p_company_id uuid, p_fin_year_id integer, p_start_date date DEFAULT NULL::date, p_end_date date DEFAULT NULL::date, p_is_get_for_organization boolean DEFAULT false)
RETURNS TABLE(account_type text, account_number text, account_name text, opening_balance numeric, debit numeric, credit numeric, closing_balance numeric)
LANGUAGE plpgsql
AS $function$
DECLARE
v_start_date DATE;
v_end_date DATE;
v_organization_id UUID;
v_company_ids UUID[];
BEGIN
-- Fetch the organization ID from the company ID
SELECT organization_id INTO v_organization_id
FROM public.companies
WHERE id = p_company_id
LIMIT 1;
-- Get financial year start and end dates
SELECT start_date, end_date
INTO v_start_date, v_end_date
FROM public.get_financial_year_dates(p_fin_year_id);
-- Override with provided values if not NULL
v_start_date := COALESCE(p_start_date, v_start_date);
v_end_date := COALESCE(p_end_date, v_end_date);
-- Determine whether to fetch for all companies in the organization
IF p_is_get_for_organization THEN
SELECT ARRAY_AGG(id)
INTO v_company_ids
FROM public.companies
WHERE organization_id = v_organization_id;
ELSE
v_company_ids := ARRAY[p_company_id];
END IF;
RETURN QUERY
WITH coa_hierarchy AS (
SELECT * FROM public.get_chart_of_accounts_hierarchy(v_organization_id)
),
opening_balances AS (
SELECT * FROM public.get_opening_balances(v_organization_id, p_fin_year_id)
),
journal_entries_filtered AS (
SELECT je.account_id,
SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END) AS debit,
SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END) AS credit
FROM public.journal_entries je
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
WHERE th.company_id = ANY(v_company_ids)
AND th.transaction_date BETWEEN v_start_date AND v_end_date
GROUP BY je.account_id
)
SELECT
ch.account_type,
ch.account_number,
ch.account_name,
COALESCE(ob.balance, 0) AS opening_balance,
COALESCE(jf.debit, 0) AS debit,
COALESCE(jf.credit, 0) AS credit,
(COALESCE(ob.balance, 0) + COALESCE(jf.debit, 0) - COALESCE(jf.credit, 0)) AS closing_balance
FROM coa_hierarchy ch
LEFT JOIN opening_balances ob ON ch.id = ob.account_id
LEFT JOIN journal_entries_filtered jf ON ch.id = jf.account_id
ORDER BY ch.account_number;
END;
$function$
-- Function: get_trial_balance_net
CREATE OR REPLACE FUNCTION public.get_trial_balance_net(p_company_id uuid, p_finance_year_id integer)
RETURNS TABLE(account_type text, account_category text, account_number text, account_name text, balance numeric, balance_type text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
WITH financial_year_range AS (
SELECT
fy.start_date::DATE AS start_date,
fy.end_date::DATE AS end_date
FROM
public.finance_year fy
WHERE
fy.id = p_finance_year_id
LIMIT 1
),
account_balances AS (
SELECT
je.account_id,
SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END) AS total_debits,
SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END) AS total_credits
FROM
public.journal_entries je
JOIN
public.transaction_headers th ON je.transaction_id = th.id
CROSS JOIN
financial_year_range fy
WHERE
th.company_id = p_company_id
AND je.is_deleted = FALSE
AND th.transaction_date BETWEEN fy.start_date AND fy.end_date
GROUP BY
je.account_id
)
SELECT
at.name AS account_type,
ac.name AS account_category,
coa.account_number,
coa.name AS account_name,
(COALESCE(ab.total_debits, 0) - COALESCE(ab.total_credits, 0)) AS balance,
CASE
WHEN (COALESCE(ab.total_debits, 0) - COALESCE(ab.total_credits, 0)) > 0 THEN 'Debit'
WHEN (COALESCE(ab.total_debits, 0) - COALESCE(ab.total_credits, 0)) < 0 THEN 'Credit'
ELSE ''
END AS balance_type
FROM
public.chart_of_accounts coa
INNER JOIN
public.account_types at ON coa.account_type_id = at.id
LEFT JOIN
public.account_categories ac ON at.account_category_id = ac.id
LEFT JOIN
account_balances ab ON coa.id = ab.account_id
WHERE
coa.organization_id = p_company_id
AND coa.is_deleted = FALSE
AND (COALESCE(ab.total_debits, 0) - COALESCE(ab.total_credits, 0)) <> 0 -- Only show accounts with a non-zero balance
ORDER BY
coa.account_number;
END;
$function$
-- Function: pgp_sym_encrypt_bytea
CREATE OR REPLACE FUNCTION public.pgp_sym_encrypt_bytea(bytea, text, text)
RETURNS bytea
LANGUAGE c
PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_sym_encrypt_bytea$function$
-- Function: get_accounts_by_company_and_type
CREATE OR REPLACE FUNCTION public.get_accounts_by_company_and_type(p_company_id uuid, p_account_type integer)
RETURNS TABLE(id uuid, account_type_id integer, name text, description text, is_default_account boolean)
LANGUAGE plpgsql
AS $function$
DECLARE
v_organization_id UUID;
v_is_apartment BOOLEAN;
v_default_sales UUID;
v_default_purchase UUID;
v_default_cash UUID;
v_default_bank UUID;
BEGIN
-- ✅ Fix ambiguous column: use alias 'c'
SELECT c.organization_id, c.is_apartment
INTO v_organization_id, v_is_apartment
FROM companies c
WHERE c.id = p_company_id;
-- ✅ Fix ambiguous column: use alias 'cp'
SELECT cp.default_sales_account_id,
cp.default_purchase_account_id,
cp.default_cash_account_id,
cp.default_bank_account_id
INTO v_default_sales, v_default_purchase, v_default_cash, v_default_bank
FROM company_preferences cp
WHERE cp.company_id = p_company_id;
-- ✅ Main query using table alias 'coa'
RETURN QUERY
SELECT
coa.id,
coa.account_type_id,
coa.name,
coa.description,
CASE
WHEN coa.id = v_default_sales THEN TRUE
WHEN coa.id = v_default_purchase THEN TRUE
WHEN coa.id = v_default_cash THEN TRUE
WHEN coa.id = v_default_bank THEN TRUE
ELSE FALSE
END AS is_default_account
FROM chart_of_accounts coa
WHERE coa.organization_id = v_organization_id
AND NOT EXISTS (
SELECT 1 FROM chart_of_accounts ca2 WHERE ca2.parent_account_id = coa.id
)
AND (
(
v_is_apartment = TRUE AND (
(p_account_type = 14 AND coa.account_type_id IN (19, 20)) OR -- SALES_REVENUE
(p_account_type IN (16, 17) AND coa.account_type_id IN (21, 22)) OR -- COGS or OPERATING
(p_account_type = 9 AND coa.account_type_id = 9) OR -- BANK
(p_account_type = 8 AND coa.account_type_id = 8) OR -- CASH
(p_account_type = 23 AND coa.account_type_id IN (8, 9)) -- BANK_AND_CASH
)
)
OR
(
v_is_apartment = FALSE AND (
(p_account_type = coa.account_type_id) OR
(p_account_type = 23 AND coa.account_type_id IN (8, 9)) -- BANK_AND_CASH
)
)
);
END;
$function$
-- Function: get_trial_balance_of_company_fin_year
CREATE OR REPLACE FUNCTION public.get_trial_balance_of_company_fin_year(p_company_id uuid, p_finance_year_id integer)
RETURNS TABLE(account_type text, account_category text, account_number text, account_name text, total_debits numeric, total_credits numeric)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
WITH RECURSIVE coa_hierarchy AS (
-- Base case: Fetch the top-level (root) accounts
SELECT
coa.id,
act.name AS account_type,
act.classification AS account_classification,
coa.account_number::integer,
coa.name,
coa.parent_account_id,
0 AS level,
coa.account_number::text AS order_sequence
FROM
public.chart_of_accounts coa
INNER JOIN
public.account_types act ON coa.account_type_id = act.id
WHERE
coa.is_deleted = FALSE
AND coa.parent_account_id = '00000000-0000-0000-0000-000000000000' -- Root-level accounts
UNION ALL
-- Recursive case: Fetch child accounts
SELECT
coa.id,
act.name AS account_type,
act.classification AS account_classification,
coa.account_number::integer,
coa.name,
coa.parent_account_id,
ch.level + 1 AS level,
ch.order_sequence || '.' || coa.account_number::text AS order_sequence
FROM
public.chart_of_accounts coa
INNER JOIN
public.account_types act ON coa.account_type_id = act.id
INNER JOIN
coa_hierarchy ch ON ch.id = coa.parent_account_id
WHERE
coa.is_deleted = FALSE
),
coa_with_balances AS (
-- Fetch financial year start and end date
SELECT
fy.start_date::DATE,
fy.end_date::DATE
FROM
public.finance_year fy
WHERE
fy.id = p_finance_year_id
LIMIT 1
),
journal_entries_filtered AS (
-- Get total debits and credits per account
SELECT
je.account_id,
SUM(CASE
WHEN je.entry_type = 'D' THEN je.amount
ELSE 0
END) AS total_debits,
SUM(CASE
WHEN je.entry_type = 'C' THEN je.amount
ELSE 0
END) AS total_credits
FROM
public.journal_entries je
JOIN
public.transaction_headers th ON je.transaction_id = th.id
JOIN
coa_with_balances fy ON th.transaction_date BETWEEN fy.start_date AND fy.end_date
WHERE
th.company_id = p_company_id
AND je.is_deleted = FALSE
GROUP BY
je.account_id
HAVING
SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END) > 0 OR
SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END) > 0
),
relevant_accounts AS (
-- Select accounts that have balances
SELECT
ch.id,
ch.account_type,
ch.account_classification,
ch.account_number,
ch.name,
ch.level,
ch.order_sequence,
ch.parent_account_id,
wb.total_debits,
wb.total_credits
FROM
coa_hierarchy ch
LEFT JOIN
journal_entries_filtered wb ON ch.id = wb.account_id
WHERE
wb.account_id IS NOT NULL
UNION ALL
-- Recursively select parent accounts
SELECT
ch.id,
ch.account_type,
ch.account_classification,
ch.account_number,
ch.name,
ch.level,
ch.order_sequence,
ch.parent_account_id,
NULL AS total_debits,
NULL AS total_credits
FROM
coa_hierarchy ch
INNER JOIN
relevant_accounts ra ON ch.id = ra.parent_account_id
)
-- Final output
SELECT DISTINCT ON (ra.order_sequence)
ra.account_type::TEXT AS account_type,
ac.name::TEXT AS account_category,
ra.account_number::TEXT AS account_number,
LPAD('', ra.level * 4, ' ') || ra.name::TEXT AS account_name,
CASE
WHEN ra.account_classification = 'EXPENSES' THEN COALESCE(ra.total_credits, 0) -- Expenses should be on the debit side
ELSE COALESCE(ra.total_debits, 0)
END AS total_debits,
CASE
WHEN ra.account_classification = 'EXPENSES' THEN COALESCE(ra.total_debits, 0) -- Expenses should not appear on the credit side
ELSE COALESCE(ra.total_credits, 0)
END AS total_credits
FROM
relevant_accounts ra
LEFT JOIN
public.chart_of_accounts coa ON ra.id = coa.id
LEFT JOIN
public.account_types at ON coa.account_type_id = at.id
LEFT JOIN
public.account_categories ac ON at.account_category_id = ac.id
CROSS JOIN
coa_with_balances fy
ORDER BY
ra.order_sequence;
END;
$function$
-- Function: pgp_sym_decrypt
CREATE OR REPLACE FUNCTION public.pgp_sym_decrypt(bytea, text, text)
RETURNS text
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_sym_decrypt_text$function$
-- Function: get_user_by_email_or_phone
CREATE OR REPLACE FUNCTION public.get_user_by_email_or_phone(p_email text, p_phone text)
RETURNS TABLE(id uuid, email text, phone_number text)
LANGUAGE sql
AS $function$
SELECT
u.id AS id,
u.email AS email,
u.phone_number AS phone_number
FROM public.users u
WHERE u.is_deleted = false
AND(
(
COALESCE(NULLIF(p_email, ''), NULL) IS NOT NULL
AND u.email ILIKE COALESCE(NULLIF(p_email, ''), NULL)
)
OR (
COALESCE(NULLIF(p_phone, ''), NULL) IS NOT NULL
AND u.phone_number = COALESCE(NULLIF(p_phone, ''), NULL)
)
)
LIMIT 1;
$function$
-- Function: digest
CREATE OR REPLACE FUNCTION public.digest(text, text)
RETURNS bytea
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pg_digest$function$
-- Function: digest
CREATE OR REPLACE FUNCTION public.digest(bytea, text)
RETURNS bytea
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pg_digest$function$
-- Function: hmac
CREATE OR REPLACE FUNCTION public.hmac(text, text, text)
RETURNS bytea
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pg_hmac$function$
-- Function: hmac
CREATE OR REPLACE FUNCTION public.hmac(bytea, bytea, text)
RETURNS bytea
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pg_hmac$function$
-- Function: crypt
CREATE OR REPLACE FUNCTION public.crypt(text, text)
RETURNS text
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pg_crypt$function$
-- Function: gen_salt
CREATE OR REPLACE FUNCTION public.gen_salt(text)
RETURNS text
LANGUAGE c
PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pg_gen_salt$function$
-- Function: gen_salt
CREATE OR REPLACE FUNCTION public.gen_salt(text, integer)
RETURNS text
LANGUAGE c
PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pg_gen_salt_rounds$function$
-- Function: encrypt
CREATE OR REPLACE FUNCTION public.encrypt(bytea, bytea, text)
RETURNS bytea
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pg_encrypt$function$
-- Function: decrypt
CREATE OR REPLACE FUNCTION public.decrypt(bytea, bytea, text)
RETURNS bytea
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pg_decrypt$function$
-- Function: encrypt_iv
CREATE OR REPLACE FUNCTION public.encrypt_iv(bytea, bytea, bytea, text)
RETURNS bytea
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pg_encrypt_iv$function$
-- Function: decrypt_iv
CREATE OR REPLACE FUNCTION public.decrypt_iv(bytea, bytea, bytea, text)
RETURNS bytea
LANGUAGE c
IMMUTABLE PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pg_decrypt_iv$function$
-- Function: gen_random_bytes
CREATE OR REPLACE FUNCTION public.gen_random_bytes(integer)
RETURNS bytea
LANGUAGE c
PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pg_random_bytes$function$
-- Function: gen_random_uuid
CREATE OR REPLACE FUNCTION public.gen_random_uuid()
RETURNS uuid
LANGUAGE c
PARALLEL SAFE
AS '$libdir/pgcrypto', $function$pg_random_uuid$function$
-- Function: pgp_sym_encrypt
CREATE OR REPLACE FUNCTION public.pgp_sym_encrypt(text, text)
RETURNS bytea
LANGUAGE c
PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_sym_encrypt_text$function$
-- Function: pgp_sym_encrypt_bytea
CREATE OR REPLACE FUNCTION public.pgp_sym_encrypt_bytea(bytea, text)
RETURNS bytea
LANGUAGE c
PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_sym_encrypt_bytea$function$
-- Function: pgp_sym_encrypt
CREATE OR REPLACE FUNCTION public.pgp_sym_encrypt(text, text, text)
RETURNS bytea
LANGUAGE c
PARALLEL SAFE STRICT
AS '$libdir/pgcrypto', $function$pgp_sym_encrypt_text$function$
-- Function: get_bank_statements_for_status
CREATE OR REPLACE FUNCTION public.get_bank_statements_for_status(p_company_id uuid, p_finyear_id integer, p_date_from timestamp without time zone DEFAULT NULL::timestamp without time zone, p_date_to timestamp without time zone DEFAULT NULL::timestamp without time zone)
RETURNS TABLE(id integer, company_id uuid, bank_id integer, bank_name text, txn_date timestamp without time zone, cheque_number character varying, description text, value_date timestamp without time zone, branch_code character varying, debit_amount numeric, credit_amount numeric, balance numeric, has_reconciled boolean, reconciliation_status_id integer, reconciliation_status text, reconciliation_description text, created_by uuid, created_on_utc timestamp without time zone, modified_by uuid, modified_on_utc timestamp without time zone, deleted_on_utc timestamp without time zone, is_deleted boolean)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
bs.id,
bs.company_id,
bs.bank_id,
b.name AS bank_name,
bs.txn_date::timestamp,
bs.cheque_number,
bs.description,
bs.value_date::timestamp,
bs.branch_code,
bs.debit_amount,
bs.credit_amount,
bs.balance,
-- 🧠 Smart has_reconciled logic
CASE
WHEN br.id IS NOT NULL
AND rs.status IN ('partial', 'matched', 'audited') THEN TRUE
ELSE FALSE
END AS has_reconciled,
-- 🧩 New reconciliation info
rs.id AS reconciliation_status_id,
rs.status::text AS reconciliation_status,
rs.description::text AS reconciliation_description,
bs.created_by,
bs.created_on_utc,
bs.modified_by,
bs.modified_on_utc,
bs.deleted_on_utc,
bs.is_deleted
FROM public.bank_statements bs
INNER JOIN public.banks b
ON b.id = bs.bank_id
AND b.is_deleted = false
INNER JOIN public.finance_year fy
ON fy.id = p_finyear_id
LEFT JOIN LATERAL (
SELECT br.*
FROM public.bank_reconciliations br
WHERE br.bank_statement_id = bs.id
ORDER BY br.matched_on_utc DESC
LIMIT 1
) br ON TRUE
LEFT JOIN public.reconciliation_statuses rs
ON rs.id = br.reconciliation_status_id
WHERE bs.company_id = p_company_id
AND bs.txn_date BETWEEN fy.start_date AND fy.end_date
AND (p_date_from IS NULL OR bs.txn_date >= p_date_from)
AND (p_date_to IS NULL OR bs.txn_date <= p_date_to)
AND bs.is_deleted = false
ORDER BY bs.txn_date, bs.id;
END;
$function$
-- Function: get_sundry_creditors_ledger
CREATE OR REPLACE FUNCTION public.get_sundry_creditors_ledger(p_company_id uuid, p_fin_year_id integer, p_start_date date DEFAULT NULL::date, p_end_date date DEFAULT NULL::date)
RETURNS TABLE(sl_no integer, ledger text, general_ledger text, opening_balance numeric, debit numeric, credit numeric, closing_balance numeric)
LANGUAGE plpgsql
AS $function$
DECLARE
v_start_date DATE;
v_end_date DATE;
BEGIN
-- Get financial year start and end date if not provided
SELECT fy.start_date, fy.end_date
INTO v_start_date, v_end_date
FROM public.finance_year fy
WHERE fy.id = p_fin_year_id
LIMIT 1;
-- Override with provided values if they are not NULL
v_start_date := COALESCE(p_start_date, v_start_date);
v_end_date := COALESCE(p_end_date, v_end_date);
RETURN QUERY
SELECT ROW_NUMBER() OVER()::INTEGER AS sl_no,
v.name::TEXT AS ledger,
'Sundry Creditors'::TEXT AS general_ledger,
0::NUMERIC AS opening_balance,
COALESCE(
SUM(CASE
WHEN je.entry_type = 'C' THEN je.amount -- Credit increases liability balance
ELSE 0
END), 0
) AS credit,
COALESCE(
SUM(CASE
WHEN je.entry_type = 'D' THEN je.amount -- Debit decreases liability balance
ELSE 0
END), 0
) AS debit,
COALESCE(
SUM(CASE
WHEN je.entry_type = 'C' THEN je.amount
ELSE 0
END), 0
) -
COALESCE(
SUM(CASE
WHEN je.entry_type = 'D' THEN je.amount
ELSE 0
END), 0
) AS closing_balance
FROM public.journal_entries je
JOIN public.transaction_headers th ON je.transaction_id = th.id
JOIN public.vendors v ON th.vendor_id = v.id
WHERE th.company_id = p_company_id
AND th.transaction_date BETWEEN v_start_date AND v_end_date
GROUP BY v.name
ORDER BY v.name;
END;
$function$
-- Function: get_account_expense
CREATE OR REPLACE FUNCTION public.get_account_expense(p_company_id uuid, p_finance_id integer, p_period_type integer, p_period integer)
RETURNS TABLE(account_name text, account_number text, total_expense numeric, financial_year text)
LANGUAGE plpgsql
STABLE PARALLEL SAFE
AS $function$
DECLARE
v_financial_year_start DATE;
v_financial_year_end DATE;
v_finance_year TEXT;
-- Period type constants
CONST_MONTHLY CONSTANT INTEGER := 1;
CONST_QUARTERLY CONSTANT INTEGER := 2;
CONST_HALF_YEARLY CONSTANT INTEGER := 3;
CONST_YEARLY CONSTANT INTEGER := 4;
EXPENSE_CATEGORY_ID CONSTANT INTEGER := 5; -- Expense category ID
BEGIN
-- Fetch financial year details
SELECT fy.start_date, fy.end_date, fy.year
INTO v_financial_year_start, v_financial_year_end, v_finance_year
FROM public.finance_year fy
WHERE fy.id = p_finance_id;
-- Validate financial year
IF v_financial_year_start IS NULL OR v_financial_year_end IS NULL THEN
RAISE EXCEPTION 'Invalid financial year ID: %', p_finance_id;
END IF;
-- Handling different period types
IF p_period_type = CONST_MONTHLY THEN
-- Handle Monthly
RETURN QUERY
SELECT
coa.name AS account_name,
coa.account_number,
ROUND(COALESCE(SUM(je.amount), 0), 2) AS total_expense,
v_finance_year AS financial_year
FROM
public.journal_entries je
JOIN
public.transaction_headers th ON je.transaction_id = th.id
JOIN
public.chart_of_accounts coa ON je.account_id = coa.id
JOIN
public.account_types act ON coa.account_type_id = act.id
JOIN
public.account_categories ac ON act.account_category_id = ac.id
WHERE
th.company_id = p_company_id
AND ac.id = EXPENSE_CATEGORY_ID
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND EXTRACT(MONTH FROM je.transaction_date) = (p_period + 3) % 12 + 1
AND je.is_deleted = FALSE
AND coa.name NOT IN ('Rounding Gain') -- Exclude specific accounts
GROUP BY
coa.name, coa.account_number, v_finance_year
ORDER BY
total_expense DESC;
ELSIF p_period_type = CONST_QUARTERLY THEN
-- Handle Quarterly
RETURN QUERY
SELECT
coa.name AS account_name,
coa.account_number,
ROUND(COALESCE(SUM(je.amount), 0), 2) AS total_expense,
v_finance_year AS financial_year
FROM
public.journal_entries je
JOIN
public.transaction_headers th ON je.transaction_id = th.id
JOIN
public.chart_of_accounts coa ON je.account_id = coa.id
JOIN
public.account_types act ON coa.account_type_id = act.id
JOIN
public.account_categories ac ON act.account_category_id = ac.id
WHERE
th.company_id = p_company_id
AND ac.id = EXPENSE_CATEGORY_ID
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND (
(p_period = 1 AND EXTRACT(MONTH FROM je.transaction_date) IN (4, 5, 6)) OR
(p_period = 2 AND EXTRACT(MONTH FROM je.transaction_date) IN (7, 8, 9)) OR
(p_period = 3 AND EXTRACT(MONTH FROM je.transaction_date) IN (10, 11, 12)) OR
(p_period = 4 AND EXTRACT(MONTH FROM je.transaction_date) IN (1, 2, 3))
)
AND je.is_deleted = FALSE
AND coa.name NOT IN ('Rounding Gain') -- Exclude specific accounts
GROUP BY
coa.name, coa.account_number, v_finance_year
ORDER BY
total_expense DESC;
ELSIF p_period_type = CONST_HALF_YEARLY THEN
-- Handle Half-Yearly
RETURN QUERY
SELECT
coa.name AS account_name,
coa.account_number,
ROUND(COALESCE(SUM(je.amount), 0), 2) AS total_expense,
v_finance_year AS financial_year
FROM
public.journal_entries je
JOIN
public.transaction_headers th ON je.transaction_id = th.id
JOIN
public.chart_of_accounts coa ON je.account_id = coa.id
JOIN
public.account_types act ON coa.account_type_id = act.id
JOIN
public.account_categories ac ON act.account_category_id = ac.id
WHERE
th.company_id = p_company_id
AND ac.id = EXPENSE_CATEGORY_ID
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND (
(p_period = 1 AND EXTRACT(MONTH FROM je.transaction_date) BETWEEN 4 AND 9) OR
(p_period = 2 AND (EXTRACT(MONTH FROM je.transaction_date) BETWEEN 10 AND 12 OR EXTRACT(MONTH FROM je.transaction_date) BETWEEN 1 AND 3))
)
AND je.is_deleted = FALSE
AND coa.name NOT IN ('Rounding Gain') -- Exclude specific accounts
GROUP BY
coa.name, coa.account_number, v_finance_year
ORDER BY
total_expense DESC;
ELSIF p_period_type = CONST_YEARLY THEN
-- Handle Yearly
RETURN QUERY
SELECT
coa.name AS account_name,
coa.account_number,
ROUND(COALESCE(SUM(je.amount), 0), 2) AS total_expense,
v_finance_year AS financial_year
FROM
public.journal_entries je
JOIN
public.transaction_headers th ON je.transaction_id = th.id
JOIN
public.chart_of_accounts coa ON je.account_id = coa.id
JOIN
public.account_types act ON coa.account_type_id = act.id
JOIN
public.account_categories ac ON act.account_category_id = ac.id
WHERE
th.company_id = p_company_id
AND ac.id = EXPENSE_CATEGORY_ID
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND je.is_deleted = FALSE
AND coa.name NOT IN ('Rounding Gain') -- Exclude specific accounts
GROUP BY
coa.name, coa.account_number, v_finance_year
ORDER BY
total_expense DESC;
ELSE
RAISE EXCEPTION 'Invalid period type: %', p_period_type;
END IF;
END;
$function$
-- Function: get_account_expense_breakdown
CREATE OR REPLACE FUNCTION public.get_account_expense_breakdown(p_company_id uuid, p_finance_id integer, p_period_type integer, p_period integer)
RETURNS TABLE(account_id uuid, account_name text, account_number text, total_expense numeric, financial_year text)
LANGUAGE plpgsql
STABLE PARALLEL SAFE
AS $function$
DECLARE
v_financial_year_start DATE;
v_financial_year_end DATE;
v_finance_year TEXT;
CONST_MONTHLY CONSTANT INTEGER := 1;
CONST_QUARTERLY CONSTANT INTEGER := 2;
CONST_HALF_YEARLY CONSTANT INTEGER := 3;
CONST_YEARLY CONSTANT INTEGER := 4;
CONST_YTD CONSTANT INTEGER := 5;
BEGIN
-- Fetch financial year details
SELECT fy.start_date, fy.end_date, fy.year
INTO v_financial_year_start, v_financial_year_end, v_finance_year
FROM public.finance_year fy
WHERE fy.id = p_finance_id;
IF v_financial_year_start IS NULL OR v_financial_year_end IS NULL THEN
RAISE EXCEPTION 'Invalid financial year ID: %', p_finance_id;
END IF;
IF p_period_type = CONST_MONTHLY THEN
RETURN QUERY
SELECT
coa.id AS account_id,
coa.name AS account_name,
coa.account_number,
ROUND(COALESCE(SUM(je.amount), 0), 2) AS total_expense,
v_finance_year AS financial_year
FROM public.journal_entries je
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
INNER JOIN public.chart_of_accounts coa ON je.account_id = coa.id
WHERE th.company_id = p_company_id
AND coa.account_type_id IN (SELECT id FROM get_account_type_hierarchy(5))
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND EXTRACT(MONTH FROM je.transaction_date) = (p_period + 3) % 12 + 1
AND je.is_deleted = FALSE
AND coa.name NOT IN ('Rounding Gain')
GROUP BY coa.id, coa.name, coa.account_number, v_finance_year
ORDER BY total_expense DESC;
ELSIF p_period_type = CONST_QUARTERLY THEN
RETURN QUERY
SELECT
coa.id AS account_id,
coa.name AS account_name,
coa.account_number,
ROUND(COALESCE(SUM(je.amount), 0), 2) AS total_expense,
v_finance_year AS financial_year
FROM public.journal_entries je
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
INNER JOIN public.chart_of_accounts coa ON je.account_id = coa.id
WHERE th.company_id = p_company_id
AND coa.account_type_id IN (SELECT id FROM get_account_type_hierarchy(5))
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND (
(p_period = 1 AND EXTRACT(MONTH FROM je.transaction_date) IN (4, 5, 6)) OR
(p_period = 2 AND EXTRACT(MONTH FROM je.transaction_date) IN (7, 8, 9)) OR
(p_period = 3 AND EXTRACT(MONTH FROM je.transaction_date) IN (10, 11, 12)) OR
(p_period = 4 AND EXTRACT(MONTH FROM je.transaction_date) IN (1, 2, 3))
)
AND je.is_deleted = FALSE
AND coa.name NOT IN ('Rounding Gain')
GROUP BY coa.id, coa.name, coa.account_number, v_finance_year
ORDER BY total_expense DESC;
ELSIF p_period_type = CONST_HALF_YEARLY THEN
RETURN QUERY
SELECT
coa.id AS account_id,
coa.name AS account_name,
coa.account_number,
ROUND(COALESCE(SUM(je.amount), 0), 2) AS total_expense,
v_finance_year AS financial_year
FROM public.journal_entries je
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
INNER JOIN public.chart_of_accounts coa ON je.account_id = coa.id
WHERE th.company_id = p_company_id
AND coa.account_type_id IN (SELECT id FROM get_account_type_hierarchy(5))
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND (
(p_period = 1 AND EXTRACT(MONTH FROM je.transaction_date) BETWEEN 4 AND 9) OR
(p_period = 2 AND (EXTRACT(MONTH FROM je.transaction_date) BETWEEN 10 AND 12 OR EXTRACT(MONTH FROM je.transaction_date) BETWEEN 1 AND 3))
)
AND je.is_deleted = FALSE
AND coa.name NOT IN ('Rounding Gain')
GROUP BY coa.id, coa.name, coa.account_number, v_finance_year
ORDER BY total_expense DESC;
ELSIF p_period_type = CONST_YEARLY THEN
RETURN QUERY
SELECT
coa.id AS account_id,
coa.name AS account_name,
coa.account_number,
ROUND(COALESCE(SUM(je.amount), 0), 2) AS total_expense,
v_finance_year AS financial_year
FROM public.journal_entries je
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
INNER JOIN public.chart_of_accounts coa ON je.account_id = coa.id
WHERE th.company_id = p_company_id
AND coa.account_type_id IN (SELECT id FROM get_account_type_hierarchy(5))
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND je.is_deleted = FALSE
AND coa.name NOT IN ('Rounding Gain')
GROUP BY coa.id, coa.name, coa.account_number, v_finance_year
ORDER BY total_expense DESC;
ELSIF p_period_type = CONST_YTD THEN
RETURN QUERY
WITH recent_years AS (
SELECT fy.id, fy.start_date, fy.end_date,
fy.year AS financial_year_text
FROM public.finance_year fy
WHERE EXTRACT(YEAR FROM fy.start_date) >= EXTRACT(YEAR FROM CURRENT_DATE) - 4
AND EXTRACT(YEAR FROM fy.start_date) <= EXTRACT(YEAR FROM CURRENT_DATE)
ORDER BY fy.start_date DESC
LIMIT 5
),
base AS (
SELECT
coa.id AS account_id,
coa.name AS account_name,
coa.account_number,
ROUND(COALESCE(SUM(je.amount), 0), 2) AS total_expense,
ry.financial_year_text AS financial_year
FROM recent_years ry
LEFT JOIN public.journal_entries je
ON je.transaction_date BETWEEN ry.start_date AND LEAST(
ry.end_date,
CASE
WHEN CURRENT_DATE BETWEEN ry.start_date AND ry.end_date
THEN CURRENT_DATE
ELSE ry.end_date
END
)
LEFT JOIN public.transaction_headers th
ON je.transaction_id = th.id
AND th.company_id = p_company_id
LEFT JOIN public.chart_of_accounts coa
ON je.account_id = coa.id
WHERE
coa.account_type_id IN (SELECT id FROM get_account_type_hierarchy(5))
AND (je.id IS NULL OR je.is_deleted = FALSE)
AND (coa.name IS NULL OR coa.name NOT IN ('Rounding Gain'))
GROUP BY coa.id, coa.name, coa.account_number, ry.financial_year_text
)
SELECT
base.account_id,
base.account_name,
base.account_number,
SUM(base.total_expense) AS total_expense,
base.financial_year
FROM base
GROUP BY base.account_id, base.account_name, base.account_number, base.financial_year
ORDER BY base.financial_year DESC, total_expense DESC;
ELSE
RAISE EXCEPTION 'Invalid period type: %', p_period_type;
END IF;
END;
$function$
-- Function: verify_user_password
CREATE OR REPLACE FUNCTION public.verify_user_password(p_user_name text, p_password text)
RETURNS uuid
LANGUAGE plpgsql
AS $function$
DECLARE
v_user_id UUID;
BEGIN
SELECT id
INTO v_user_id
FROM users
WHERE user_name = p_user_name
AND is_deleted = false
AND password_hash = crypt(p_password, password_hash);
RETURN v_user_id;
END;
$function$
-- Function: get_account_ledger
CREATE OR REPLACE FUNCTION public.get_account_ledger(p_account_id uuid, p_company_id uuid, p_organization_id uuid, p_start_date date, p_end_date date)
RETURNS TABLE(transaction_date date, account_name text, debit numeric, credit numeric, balance numeric, transaction_source_type integer, document_number text, document_id uuid, customer_id uuid, customer_name text, employee_id uuid, employee_name text, vendor_id uuid, vendor_name text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
WITH RECURSIVE account_tree AS (
SELECT id
FROM public.chart_of_accounts
WHERE id = p_account_id
AND organization_id = p_organization_id
AND is_deleted = false
UNION ALL
SELECT coa.id
FROM public.chart_of_accounts coa
INNER JOIN account_tree at ON coa.parent_account_id = at.id
WHERE coa.organization_id = p_organization_id
AND coa.is_deleted = false
),
account_transactions AS (
SELECT
th.transaction_date::date AS transaction_date,
coa.name AS account_name,
coa.id AS account_id,
CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END AS debit,
CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END AS credit,
th.document_number,
th.document_id,
th.transaction_source_type,
tst.sort_order,
th.id AS transaction_id,
je.id AS journal_entry_id,
th.customer_id,
th.employee_id,
th.vendor_id
FROM
public.transaction_headers th
INNER JOIN
public.journal_entries je ON th.id = je.transaction_id
INNER JOIN
public.chart_of_accounts coa ON je.account_id = coa.id
INNER JOIN
account_tree at ON je.account_id = at.id
INNER JOIN
public.transaction_source_types tst ON th.transaction_source_type = tst.id
WHERE
th.company_id = p_company_id
AND coa.organization_id = p_organization_id
AND th.is_deleted = false
AND je.is_deleted = false
AND th.transaction_date BETWEEN p_start_date AND p_end_date
)
SELECT
at.transaction_date,
at.account_name,
at.debit,
at.credit,
SUM(at.debit - at.credit) OVER (
ORDER BY
at.transaction_date,
at.sort_order,
at.transaction_id,
at.journal_entry_id
) AS balance,
at.transaction_source_type,
at.document_number,
at.document_id,
c.id AS customer_id,
c.name::text AS customer_name,
e.id AS employee_id,
CASE
WHEN e.id IS NOT NULL THEN (e.first_name || ' ' || e.last_name)::text
ELSE NULL
END AS employee_name,
v.id AS vendor_id,
v.name::text AS vendor_name
FROM
account_transactions at
LEFT JOIN public.customers c ON at.customer_id = c.id AND c.company_id = p_company_id
LEFT JOIN public.employees e ON at.employee_id = e.id AND e.company_id = p_company_id
LEFT JOIN public.vendors v ON at.vendor_id = v.id AND v.company_id = p_company_id
WHERE
NOT (at.debit = 0 AND at.credit = 0)
ORDER BY
at.transaction_date,
at.sort_order,
at.transaction_id,
at.journal_entry_id;
END;
$function$
-- Function: get_account_total_amount
CREATE OR REPLACE FUNCTION public.get_account_total_amount(p_company_id uuid, p_finance_id integer, p_account_id uuid)
RETURNS numeric
LANGUAGE plpgsql
AS $function$
DECLARE
v_financial_year_start DATE;
v_financial_year_end DATE;
v_total_amount NUMERIC := 0;
v_organization_id UUID;
BEGIN
-- Step 1: Get the organization ID for the given company
SELECT organization_id
INTO v_organization_id
FROM public.companies
WHERE id = p_company_id;
-- Step 2: Validate if the organization ID exists
IF v_organization_id IS NULL THEN
RAISE EXCEPTION 'Organization ID not found for company ID %', p_company_id;
END IF;
-- Step 3: Get the financial year start and end dates
SELECT fy.start_date, fy.end_date
INTO v_financial_year_start, v_financial_year_end
FROM public.finance_year fy
WHERE fy.id = p_finance_id;
-- Step 4: Check if the financial year exists
IF v_financial_year_start IS NULL OR v_financial_year_end IS NULL THEN
RAISE EXCEPTION 'Financial year with ID % not found', p_finance_id;
END IF;
-- Step 5: Calculate the total amount for the specific account within the financial year,
-- ensuring the `organization_id` matches
SELECT COALESCE(SUM(je.amount), 0) INTO v_total_amount
FROM public.journal_entries je
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
INNER JOIN public.chart_of_accounts coa ON je.account_id = coa.id
WHERE th.company_id = p_company_id
AND je.account_id = p_account_id
AND coa.organization_id = v_organization_id -- Match organization ID
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND je.is_deleted = FALSE;
-- Step 6: Return the total amount
RETURN v_total_amount;
END;
$function$
-- Function: get_account_transactions
CREATE OR REPLACE FUNCTION public.get_account_transactions(p_company_id uuid, p_fin_year_id integer, p_chart_of_account_id uuid)
RETURNS TABLE(account_id uuid, account_name text, parent_account_id uuid, account_type text, year integer, apr numeric, may numeric, jun numeric, jul numeric, aug numeric, sep numeric, oct numeric, nov numeric, "dec" numeric, jan numeric, feb numeric, mar numeric, total_current_year numeric, total_last_year numeric, difference numeric)
LANGUAGE plpgsql
AS $function$
DECLARE
v_start_date TIMESTAMP DEFAULT NULL;
v_end_date TIMESTAMP DEFAULT NULL;
v_prev_start_date TIMESTAMP DEFAULT NULL;
v_prev_end_date TIMESTAMP DEFAULT NULL;
BEGIN
-- Get start and end date for the given financial year(fn)
SELECT fn.start_date, fn.end_date
INTO v_start_date, v_end_date
FROM finance_year fn
WHERE fn.id = p_fin_year_id
LIMIT 1;
-- If no result, raise an error
IF v_start_date IS NULL OR v_end_date IS NULL THEN
RAISE EXCEPTION 'Financial year % not found in finance_year table', p_fin_year_id;
END IF;
-- Get the previous financial year's(pfn) start and end date
SELECT pfn.start_date, pfn.end_date
INTO v_prev_start_date, v_prev_end_date
FROM finance_year pfn
WHERE pfn.id = (p_fin_year_id - 1)
LIMIT 1;
-- Return the final query
RETURN QUERY
WITH direct_children AS (
SELECT coa.id AS child_id, coa.name AS child_name, coa.parent_account_id AS direct_parent_id, coa.account_type_id
FROM chart_of_accounts coa
WHERE coa.parent_account_id = p_chart_of_account_id
),
all_descendants AS (
WITH RECURSIVE hierarchy AS (
SELECT ca.id AS descendant_id, ca.parent_account_id AS ancestor_parent_id
FROM chart_of_accounts ca
WHERE ca.parent_account_id IN (SELECT child_id FROM direct_children)
UNION ALL
SELECT coa.id AS descendant_id, coa.parent_account_id AS ancestor_parent_id
FROM chart_of_accounts coa
INNER JOIN hierarchy h ON coa.parent_account_id = h.descendant_id
)
SELECT descendant_id, ancestor_parent_id FROM hierarchy
),
years AS (
SELECT p_fin_year_id AS year
UNION ALL
SELECT (p_fin_year_id - 1) AS year
),
all_accounts AS (
SELECT
dc.child_id AS account_id,
dc.child_name AS account_name,
dc.direct_parent_id AS parent_account_id,
dc.account_type_id,
y.year,
dc.child_id AS summation_base_id
FROM direct_children dc
CROSS JOIN years y
UNION ALL
SELECT
ad.descendant_id AS account_id,
dc.child_name AS account_name,
dc.direct_parent_id AS parent_account_id,
dc.account_type_id,
y.year,
dc.child_id AS summation_base_id
FROM direct_children dc
JOIN all_descendants ad ON dc.child_id = ad.ancestor_parent_id
CROSS JOIN years y
)
SELECT
aa.summation_base_id AS account_id,
aa.account_name,
aa.parent_account_id,
at.name AS account_type,
aa.year,
-- Monthly values, ensuring correct year filtering
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date AND EXTRACT(MONTH FROM je.transaction_date) = 4 THEN je.amount ELSE 0 END), 0) AS apr,
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date AND EXTRACT(MONTH FROM je.transaction_date) = 5 THEN je.amount ELSE 0 END), 0) AS may,
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date AND EXTRACT(MONTH FROM je.transaction_date) = 6 THEN je.amount ELSE 0 END), 0) AS jun,
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date AND EXTRACT(MONTH FROM je.transaction_date) = 7 THEN je.amount ELSE 0 END), 0) AS jul,
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date AND EXTRACT(MONTH FROM je.transaction_date) = 8 THEN je.amount ELSE 0 END), 0) AS aug,
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date AND EXTRACT(MONTH FROM je.transaction_date) = 9 THEN je.amount ELSE 0 END), 0) AS sep,
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date AND EXTRACT(MONTH FROM je.transaction_date) = 10 THEN je.amount ELSE 0 END), 0) AS oct,
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date AND EXTRACT(MONTH FROM je.transaction_date) = 11 THEN je.amount ELSE 0 END), 0) AS nov,
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date AND EXTRACT(MONTH FROM je.transaction_date) = 12 THEN je.amount ELSE 0 END), 0) AS "dec",
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date AND EXTRACT(MONTH FROM je.transaction_date) = 1 THEN je.amount ELSE 0 END), 0) AS jan,
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date AND EXTRACT(MONTH FROM je.transaction_date) = 2 THEN je.amount ELSE 0 END), 0) AS feb,
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date AND EXTRACT(MONTH FROM je.transaction_date) = 3 THEN je.amount ELSE 0 END), 0) AS mar,
-- Totals for each year
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date THEN je.amount ELSE 0 END), 0) AS total_current_year,
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_prev_start_date AND v_prev_end_date THEN je.amount ELSE 0 END), 0) AS total_last_year,
-- Difference calculation
(COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_start_date AND v_end_date THEN je.amount ELSE 0 END), 0) -
COALESCE(SUM(CASE WHEN je.transaction_date BETWEEN v_prev_start_date AND v_prev_end_date THEN je.amount ELSE 0 END), 0))
AS difference
FROM all_accounts aa
LEFT JOIN journal_entries je
ON je.account_id = aa.account_id
AND (je.transaction_date BETWEEN v_start_date AND v_end_date OR
je.transaction_date BETWEEN v_prev_start_date AND v_prev_end_date)
LEFT JOIN account_types at ON at.id = aa.account_type_id
GROUP BY aa.year, aa.summation_base_id, aa.account_name, aa.parent_account_id, at.name
ORDER BY aa.summation_base_id, aa.year;
END;
$function$
-- Function: get_account_type_hierarchy
CREATE OR REPLACE FUNCTION public.get_account_type_hierarchy(parent_id integer)
RETURNS TABLE(id integer, name text, parent_account_type_id integer)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
WITH RECURSIVE account_hierarchy AS (
-- Base case: Start with the given parent account type
SELECT
p.id,
p.name,
p.parent_account_type_id
FROM
public.account_types p
WHERE
p.id = parent_id
UNION ALL
-- Recursive case: Include child account types
SELECT
at.id,
at.name,
at.parent_account_type_id
FROM
public.account_types at
INNER JOIN
account_hierarchy ah ON at.parent_account_type_id = ah.id
)
SELECT
acch.id,
acch.name,
acch.parent_account_type_id
FROM
account_hierarchy acch
ORDER BY acch.id;
END;
$function$
-- Function: get_coa_by_account_type
CREATE OR REPLACE FUNCTION public.get_coa_by_account_type(account_type_id_input integer)
RETURNS TABLE(coa_id uuid, coa_name text, coa_account_number text, coa_description text, coa_current_balance numeric, coa_opening_balance numeric)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
coa.id AS coa_id,
coa.name AS coa_name,
coa.account_number AS coa_account_number,
coa.description AS coa_description,
coa.current_balance AS coa_current_balance,
coa.opening_balance AS coa_opening_balance
FROM
chart_of_accounts coa
WHERE
coa.account_type_id = account_type_id_input
AND coa.is_deleted = FALSE; -- Ensuring only active records are returned
END;
$function$
-- Function: get_all_bank_transfers_by_company_and_datespan
CREATE OR REPLACE FUNCTION public.get_all_bank_transfers_by_company_and_datespan(p_company_id uuid, p_start_date date, p_end_date date)
RETURNS TABLE(id uuid, company_id uuid, company_name text, source_account_id uuid, target_account_id uuid, amount numeric, transfer_date timestamp without time zone, cheque_number text, description text, is_bulk boolean, created_by uuid, created_by_name character varying, modified_by uuid, modified_by_name character varying, created_on_utc timestamp without time zone, modified_on_utc timestamp without time zone, deleted_on_utc timestamp without time zone, is_deleted boolean)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
bt.id,
bt.company_id,
c.name as company_name,
bt.source_account_id,
bt.target_account_id,
bt.amount,
bt.transfer_date,
bt.cheque_number,
bt.description,
bt.is_bulk,
bt.created_by,
CONCAT(cu.first_name, ' ', cu.last_name)::character varying AS created_by_name,
bt.modified_by,
CONCAT(mu.first_name, ' ', mu.last_name)::character varying AS modified_by_name,
bt.created_on_utc,
bt.modified_on_utc,
bt.deleted_on_utc,
bt.is_deleted
FROM public.bank_transfers AS bt
join public.companies as c on bt.company_id = c.id
LEFT JOIN users cu ON cu.id = bt.created_by
LEFT JOIN users mu ON mu.id = bt.modified_by
WHERE
bt.company_id = p_company_id
AND bt.transfer_date::date >= p_start_date
AND bt.transfer_date::date <= p_end_date
AND bt.is_deleted = false;
END;
$function$
-- Function: get_all_expense_categories
CREATE OR REPLACE FUNCTION public.get_all_expense_categories(p_company_id uuid, p_finance_year_id integer)
RETURNS TABLE(account_name text, account_number text, total_expense numeric)
LANGUAGE plpgsql
STABLE PARALLEL SAFE
AS $function$
DECLARE
v_financial_year_start date;
v_financial_year_end date;
BEGIN
-- Get financial year start and end dates
SELECT fy.start_date, fy.end_date
INTO v_financial_year_start, v_financial_year_end
FROM public.finance_year fy
WHERE fy.id = p_finance_year_id;
-- Handle case where no financial year is found
IF v_financial_year_start IS NULL OR v_financial_year_end IS NULL THEN
RAISE EXCEPTION 'Financial year with ID % not found', p_finance_year_id;
END IF;
RETURN QUERY
WITH RECURSIVE AccountHierarchy AS (
-- Base case: Select root accounts (only considering expense accounts, i.e., account_type_id = 5)
SELECT
coa.id AS account_id,
coa.name,
coa.account_number,
coa.parent_account_id
FROM
public.chart_of_accounts coa
WHERE
coa.account_type_id = 5
UNION ALL
-- Recursive case: Select all child accounts under the root accounts
SELECT
coa.id AS account_id,
coa.name,
coa.account_number,
coa.parent_account_id
FROM
public.chart_of_accounts coa
INNER JOIN
AccountHierarchy ah ON coa.parent_account_id = ah.account_id
)
-- Calculate total expenses grouped by account, filtering by company_id in transaction_headers
SELECT
ah.name AS account_name,
ah.account_number,
COALESCE(SUM(je.amount), 0) AS total_expense
FROM
AccountHierarchy ah
JOIN
public.journal_entries je ON je.account_id = ah.account_id
JOIN
public.transaction_headers th ON je.transaction_id = th.id
WHERE
th.company_id = p_company_id
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND je.is_deleted = FALSE
GROUP BY
ah.name, ah.account_number
ORDER BY
total_expense DESC;
END;
$function$
-- Function: get_all_coa
CREATE OR REPLACE FUNCTION public.get_all_coa(p_organization_id uuid, p_finyear_id integer)
RETURNS TABLE(id uuid, account_type text, account_number integer, name text, opening_balance numeric, level integer, order_sequence character varying, is_default_account boolean, schedule text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
WITH RECURSIVE coa_view AS (
SELECT
coa.id,
act.name as account_type,
coa.account_number::integer,
coa.name,
coa.parent_account_id,
0 AS level,
coa.account_number::text AS order_sequence,
coa.is_default_account,
NULL::text AS schedule
FROM public.chart_of_accounts coa
INNER JOIN public.account_types act ON coa.account_type_id = act.id
WHERE coa.organization_id = p_organization_id
AND (coa.parent_account_id = '00000000-0000-0000-0000-000000000000' OR coa.parent_account_id IS NULL)
AND coa.is_deleted = FALSE
UNION ALL
SELECT
coa.id,
act.name as account_type,
coa.account_number::integer,
coa.name,
coa.parent_account_id,
view.level + 1 AS level,
view.order_sequence || '.' || coa.account_number::text AS order_sequence,
coa.is_default_account,
NULL::text AS schedule
FROM public.chart_of_accounts coa
INNER JOIN coa_view view ON view.id = coa.parent_account_id
INNER JOIN public.account_types act ON coa.account_type_id = act.id
WHERE coa.organization_id = p_organization_id
AND coa.is_deleted = FALSE
),
balances AS (
SELECT
je.account_id,
SUM(
CASE WHEN je.entry_type = 'D' THEN je.amount
ELSE -je.amount END
) AS balance
FROM public.journal_entries je
INNER JOIN public.transaction_headers th
ON th.id = je.transaction_id
INNER JOIN public.companies c
ON th.company_id = c.id
AND c.organization_id = p_organization_id
INNER JOIN public.finance_year fy
ON th.transaction_date BETWEEN fy.start_date AND fy.end_date
AND fy.id = p_finyear_id
WHERE th.is_deleted = FALSE
GROUP BY je.account_id
)
SELECT
view.id,
view.account_type,
view.account_number,
LPAD('', view.level * 4, ' ') || view.name AS name,
COALESCE(balances.balance, 0) AS opening_balance,
view.level,
view.order_sequence::character varying AS order_sequence,
view.is_default_account,
view.schedule
FROM coa_view view
LEFT JOIN balances ON balances.account_id = view.id
ORDER BY view.order_sequence;
END;
$function$
-- Function: get_all_coa_by_account_type_id
CREATE OR REPLACE FUNCTION public.get_all_coa_by_account_type_id(p_organization_id uuid, p_account_type_id integer)
RETURNS TABLE(id uuid, account_type text, account_number integer, name text, opening_balance numeric, level integer, order_sequence character varying, schedule text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN query
WITH RECURSIVE coa_view AS (
SELECT
coa.id,
act.name as account_type,
coa.account_number::integer,
coa.name,
coa.parent_account_id,
0 AS level,
coa.account_number::text AS order_sequence,
NULL::text AS schedule
FROM
public.chart_of_accounts coa
INNER JOIN
public.account_types act ON coa.account_type_id = act.id
WHERE
coa.parent_account_id IS NULL
AND coa.organization_id = p_organization_id
AND coa.account_type_id = p_account_type_id
UNION ALL
SELECT
coa.id,
act.name as account_type,
coa.account_number::integer,
coa.name,
coa.parent_account_id,
view.level + 1 AS level,
view.order_sequence || '.' || coa.account_number::text AS order_sequence,
NULL::text AS schedule
FROM
public.chart_of_accounts coa
INNER JOIN
coa_view view ON view.id = coa.parent_account_id
INNER JOIN
public.account_types act ON coa.account_type_id = act.id
WHERE
coa.organization_id = p_organization_id
AND coa.account_type_id = p_account_type_id
)
SELECT
view.id,
view.account_type,
view.account_number,
LPAD('', view.level * 4, ' ') || view.name AS name,
0::numeric AS opening_balance,
view.level,
view.order_sequence::character varying AS order_sequence,
view.schedule
FROM
coa_view view
ORDER BY
view.order_sequence;
END;
$function$
-- Function: get_all_coa_with_pi
CREATE OR REPLACE FUNCTION public.get_all_coa_with_pi(p_organization_id uuid)
RETURNS TABLE(id uuid, account_type text, account_number integer, name text, opening_balance numeric, level integer, order_sequence character varying, schedule text, parent_account_id uuid)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN query
WITH RECURSIVE coa_view AS (
SELECT
coa.id,
act.name as account_type,
coa.account_number::integer,
coa.name,
coa.parent_account_id,
0 AS level,
coa.account_number::text AS order_sequence,
NULL::text AS schedule
FROM
public.chart_of_accounts coa
INNER JOIN
public.account_types act ON coa.account_type_id = act.id
WHERE
coa.organization_id = p_organization_id
AND coa.parent_account_id = '00000000-0000-0000-0000-000000000000'
AND coa.is_deleted = FALSE
UNION ALL
SELECT
coa.id,
act.name as account_type,
coa.account_number::integer,
coa.name,
coa.parent_account_id,
view.level + 1 AS level,
view.order_sequence || '.' || coa.account_number::text AS order_sequence,
NULL::text AS schedule
FROM
public.chart_of_accounts coa
INNER JOIN
coa_view view ON view.id = coa.parent_account_id
INNER JOIN
public.account_types act ON coa.account_type_id = act.id
WHERE
coa.organization_id = p_organization_id
AND coa.is_deleted = FALSE
)
SELECT
view.id,
view.account_type,
view.account_number,
LPAD('', view.level * 4, ' ') || view.name AS name,
0::numeric AS opening_balance,
view.level,
view.order_sequence::character varying AS order_sequence,
view.schedule,
view.parent_account_id
FROM
coa_view view
ORDER BY
view.order_sequence;
END;
$function$
-- Function: change_user_password
CREATE OR REPLACE FUNCTION public.change_user_password(p_user_name text, p_current_password text, p_new_password text, p_modified_by uuid)
RETURNS boolean
LANGUAGE plpgsql
AS $function$
DECLARE
v_user_id UUID;
BEGIN
-- Step 1: verify current password
SELECT id
INTO v_user_id
FROM users
WHERE user_name = p_user_name
AND is_deleted = false
AND password_hash = crypt(p_current_password, password_hash);
IF v_user_id IS NULL THEN
RETURN FALSE;
END IF;
-- Step 2: update password
UPDATE users
SET password_hash = crypt(p_new_password, gen_salt('bf', 12)),
modified_on_utc = NOW(),
modified_by = p_modified_by
WHERE id = v_user_id;
RETURN TRUE;
END;
$function$
-- Function: get_coa_opening_balance
CREATE OR REPLACE FUNCTION public.get_coa_opening_balance(p_coa_id uuid, p_finyear_id integer)
RETURNS TABLE(transaction_date timestamp without time zone, coa_id uuid, coa_name text, description text, debit numeric, credit numeric)
LANGUAGE plpgsql
AS $function$
DECLARE
v_year_text text;
BEGIN
-- Fetch the year text for the given finance year id
SELECT year INTO v_year_text FROM public.finance_year WHERE id = p_finyear_id;
IF v_year_text IS NULL THEN
RAISE EXCEPTION 'Finance year with id % not found!', p_finyear_id;
END IF;
RETURN QUERY
SELECT
th.transaction_date,
th.document_id AS coa_id,
coa.name AS coa_name,
th.description,
COALESCE(CASE WHEN je.entry_type = 'D' THEN je.amount END, 0) AS debit,
COALESCE(CASE WHEN je.entry_type = 'C' THEN je.amount END, 0) AS credit
FROM public.transaction_headers th
JOIN public.journal_entries je ON th.id = je.transaction_id
JOIN public.chart_of_accounts coa ON th.document_id = coa.id
WHERE th.document_id = p_coa_id
AND je.account_id = th.document_id
AND th.transaction_source_type = 13
AND th.description LIKE 'Opening Balance for %' || v_year_text || '%'
ORDER BY th.transaction_date;
END;
$function$
-- Function: get_budget_planning_data
CREATE OR REPLACE FUNCTION public.get_budget_planning_data(p_company_id uuid, p_current_finance_year_id integer)
RETURNS TABLE(row_id bigint, budget_id uuid, budget_status_id integer, budget_status_name text, account_id uuid, acc_number text, account_name text, acc_type text, acc_level integer, last_year_actual numeric, current_budget_amount numeric, line_id uuid)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
sub.rownum AS row_id,
sub.draft_budget_id AS budget_id,
sub.draft_status_id AS budget_status_id,
sub.draft_status_name AS budget_status_name,
sub.account_id,
sub.acc_number,
sub.account_name,
sub.acc_type,
sub.acc_level,
sub.last_year_actual,
sub.current_budget_amount,
sub.line_id
FROM (
WITH curr_year AS (
SELECT * FROM public.finance_year WHERE id = p_current_finance_year_id
),
prev_year AS (
SELECT fy.*
FROM public.finance_year fy
JOIN public.company_finance_year cfy ON cfy.finance_year_id = fy.id
WHERE cfy.company_id = p_company_id
AND fy.start_date < (SELECT start_date FROM curr_year)
ORDER BY fy.start_date DESC
LIMIT 1
),
coa_hierarchy AS (
SELECT
id AS coa_id,
account_number::text,
name,
account_type,
level
FROM public.get_all_coa((SELECT organization_id FROM companies WHERE id = p_company_id))
),
draft_budget AS (
SELECT
b.id AS draft_budget_id,
b.status_id AS draft_status_id,
s.name AS draft_status_name
FROM public.budgets b
JOIN public.budget_statuses s ON b.status_id = s.id
WHERE b.company_id = p_company_id
AND b.finance_year_id = p_current_finance_year_id
AND b.is_deleted = false
AND b.status_id = 1 -- Draft
LIMIT 1
),
last_year_actuals AS (
SELECT
je.account_id,
SUM(je.amount) AS actual_amount
FROM public.journal_entries je
JOIN public.transaction_headers th ON je.transaction_id = th.id
JOIN prev_year fy ON je.transaction_date BETWEEN fy.start_date AND fy.end_date
WHERE th.company_id = p_company_id
AND je.is_deleted = false
GROUP BY je.account_id
),
current_budget_lines AS (
SELECT
bl.account_id,
SUM(bl.amount) AS budget_amount,
(ARRAY_AGG(bl.id))[1] AS line_id -- ✅ Fixed aggregation for UUID
FROM public.budget_lines bl
JOIN public.budgets b ON bl.budget_id = b.id
WHERE b.company_id = p_company_id
AND b.finance_year_id = p_current_finance_year_id
AND b.is_deleted = false
AND bl.is_deleted = false
GROUP BY bl.account_id
)
SELECT
ROW_NUMBER() OVER (ORDER BY coa.account_number) AS rownum,
db.draft_budget_id,
db.draft_status_id,
db.draft_status_name,
coa.coa_id AS account_id,
coa.account_number AS acc_number,
coa.name AS account_name,
coa.account_type AS acc_type,
coa.level AS acc_level,
COALESCE(lya.actual_amount, 0) AS last_year_actual,
COALESCE(cbl.budget_amount, 0) AS current_budget_amount,
cbl.line_id
FROM coa_hierarchy coa
CROSS JOIN draft_budget db
LEFT JOIN last_year_actuals lya ON lya.account_id = coa.coa_id
LEFT JOIN current_budget_lines cbl ON cbl.account_id = coa.coa_id
) sub
ORDER BY sub.acc_number;
END;
$function$
-- Function: get_cash_flow
CREATE OR REPLACE FUNCTION public.get_cash_flow()
RETURNS TABLE(transaction_date date, account_number text, name text, net_cash_flow numeric)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
je.transaction_date,
coa.account_number,
coa.name,
SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE -je.amount END) AS net_cash_flow
FROM
public.journal_entries je
JOIN
public.chart_of_accounts coa ON je.account_id = coa.id
WHERE
coa.account_type_id = 1 -- Current Assets, focusing on cash accounts
AND je.is_deleted = false
GROUP BY
je.transaction_date, coa.account_number, coa.name
ORDER BY
coa.account_number, je.transaction_date ;
END;
$function$
-- Function: get_cash_flow
CREATE OR REPLACE FUNCTION public.get_cash_flow(p_company_id uuid)
RETURNS TABLE(transaction_date date, account_number text, name text, net_cash_flow numeric)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
je.transaction_date::date,
coa.account_number,
coa.name,
SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE -je.amount END) AS net_cash_flow
FROM
public.journal_entries je
JOIN
public.chart_of_accounts coa ON je.account_id = coa.id
JOIN
public.transaction_headers th ON je.transaction_id = th.id
WHERE
coa.account_type_id = 1 -- Current Assets, focusing on cash accounts
AND je.is_deleted = false
AND th.company_id = p_company_id
GROUP BY
je.transaction_date, coa.account_number, coa.name
ORDER BY
coa.account_number, je.transaction_date ;
END;
$function$
-- Function: get_chart_of_accounts_hierarchy
CREATE OR REPLACE FUNCTION public.get_chart_of_accounts_hierarchy(p_organization_id uuid)
RETURNS TABLE(id uuid, account_type text, account_number text, account_name text, parent_account_id uuid, is_default_account boolean, level integer)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
WITH RECURSIVE coa_hierarchy AS (
SELECT
coa.id,
act.name AS account_type,
coa.account_number,
coa.name as account_name,
coa.parent_account_id,
coa.is_default_account,
0 AS level
FROM
public.chart_of_accounts coa
INNER JOIN
public.account_types act ON coa.account_type_id = act.id
WHERE
coa.is_deleted = false
AND coa.parent_account_id = '00000000-0000-0000-0000-000000000000'
AND coa.organization_id = p_organization_id
UNION ALL
SELECT
coa.id,
act.name AS account_type,
coa.account_number,
coa.name as account_name,
coa.parent_account_id,
coa.is_default_account,
ch.level + 1 AS level
FROM
public.chart_of_accounts coa
INNER JOIN
public.account_types act ON coa.account_type_id = act.id
INNER JOIN
coa_hierarchy ch ON ch.id = coa.parent_account_id
WHERE
coa.is_deleted = FALSE
)
SELECT
ch.id,
ch.account_type,
ch.account_number,
LPAD(ch.account_name, LENGTH(ch.account_name) + ch.level * 4, ' ') AS account_name,
ch.parent_account_id,
ch.is_default_account,
ch.level
FROM coa_hierarchy ch
ORDER BY ch.account_number;
END;
$function$
-- Function: get_company_details
CREATE OR REPLACE FUNCTION public.get_company_details(p_company_id uuid)
RETURNS TABLE(id uuid, organization_id uuid, name text, short_name text, gst_in text, pan text, tan text, currency text, proprietor_name text, outstanding_limit numeric, is_non_work boolean, is_apartment boolean, interest_percentage numeric, billing_address_id uuid, billing_address jsonb, shipping_address_id uuid, shipping_address jsonb, bank_accounts jsonb, contacts jsonb, description text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
com.id,
com.organization_id,
com.name,
com.short_name,
com.gstin,
com.pan,
com.tan,
com.currency,
com.proprietor_name,
com.outstanding_limit,
com.is_non_work,
com.is_apartment,
com.interest_percentage,
com.billing_address_id,
CASE
WHEN ba.id IS NOT NULL
THEN jsonb_build_object(
'AddressLine1', ba.address_line1,
'AddressLine2', ba.address_line2,
'ZipCode', ba.zip_code,
'CountryId', ba.country_id,
'CountryName', bc.name,
'StateId', ba.state_id,
'StateName', bs.name,
'CityId', ba.city_id,
'CityName', bci.name
)
ELSE NULL
END AS billing_address,
com.shipping_address_id,
CASE
WHEN sa.id IS NOT NULL
THEN jsonb_build_object(
'AddressLine1', sa.address_line1,
'AddressLine2', sa.address_line2,
'ZipCode', sa.zip_code,
'CountryId', sa.country_id,
'CountryName', sc.name,
'StateId', sa.state_id,
'StateName', ss.name,
'CityId', sa.city_id,
'CityName', sci.name
)
ELSE NULL
END AS shipping_address,
COALESCE(
(
SELECT jsonb_agg(
jsonb_build_object(
'Id', ba.id,
'BankId', ba.bank_id,
'BankName', b.name,
'BranchName', ba.branch_name,
'AccountNumber', ba.account_number,
'IFSC', ba.ifsc
)
)
FROM company_bank_accounts AS cba
JOIN bank_accounts AS ba ON ba.id = cba.bank_account_id
JOIN banks AS b ON b.id = ba.bank_id
WHERE cba.company_id = com.id AND ba.is_deleted = false
),
'[]'::jsonb
) AS bank_accounts,
COALESCE(
(
SELECT jsonb_agg(
jsonb_build_object(
'Id', c.id,
'Salutation', c.salutation,
'FirstName', c.first_name,
'LastName', c.last_name,
'Email', c.email,
'PhoneNumber', c.phone_number,
'MobileNumber', c.mobile_number,
'IsPrimary', c.is_primary
)
)
FROM company_contacts AS cc
JOIN contacts AS c ON c.id = cc.contact_id
WHERE cc.company_id = com.id AND c.is_deleted = false
),
'[]'::jsonb
) AS contacts,
com.description
FROM companies AS com
LEFT JOIN addresses AS ba ON ba.id = com.billing_address_id
LEFT JOIN countries AS bc ON bc.id = ba.country_id
LEFT JOIN states AS bs ON bs.id = ba.state_id
LEFT JOIN cities AS bci ON bci.id = ba.city_id
LEFT JOIN addresses AS sa ON sa.id = com.shipping_address_id
LEFT JOIN countries AS sc ON sc.id = sa.country_id
LEFT JOIN states AS ss ON ss.id = sa.state_id
LEFT JOIN cities AS sci ON sci.id = sa.city_id
WHERE com.id = p_company_id AND com.is_deleted = false;
END;
$function$
-- Function: get_customer_dues
CREATE OR REPLACE FUNCTION public.get_customer_dues()
RETURNS TABLE(type text, name text, account_name text, amount numeric, due_date date, aging_bucket text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
'Customer' AS type,
c.name AS name,
coa.name AS account_name,
je.amount,
th.transaction_date + INTERVAL '30 days' AS due_date, -- Assuming a 30-day payment term
CASE
WHEN th.transaction_date + INTERVAL '30 days' <= CURRENT_DATE THEN 'Current'
WHEN th.transaction_date + INTERVAL '30 days' BETWEEN CURRENT_DATE - INTERVAL '30 days' AND CURRENT_DATE THEN '1-30 Days Past Due'
WHEN th.transaction_date + INTERVAL '30 days' BETWEEN CURRENT_DATE - INTERVAL '60 days' AND CURRENT_DATE - INTERVAL '31 days' THEN '31-60 Days Past Due'
ELSE 'Over 60 Days Past Due'
END AS aging_bucket
FROM
public.journal_entries je
JOIN
public.transaction_headers th ON je.transaction_id = th.id
JOIN
public.chart_of_accounts coa ON je.account_id = coa.id
JOIN
public.customers c ON th.customer_id = c.id
WHERE
coa.account_type_id = (SELECT id FROM account_types WHERE name = 'Accounts Receivable')
AND je.is_deleted = false
AND th.transaction_date + INTERVAL '30 days' <= CURRENT_DATE
ORDER BY
name, due_date;
END;
$function$
-- Function: get_customer_opening_balance
CREATE OR REPLACE FUNCTION public.get_customer_opening_balance(p_customer_id uuid, p_finyear_id integer)
RETURNS TABLE(transaction_date timestamp without time zone, customer_id uuid, customer_name text, description text, debit numeric, credit numeric)
LANGUAGE plpgsql
AS $function$
DECLARE
v_year_text text;
BEGIN
-- Fetch the year text for the given finance year id
SELECT year INTO v_year_text FROM public.finance_year WHERE id = p_finyear_id;
IF v_year_text IS NULL THEN
RAISE EXCEPTION 'Finance year with id % not found!', p_finyear_id;
END IF;
RETURN QUERY
SELECT
th.transaction_date,
th.customer_id,
c.name::text AS customer_name, -- explicit cast here!
th.description,
COALESCE(CASE WHEN je.entry_type = 'D' THEN je.amount END, 0) AS debit,
COALESCE(CASE WHEN je.entry_type = 'C' THEN je.amount END, 0) AS credit
FROM public.transaction_headers th
JOIN public.customers c ON th.customer_id = c.id
JOIN public.journal_entries je ON th.id = je.transaction_id
JOIN public.chart_of_accounts sd ON sd.name ILIKE '%Sundry Debtors%'
WHERE th.customer_id = p_customer_id
AND je.account_id = sd.id
AND th.transaction_source_type = 13
AND th.description LIKE 'Opening Balance for %' || v_year_text || '%'
ORDER BY th.transaction_date;
END;
$function$
-- Function: get_customer_outstanding
CREATE OR REPLACE FUNCTION public.get_customer_outstanding()
RETURNS TABLE(customer_name text, account_name text, amount numeric, due_date date, aging_bucket text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
c.name AS customer_name,
coa.name AS account_name,
je.amount,
th.transaction_date + INTERVAL '30 days' AS due_date, -- Assuming a 30-day payment term
CASE
WHEN th.transaction_date + INTERVAL '30 days' <= CURRENT_DATE THEN 'Current'
WHEN th.transaction_date + INTERVAL '30 days' BETWEEN CURRENT_DATE - INTERVAL '30 days' AND CURRENT_DATE THEN '1-30 Days Past Due'
WHEN th.transaction_date + INTERVAL '30 days' BETWEEN CURRENT_DATE - INTERVAL '60 days' AND CURRENT_DATE - INTERVAL '31 days' THEN '31-60 Days Past Due'
ELSE 'Over 60 Days Past Due'
END AS aging_bucket
FROM
public.journal_entries je
JOIN
public.transaction_headers th ON je.transaction_id = th.id
JOIN
public.chart_of_accounts coa ON je.account_id = coa.id
JOIN
public.customers c ON th.customer_id = c.id
WHERE
coa.account_type_id = (SELECT id FROM account_types WHERE name = 'Accounts Receivable')
AND je.is_deleted = false
ORDER BY
c.name, th.transaction_date;
END;
$function$
-- Function: get_trial_balance_test
CREATE OR REPLACE FUNCTION public.get_trial_balance_test(p_company_id uuid, p_finance_year_id integer)
RETURNS TABLE(account_type text, account_category text, account_number text, account_name text, total_debits numeric, total_credits numeric)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
WITH financial_year_range AS (
SELECT
fy.start_date::DATE AS start_date,
fy.end_date::DATE AS end_date
FROM
public.finance_year fy
WHERE
fy.id = p_finance_year_id
LIMIT 1
),
account_balances AS (
SELECT
je.account_id,
SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END) AS total_debits,
SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END) AS total_credits
FROM
public.journal_entries je
JOIN
public.transaction_headers th ON je.transaction_id = th.id
CROSS JOIN
financial_year_range fy
WHERE
th.company_id = p_company_id
AND je.is_deleted = FALSE
AND th.transaction_date BETWEEN fy.start_date AND fy.end_date
GROUP BY
je.account_id
)
SELECT
at.name AS account_type,
ac.name::text AS account_category, -- Explicitly cast to text
coa.account_number,
coa.name AS account_name,
COALESCE(ab.total_debits, 0) AS total_debits,
COALESCE(ab.total_credits, 0) AS total_credits
FROM
public.chart_of_accounts coa
INNER JOIN
public.account_types at ON coa.account_type_id = at.id
LEFT JOIN
public.account_categories ac ON at.account_category_id = ac.id
LEFT JOIN
account_balances ab ON coa.id = ab.account_id
WHERE
coa.organization_id = p_company_id
AND coa.is_deleted = FALSE
ORDER BY
coa.account_number;
END;
$function$
-- Function: get_dashboard_totals_by_company_id
CREATE OR REPLACE FUNCTION public.get_dashboard_totals_by_company_id(p_company_id uuid, p_finance_year_id integer)
RETURNS TABLE(income_total numeric, expense_total numeric, pending_dues_total numeric, pending_payments_total numeric)
LANGUAGE plpgsql
AS $function$
DECLARE
v_organization_id UUID;
v_financial_year_start DATE;
v_financial_year_end DATE;
v_finance_year TEXT;
BEGIN
-- Fetch organization ID
SELECT organization_id INTO v_organization_id
FROM public.companies
WHERE id = p_company_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Invalid company ID: %', p_company_id;
END IF;
-- Fetch financial year details
SELECT start_date, end_date, year INTO v_financial_year_start, v_financial_year_end, v_finance_year
FROM public.finance_year
WHERE id = p_finance_year_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Invalid finance year ID: %', p_finance_year_id;
END IF;
-- Calculate totals
RETURN QUERY
SELECT
-- Income Total
ROUND(
(
SELECT COALESCE(SUM(je.amount), 0)
FROM public.journal_entries je
INNER JOIN public.chart_of_accounts ca ON je.account_id = ca.id
INNER JOIN public.account_types at ON ca.account_type_id = at.id
INNER JOIN public.account_categories ac ON at.account_category_id = ac.id
INNER JOIN public.transaction_headers tr ON je.transaction_id = tr.id
WHERE ac.id = 4
AND tr.company_id = p_company_id
AND ca.organization_id = v_organization_id
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND je.is_deleted = FALSE
), 2
) AS income_total,
-- Expense Total
ROUND(
(
SELECT COALESCE(SUM(je.amount), 0)
FROM public.journal_entries je
INNER JOIN public.chart_of_accounts ca ON je.account_id = ca.id
INNER JOIN public.account_types at ON ca.account_type_id = at.id
INNER JOIN public.account_categories ac ON at.account_category_id = ac.id
INNER JOIN public.transaction_headers tr ON je.transaction_id = tr.id
WHERE ac.id = 5
AND tr.company_id = p_company_id
AND ca.organization_id = v_organization_id
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND je.is_deleted = FALSE
), 2
) AS expense_total,
ROUND(
(
SELECT
COALESCE(SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END), 0) -
COALESCE(SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END), 0)
FROM public.journal_entries je
INNER JOIN public.chart_of_accounts ca ON je.account_id = ca.id
INNER JOIN public.account_types at ON ca.account_type_id = at.id
INNER JOIN public.account_categories ac ON at.account_category_id = ac.id
INNER JOIN public.transaction_headers tr ON je.transaction_id = tr.id
WHERE at.name IN ('Accounts Receivable', 'Current Assets') -- Filter specifically for receivables
AND tr.company_id = p_company_id
AND ca.organization_id = v_organization_id
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND je.is_deleted = FALSE
), 2
) AS pending_dues_total,
-- Pending Payments Total
ROUND(
(
SELECT COALESCE(SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END), 0) -
COALESCE(SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END), 0)
FROM public.journal_entries je
INNER JOIN public.chart_of_accounts ca ON je.account_id = ca.id
INNER JOIN public.account_types at ON ca.account_type_id = at.id
INNER JOIN public.transaction_headers tr ON je.transaction_id = tr.id
WHERE ca.account_type_id IN (
SELECT id
FROM public.get_account_type_hierarchy(2)
)
AND tr.company_id = p_company_id
AND ca.organization_id = v_organization_id
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND je.is_deleted = FALSE
AND je.entry_type IN ('C', 'D') -- Ensure only valid entry types are included
), 2
) AS pending_payments_total
;
END;
$function$
-- Function: get_dashboard_totals_test
CREATE OR REPLACE FUNCTION public.get_dashboard_totals_test(p_company_id uuid, p_finance_year_id integer)
RETURNS TABLE(income_total numeric, expense_total numeric, pending_dues_total numeric, pending_payments_total numeric, prev_income_total numeric, prev_expense_total numeric, prev_pending_dues_total numeric, prev_pending_payments_total numeric)
LANGUAGE plpgsql
AS $function$
DECLARE
v_organization_id UUID;
v_financial_year_start DATE;
v_financial_year_end DATE;
v_prev_financial_year_start DATE;
v_prev_financial_year_end DATE;
v_finance_year INTEGER;
v_today DATE := CURRENT_DATE;
BEGIN
-- Fetch organization ID
SELECT organization_id INTO v_organization_id
FROM public.companies
WHERE id = p_company_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Invalid company ID: %', p_company_id;
END IF;
-- Get financial year integer from 'YYYY-YY'
SELECT LEFT(year, 4)::INTEGER INTO v_finance_year
FROM public.finance_year
WHERE id = p_finance_year_id
LIMIT 1;
IF NOT FOUND THEN
RAISE EXCEPTION 'Invalid finance year ID: %', p_finance_year_id;
END IF;
-- Define financial year range
v_financial_year_start := MAKE_DATE(v_finance_year, 4, 1);
v_financial_year_end := MAKE_DATE(v_finance_year + 1, 3, 31);
-- Define previous financial year range
v_prev_financial_year_start := MAKE_DATE(v_finance_year - 1, 4, 1);
v_prev_financial_year_end := MAKE_DATE(v_finance_year, 3, 31);
-- Adjust financial year ends if the year is ongoing
IF v_today < v_financial_year_end THEN
v_financial_year_end := v_today;
END IF;
IF v_today < v_prev_financial_year_end THEN
v_prev_financial_year_end := v_today - INTERVAL '1 year';
END IF;
-- Align both end dates to same day/month by limiting to shorter period
v_financial_year_end := LEAST(v_financial_year_end, v_prev_financial_year_end + INTERVAL '1 year');
v_prev_financial_year_end := v_financial_year_end - INTERVAL '1 year';
-- Return calculated data
RETURN QUERY
SELECT
-- Current Year Income
ROUND(COALESCE((
SELECT SUM(je.amount)
FROM journal_entries je
JOIN chart_of_accounts ca ON je.account_id = ca.id
JOIN account_types at ON ca.account_type_id = at.id
JOIN account_categories ac ON at.account_category_id = ac.id
JOIN transaction_headers tr ON je.transaction_id = tr.id
WHERE ac.id = 4
AND tr.company_id = p_company_id
AND ca.organization_id = v_organization_id
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND je.is_deleted = FALSE
), 0), 2),
-- Current Year Expense
ROUND(COALESCE((
SELECT SUM(je.amount)
FROM journal_entries je
JOIN chart_of_accounts ca ON je.account_id = ca.id
JOIN account_types at ON ca.account_type_id = at.id
JOIN account_categories ac ON at.account_category_id = ac.id
JOIN transaction_headers tr ON je.transaction_id = tr.id
WHERE ac.id = 5
and tr.transaction_source_type = 2
AND tr.company_id = p_company_id
AND ca.organization_id = v_organization_id
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND je.is_deleted = FALSE
), 0), 2),
-- Pending Dues
ROUND(COALESCE((
SELECT SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END) -
SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END)
FROM journal_entries je
JOIN chart_of_accounts ca ON je.account_id = ca.id
JOIN account_types at ON ca.account_type_id = at.id
JOIN transaction_headers tr ON je.transaction_id = tr.id
WHERE at.name IN ('Accounts Receivable', 'Current Assets')
AND tr.company_id = p_company_id
AND ca.organization_id = v_organization_id
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND je.is_deleted = FALSE
), 0), 2),
-- Pending Payments
ROUND(COALESCE((
SELECT SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END) -
SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END)
FROM journal_entries je
JOIN chart_of_accounts ca ON je.account_id = ca.id
JOIN account_types at ON ca.account_type_id = at.id
JOIN transaction_headers tr ON je.transaction_id = tr.id
WHERE ca.account_type_id IN (SELECT id FROM get_account_type_hierarchy(2))
AND tr.company_id = p_company_id
AND ca.organization_id = v_organization_id
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND je.is_deleted = FALSE
), 0), 2),
-- Previous Year Income
ROUND(COALESCE((
SELECT SUM(je.amount)
FROM journal_entries je
JOIN chart_of_accounts ca ON je.account_id = ca.id
JOIN account_types at ON ca.account_type_id = at.id
JOIN account_categories ac ON at.account_category_id = ac.id
JOIN transaction_headers tr ON je.transaction_id = tr.id
WHERE ac.id = 4
AND tr.company_id = p_company_id
AND ca.organization_id = v_organization_id
AND je.transaction_date BETWEEN v_prev_financial_year_start AND v_prev_financial_year_end
AND je.is_deleted = FALSE
), 0), 2),
-- Previous Year Expense
ROUND(COALESCE((
SELECT SUM(je.amount)
FROM journal_entries je
JOIN chart_of_accounts ca ON je.account_id = ca.id
JOIN account_types at ON ca.account_type_id = at.id
JOIN account_categories ac ON at.account_category_id = ac.id
JOIN transaction_headers tr ON je.transaction_id = tr.id
WHERE ac.id = 5
AND tr.company_id = p_company_id
AND ca.organization_id = v_organization_id
AND je.transaction_date BETWEEN v_prev_financial_year_start AND v_prev_financial_year_end
AND je.is_deleted = FALSE
), 0), 2),
-- Previous Year Pending Dues
ROUND(COALESCE((
SELECT SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END) -
SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END)
FROM journal_entries je
JOIN chart_of_accounts ca ON je.account_id = ca.id
JOIN account_types at ON ca.account_type_id = at.id
JOIN transaction_headers tr ON je.transaction_id = tr.id
WHERE at.name IN ('Accounts Receivable', 'Current Assets')
AND tr.company_id = p_company_id
AND ca.organization_id = v_organization_id
AND je.transaction_date BETWEEN v_prev_financial_year_start AND v_prev_financial_year_end
AND je.is_deleted = FALSE
), 0), 2),
-- Previous Year Pending Payments
ROUND(COALESCE((
SELECT SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END) -
SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END)
FROM journal_entries je
JOIN chart_of_accounts ca ON je.account_id = ca.id
JOIN account_types at ON ca.account_type_id = at.id
JOIN transaction_headers tr ON je.transaction_id = tr.id
WHERE ca.account_type_id IN (SELECT id FROM get_account_type_hierarchy(2))
AND tr.company_id = p_company_id
AND ca.organization_id = v_organization_id
AND je.transaction_date BETWEEN v_prev_financial_year_start AND v_prev_financial_year_end
AND je.is_deleted = FALSE
), 0), 2);
END;
$function$
-- Function: get_user_deletion_request_by_email
CREATE OR REPLACE FUNCTION public.get_user_deletion_request_by_email(email_input text)
RETURNS TABLE(user_id uuid, first_name text, last_name text, email text, deletion_request_id integer, status_id integer, status_name text, created_on_utc timestamp without time zone)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT u.id AS user_id,
u.first_name::TEXT AS first_name, -- Explicit cast to TEXT
u.last_name::TEXT AS last_name, -- Explicit cast to TEXT
u.email::TEXT AS email, -- Explicit cast to TEXT
ur.id AS deletion_request_id,
ur.status_id,
s.name::TEXT AS status_name, -- Explicit cast to TEXT
ur.created_on_utc
FROM public.users u
JOIN public.user_deletion_requests ur ON u.id = ur.user_id
JOIN public.user_deletion_request_statuses s ON ur.status_id = s.id
WHERE u.email = email_input
AND u.is_deleted = FALSE
LIMIT 1;
END;
$function$
-- Function: get_journal_voucher_by_header_id
CREATE OR REPLACE FUNCTION public.get_journal_voucher_by_header_id(p_header_id uuid)
RETURNS TABLE(id uuid, date timestamp without time zone, amount numeric, note text, account_id uuid, account_name text, is_debit boolean, details jsonb)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
jvh.id,
jvh.date,
jvh.amount,
jvh.note,
jvh.account_id,
coa_header.name AS account_name,
jvh.is_debit,
jsonb_agg(
jsonb_build_object(
'id', jvd.id,
'credit_account_id', jvd.credit_account_id,
'credit_account_name', coa_credit.name,
'debit_account_id', jvd.debit_account_id,
'debit_account_name', coa_debit.name,
'amount', jvd.amount,
'tds_tcs', jvd.tds_tcs,
'narration', jvd.narration
)
) AS details
FROM
public.journal_voucher_headers jvh
LEFT JOIN
public.journal_voucher_details jvd ON jvh.id = jvd.journal_header_id
LEFT JOIN
public.chart_of_accounts coa_header ON jvh.account_id = coa_header.id
LEFT JOIN
public.chart_of_accounts coa_credit ON jvd.credit_account_id = coa_credit.id
LEFT JOIN
public.chart_of_accounts coa_debit ON jvd.debit_account_id = coa_debit.id
WHERE
jvh.id = p_header_id
AND jvh.is_deleted = FALSE
AND (jvd.is_deleted = FALSE OR jvd.is_deleted IS NULL)
GROUP BY
jvh.id, coa_header.name;
END;
$function$
-- Function: get_sundry_debtors_ledger
CREATE OR REPLACE FUNCTION public.get_sundry_debtors_ledger(p_company_id uuid, p_fin_year_id integer, p_start_date date DEFAULT NULL::date, p_end_date date DEFAULT NULL::date)
RETURNS TABLE(sl_no integer, ledger text, general_ledger text, opening_balance numeric, debit numeric, credit numeric, closing_balance numeric)
LANGUAGE plpgsql
AS $function$
DECLARE
v_start_date DATE;
v_end_date DATE;
BEGIN
-- Get financial year start and end date if not provided
SELECT fy.start_date, fy.end_date
INTO v_start_date, v_end_date
FROM public.finance_year fy
WHERE fy.id = p_fin_year_id
LIMIT 1;
-- Override with provided values if they are not NULL
v_start_date := COALESCE(p_start_date, v_start_date);
v_end_date := COALESCE(p_end_date, v_end_date);
RETURN QUERY
SELECT ROW_NUMBER() OVER()::INTEGER AS sl_no,
cu.name::TEXT AS ledger,
'Sundry Debtors'::TEXT AS general_ledger,
0::NUMERIC AS opening_balance,
COALESCE(
SUM(CASE
WHEN je.entry_type = 'D' THEN je.amount -- Debit increases asset balance
ELSE 0
END), 0
) AS debit,
COALESCE(
SUM(CASE
WHEN je.entry_type = 'C' THEN je.amount -- Credit decreases asset balance
ELSE 0
END), 0
) AS credit,
COALESCE(
SUM(CASE
WHEN je.entry_type = 'D' THEN je.amount -- Credit decreases asset balance
ELSE 0
END), 0
) -
COALESCE(
SUM(CASE
WHEN je.entry_type = 'C' THEN je.amount -- Debit increases asset balance
ELSE 0
END), 0
) AS closing_balance
FROM public.journal_entries je
JOIN public.transaction_headers th ON je.transaction_id = th.id
JOIN public.customers cu ON th.customer_id = cu.id
WHERE th.company_id = p_company_id
AND th.transaction_date BETWEEN v_start_date AND v_end_date
GROUP BY cu.name
ORDER BY cu.name;
END;
$function$
-- Function: get_dashboard_totals_with_previous_year_comparison
CREATE OR REPLACE FUNCTION public.get_dashboard_totals_with_previous_year_comparison(p_company_id uuid, p_finance_year_id integer)
RETURNS TABLE(income_total numeric, expense_total numeric, pending_dues_total numeric, pending_payments_total numeric, prev_income_total numeric, prev_expense_total numeric, prev_pending_dues_total numeric, prev_pending_payments_total numeric)
LANGUAGE plpgsql
AS $function$
DECLARE
v_organization_id UUID;
v_financial_year_start DATE;
v_financial_year_end DATE;
v_prev_financial_year_start DATE;
v_prev_financial_year_end DATE;
v_finance_year INTEGER;
v_today DATE := CURRENT_DATE;
BEGIN
-- Fetch organization ID
SELECT organization_id INTO v_organization_id
FROM public.companies
WHERE id = p_company_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Invalid company ID: %', p_company_id;
END IF;
-- Get financial year integer from 'YYYY-YY'
SELECT LEFT(year, 4)::INTEGER INTO v_finance_year
FROM public.finance_year
WHERE id = p_finance_year_id
LIMIT 1;
IF NOT FOUND THEN
RAISE EXCEPTION 'Invalid finance year ID: %', p_finance_year_id;
END IF;
-- Define financial year range
v_financial_year_start := MAKE_DATE(v_finance_year, 4, 1);
v_financial_year_end := MAKE_DATE(v_finance_year + 1, 3, 31);
-- Define previous financial year range
v_prev_financial_year_start := MAKE_DATE(v_finance_year - 1, 4, 1);
v_prev_financial_year_end := MAKE_DATE(v_finance_year, 3, 31);
-- Adjust financial year ends if the year is ongoing
IF v_today < v_financial_year_end THEN
v_financial_year_end := v_today;
END IF;
IF v_today < v_prev_financial_year_end THEN
v_prev_financial_year_end := v_today - INTERVAL '1 year';
END IF;
-- Align both end dates to same day/month by limiting to shorter period
v_financial_year_end := LEAST(v_financial_year_end, v_prev_financial_year_end + INTERVAL '1 year');
v_prev_financial_year_end := v_financial_year_end - INTERVAL '1 year';
-- Return calculated data
RETURN QUERY
SELECT
-- Current Year Income
ROUND(COALESCE((
SELECT SUM(je.amount)
FROM journal_entries je
JOIN chart_of_accounts ca ON je.account_id = ca.id
JOIN account_types at ON ca.account_type_id = at.id
JOIN account_categories ac ON at.account_category_id = ac.id
JOIN transaction_headers tr ON je.transaction_id = tr.id
WHERE ac.id = 4
AND tr.company_id = p_company_id
AND ca.organization_id = v_organization_id
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND je.is_deleted = FALSE
), 0), 2),
-- Current Year Expense
ROUND(COALESCE((
SELECT SUM(je.amount)
FROM journal_entries je
JOIN chart_of_accounts ca ON je.account_id = ca.id
JOIN account_types at ON ca.account_type_id = at.id
JOIN account_categories ac ON at.account_category_id = ac.id
JOIN transaction_headers tr ON je.transaction_id = tr.id
WHERE ac.id = 5
AND tr.company_id = p_company_id
AND ca.organization_id = v_organization_id
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND je.is_deleted = FALSE
), 0), 2),
-- Pending Dues
ROUND(COALESCE((
SELECT SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END) -
SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END)
FROM journal_entries je
JOIN chart_of_accounts ca ON je.account_id = ca.id
JOIN account_types at ON ca.account_type_id = at.id
JOIN transaction_headers tr ON je.transaction_id = tr.id
WHERE at.name IN ('Accounts Receivable', 'Current Assets')
AND tr.company_id = p_company_id
AND ca.organization_id = v_organization_id
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND je.is_deleted = FALSE
), 0), 2),
-- Pending Payments
ROUND(COALESCE((
SELECT SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END) -
SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END)
FROM journal_entries je
JOIN chart_of_accounts ca ON je.account_id = ca.id
JOIN account_types at ON ca.account_type_id = at.id
JOIN transaction_headers tr ON je.transaction_id = tr.id
WHERE ca.account_type_id IN (SELECT id FROM get_account_type_hierarchy(2))
AND tr.company_id = p_company_id
AND ca.organization_id = v_organization_id
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND je.is_deleted = FALSE
), 0), 2),
-- Previous Year Income
ROUND(COALESCE((
SELECT SUM(je.amount)
FROM journal_entries je
JOIN chart_of_accounts ca ON je.account_id = ca.id
JOIN account_types at ON ca.account_type_id = at.id
JOIN account_categories ac ON at.account_category_id = ac.id
JOIN transaction_headers tr ON je.transaction_id = tr.id
WHERE ac.id = 4
AND tr.company_id = p_company_id
AND ca.organization_id = v_organization_id
AND je.transaction_date BETWEEN v_prev_financial_year_start AND v_prev_financial_year_end
AND je.is_deleted = FALSE
), 0), 2),
-- Previous Year Expense
ROUND(COALESCE((
SELECT SUM(je.amount)
FROM journal_entries je
JOIN chart_of_accounts ca ON je.account_id = ca.id
JOIN account_types at ON ca.account_type_id = at.id
JOIN account_categories ac ON at.account_category_id = ac.id
JOIN transaction_headers tr ON je.transaction_id = tr.id
WHERE ac.id = 5
AND tr.company_id = p_company_id
AND ca.organization_id = v_organization_id
AND je.transaction_date BETWEEN v_prev_financial_year_start AND v_prev_financial_year_end
AND je.is_deleted = FALSE
), 0), 2),
-- Previous Year Pending Dues
ROUND(COALESCE((
SELECT SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END) -
SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END)
FROM journal_entries je
JOIN chart_of_accounts ca ON je.account_id = ca.id
JOIN account_types at ON ca.account_type_id = at.id
JOIN transaction_headers tr ON je.transaction_id = tr.id
WHERE at.name IN ('Accounts Receivable', 'Current Assets')
AND tr.company_id = p_company_id
AND ca.organization_id = v_organization_id
AND je.transaction_date BETWEEN v_prev_financial_year_start AND v_prev_financial_year_end
AND je.is_deleted = FALSE
), 0), 2),
-- Previous Year Pending Payments
ROUND(COALESCE((
SELECT SUM(CASE WHEN je.entry_type = 'C' THEN je.amount ELSE 0 END) -
SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE 0 END)
FROM journal_entries je
JOIN chart_of_accounts ca ON je.account_id = ca.id
JOIN account_types at ON ca.account_type_id = at.id
JOIN transaction_headers tr ON je.transaction_id = tr.id
WHERE ca.account_type_id IN (SELECT id FROM get_account_type_hierarchy(2))
AND tr.company_id = p_company_id
AND ca.organization_id = v_organization_id
AND je.transaction_date BETWEEN v_prev_financial_year_start AND v_prev_financial_year_end
AND je.is_deleted = FALSE
), 0), 2);
END;
$function$
-- Function: get_employee_opening_balance
CREATE OR REPLACE FUNCTION public.get_employee_opening_balance(p_employee_id uuid, p_finyear_id integer)
RETURNS TABLE(transaction_date timestamp without time zone, employee_id uuid, employee_name text, description text, debit numeric, credit numeric)
LANGUAGE plpgsql
AS $function$
DECLARE
v_year_text text;
BEGIN
-- Fetch the year text for the given finance year id
SELECT year INTO v_year_text FROM public.finance_year WHERE id = p_finyear_id;
IF v_year_text IS NULL THEN
RAISE EXCEPTION 'Finance year with id % not found!', p_finyear_id;
END IF;
RETURN QUERY
SELECT
th.transaction_date,
th.employee_id,
(e.first_name || ' ' || e.last_name) AS employee_name,
th.description,
COALESCE(CASE WHEN je.entry_type = 'D' THEN je.amount END, 0) AS debit,
COALESCE(CASE WHEN je.entry_type = 'C' THEN je.amount END, 0) AS credit
FROM public.transaction_headers th
JOIN public.employees e ON th.employee_id = e.id
JOIN public.journal_entries je ON th.id = je.transaction_id
JOIN public.chart_of_accounts sp ON sp.name ILIKE '%Salary Payable%'
WHERE th.employee_id = p_employee_id
AND je.account_id = sp.id
AND th.transaction_source_type = 13
AND th.description LIKE 'Opening Balance for %' || v_year_text || '%'
ORDER BY th.transaction_date;
END;
$function$
-- Function: get_expense_monthly_data
CREATE OR REPLACE FUNCTION public.get_expense_monthly_data(p_company_id uuid, p_finance_year_id integer)
RETURNS TABLE(account_name text, year integer, apr numeric, may numeric, jun numeric, jul numeric, aug numeric, sep numeric, oct numeric, nov numeric, "dec" numeric, jan numeric, feb numeric, mar numeric, total numeric, difference numeric)
LANGUAGE plpgsql
AS $function$
DECLARE
v_organization_id UUID;
v_start_date DATE;
v_end_date DATE;
v_prev_start_date DATE;
v_prev_end_date DATE;
BEGIN
-- Get the organization_id for the given company_id
SELECT organization_id INTO v_organization_id
FROM public.companies
WHERE id = p_company_id;
-- Get the start and end dates for the selected financial year
SELECT start_date, end_date INTO v_start_date, v_end_date
FROM public.finance_year
WHERE id = p_finance_year_id;
-- Get the start and end dates for the previous financial year
SELECT start_date, end_date INTO v_prev_start_date, v_prev_end_date
FROM public.finance_year
WHERE id = (p_finance_year_id - 1); -- Fetch previous year finance data
RETURN QUERY
WITH expense_accounts AS (
-- Fetch all expense accounts
SELECT coa.id, coa.name
FROM chart_of_accounts coa
JOIN account_types at ON coa.account_type_id = at.id
WHERE at.id IN (SELECT id FROM get_account_type_hierarchy(5))
AND coa.organization_id = v_organization_id
),
monthly_expenses AS (
-- Get monthly totals for the selected and previous financial years
SELECT
je.account_id,
CASE
WHEN je.transaction_date BETWEEN v_start_date AND v_end_date THEN p_finance_year_id
WHEN je.transaction_date BETWEEN v_prev_start_date AND v_prev_end_date THEN (p_finance_year_id - 1)
END AS year,
EXTRACT(MONTH FROM je.transaction_date) AS month,
SUM(je.amount) AS amount
FROM journal_entries je
JOIN transaction_headers th ON je.transaction_id = th.id
WHERE th.company_id = p_company_id
AND je.transaction_date BETWEEN v_prev_start_date AND v_end_date -- Filter for both years
GROUP BY je.account_id, year, EXTRACT(MONTH FROM je.transaction_date)
),
pivoted_expenses AS (
-- Pivoting data to month-wise columns
SELECT
ea.name AS account_name,
me.year,
COALESCE(SUM(CASE WHEN me.month = 4 THEN me.amount ELSE 0 END), 0) AS apr,
COALESCE(SUM(CASE WHEN me.month = 5 THEN me.amount ELSE 0 END), 0) AS may,
COALESCE(SUM(CASE WHEN me.month = 6 THEN me.amount ELSE 0 END), 0) AS jun,
COALESCE(SUM(CASE WHEN me.month = 7 THEN me.amount ELSE 0 END), 0) AS jul,
COALESCE(SUM(CASE WHEN me.month = 8 THEN me.amount ELSE 0 END), 0) AS aug,
COALESCE(SUM(CASE WHEN me.month = 9 THEN me.amount ELSE 0 END), 0) AS sep,
COALESCE(SUM(CASE WHEN me.month = 10 THEN me.amount ELSE 0 END), 0) AS oct,
COALESCE(SUM(CASE WHEN me.month = 11 THEN me.amount ELSE 0 END), 0) AS nov,
COALESCE(SUM(CASE WHEN me.month = 12 THEN me.amount ELSE 0 END), 0) AS "dec",
COALESCE(SUM(CASE WHEN me.month = 1 THEN me.amount ELSE 0 END), 0) AS jan,
COALESCE(SUM(CASE WHEN me.month = 2 THEN me.amount ELSE 0 END), 0) AS feb,
COALESCE(SUM(CASE WHEN me.month = 3 THEN me.amount ELSE 0 END), 0) AS mar,
SUM(me.amount) AS total
FROM expense_accounts ea
JOIN monthly_expenses me ON ea.id = me.account_id
WHERE me.year IN (p_finance_year_id, p_finance_year_id - 1)
GROUP BY ea.name, me.year
)
-- Final table with the difference calculation
SELECT
pe.account_name,
pe.year,
pe.apr, pe.may, pe.jun, pe.jul, pe.aug, pe.sep, pe.oct, pe.nov, pe.dec, pe.jan, pe.feb, pe.mar,
pe.total,
COALESCE(pe.total - LAG(pe.total) OVER (PARTITION BY pe.account_name ORDER BY pe.year), 0) AS difference
FROM pivoted_expenses pe
ORDER BY pe.account_name, pe.year;
END;
$function$
-- Function: get_expense_overview
CREATE OR REPLACE FUNCTION public.get_expense_overview(p_company_id uuid, p_period_type integer, p_finance_id integer)
RETURNS TABLE(period text, year numeric, account_name text, total_expense numeric)
LANGUAGE plpgsql
AS $function$
DECLARE
v_financial_year_start date;
v_financial_year_end date;
v_finance_year text;
v_organization_id uuid;
-- Define constants for period types
CONST_MONTHLY CONSTANT INTEGER := 1;
CONST_QUARTERLY CONSTANT INTEGER := 2;
CONST_HALF_YEARLY CONSTANT INTEGER := 3;
CONST_YEARLY CONSTANT INTEGER := 4;
BEGIN
-- Step 1: Fetch the organization ID based on the company ID
SELECT organization_id INTO v_organization_id
FROM public.companies
WHERE id = p_company_id;
-- Step 2: Get financial year boundaries
SELECT fy.start_date, fy.end_date, fy.year INTO v_financial_year_start, v_financial_year_end, v_finance_year
FROM public.finance_year fy
WHERE id = p_finance_id;
-- Check the period type and execute the appropriate logic
IF p_period_type = CONST_MONTHLY THEN
RETURN QUERY
WITH RECURSIVE AccountHierarchy AS (
-- Base case: Select root accounts (expense accounts)
SELECT
coa.id AS account_id,
coa.name AS account_name,
coa.account_number,
coa.parent_account_id
FROM
public.chart_of_accounts coa
WHERE
coa.account_type_id = 5 -- Expense accounts
AND coa.organization_id = v_organization_id
UNION ALL
-- Recursive case: Select child accounts under the root accounts
SELECT
coa.id AS account_id,
coa.name AS account_name,
coa.account_number,
coa.parent_account_id
FROM
public.chart_of_accounts coa
INNER JOIN
AccountHierarchy ah ON coa.parent_account_id = ah.account_id
),
months AS (
SELECT
TO_CHAR(months.m, 'Mon') AS period,
EXTRACT(YEAR FROM months.m) AS extracted_year,
EXTRACT(MONTH FROM months.m) AS month_num
FROM
generate_series(v_financial_year_start, v_financial_year_end, '1 month'::interval) AS months(m)
)
SELECT
months.period,
months.extracted_year AS year,
ah.account_name,
COALESCE(SUM(je.amount), 0) AS total_expense
FROM
months
INNER JOIN journal_entries je
ON EXTRACT(MONTH FROM je.transaction_date) = months.month_num
AND EXTRACT(YEAR FROM je.transaction_date) = months.extracted_year
INNER JOIN transaction_headers tr
ON je.transaction_id = tr.id
AND tr.company_id = p_company_id -- Ensure filtering by company_id
INNER JOIN AccountHierarchy ah
ON je.account_id = ah.account_id
WHERE
je.entry_type = 'D' -- Only debits are considered expenses
AND je.is_deleted = FALSE -- Filter non-deleted entries
GROUP BY
months.period, months.extracted_year, months.month_num, ah.account_name
ORDER BY
months.extracted_year, months.month_num, ah.account_name;
ELSIF p_period_type = CONST_QUARTERLY THEN
RETURN QUERY
WITH RECURSIVE AccountHierarchy AS (
-- Base case: Select root accounts (expense accounts)
SELECT
coa.id AS account_id,
coa.name AS account_name,
coa.account_number,
coa.parent_account_id
FROM
public.chart_of_accounts coa
WHERE
coa.account_type_id = 5 -- Expense accounts
AND coa.organization_id = v_organization_id
UNION ALL
-- Recursive case: Select child accounts under the root accounts
SELECT
coa.id AS account_id,
coa.name AS account_name,
coa.account_number,
coa.parent_account_id
FROM
public.chart_of_accounts coa
INNER JOIN
AccountHierarchy ah ON coa.parent_account_id = ah.account_id
),
quarters AS (
SELECT
'Q1' AS period, 1 AS quarter_num, EXTRACT(YEAR FROM v_financial_year_start) AS extracted_year,
v_financial_year_start AS start_date, v_financial_year_start + interval '2 month' AS end_date
UNION ALL
SELECT
'Q2' AS period, 2 AS quarter_num, EXTRACT(YEAR FROM v_financial_year_start) AS extracted_year,
v_financial_year_start + interval '3 month' AS start_date, v_financial_year_start + interval '5 month' AS end_date
UNION ALL
SELECT
'Q3' AS period, 3 AS quarter_num, EXTRACT(YEAR FROM v_financial_year_start) AS extracted_year,
v_financial_year_start + interval '6 month' AS start_date, v_financial_year_start + interval '8 month' AS end_date
UNION ALL
SELECT
'Q4' AS period, 4 AS quarter_num, EXTRACT(YEAR FROM v_financial_year_start) + 1 AS extracted_year,
v_financial_year_start + interval '9 month' AS start_date, v_financial_year_end AS end_date
)
SELECT
quarters.period,
quarters.extracted_year AS year,
ah.account_name,
COALESCE(SUM(je.amount), 0) AS total_expense
FROM
quarters
LEFT JOIN journal_entries je
ON je.transaction_date BETWEEN quarters.start_date AND quarters.end_date
LEFT JOIN transaction_headers tr
ON je.transaction_id = tr.id
AND tr.company_id = p_company_id -- Ensure filtering by company_id
INNER JOIN AccountHierarchy ah
ON je.account_id = ah.account_id
WHERE
je.entry_type = 'D' -- Only debits are considered expenses
AND je.is_deleted = FALSE -- Filter non-deleted entries
GROUP BY
quarters.period, quarters.extracted_year, quarters.quarter_num, ah.account_name
ORDER BY
quarters.extracted_year, quarters.quarter_num, ah.account_name;
-- Repeat similar adjustments for Half-Yearly and Yearly periods.
ELSIF p_period_type = CONST_HALF_YEARLY THEN
RETURN QUERY
WITH RECURSIVE AccountHierarchy AS (
-- Base case: Select root accounts (expense accounts)
SELECT
coa.id AS account_id,
coa.name AS account_name,
coa.account_number,
coa.parent_account_id
FROM
public.chart_of_accounts coa
WHERE
coa.account_type_id = 5 -- Expense accounts
AND coa.organization_id = v_organization_id
UNION ALL
-- Recursive case: Select child accounts under the root accounts
SELECT
coa.id AS account_id,
coa.name AS account_name,
coa.account_number,
coa.parent_account_id
FROM
public.chart_of_accounts coa
INNER JOIN
AccountHierarchy ah ON coa.parent_account_id = ah.account_id
),
half_years AS (
SELECT
'H1' AS period,
1 AS half_year_num,
EXTRACT(YEAR FROM v_financial_year_start) AS extracted_year,
v_financial_year_start AS start_date,
v_financial_year_start + interval '5 month' AS end_date
UNION ALL
SELECT
'H2' AS period,
2 AS half_year_num,
EXTRACT(YEAR FROM v_financial_year_start) AS extracted_year,
v_financial_year_start + interval '6 month' AS start_date,
v_financial_year_end AS end_date
)
SELECT
half_years.period,
half_years.extracted_year AS year,
ah.account_name,
COALESCE(SUM(je.amount), 0) AS total_expense
FROM
half_years
LEFT JOIN journal_entries je
ON je.transaction_date BETWEEN half_years.start_date AND half_years.end_date
LEFT JOIN transaction_headers tr
ON je.transaction_id = tr.id
AND tr.company_id = p_company_id -- Ensure filtering by company_id
INNER JOIN AccountHierarchy ah
ON je.account_id = ah.account_id
WHERE
je.entry_type = 'D' -- Only debits are considered expenses
AND je.is_deleted = FALSE -- Filter non-deleted entries
GROUP BY
half_years.period, half_years.extracted_year, half_years.half_year_num, ah.account_name
ORDER BY
half_years.extracted_year, half_years.half_year_num, ah.account_name;
ELSIF p_period_type = CONST_YEARLY THEN
RETURN QUERY
WITH RECURSIVE AccountHierarchy AS (
-- Base case: Select root accounts (expense accounts)
SELECT
coa.id AS account_id,
coa.name AS account_name,
coa.account_number,
coa.parent_account_id
FROM
public.chart_of_accounts coa
WHERE
coa.account_type_id = 5 -- Expense accounts
AND coa.organization_id = v_organization_id
UNION ALL
-- Recursive case: Select child accounts under the root accounts
SELECT
coa.id AS account_id,
coa.name AS account_name,
coa.account_number,
coa.parent_account_id
FROM
public.chart_of_accounts coa
INNER JOIN
AccountHierarchy ah ON coa.parent_account_id = ah.account_id
)
SELECT
'Year' AS period,
EXTRACT(YEAR FROM v_financial_year_start) AS year,
ah.account_name,
COALESCE(SUM(je.amount), 0) AS total_expense
FROM
journal_entries je
LEFT JOIN transaction_headers tr
ON je.transaction_id = tr.id
AND tr.company_id = p_company_id -- Ensure filtering by company_id
INNER JOIN AccountHierarchy ah
ON je.account_id = ah.account_id
WHERE
je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND je.entry_type = 'D' -- Only debits are considered expenses
AND je.is_deleted = FALSE -- Filter non-deleted entries
GROUP BY
year, ah.account_name
ORDER BY
year, ah.account_name;
END IF;
END;
$function$
-- Function: get_financial_year_dates
CREATE OR REPLACE FUNCTION public.get_financial_year_dates(p_fin_year_id integer)
RETURNS TABLE(start_date date, end_date date)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT fy.start_date::DATE, fy.end_date::DATE
FROM public.finance_year fy
WHERE fy.id = p_fin_year_id
LIMIT 1;
END;
$function$
-- Function: get_new_voucher_number
CREATE OR REPLACE FUNCTION public.get_new_voucher_number(p_company_id uuid, p_date date)
RETURNS character varying
LANGUAGE plpgsql
AS $function$
DECLARE
v_finance_year_id integer;
v_voucher_prefix text;
v_last_voucher_id integer;
v_new_voucher_number character varying;
BEGIN
-- Get the financial year ID based on the provided date
SELECT id INTO v_finance_year_id
FROM finance_year
WHERE start_date <= p_date AND end_date >= p_date;
-- Try updating existing record
WITH updated AS (
UPDATE public.journal_voucher_header_id
SET last_voucher_id = COALESCE(last_voucher_id, 0) + 1
WHERE company_id = p_company_id AND fin_year = v_finance_year_id
RETURNING last_voucher_id, voucher_prefix
),
inserted AS (
INSERT INTO public.journal_voucher_header_id (
company_id, fin_year, voucher_prefix, last_voucher_id
)
SELECT p_company_id, v_finance_year_id, 'JV', 1
WHERE NOT EXISTS (SELECT 1 FROM updated)
RETURNING last_voucher_id, voucher_prefix
)
SELECT
COALESCE((SELECT last_voucher_id FROM updated LIMIT 1), (SELECT last_voucher_id FROM inserted LIMIT 1)),
COALESCE((SELECT voucher_prefix FROM updated LIMIT 1), (SELECT voucher_prefix FROM inserted LIMIT 1))
INTO v_last_voucher_id, v_voucher_prefix;
-- Generate the voucher number
v_new_voucher_number := v_voucher_prefix || LPAD(v_last_voucher_id::text, 5, '0');
RETURN v_new_voucher_number;
END;
$function$
-- Function: get_full_account_hierarchy_overview
CREATE OR REPLACE FUNCTION public.get_full_account_hierarchy_overview(p_company_id uuid, p_fin_year_id integer)
RETURNS TABLE(account_id uuid, account_name text, parent_account_id uuid, hierarchy_path text[], financial_year integer, apr numeric, may numeric, jun numeric, jul numeric, aug numeric, sep numeric, oct numeric, nov numeric, "dec" numeric, jan numeric, feb numeric, mar numeric, total_sum numeric, difference numeric)
LANGUAGE plpgsql
AS $function$
#variable_conflict use_column
DECLARE
v_current_year INTEGER := p_fin_year_id;
v_previous_year INTEGER := p_fin_year_id - 1;
v_organization_id UUID;
BEGIN
SELECT c.organization_id INTO v_organization_id
FROM public.companies c
WHERE c.id = p_company_id;
RETURN QUERY
WITH RECURSIVE account_with_path AS (
-- This is a much safer way to build the path
SELECT
ca.id,
ca.name,
ca.parent_account_id,
-- If parent is missing or is self, start a new path. Otherwise, build the path.
COALESCE(p.name, '') || '/' || ca.name AS path_str
FROM chart_of_accounts ca
LEFT JOIN chart_of_accounts p ON ca.parent_account_id = p.id AND ca.parent_account_id != ca.id
WHERE ca.organization_id = v_organization_id
),
financial_years AS (
SELECT id AS fin_year_id, start_date, end_date
FROM finance_year
WHERE id IN (v_previous_year, v_current_year)
),
aggregated_data AS (
SELECT
awp.id, awp.name, awp.parent_account_id,
-- Convert the path string to an array for AG Grid
string_to_array(ltrim(awp.path_str, '/'), '/') AS hierarchy_path,
fy.fin_year_id,
COALESCE(SUM(je.amount * CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 4 THEN 1 ELSE 0 END), 0) AS apr,
COALESCE(SUM(je.amount * CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 5 THEN 1 ELSE 0 END), 0) AS may,
COALESCE(SUM(je.amount * CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 6 THEN 1 ELSE 0 END), 0) AS jun,
COALESCE(SUM(je.amount * CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 7 THEN 1 ELSE 0 END), 0) AS jul,
COALESCE(SUM(je.amount * CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 8 THEN 1 ELSE 0 END), 0) AS aug,
COALESCE(SUM(je.amount * CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 9 THEN 1 ELSE 0 END), 0) AS sep,
COALESCE(SUM(je.amount * CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 10 THEN 1 ELSE 0 END), 0) AS oct,
COALESCE(SUM(je.amount * CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 11 THEN 1 ELSE 0 END), 0) AS nov,
COALESCE(SUM(je.amount * CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 12 THEN 1 ELSE 0 END), 0) AS "dec",
COALESCE(SUM(je.amount * CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 1 THEN 1 ELSE 0 END), 0) AS jan,
COALESCE(SUM(je.amount * CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 2 THEN 1 ELSE 0 END), 0) AS feb,
COALESCE(SUM(je.amount * CASE WHEN EXTRACT(MONTH FROM je.transaction_date) = 3 THEN 1 ELSE 0 END), 0) AS mar,
COALESCE(SUM(je.amount), 0) AS total_sum
FROM account_with_path awp
CROSS JOIN financial_years fy
LEFT JOIN journal_entries je ON je.account_id = awp.id AND je.transaction_date BETWEEN fy.start_date AND fy.end_date
LEFT JOIN transaction_headers th ON th.id = je.transaction_id AND th.company_id = p_company_id
WHERE (th.transaction_source_type IS NULL OR th.transaction_source_type != 13)
GROUP BY awp.id, awp.name, awp.parent_account_id, awp.path_str, fy.fin_year_id
)
SELECT
a1.id, a1.name, a1.parent_account_id, a1.hierarchy_path, a1.fin_year_id,
a1.apr, a1.may, a1.jun, a1.jul, a1.aug, a1.sep, a1.oct, a1.nov, a1."dec",
a1.jan, a1.feb, a1.mar,
a1.total_sum,
(COALESCE(a1.total_sum, 0) - COALESCE(a2.total_sum, 0)) AS difference
FROM aggregated_data a1
LEFT JOIN aggregated_data a2 ON a1.id = a2.id AND a1.fin_year_id = v_current_year AND a2.fin_year_id = v_previous_year
ORDER BY a1.hierarchy_path, a1.fin_year_id;
END;
$function$
-- Function: get_income_expense_monthly_hierarchy
CREATE OR REPLACE FUNCTION public.get_income_expense_monthly_hierarchy(p_company_id uuid, p_finance_year_id integer)
RETURNS jsonb
LANGUAGE plpgsql
AS $function$
DECLARE
v_organization_id UUID;
v_start_date DATE;
v_end_date DATE;
v_prev_start_date DATE;
v_prev_end_date DATE;
result JSONB;
BEGIN
-- Get the organization_id for the given company_id
SELECT c.organization_id INTO v_organization_id
FROM public.companies c
WHERE c.id = p_company_id;
-- Get the start and end dates for the selected financial year
SELECT fy.start_date, fy.end_date INTO v_start_date, v_end_date
FROM public.finance_year fy
WHERE fy.id = p_finance_year_id;
-- Get the start and end dates for the previous financial year
SELECT pfy.start_date, pfy.end_date INTO v_prev_start_date, v_prev_end_date
FROM public.finance_year pfy
WHERE pfy.id = (p_finance_year_id - 1);
-- Recursive CTE to build parent-child hierarchy
WITH RECURSIVE account_hierarchy AS (
-- Base case: Fetch top-level accounts
SELECT
coa.id,
coa.name AS account_name,
coa.parent_account_id,
0 AS level,
at.name AS account_type
FROM chart_of_accounts coa
JOIN account_types at ON coa.account_type_id = at.id
WHERE coa.organization_id = v_organization_id
AND at.name IN ('Revenue', 'Expenses')
AND (coa.parent_account_id IS NULL OR coa.parent_account_id = '00000000-0000-0000-0000-000000000000')
AND coa.is_deleted = FALSE
UNION ALL
-- Recursive case: Fetch children accounts
SELECT
coa.id,
coa.name AS account_name,
coa.parent_account_id,
parent.level + 1 AS level,
parent.account_type
FROM chart_of_accounts coa
JOIN account_hierarchy parent ON parent.id = coa.parent_account_id
WHERE coa.organization_id = v_organization_id
AND coa.is_deleted = FALSE
),
monthly_totals AS (
-- Get monthly totals
SELECT
je.account_id AS id,
SUM(je.amount) AS total
FROM journal_entries je
JOIN transaction_headers th ON je.transaction_id = th.id
WHERE th.company_id = p_company_id
AND je.transaction_date BETWEEN v_prev_start_date AND v_end_date
GROUP BY je.account_id
),
pivoted_data AS (
-- Join accounts with totals
SELECT
ah.id,
ah.account_type,
ah.account_name,
ah.parent_account_id,
ah.level,
COALESCE(mt.total, 0) AS total
FROM account_hierarchy ah
LEFT JOIN monthly_totals mt ON ah.id = mt.id
),
full_hierarchy AS (
-- Ensure all accounts are present in JSON format
SELECT
p.id,
p.account_name,
p.account_type,
p.level,
p.total,
jsonb_agg(
jsonb_build_object(
'id', c.id,
'account_name', c.account_name,
'account_type', c.account_type,
'level', c.level,
'total', c.total,
'children', '[]'::jsonb -- Placeholder for child accounts
)
) FILTER (WHERE c.id IS NOT NULL) AS children
FROM pivoted_data p
LEFT JOIN pivoted_data c ON p.id = c.parent_account_id
GROUP BY p.id, p.account_name, p.account_type, p.level, p.total
)
-- Aggregate only the **top-level accounts** into JSON
SELECT jsonb_agg(full_hierarchy.*) INTO result FROM full_hierarchy WHERE full_hierarchy.level = 0;
RETURN result;
END;
$function$
-- Function: get_income_expense_ytd_all_years
CREATE OR REPLACE FUNCTION public.get_income_expense_ytd_all_years(p_company_id uuid, p_years_back integer)
RETURNS TABLE(period text, year numeric, total_income numeric, total_expense numeric, actual_income numeric, actual_expense numeric)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
WITH target_years AS (
SELECT id, start_date, end_date,
EXTRACT(YEAR FROM start_date) AS year_start,
EXTRACT(YEAR FROM end_date) AS year_end
FROM finance_year
WHERE start_date <= CURRENT_DATE
ORDER BY start_date DESC
LIMIT p_years_back
),
ytd_periods AS (
SELECT
fy.id AS finance_id,
'YTD H1' AS period,
make_date(fy.year_start::int, 9, 30) AS period_end,
fy.year_start AS year,
fy.start_date AS year_start,
fy.end_date AS year_end
FROM target_years fy
UNION ALL
SELECT
fy.id,
'YTD H2',
fy.end_date,
fy.year_end,
fy.start_date,
fy.end_date
FROM target_years fy
),
filtered_je AS (
SELECT
je.id,
je.account_id,
je.transaction_id,
je.entry_type,
je.amount,
je.is_deleted,
tr.company_id,
tr.transaction_date,
coa.account_type_id
FROM journal_entries je
JOIN transaction_headers tr ON je.transaction_id = tr.id
JOIN chart_of_accounts coa ON je.account_id = coa.id
WHERE je.is_deleted = FALSE
)
SELECT
CONCAT(yp.period, ' ', yp.year) AS period,
yp.year,
COALESCE(SUM(CASE WHEN fje.entry_type = 'C' AND fje.account_type_id IN (4,14,15,19,20)
AND fje.transaction_date BETWEEN yp.year_start AND yp.period_end THEN fje.amount ELSE 0 END), 0),
COALESCE(SUM(CASE WHEN fje.entry_type = 'D' AND fje.account_type_id IN (5,16,17,18,21,22)
AND fje.transaction_date BETWEEN yp.year_start AND yp.period_end THEN fje.amount ELSE 0 END), 0),
COALESCE(SUM(CASE WHEN fje.entry_type = 'D' AND fje.account_type_id IN (8,9)
AND fje.transaction_date BETWEEN yp.year_start AND yp.period_end THEN fje.amount ELSE 0 END), 0),
COALESCE(SUM(CASE WHEN fje.entry_type = 'C' AND fje.account_type_id IN (8,9)
AND fje.transaction_date BETWEEN yp.year_start AND yp.period_end THEN fje.amount ELSE 0 END), 0)
FROM ytd_periods yp
LEFT JOIN filtered_je fje ON fje.company_id = p_company_id
GROUP BY yp.period, yp.year, yp.period_end
ORDER BY yp.year, yp.period;
END;
$function$
-- Function: get_total_income
CREATE OR REPLACE FUNCTION public.get_total_income(p_company_id uuid, p_finance_id integer)
RETURNS numeric
LANGUAGE plpgsql
AS $function$
DECLARE
v_financial_year_start DATE;
v_financial_year_end DATE;
v_total_income NUMERIC := 0;
v_organization_id UUID;
BEGIN
-- Step 1: Get the organization ID for the given company
SELECT organization_id
INTO v_organization_id
FROM public.companies
WHERE id = p_company_id;
-- Step 2: Validate if the organization ID exists
IF v_organization_id IS NULL THEN
RAISE EXCEPTION 'Organization ID not found for company ID %', p_company_id;
END IF;
-- Step 3: Get the financial year start and end dates
SELECT fy.start_date, fy.end_date
INTO v_financial_year_start, v_financial_year_end
FROM public.finance_year fy
WHERE fy.id = p_finance_id;
-- Step 4: Validate if the financial year exists
IF v_financial_year_start IS NULL OR v_financial_year_end IS NULL THEN
RAISE EXCEPTION 'Financial year with ID % not found', p_finance_id;
END IF;
-- Step 5: Calculate the total income
SELECT COALESCE(SUM(je.amount), 0) INTO v_total_income
FROM public.journal_entries je
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
INNER JOIN public.chart_of_accounts coa ON je.account_id = coa.id
WHERE th.company_id = p_company_id
AND coa.organization_id = v_organization_id -- Match organization ID
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND je.is_deleted = FALSE
AND EXISTS (
SELECT 1
FROM public.account_types at
WHERE at.id = coa.account_type_id
AND at.account_category_id = 4 -- Income/Revenue category
);
-- Step 6: Return the total income
RETURN v_total_income;
END;
$function$
-- Function: get_top_expense_categories
CREATE OR REPLACE FUNCTION public.get_top_expense_categories(p_company_id uuid, p_finance_year_id integer)
RETURNS TABLE(account_id uuid, account_name text, account_number text, total_expense numeric)
LANGUAGE plpgsql
AS $function$
DECLARE
v_financial_year_start DATE;
v_financial_year_end DATE;
BEGIN
-- Fetch financial year start and end dates
SELECT start_date, end_date
INTO v_financial_year_start, v_financial_year_end
FROM public.finance_year
WHERE id = p_finance_year_id;
-- Raise an exception if financial year is not found
IF v_financial_year_start IS NULL OR v_financial_year_end IS NULL THEN
RAISE EXCEPTION 'Financial year with ID % not found', p_finance_year_id;
END IF;
-- Calculate and return top 10 Expense accounts using `get_account_type_hierarchy(5)`
RETURN QUERY
SELECT
coa.id As account_id,
coa.name AS account_name,
coa.account_number AS account_number,
ROUND(COALESCE(SUM(je.amount), 0), 2) AS total_expense
FROM public.journal_entries je
INNER JOIN public.chart_of_accounts coa ON je.account_id = coa.id
INNER JOIN public.account_types at ON coa.account_type_id = at.id
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
WHERE at.id IN (SELECT id FROM get_account_type_hierarchy(5)) -- Dynamically fetch all expense types
AND th.company_id = p_company_id
AND je.transaction_date BETWEEN v_financial_year_start AND v_financial_year_end
AND je.is_deleted = FALSE
GROUP BY coa.id, coa.name, coa.account_number
ORDER BY total_expense DESC
LIMIT 10;
END;
$function$
-- Function: get_ledger_report
CREATE OR REPLACE FUNCTION public.get_ledger_report(p_company_id uuid, p_fin_year_id integer, p_start_date date DEFAULT NULL::date, p_end_date date DEFAULT NULL::date)
RETURNS TABLE(sl_no integer, ledger text, general_ledger text, opening_balance numeric, total_debit numeric, total_credit numeric, closing_balance numeric, parent_ledger text)
LANGUAGE plpgsql
AS $function$
DECLARE
v_start_date DATE;
v_end_date DATE;
BEGIN
-- Get financial year start and end date if not provided
SELECT fy.start_date, fy.end_date
INTO v_start_date, v_end_date
FROM public.finance_year fy
WHERE fy.id = p_fin_year_id
LIMIT 1;
-- Override with provided values if they are not NULL
v_start_date := COALESCE(p_start_date, v_start_date);
v_end_date := COALESCE(p_end_date, v_end_date);
RETURN QUERY
WITH trial_balance AS (
SELECT
account_type AS account_category,
account_number,
account_name,
debit,
credit
FROM public.get_trial_balance_by_date_range(
p_company_id, p_fin_year_id, v_start_date, v_end_date
)
),
opening_balances AS (
SELECT
je.account_id::TEXT AS account_id,
SUM(CASE WHEN je.entry_type = 'D' THEN je.amount ELSE -je.amount END) AS opening_balance
FROM public.journal_entries je
INNER JOIN public.transaction_headers th ON je.transaction_id = th.id
WHERE th.company_id = p_company_id
AND th.transaction_date BETWEEN v_start_date AND v_end_date
GROUP BY je.account_id
)
-- Main Trial Balance Rows
SELECT
CAST(ROW_NUMBER() OVER() AS INTEGER) AS sl_no,
tb.account_name AS ledger,
tb.account_category AS general_ledger,
COALESCE(ob.opening_balance, 0) AS opening_balance,
COALESCE(tb.debit, 0) AS total_debit,
COALESCE(tb.credit, 0) AS total_credit,
COALESCE(ob.opening_balance, 0) + COALESCE(tb.debit, 0) - COALESCE(tb.credit, 0) AS closing_balance,
CASE
WHEN tb.account_category IN ('Assets', 'Current Assets', 'Non-Current Assets', 'Cash', 'Bank') THEN 'Asset'
WHEN tb.account_category IN ('Liabilities', 'Current Liabilities', 'Non-Current Liabilities') THEN 'Liability'
WHEN tb.account_category IN ('Revenue', 'Sales Revenue', 'Service Revenue', 'Non Member Income', 'Member Income') THEN 'Income'
WHEN tb.account_category IN ('Expenses', 'Direct Expenses', 'Indirect Expenses', 'Operating Expenses', 'Cost of Goods Sold', 'Non-Operating Expense') THEN 'Expense'
ELSE NULL
END AS parent_ledger
FROM trial_balance tb
LEFT JOIN opening_balances ob ON tb.account_number::TEXT = ob.account_id
UNION ALL
-- Sundry Debtors Parent Row
SELECT 9999, 'Sundry Debtors', 'Assets', 0, 0, 0, 0, 'Asset'
UNION ALL
-- Sundry Debtors Children
SELECT
sd.sl_no + 10000,
sd.ledger,
sd.general_ledger,
sd.opening_balance,
sd.debit AS total_debit,
sd.credit AS total_credit,
sd.closing_balance,
'Asset'
FROM public.get_sundry_debtors_ledger(p_company_id, p_fin_year_id, v_start_date, v_end_date) sd
UNION ALL
-- Sundry Creditors Parent Row
SELECT 9998, 'Sundry Creditors', 'Liabilities', 0, 0, 0, 0, 'Liability'
UNION ALL
-- Sundry Creditors Children
SELECT
sc.sl_no + 20000,
sc.ledger,
sc.general_ledger,
sc.opening_balance,
sc.debit AS total_debit,
sc.credit AS total_credit,
sc.closing_balance,
'Liability'
FROM public.get_sundry_creditors_ledger(p_company_id, p_fin_year_id, v_start_date, v_end_date) sc
ORDER BY parent_ledger NULLS FIRST, ledger;
END;
$function$
-- Function: get_maintenance_collection_status
CREATE OR REPLACE FUNCTION public.get_maintenance_collection_status(p_company_id uuid, p_start_date date, p_end_date date)
RETURNS TABLE(year numeric, total_income numeric, total_expense numeric)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
EXTRACT(YEAR FROM je.transaction_date) AS year,
SUM(CASE
WHEN ac.id = 4 THEN je.amount -- 4 corresponds to 'Revenue'
ELSE 0
END) AS total_income,
SUM(CASE
WHEN ac.id = 5 THEN je.amount -- 5 corresponds to 'Expenses'
ELSE 0
END) AS total_expense
FROM
journal_entries je
JOIN
chart_of_accounts coa ON je.account_id = coa.id
JOIN
account_types act ON coa.account_type_id = act.id
JOIN
account_categories ac ON act.account_category_id = ac.id
WHERE
je.transaction_date BETWEEN p_start_date AND p_end_date
And je.company_id = p_company_id
GROUP BY
year
ORDER BY
year ;
END;
$function$
-- Function: get_n_years_expenses
CREATE OR REPLACE FUNCTION public.get_n_years_expenses(p_company_id uuid, p_fin_year_ids integer[])
RETURNS TABLE(account_id uuid, account_name text, finance_year_id integer, finance_year_label text, month_number integer, month_label text, amount numeric)
LANGUAGE plpgsql
STABLE PARALLEL SAFE
AS $function$
DECLARE
months_arr TEXT[] := ARRAY['Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec','Jan','Feb','Mar'];
yrec RECORD;
m INT;
month_start DATE;
month_end DATE;
BEGIN
FOR yrec IN
SELECT
fy.id,
fy.start_date,
fy.end_date,
CONCAT(EXTRACT(YEAR FROM fy.start_date), '-', RIGHT(TO_CHAR(fy.end_date, 'YYYY'), 2)) AS label
FROM public.finance_year fy
WHERE fy.id = ANY(p_fin_year_ids)
ORDER BY fy.start_date
LOOP
FOR m IN 1..12 LOOP
month_start := yrec.start_date + ((m - 1) * INTERVAL '1 month');
month_end := (month_start + INTERVAL '1 month' - INTERVAL '1 day')::DATE;
RETURN QUERY
WITH RECURSIVE AccountHierarchy AS (
SELECT coa.id, coa.name, coa.parent_account_id
FROM public.chart_of_accounts coa
WHERE coa.account_type_id = 5
UNION ALL
SELECT coa.id, coa.name, coa.parent_account_id
FROM public.chart_of_accounts coa
INNER JOIN AccountHierarchy ah ON coa.parent_account_id = ah.id
)
SELECT
ah.id AS account_id,
ah.name AS account_name,
yrec.id AS finance_year_id,
yrec.label AS finance_year_label,
m AS month_number,
months_arr[m] AS month_label,
COALESCE(SUM(je.amount), 0) AS amount
FROM AccountHierarchy ah
LEFT JOIN public.journal_entries je
ON je.account_id = ah.id
AND je.transaction_date BETWEEN month_start AND month_end
AND je.is_deleted = FALSE
LEFT JOIN public.transaction_headers th
ON je.transaction_id = th.id
AND th.company_id = p_company_id
GROUP BY ah.id, ah.name;
END LOOP;
END LOOP;
END;
$function$
-- Function: get_user_notifications
CREATE OR REPLACE FUNCTION public.get_user_notifications(p_user_id uuid)
RETURNS TABLE(id integer, message text, short_desc text, route text, created_time timestamp with time zone, additional_parameter_id text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
nt.id AS iid,
nt.message,
nt.short_description AS short_desc,
nt.route,
m.created_time::timestamp with time zone,
m.additional_parameter_id
FROM
public.notification_types nt
LEFT JOIN
public.messages m ON m.notification_type_id = nt.id
WHERE
nt.user_id = p_user_id
AND nt.created_on_utc >= CURRENT_TIMESTAMP - INTERVAL '24 hours'
AND nt.is_deleted = false
AND m.is_deleted = false
ORDER BY
m.created_time DESC;
END;
$function$
-- Function: get_new_transfer_number
CREATE OR REPLACE FUNCTION public.get_new_transfer_number(p_company_id uuid, p_date date)
RETURNS character varying
LANGUAGE plpgsql
AS $function$
DECLARE
v_finance_year_id integer;
v_transfer_prefix text;
v_last_transfer_id integer;
v_new_transfer_number character varying;
BEGIN
-- Determine financial year for the given date
SELECT id INTO v_finance_year_id
FROM finance_year
WHERE start_date <= p_date AND end_date >= p_date;
-- Try update; if no row, insert a new counter row
WITH updated AS (
UPDATE public.bank_transfer_ids
SET last_transfer_id = COALESCE(last_transfer_id, 0) + 1
WHERE company_id = p_company_id AND fin_year = v_finance_year_id
RETURNING last_transfer_id, transfer_prefix
),
inserted AS (
INSERT INTO public.bank_transfer_ids (
company_id, fin_year, transfer_prefix, last_transfer_id
)
SELECT p_company_id, v_finance_year_id, 'BT', 1
WHERE NOT EXISTS (SELECT 1 FROM updated)
RETURNING last_transfer_id, transfer_prefix
)
SELECT
COALESCE((SELECT last_transfer_id FROM updated LIMIT 1), (SELECT last_transfer_id FROM inserted LIMIT 1)),
COALESCE((SELECT transfer_prefix FROM updated LIMIT 1), (SELECT transfer_prefix FROM inserted LIMIT 1))
INTO v_last_transfer_id, v_transfer_prefix;
-- Build the transfer number
v_new_transfer_number := v_transfer_prefix || LPAD(v_last_transfer_id::text, 5,'0');
RETURN v_new_transfer_number;
END;
$function$
-- Function: get_user_by_email
CREATE OR REPLACE FUNCTION public.get_user_by_email(p_email text)
RETURNS TABLE(id uuid, user_id uuid, third_party_id text, first_name character varying, last_name character varying, email character varying, phone_number character varying, company_id uuid, company_name text, company_description text, company_gstin text, company_is_apartment boolean, company_org_id uuid, organization_id uuid, organization_name text, organization_gstin text, organization_pan text, organization_tan text, organization_short_name text, organization_type_id integer, roles text[], role_name text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
u.id as user_id,
u.id as user_id,
u.third_party_id,
u.first_name,
u.last_name,
u.email,
u.phone_number,
resolved_company.id,
resolved_company.name,
resolved_company.description,
resolved_company.gstin,
resolved_company.is_apartment,
resolved_company.organization_id,
o.id,
o.name,
o.gstin,
o.pan,
o.tan,
o.short_name,
o.organization_type_id,
ARRAY(
SELECT DISTINCT r2.name
FROM user_roles ur2
JOIN roles r2 ON r2.id = ur2.role_id
WHERE ur2.user_id = u.id AND r2.name IS NOT NULL
),
MIN(r.name) -- Primary role
FROM users u
LEFT JOIN organization_users ou ON ou.user_id = u.id
-- Resolve company using COALESCE fallback
LEFT JOIN LATERAL (
SELECT *
FROM companies c
WHERE c.id = COALESCE(
NULLIF(u.company_id, '00000000-0000-0000-0000-000000000000'),
(
SELECT MIN(c2.id::text)::uuid
FROM companies c2
JOIN organization_users ou2 ON c2.organization_id = ou2.organization_id
WHERE ou2.user_id = u.id
)
)
LIMIT 1
) AS resolved_company ON TRUE
LEFT JOIN organizations o ON resolved_company.organization_id = o.id
LEFT JOIN user_roles ur ON ur.user_id = u.id
LEFT JOIN roles r ON r.id = ur.role_id
WHERE u.email = p_email
AND u.is_deleted = false
GROUP BY
u.id, u.third_party_id, u.first_name, u.last_name, u.email, u.phone_number,
resolved_company.id, resolved_company.name, resolved_company.description, resolved_company.gstin,
resolved_company.is_apartment, resolved_company.organization_id,
o.id, o.name, o.gstin, o.pan, o.tan, o.short_name, o.organization_type_id;
END;
$function$
-- Function: get_user_by_username
CREATE OR REPLACE FUNCTION public.get_user_by_username(p_username text)
RETURNS TABLE(id uuid, user_id uuid, third_party_id text, first_name character varying, last_name character varying, user_name character varying, email character varying, phone_number character varying, company_id uuid, company_name character varying, company_description character varying, company_gstin text, company_is_apartment boolean, company_org_id uuid, organization_id uuid, organization_name character varying, organization_gstin text, organization_pan text, organization_tan text, organization_short_name text, organization_type_id integer, roles text[], role_name text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
u.id as user_id, -- Ensure this is referring to the 'users' table alias 'u'
u.id as user_id,
u.third_party_id,
u.first_name::character varying, -- Cast first_name to character varying
u.last_name::character varying, -- Cast last_name to character varying
u.user_name::character varying, -- Cast user_name to character varying
u.email::character varying, -- Include email as character varying
u.phone_number::character varying, -- Cast phone_number to character varying
resolved_company.id,
resolved_company.name::character varying, -- Cast company name to character varying
resolved_company.description::character varying, -- Cast company description to character varying
resolved_company.gstin,
resolved_company.is_apartment,
resolved_company.organization_id,
o.id,
o.name::character varying, -- Cast organization name to character varying
o.gstin,
o.pan,
o.tan,
o.short_name,
o.organization_type_id,
ARRAY(
SELECT DISTINCT r2.name
FROM user_roles ur2
JOIN roles r2 ON r2.id = ur2.role_id
WHERE ur2.user_id = u.id AND r2.name IS NOT NULL
),
-- Use COALESCE to handle NULL role_name and return a default value if NULL
COALESCE(MIN(r.name), 'No Role Assigned') AS role_name
FROM users u -- Ensure 'u' is the correct alias for the 'users' table
LEFT JOIN organization_users ou ON ou.user_id = u.id
-- Resolve company using COALESCE fallback
LEFT JOIN LATERAL (
SELECT *
FROM companies c
WHERE c.id = COALESCE(
NULLIF(u.company_id, '00000000-0000-0000-0000-000000000000'),
(
SELECT MIN(c2.id::text)::uuid
FROM companies c2
JOIN organization_users ou2 ON c2.organization_id = ou2.organization_id
WHERE ou2.user_id = u.id
)
)
LIMIT 1
) AS resolved_company ON TRUE
LEFT JOIN organizations o ON resolved_company.organization_id = o.id
LEFT JOIN user_roles ur ON ur.user_id = u.id
LEFT JOIN roles r ON r.id = ur.role_id
WHERE u.user_name = p_username -- Filter by user_name instead of email
AND u.is_deleted = false
GROUP BY
u.id, u.third_party_id, u.first_name, u.last_name, u.user_name, u.email, u.phone_number,
resolved_company.id, resolved_company.name, resolved_company.description, resolved_company.gstin,
resolved_company.is_apartment, resolved_company.organization_id,
o.id, o.name, o.gstin, o.pan, o.tan, o.short_name, o.organization_type_id;
END;
$function$
-- Function: get_vendor_dues
CREATE OR REPLACE FUNCTION public.get_vendor_dues(p_company_id uuid)
RETURNS TABLE(vendor_name text, coa_name text, amount numeric, due_date date, aging_bucket text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
v.name::text AS vendor_name,
coa.name AS coa_name,
je.amount,
(th.transaction_date + INTERVAL '30 days')::date AS due_date, -- Assuming a 30-day payment term
CASE
WHEN (th.transaction_date + INTERVAL '30 days') <= CURRENT_DATE THEN 'Current'
WHEN (th.transaction_date + INTERVAL '30 days') BETWEEN CURRENT_DATE - INTERVAL '30 days' AND CURRENT_DATE THEN '1-30 Days Past Due'
WHEN (th.transaction_date + INTERVAL '60 days') BETWEEN CURRENT_DATE - INTERVAL '60 days' AND CURRENT_DATE - INTERVAL '31 days' THEN '31-60 Days Past Due'
ELSE 'Over 60 Days Past Due'
END AS aging_bucket
FROM
public.journal_entries je
JOIN
public.transaction_headers th ON je.transaction_id = th.id
JOIN
public.chart_of_accounts coa ON je.account_id = coa.id
JOIN
public.vendors v ON th.vendor_id = v.id
WHERE
coa.account_type_id = (SELECT id FROM public.account_types WHERE name = 'Accounts Payable')
AND je.is_deleted = false
AND (th.transaction_date + INTERVAL '30 days') <= CURRENT_DATE
AND th.company_id = p_company_id
ORDER BY
v.name, (th.transaction_date + INTERVAL '30 days')::date;
END;
$function$
-- Function: get_vendor_ledger
CREATE OR REPLACE FUNCTION public.get_vendor_ledger(p_vendor_id uuid, p_company_id uuid, p_organization_id uuid, p_fin_year_id integer, p_start_date date DEFAULT NULL::date, p_end_date date DEFAULT NULL::date)
RETURNS TABLE(transaction_date date, account_name text, debit numeric, credit numeric, balance numeric, transaction_source_type integer, document_number text, document_id uuid, sort_order integer)
LANGUAGE plpgsql
AS $function$
DECLARE
v_opening_balance NUMERIC;
v_start_date DATE;
v_end_date DATE;
BEGIN
SELECT start_date, end_date
INTO v_start_date, v_end_date
FROM public.get_financial_year_dates(p_fin_year_id);
v_start_date := COALESCE(p_start_date, v_start_date);
v_end_date := COALESCE(p_end_date, v_end_date);
SELECT COALESCE(
SUM(
CASE
WHEN ob.entry_type = 'C' THEN ob.balance -- Debit increases balance
ELSE -ob.balance -- Credit reduces balance
END
), 0)
INTO v_opening_balance
FROM public.opening_balances ob
WHERE ob.organization_id = p_organization_id
AND ob.finyear_id = p_fin_year_id
AND ob.vendor_id = p_vendor_id
AND ob.is_deleted = FALSE;
RETURN QUERY
WITH vendor_transactions AS (
SELECT
th.transaction_date::date AS transaction_date,
coa.name AS account_name,
CASE
WHEN je.entry_type = 'D' THEN je.amount
ELSE 0
END AS debit,
CASE
WHEN je.entry_type = 'C' THEN je.amount
ELSE 0
END AS credit,
th.company_id,
th.vendor_id,
th.transaction_source_type,
th.document_number,
th.document_id,
2 AS sort_order
FROM
public.transaction_headers th
INNER JOIN
public.journal_entries je ON th.id = je.transaction_id
INNER JOIN
public.chart_of_accounts coa ON je.account_id = coa.id
WHERE
th.vendor_id = p_vendor_id
AND th.company_id = p_company_id
AND coa.organization_id = p_organization_id
AND th.is_deleted = false
AND je.is_deleted = false
AND th.transaction_date >= p_start_date
AND th.transaction_date <= p_end_date
)
SELECT
v_start_date AS transaction_date,
'Opening Balance' AS account_name,
0 AS debit,
0 AS credit,
v_opening_balance AS balance,
NULL AS document_number,
NULL AS document_id,
NULL AS transaction_source_type,
1 AS sort_order
UNION ALL
SELECT
vt.transaction_date,
vt.account_name,
vt.debit,
vt.credit,
v_opening_balance +
SUM(vt.debit - vt.credit) OVER (PARTITION BY vt.vendor_id ORDER BY vt.transaction_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS balance,
vt.transaction_source_type,
vt.document_number,
vt.document_id,
vt.sort_order
FROM
vendor_transactions vt
ORDER BY
vt.transaction_date,
vt.sort_order ASC;
END;
$function$
-- Function: get_vendor_outstanding
CREATE OR REPLACE FUNCTION public.get_vendor_outstanding()
RETURNS TABLE(vendor_name text, account_name text, amount numeric, due_date date, aging_bucket text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
v.name AS vendor_name,
coa.name AS account_name,
je.amount,
th.transaction_date + INTERVAL '30 days' AS due_date, -- Assuming a 30-day payment term
CASE
WHEN th.transaction_date + INTERVAL '30 days' <= CURRENT_DATE THEN 'Current'
WHEN th.transaction_date + INTERVAL '30 days' BETWEEN CURRENT_DATE - INTERVAL '30 days' AND CURRENT_DATE THEN '1-30 Days Past Due'
WHEN th.transaction_date + INTERVAL '30 days' BETWEEN CURRENT_DATE - INTERVAL '60 days' AND CURRENT_DATE - INTERVAL '31 days' THEN '31-60 Days Past Due'
ELSE 'Over 60 Days Past Due'
END AS aging_bucket
FROM
public.journal_entries je
JOIN
public.transaction_headers th ON je.transaction_id = th.id
JOIN
public.chart_of_accounts coa ON je.account_id = coa.id
JOIN
public.vendors v ON th.vendor_id = v.id
WHERE
coa.account_type_id = (SELECT id FROM account_types WHERE name = 'Accounts Payable')
AND je.is_deleted = false
ORDER BY
v.name, th.transaction_date;
END;
$function$
-- Function: get_year_wise_opening_balance
CREATE OR REPLACE FUNCTION public.get_year_wise_opening_balance(p_finyear_id integer)
RETURNS TABLE(id bigint, transaction_date timestamp without time zone, entity_id uuid, entity_name text, description text, debit numeric, credit numeric, entity_type text)
LANGUAGE plpgsql
AS $function$
DECLARE
v_year_text text;
BEGIN
-- Fetch the year text for the given finance year id
SELECT fy.year INTO v_year_text FROM public.finance_year fy WHERE fy.id = p_finyear_id;
IF v_year_text IS NULL THEN
RAISE EXCEPTION 'Finance year with id % not found!', p_finyear_id;
END IF;
RETURN QUERY
-- VENDORS
SELECT
th.id,
th.transaction_date,
th.vendor_id as entity_id,
v.name::text as entity_name,
th.description,
COALESCE(CASE WHEN je.entry_type = 'D' THEN je.amount END, 0) as debit,
COALESCE(CASE WHEN je.entry_type = 'C' THEN je.amount END, 0) as credit,
'Vendor' as entity_type
FROM public.transaction_headers th
JOIN public.vendors v ON th.vendor_id = v.id
JOIN public.journal_entries je ON th.id = je.transaction_id
JOIN public.chart_of_accounts sc ON je.account_id = sc.id AND sc.name ILIKE '%Sundry Creditors%'
WHERE th.transaction_source_type = 13
AND th.description LIKE 'Opening Balance for %' || v_year_text || '%'
UNION ALL
-- CUSTOMERS
SELECT
th.id,
th.transaction_date,
th.customer_id as entity_id,
c.name::text as entity_name,
th.description,
COALESCE(CASE WHEN je.entry_type = 'D' THEN je.amount END, 0) as debit,
COALESCE(CASE WHEN je.entry_type = 'C' THEN je.amount END, 0) as credit,
'Customer' as entity_type
FROM public.transaction_headers th
JOIN public.customers c ON th.customer_id = c.id
JOIN public.journal_entries je ON th.id = je.transaction_id
JOIN public.chart_of_accounts sd ON je.account_id = sd.id AND sd.name ILIKE '%Sundry Debtors%'
WHERE th.transaction_source_type = 13
AND th.description LIKE 'Opening Balance for %' || v_year_text || '%'
UNION ALL
-- EMPLOYEES
SELECT
th.id,
th.transaction_date,
th.employee_id as entity_id,
e.first_name::text as entity_name,
th.description,
COALESCE(CASE WHEN je.entry_type = 'D' THEN je.amount END, 0) as debit,
COALESCE(CASE WHEN je.entry_type = 'C' THEN je.amount END, 0) as credit,
'Employee' as entity_type
FROM public.transaction_headers th
JOIN public.employees e ON th.employee_id = e.id
JOIN public.journal_entries je ON th.id = je.transaction_id
JOIN public.chart_of_accounts sp ON je.account_id = sp.id AND sp.name ILIKE '%Salary Payable%'
WHERE th.transaction_source_type = 13
AND th.description LIKE 'Opening Balance for %' || v_year_text || '%'
UNION ALL
-- COA ACCOUNTS (other than above)
SELECT
th.id,
th.transaction_date,
th.document_id as entity_id,
coa.name::text as entity_name,
th.description,
COALESCE(CASE WHEN je.entry_type = 'D' THEN je.amount END, 0) as debit,
COALESCE(CASE WHEN je.entry_type = 'C' THEN je.amount END, 0) as credit,
'COA' as entity_type
FROM public.transaction_headers th
JOIN public.journal_entries je ON th.id = je.transaction_id
JOIN public.chart_of_accounts coa ON th.document_id = coa.id AND je.account_id = th.document_id
WHERE th.transaction_source_type = 13
AND th.description LIKE 'Opening Balance for %' || v_year_text || '%'
ORDER BY entity_name;
END;
$function$
-- Function: validate_organization_accounts
CREATE OR REPLACE FUNCTION public.validate_organization_accounts()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
DECLARE
invalid_account_id UUID;
BEGIN
-- Check if any account ID does not belong to the specified organization
SELECT id INTO invalid_account_id
FROM chart_of_accounts
WHERE id IN (
NEW.accounts_receivable_account_id,
NEW.accounts_payable_account_id,
NEW.sales_revenue_account_id,
NEW.cgst_receivable_account_id,
NEW.sgst_receivable_account_id,
NEW.igst_receivable_account_id,
NEW.cgst_payable_account_id,
NEW.sgst_payable_account_id,
NEW.igst_payable_account_id,
NEW.round_off_gain_account_id,
NEW.round_off_loss_account_id,
NEW.sales_tax_payable_account_id,
NEW.purchase_tax_receivable_account_id,
NEW.discounts_given_account_id,
NEW.discounts_received_account_id,
NEW.interest_income_account_id,
NEW.interest_expense_account_id,
NEW.depreciation_expense_account_id,
NEW.bad_debt_expense_account_id,
NEW.bank_charges_account_id,
NEW.foreign_exchange_gain_loss_account_id,
NEW.cost_of_goods_sold_account_id,
NEW.inventory_account_id,
NEW.salary_expense_account_id,
NEW.salary_payable_account_id,
NEW.tds_receivable_account_id,
NEW.penalty_receivable_account_id,
NEW.tds_payable_account_id
) AND organization_id != NEW.organization_id
LIMIT 1;
-- If an invalid account ID is found, raise an exception
IF invalid_account_id IS NOT NULL THEN
RAISE EXCEPTION 'Account ID % does not belong to the specified organization %',
invalid_account_id, NEW.organization_id;
END IF;
RETURN NEW;
END;
$function$
-- Function: insert_multiple_transactions_and_journal_entries
CREATE OR REPLACE FUNCTION public.insert_multiple_transactions_and_journal_entries(p_transactions_data jsonb)
RETURNS TABLE(transaction_id bigint, document_id uuid, reference text)
LANGUAGE plpgsql
AS $function$
DECLARE
v_transaction_id BIGINT;
v_transaction_record JSONB;
v_journal_entry JSONB;
v_narration_id BIGINT;
v_reference TEXT;
BEGIN
-- Loop through each transaction in the input array
FOR v_transaction_record IN SELECT * FROM jsonb_array_elements(p_transactions_data)
LOOP
-- Insert into transaction_headers
INSERT INTO public.transaction_headers (
company_id,
customer_id,
vendor_id,
employee_id,
transaction_date,
transaction_source_type,
status_id,
document_id,
document_number,
description,
created_by,
created_on_utc
)
VALUES(
(v_transaction_record->>'company_id')::uuid,
NULLIF((v_transaction_record->>'customer_id')::uuid, '00000000-0000-0000-0000-000000000000'),
NULLIF((v_transaction_record->>'vendor_id')::uuid, '00000000-0000-0000-0000-000000000000'),
NULLIF((v_transaction_record->>'employee_id')::uuid, '00000000-0000-0000-0000-000000000000'),
(v_transaction_record->>'transaction_date')::date,
(v_transaction_record->>'transaction_source_type')::integer,
(v_transaction_record->>'status_id')::integer,
(v_transaction_record->>'document_id')::uuid,
v_transaction_record->>'document_number',
v_transaction_record->>'description',
(v_transaction_record->>'user_id')::uuid,
CURRENT_TIMESTAMP
)
RETURNING id INTO v_transaction_id;
-- ✅ Insert into payment_references if reference exists and not empty
v_reference := NULLIF(TRIM(v_transaction_record->>'reference'), '');
IF v_reference IS NOT NULL THEN
INSERT INTO public.payment_references (
transaction_id,
reference
)
VALUES (
v_transaction_id,
v_reference
);
END IF;
-- Loop through each journal entry associated with the transaction
FOR v_journal_entry IN SELECT * FROM jsonb_array_elements(v_transaction_record->'journal_entries')
LOOP
-- Insert narration first
INSERT INTO public.journal_narrations (
transaction_date,
description_template_id,
dynamic_data,
created_by,
created_on_utc
)
VALUES(
(v_journal_entry->>'transaction_date')::timestamp,
(v_journal_entry->>'description_template_id')::integer,
COALESCE((v_journal_entry->>'dynamic_data'), ''),
(v_transaction_record->>'user_id')::uuid,
CURRENT_TIMESTAMP
)
RETURNING id INTO v_narration_id;
-- Insert journal entry
INSERT INTO public.journal_entries (
transaction_id,
account_id,
transaction_date,
amount,
entry_type,
entry_source_id,
journal_narration_id
)
VALUES(
v_transaction_id,
(v_journal_entry->>'account_id')::uuid,
(v_journal_entry->>'transaction_date')::date,
(v_journal_entry->>'amount')::numeric,
(v_journal_entry->>'entry_type')::char,
(v_journal_entry->>'entry_source_id')::integer,
v_narration_id
);
END LOOP;
-- Return transaction ID, document ID, and reference for this transaction
RETURN QUERY SELECT v_transaction_id, (v_transaction_record->>'document_id')::uuid, v_reference;
END LOOP;
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION 'Error occurred: %', SQLERRM;
END;
$function$
-- Function: assign_user_default_permissions
CREATE OR REPLACE FUNCTION public.assign_user_default_permissions(p_user_id uuid, p_role_id integer, p_organization_id uuid, p_created_by uuid DEFAULT NULL::uuid)
RETURNS void
LANGUAGE plpgsql
AS $function$
DECLARE
v_permission_count INT;
v_existing_organization_user INT;
v_existing_user_role INT;
BEGIN
RAISE NOTICE 'Assigning default permissions for User=% to Role=%', p_user_id, p_role_id;
-- 1️⃣ Ensure user exists in organization_users
SELECT COUNT(*) INTO v_existing_organization_user
FROM organization_users ou
WHERE ou.user_id = p_user_id
AND ou.organization_id = p_organization_id
AND ou.is_deleted = FALSE;
IF v_existing_organization_user = 0 THEN
INSERT INTO organization_users (
id, user_id, effective_start_date, created_on_utc,
created_by, organization_id, is_deleted
)
VALUES (
uuid_generate_v4(),
p_user_id,
NOW(),
NOW(),
COALESCE(p_created_by, p_user_id),
p_organization_id,
FALSE
);
RAISE NOTICE '✅ Added user to organization_users';
END IF;
-- 2️⃣ Ensure user has matching role in user_roles
SELECT COUNT(*) INTO v_existing_user_role
FROM user_roles ur
WHERE ur.user_id = p_user_id
AND ur.role_id = p_role_id
AND ur.organization_id = p_organization_id
AND ur.is_deleted = FALSE;
IF v_existing_user_role = 0 THEN
INSERT INTO user_roles (
user_id, role_id, created_on_utc, created_by,
is_deleted, start_date, organization_id
)
VALUES (
p_user_id,
p_role_id,
NOW(),
COALESCE(p_created_by, p_user_id),
FALSE,
NOW(),
p_organization_id
);
RAISE NOTICE '✅ Added role assignment for user';
END IF;
-- 3️⃣ Assign permissions from role_permissions (NOT template mappings)
INSERT INTO user_permissions (
user_id,
permission_id,
organization_id,
created_on_utc,
created_by,
is_deleted
)
SELECT
p_user_id,
rp.permission_id,
p_organization_id,
NOW(),
COALESCE(p_created_by, p_user_id),
FALSE
FROM role_permissions rp
WHERE rp.role_id = p_role_id
AND NOT EXISTS (
SELECT 1
FROM user_permissions up
WHERE up.user_id = p_user_id
AND up.permission_id = rp.permission_id
AND up.organization_id = p_organization_id
AND up.is_deleted = FALSE
);
GET DIAGNOSTICS v_permission_count = ROW_COUNT;
RAISE NOTICE '✅ Assigned % permissions to User=% for Role=%',
v_permission_count, p_user_id, p_role_id;
EXCEPTION
WHEN OTHERS THEN
RAISE WARNING '⚠️ Error assigning permissions for user %: %', p_user_id, SQLERRM;
END;
$function$
-- Function: upsert_role_permissions
CREATE OR REPLACE FUNCTION public.upsert_role_permissions(p_role_id integer, p_permission_ids uuid[], p_user_id uuid)
RETURNS void
LANGUAGE plpgsql
AS $function$
BEGIN
-- 1️⃣ Reactivate existing soft-deleted permissions that are now in the list
UPDATE public.role_permissions
SET is_deleted = FALSE,
deleted_on_utc = NULL,
modified_on_utc = NOW(),
modified_by = p_user_id
WHERE role_id = p_role_id
AND permission_id = ANY(p_permission_ids)
AND is_deleted = TRUE;
-- 2️⃣ Insert missing (new) permissions
INSERT INTO public.role_permissions (
role_id,
permission_id,
created_on_utc,
created_by,
is_deleted
)
SELECT
p_role_id,
pid,
NOW(),
p_user_id,
FALSE
FROM UNNEST(p_permission_ids) AS pid
WHERE NOT EXISTS (
SELECT 1
FROM public.role_permissions rp
WHERE rp.role_id = p_role_id
AND rp.permission_id = pid
);
-- 3️⃣ Soft delete permissions not in the provided list
UPDATE public.role_permissions
SET is_deleted = TRUE,
deleted_on_utc = NOW(),
modified_on_utc = NOW(),
modified_by = p_user_id
WHERE role_id = p_role_id
AND permission_id NOT IN (SELECT unnest(p_permission_ids))
AND is_deleted = FALSE;
END;
$function$
-- Function: get_outstanding_invoices_by_customer_id
CREATE OR REPLACE FUNCTION public.get_outstanding_invoices_by_customer_id(p_company_id uuid, p_customer_id uuid)
RETURNS TABLE(customer_name text, coa_name text, amount numeric, due_date date, aging_bucket text)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY
SELECT
c.name::text AS customer_name,
coa.name AS coa_name,
je.amount,
(th.transaction_date + INTERVAL '30 days')::date AS due_date,
CASE
WHEN (th.transaction_date + INTERVAL '30 days') <= CURRENT_DATE
THEN 'Current'
WHEN (th.transaction_date + INTERVAL '30 days') BETWEEN CURRENT_DATE - INTERVAL '30 days' AND CURRENT_DATE
THEN '1-30 Days Past Due'
WHEN (th.transaction_date + INTERVAL '60 days') BETWEEN CURRENT_DATE - INTERVAL '60 days' AND CURRENT_DATE - INTERVAL '31 days'
THEN '31-60 Days Past Due'
ELSE 'Over 60 Days Past Due'
END AS aging_bucket
FROM
public.journal_entries je
JOIN
public.transaction_headers th ON je.transaction_id = th.id
JOIN
public.chart_of_accounts coa ON je.account_id = coa.id
JOIN
public.customers c ON th.customer_id = c.id
WHERE
coa.account_type_id = (SELECT id FROM public.account_types WHERE name = 'Accounts Receivable')
AND je.is_deleted = false
AND th.company_id = p_company_id
AND th.customer_id = p_customer_id
AND (th.transaction_date + INTERVAL '30 days') <= CURRENT_DATE
ORDER BY
(th.transaction_date + INTERVAL '30 days')::date;
END;
$function$
-- Function: get_outstanding_invoices_by_customer
CREATE OR REPLACE FUNCTION public.get_outstanding_invoices_by_customer(p_company_id uuid, p_customer_id uuid, p_start_date date DEFAULT NULL::date, p_end_date date DEFAULT NULL::date)
RETURNS TABLE(customer_name text, coa_name text, amount numeric, due_date date, aging_bucket text)
LANGUAGE plpgsql
AS $function$
DECLARE
v_start_date date := COALESCE(p_start_date, CURRENT_DATE - INTERVAL '365 days');
v_end_date date := COALESCE(p_end_date, CURRENT_DATE);
BEGIN
RETURN QUERY
SELECT
c.name::text AS customer_name,
coa.name AS coa_name,
je.amount,
(th.transaction_date + INTERVAL '30 days')::date AS due_date,
/* Aging bucket calculation */
CASE
WHEN (th.transaction_date + INTERVAL '30 days') <= CURRENT_DATE
THEN 'Current'
WHEN (th.transaction_date + INTERVAL '30 days') BETWEEN CURRENT_DATE - INTERVAL '30 days' AND CURRENT_DATE
THEN '1-30 Days Past Due'
WHEN (th.transaction_date + INTERVAL '60 days') BETWEEN CURRENT_DATE - INTERVAL '60 days' AND CURRENT_DATE - INTERVAL '31 days'
THEN '31-60 Days Past Due'
ELSE 'Over 60 Days Past Due'
END AS aging_bucket
FROM public.journal_entries je
JOIN public.transaction_headers th ON je.transaction_id = th.id
JOIN public.chart_of_accounts coa ON je.account_id = coa.id
JOIN public.customers c ON th.customer_id = c.id
WHERE
th.company_id = p_company_id
AND th.customer_id = p_customer_id
AND je.is_deleted = false
AND coa.account_type_id = (
SELECT id FROM public.account_types WHERE name = 'Accounts Receivable'
)
/* Overdue invoices only (due_date <= today) */
AND (th.transaction_date + INTERVAL '30 days')::date <= CURRENT_DATE
/* Date range filtering */
AND th.transaction_date::date BETWEEN v_start_date AND v_end_date
ORDER BY (th.transaction_date + INTERVAL '30 days')::date;
END;
$function$
-- Function: postgres_fdw_handler
CREATE OR REPLACE FUNCTION public.postgres_fdw_handler()
RETURNS fdw_handler
LANGUAGE c
STRICT
AS '$libdir/postgres_fdw', $function$postgres_fdw_handler$function$
-- Function: postgres_fdw_validator
CREATE OR REPLACE FUNCTION public.postgres_fdw_validator(text[], oid)
RETURNS void
LANGUAGE c
STRICT
AS '$libdir/postgres_fdw', $function$postgres_fdw_validator$function$
-- Function: postgres_fdw_disconnect
CREATE OR REPLACE FUNCTION public.postgres_fdw_disconnect(text)
RETURNS boolean
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/postgres_fdw', $function$postgres_fdw_disconnect$function$
-- Function: postgres_fdw_disconnect_all
CREATE OR REPLACE FUNCTION public.postgres_fdw_disconnect_all()
RETURNS boolean
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/postgres_fdw', $function$postgres_fdw_disconnect_all$function$
-- Function: postgres_fdw_get_connections
CREATE OR REPLACE FUNCTION public.postgres_fdw_get_connections(check_conn boolean DEFAULT false, OUT server_name text, OUT user_name text, OUT valid boolean, OUT used_in_xact boolean, OUT closed boolean, OUT remote_backend_pid integer)
RETURNS SETOF record
LANGUAGE c
PARALLEL RESTRICTED STRICT
AS '$libdir/postgres_fdw', $function$postgres_fdw_get_connections_1_2$function$
-- Function: notify_user_permission_change
CREATE OR REPLACE FUNCTION public.notify_user_permission_change()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
DECLARE
v_user_id uuid;
v_org_id uuid;
payload json;
BEGIN
-- Handle DELETE separately (NEW is null)
IF TG_OP = 'DELETE' THEN
v_user_id := OLD.user_id;
v_org_id := OLD.organization_id;
ELSE
v_user_id := NEW.user_id;
v_org_id := NEW.organization_id;
END IF;
payload := json_build_object(
'entity', 'user_permissions',
'operation', TG_OP,
'userId', v_user_id,
'organizationId', v_org_id,
'occurredAt', now()
);
PERFORM pg_notify('permission_user_changed', payload::text);
RETURN NULL;
END;
$function$
-- Function: get_system_user_id
CREATE OR REPLACE FUNCTION public.get_system_user_id()
RETURNS uuid
LANGUAGE plpgsql
AS $function$
DECLARE
v_user_id uuid;
BEGIN
SELECT id
INTO v_user_id
FROM public.users
WHERE first_name = 'System'
AND is_deleted = false
LIMIT 1;
RETURN COALESCE(
v_user_id,
'dd4f94f2-0f79-4748-b94b-bf935e3944c7'::uuid
);
END;
$function$
-- Function: upsert_user_roles
CREATE OR REPLACE FUNCTION public.upsert_user_roles(p_user_id uuid, p_role_ids integer[], p_organization_id uuid, p_created_by uuid)
RETURNS void
LANGUAGE plpgsql
AS $function$
BEGIN
INSERT INTO public.user_roles
(
user_id,
role_id,
organization_id,
created_on_utc,
created_by,
is_deleted,
start_date,
end_date
)
SELECT
p_user_id,
role_id,
p_organization_id,
NOW(),
p_created_by,
false,
NOW(),
NOW() + INTERVAL '1 year'
FROM UNNEST(p_role_ids) AS role_id
ON CONFLICT (user_id, role_id, organization_id)
DO UPDATE
SET
is_deleted = false,
start_date = NOW(),
end_date = NOW() + INTERVAL '1 year',
modified_on_utc = NOW(),
modified_by = p_created_by;
END;
$function$
-- Function: upsert_user_permissions_from_user_roles
CREATE OR REPLACE FUNCTION public.upsert_user_permissions_from_user_roles(p_user_id uuid, p_organization_id uuid, p_role_ids integer[], p_created_by uuid)
RETURNS void
LANGUAGE plpgsql
AS $function$
BEGIN
/*
* STEP 1: Insert or revive permissions coming from roles
*/
INSERT INTO public.user_permissions (
user_id,
organization_id,
permission_id,
created_on_utc,
created_by,
is_deleted,
is_explicit
)
SELECT DISTINCT
p_user_id,
p_organization_id,
rp.permission_id,
now(),
p_created_by,
false,
false -- role-derived
FROM public.role_permissions rp
WHERE rp.role_id = ANY (p_role_ids)
AND rp.is_deleted = false
ON CONFLICT (user_id, organization_id, permission_id)
DO UPDATE
SET
is_deleted = false,
deleted_on_utc = NULL,
modified_on_utc = now(),
modified_by = p_created_by,
is_explicit = false;
/*
* STEP 2: Soft-delete role-derived permissions
* that are no longer present in current roles
*/
UPDATE public.user_permissions up
SET
is_deleted = true,
deleted_on_utc = now(),
modified_on_utc = now(),
modified_by = p_created_by
WHERE up.user_id = p_user_id
AND up.organization_id = p_organization_id
AND up.is_explicit = false
AND up.is_deleted = false
AND up.permission_id NOT IN (
SELECT DISTINCT rp.permission_id
FROM public.role_permissions rp
WHERE rp.role_id = ANY (p_role_ids)
AND rp.is_deleted = false
);
END;
$function$
-- Function: sync_user_permissions_from_roles
CREATE OR REPLACE FUNCTION public.sync_user_permissions_from_roles(p_user_id uuid DEFAULT NULL::uuid, p_organization_id uuid DEFAULT NULL::uuid, p_triggered_by text DEFAULT 'manual'::text, p_notes text DEFAULT NULL::text)
RETURNS uuid
LANGUAGE plpgsql
AS $function$
DECLARE
-- Sync run tracking
v_sync_run_id uuid := gen_random_uuid();
-- Loop variables
v_user_id uuid;
v_org_id uuid;
v_role_id int;
v_permission_id uuid;
-- For UPSERT result
v_user_permission_id int;
BEGIN
/*
============================================================
START SYNC RUN
============================================================
*/
INSERT INTO public.user_permission_sync_runs (
id,
started_on_utc,
triggered_by,
notes
)
VALUES (
v_sync_run_id,
NOW(),
p_triggered_by,
p_notes
);
/*
============================================================
MAIN SYNC LOOP
- Filtered by optional user / organization
============================================================
*/
FOR v_user_id, v_org_id IN
SELECT DISTINCT
ur.user_id,
ur.organization_id
FROM public.user_roles ur
WHERE ur.is_deleted = false
AND (p_user_id IS NULL OR ur.user_id = p_user_id)
AND (p_organization_id IS NULL OR ur.organization_id = p_organization_id)
LOOP
/*
--------------------------------------------------------
LOOP ROLES FOR USER + ORGANIZATION
--------------------------------------------------------
*/
FOR v_role_id IN
SELECT ur.role_id
FROM public.user_roles ur
WHERE ur.user_id = v_user_id
AND ur.organization_id = v_org_id
AND ur.is_deleted = false
LOOP
/*
----------------------------------------------------
LOOP PERMISSIONS FOR ROLE
----------------------------------------------------
*/
FOR v_permission_id IN
SELECT rp.permission_id
FROM public.role_permissions rp
WHERE rp.role_id = v_role_id
AND rp.is_deleted = false
LOOP
/*
------------------------------------------------
UPSERT USER PERMISSION (SAFE)
------------------------------------------------
*/
INSERT INTO public.user_permissions (
user_id,
permission_id,
organization_id,
created_on_utc,
created_by,
is_deleted,
modified_on_utc,
modified_by,
deleted_on_utc
)
VALUES (
v_user_id,
v_permission_id,
v_org_id,
NOW(),
v_user_id,
false,
NULL,
NULL,
NULL
)
ON CONFLICT (user_id, organization_id, permission_id)
DO UPDATE
SET
is_deleted = false,
deleted_on_utc = NULL,
modified_on_utc = NOW(),
modified_by = v_user_id
WHERE public.user_permissions.is_deleted = true
RETURNING id
INTO v_user_permission_id;
/*
------------------------------------------------
BACKUP / DELTA SNAPSHOT
- Only when INSERT or REVIVE happened
------------------------------------------------
*/
IF FOUND THEN
INSERT INTO public.user_permission_sync_deltas (
id,
sync_run_id,
user_permission_id,
user_id,
organization_id,
permission_id,
action,
created_on_utc
)
VALUES (
gen_random_uuid(),
v_sync_run_id,
v_user_permission_id,
v_user_id,
v_org_id,
v_permission_id,
'UPSERT',
NOW()
);
END IF;
END LOOP; -- permissions
END LOOP; -- roles
END LOOP; -- users + organizations
/*
============================================================
COMPLETE SYNC RUN
============================================================
*/
UPDATE public.user_permission_sync_runs
SET completed_on_utc = NOW()
WHERE id = v_sync_run_id;
RETURN v_sync_run_id;
END;
$function$
-- Procedure: delete_journal_entries_by_accounts
CREATE OR REPLACE PROCEDURE public.delete_journal_entries_by_accounts(IN p_account_ids uuid[])
LANGUAGE plpgsql
AS $procedure$
BEGIN
DELETE FROM ONLY public.journal_entries
WHERE account_id = ANY(p_account_ids);
RAISE NOTICE 'Deleted entries for Account IDs: %', p_account_ids;
END;
$procedure$
-- Procedure: copy_user_permissions
CREATE OR REPLACE PROCEDURE public.copy_user_permissions(IN p_source_user_id uuid, IN p_target_user_id uuid, IN p_created_by uuid, IN p_is_override boolean)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_new_id INTEGER;
BEGIN
-- If override is true, delete existing permissions for the target user
IF p_is_override THEN
DELETE FROM public.user_permissions
WHERE user_id = p_target_user_id;
END IF;
-- Get the current maximum id from user_permissions
SELECT COALESCE(MAX(id), 0) INTO v_new_id FROM public.user_permissions;
-- Insert permissions from the source user to the target user, with checks to avoid duplicates and child assignments if parent exists
INSERT INTO public.user_permissions (
id,
user_id,
organization_id,
permission_id,
created_on_utc,
created_by
)
SELECT
v_new_id + ROW_NUMBER() OVER (), -- generate incremented ids starting from max_id + 1
p_target_user_id,
up.organization_id,
up.permission_id,
NOW(),
p_created_by
FROM
public.user_permissions AS up
WHERE
up.user_id = p_source_user_id
AND up.is_deleted = false
-- Avoid assigning child permissions if a parent permission is already assigned
AND NOT EXISTS (
SELECT 1
FROM public.user_permissions AS target_up
JOIN public.permissions AS parent_perm ON target_up.permission_id = parent_perm.id
WHERE target_up.user_id = p_target_user_id
AND parent_perm.id = up.permission_id
AND parent_perm.parent_permission_id IS NULL -- Checking only for root or parent permissions
AND parent_perm.is_deleted = false
)
-- Avoid duplicate permission entries
AND NOT EXISTS (
SELECT 1
FROM public.user_permissions AS target_up
WHERE target_up.user_id = p_target_user_id
AND target_up.permission_id = up.permission_id
AND target_up.is_deleted = false
);
END;
$procedure$
-- Procedure: create_company1
CREATE OR REPLACE PROCEDURE public.create_company1(IN p_id uuid, IN p_organization_id uuid, IN p_name text, IN p_description text, IN p_account_id uuid, IN p_gstin text, IN p_pan text, IN p_tan text, IN p_currency text, IN p_short_name text, IN p_tag_line text, IN p_proprietor_name text, IN p_outstanding_limit numeric, IN p_is_non_work boolean, IN p_interest_percentage numeric, IN p_created_by uuid, IN p_created_on_utc timestamp without time zone)
LANGUAGE plpgsql
AS $procedure$
BEGIN
INSERT INTO public.companies (
id, organization_id, name, description, account_id, gstin, pan, tan,
currency, short_name, tag_line, proprietor_name, outstanding_limit,
is_non_work, interest_percentage, created_on_utc, created_by, modified_by
) VALUES (
p_id, p_organization_id, p_name, p_description, p_account_id, p_gstin,
p_pan, p_tan, p_currency, p_short_name, p_tag_line, p_proprietor_name,
p_outstanding_limit, p_is_non_work, p_interest_percentage,
p_created_on_utc, p_created_by
);
END;
$procedure$
-- Procedure: delete_journal_entries_by_account
CREATE OR REPLACE PROCEDURE public.delete_journal_entries_by_account(IN p_account_id uuid)
LANGUAGE plpgsql
AS $procedure$
BEGIN
DELETE FROM ONLY public.journal_entries
WHERE account_id = p_account_id;
RAISE NOTICE 'Deleted entries for Account ID: %', p_account_id;
END;
$procedure$
-- Procedure: get_all_chart_of_accounts
CREATE OR REPLACE PROCEDURE public.get_all_chart_of_accounts()
LANGUAGE plpgsql
AS $procedure$
begin
select * from chart_of_accounts;
end;
$procedure$
-- Procedure: assign_default_users_to_organization
CREATE OR REPLACE PROCEDURE public.assign_default_users_to_organization(IN p_organization_id uuid, IN p_permission_ids uuid[], IN p_created_by uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
loop_user_id UUID;
BEGIN
-- Insert default users into organization_users (only if they don't exist)
INSERT INTO public.organization_users (
id,
user_id,
effective_start_date,
effective_end_date,
created_on_utc,
created_by,
organization_id
)
SELECT
gen_random_uuid(),
d.user_id,
NOW(),
DATE '9999-12-31',
NOW(),
p_created_by,
p_organization_id
FROM
public.default_organization_users d
WHERE NOT EXISTS (
SELECT 1
FROM public.organization_users ou
WHERE ou.user_id = d.user_id
AND ou.organization_id = p_organization_id
);
RAISE NOTICE 'Added % default users to organization %',
(SELECT COUNT(*) FROM public.default_organization_users), p_organization_id;
-- Assign permissions for these users by calling insert_user_permission
FOR loop_user_id IN (SELECT d.user_id
FROM public.default_organization_users d)
LOOP
CALL public.insert_user_permission(
loop_user_id,
p_organization_id,
p_permission_ids,
p_created_by
);
END LOOP;
RAISE NOTICE 'Attempted to assign permission % to default users in organization %',
p_permission_ids,
p_organization_id;
END;
$procedure$
-- Procedure: create_organization
CREATE OR REPLACE PROCEDURE public.create_organization(IN p_id uuid, IN p_name text, IN p_description text, IN p_phone_number text, IN p_email text, IN p_address_line text, IN p_gstin text, IN p_tag_line text, IN p_city_id uuid, IN p_short_name text, IN p_pan text, IN p_tan text, IN p_created_by uuid)
LANGUAGE plpgsql
AS $procedure$
BEGIN
-- Step 1: Insert into organizations table
INSERT INTO public.organizations (
id,
name,
description,
phone_number,
email,
address_line,
gstin,
tag_line,
city_id,
short_name,
pan,
tan,
outstanding_limit,
is_non_work,
interest_percentage,
created_on_utc,
created_by
) VALUES (
p_id, -- Organization ID
p_name, -- Organization Name
p_description, -- Description
p_phone_number, -- Phone Number
p_email, -- Email
p_address_line, -- Address Line
p_gstin, -- GSTIN
p_tag_line, -- Tag Line
p_city_id, -- City ID
p_short_name, -- Short Name
p_pan, -- PAN
p_tan, -- TAN
0, -- Outstanding Limit
false, -- Is Non-Work
0, -- Interest Percentage
NOW(), -- Created On Timestamp
p_created_by -- Created By
);
-- Notify the successful creation of the organization
RAISE NOTICE 'Organization created successfully with ID: %', p_id;
END;
$procedure$
-- Procedure: delete_journal_vouchers_by_company
CREATE OR REPLACE PROCEDURE public.delete_journal_vouchers_by_company(IN p_company_id uuid)
LANGUAGE plpgsql
AS $procedure$
BEGIN
-- Delete from journal_voucher_details first to maintain referential integrity
DELETE FROM public.journal_voucher_details
WHERE journal_header_id IN (
SELECT id FROM public.journal_voucher_headers WHERE company_id = p_company_id
);
-- Delete from journal_voucher_headers
DELETE FROM public.journal_voucher_headers
WHERE company_id = p_company_id;
RAISE NOTICE 'Data deleted successfully for company_id: %', p_company_id;
END;
$procedure$
-- Procedure: delete_transactions_by_company
CREATE OR REPLACE PROCEDURE public.delete_transactions_by_company(IN p_company_id uuid)
LANGUAGE plpgsql
AS $procedure$
BEGIN
-- Delete from journal_entries first to maintain referential integrity
DELETE FROM public.journal_entries
WHERE transaction_id IN (
SELECT id FROM public.transaction_headers WHERE company_id = p_company_id
);
-- Delete from transaction_headers
DELETE FROM public.transaction_headers
WHERE company_id = p_company_id;
RAISE NOTICE 'Data deleted successfully for company_id: %', p_company_id;
END;
$procedure$
-- Procedure: dummy_log_procedure
CREATE OR REPLACE PROCEDURE public.dummy_log_procedure(IN msg text)
LANGUAGE plpgsql
AS $procedure$
BEGIN
INSERT INTO public.dummy_log_table(message) VALUES (msg);
END;
$procedure$
-- Procedure: hard_delete_org_common
CREATE OR REPLACE PROCEDURE public.hard_delete_org_common(IN p_organization_id uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_company_id uuid;
BEGIN
-- ========== Delete Common Organization-Level Data ==========
DELETE FROM organization_accounts WHERE organization_id = p_organization_id;
DELETE FROM email_templates WHERE organization_id = p_organization_id;
DELETE FROM user_permissions WHERE organization_id = p_organization_id;
DELETE FROM organization_users WHERE organization_id = p_organization_id;
-- ========== Delete Company-Level Data ==========
FOR v_company_id IN
SELECT id FROM companies WHERE organization_id = p_organization_id
LOOP
-- Journal data
DELETE FROM journal_voucher_details WHERE journal_header_id IN (
SELECT id FROM journal_voucher_headers WHERE company_id = v_company_id
);
DELETE FROM journal_voucher_headers WHERE company_id = v_company_id;
DELETE FROM journal_voucher_header_id WHERE company_id = v_company_id;
-- General ledgers
DELETE FROM general_ledgers WHERE company_id = v_company_id;
-- Company-specific info
DELETE FROM company_bank_accounts WHERE company_id = v_company_id;
DELETE FROM company_contacts WHERE company_id = v_company_id;
DELETE FROM company_finance_year WHERE company_id = v_company_id;
DELETE FROM company_preferences WHERE company_id = v_company_id;
DELETE FROM company_upis WHERE company_id = v_company_id;
DELETE FROM company_users WHERE company_id = v_company_id;
-- Delete journal entries by company-linked transactions
DELETE FROM journal_entries WHERE transaction_id IN (
SELECT id FROM transaction_headers WHERE company_id = v_company_id
);
-- Delete transaction headers (must be before customers/vendors)
DELETE FROM transaction_headers WHERE company_id = v_company_id;
-- Linked business entities (now safe to delete)
DELETE FROM customers WHERE company_id = v_company_id;
DELETE FROM vendors WHERE company_id = v_company_id;
DELETE FROM warehouses WHERE company_id = v_company_id;
-- Remove user_roles BEFORE users
DELETE FROM user_roles WHERE user_id IN (
SELECT id FROM users WHERE company_id = v_company_id
) OR created_by IN (
SELECT id FROM users WHERE company_id = v_company_id
);
-- Now it's safe to delete users
DELETE FROM users WHERE company_id = v_company_id;
END LOOP;
-- Chart of accounts and account-linked entries
DELETE FROM general_ledgers
WHERE credit_account_id IN (
SELECT id FROM chart_of_accounts WHERE organization_id = p_organization_id
) OR debit_account_id IN (
SELECT id FROM chart_of_accounts WHERE organization_id = p_organization_id
);
DELETE FROM journal_entries
WHERE account_id IN (
SELECT id FROM chart_of_accounts WHERE organization_id = p_organization_id
);
DELETE FROM chart_of_accounts WHERE organization_id = p_organization_id;
-- Delete all companies under the organization
DELETE FROM companies WHERE organization_id = p_organization_id;
-- Delete the organization itself
DELETE FROM organizations WHERE id = p_organization_id;
RAISE NOTICE 'Organization with ID % and related data from all common tables has been deleted.', p_organization_id;
END;
$procedure$
-- Procedure: hard_delete_organization
CREATE OR REPLACE PROCEDURE public.hard_delete_organization(IN p_organization_id uuid)
LANGUAGE plpgsql
AS $procedure$
BEGIN
-- Delete from user_permissions (cascading through organization and users)
DELETE FROM user_permissions
WHERE organization_id = p_organization_id;
-- Delete from organization_users (cascading through organization)
DELETE FROM organization_users
WHERE organization_id = p_organization_id;
-- Delete from company_finance_year (cascading through companies)
DELETE FROM company_finance_year
WHERE company_id IN (
SELECT id FROM companies WHERE organization_id = p_organization_id
);
-- Delete from company_users (cascading through companies)
DELETE FROM company_users
WHERE company_id IN (
SELECT id FROM companies WHERE organization_id = p_organization_id
);
-- Delete from company_contacts (cascading through companies)
DELETE FROM company_contacts
WHERE company_id IN (
SELECT id FROM companies WHERE organization_id = p_organization_id
);
-- Delete from contacts (cascading through companies)
DELETE FROM contacts
WHERE id IN (
SELECT contact_id FROM company_contacts
WHERE company_id IN (
SELECT id FROM companies WHERE organization_id = p_organization_id
)
);
--delete from company_preference
DELETE FROM public.company_preferences
WHERE company_id in (SELECT id from public.companies where organization_id = p_organization_id);
-- Delete from companies (cascading through organization)
DELETE FROM companies
WHERE organization_id = p_organization_id;
DELETE FROM organization_accounts
WHERE organization_id = p_organization_id;
-- Delete from chart_of_accounts (cascading through organization)
DELETE FROM chart_of_accounts
WHERE organization_id = p_organization_id;
-- Finally, delete the organization itself
DELETE FROM organizations
WHERE id = p_organization_id;
-- Optional: Log the deletion action
RAISE NOTICE 'Organization with ID % and all related data have been hard deleted.',
p_organization_id;
END;
$procedure$
-- Procedure: initialize_company
CREATE OR REPLACE PROCEDURE public.initialize_company(IN p_id uuid, IN p_organization_id uuid, IN p_name text, IN p_description text, IN p_gstin text, IN p_pan text, IN p_tan text, IN p_currency text, IN p_short_name text, IN p_tag_line text, IN p_proprietor_name text, IN p_phone_number character varying, IN p_email character varying, IN p_created_by uuid, IN p_user_id uuid, IN p_default_company_id uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_contact_id uuid := gen_random_uuid();
v_company_contact_id uuid := gen_random_uuid();
v_finance_year_id int;
v_prev_finance_year_id INTEGER;
v_next_finance_year_id INTEGER;
DEFAULT_USER_ID CONSTANT uuid := '50fd5012-940b-4fec-ad0a-2ac900239c8b';
v_company_exists boolean;
v_contact_exists boolean;
v_company_contact_exists boolean;
v_user_company_exists boolean;
v_default_user_company_exists boolean;
BEGIN
-- Check if company already exists
SELECT EXISTS (
SELECT 1 FROM public.companies
WHERE id = p_id
OR (organization_id = p_organization_id AND name = p_name)
) INTO v_company_exists;
IF NOT v_company_exists THEN
-- Insert into companies table
INSERT INTO public.companies (
id, organization_id, name, description, gstin, pan, tan,
currency, short_name, tag_line, proprietor_name,
outstanding_limit, is_non_work, is_apartment, interest_percentage,
created_on_utc, created_by
) VALUES (
p_id, p_organization_id, p_name, p_description, p_gstin, p_pan, p_tan,
p_currency, p_short_name, p_tag_line, p_proprietor_name,
0, false, true, 0, NOW(), p_created_by
);
RAISE NOTICE 'Company % created successfully.', p_name;
ELSE
RAISE NOTICE 'Company with ID %, name %, or GSTIN/PAN % already exists. Skipping company creation.',
p_id, p_name, p_gstin;
END IF;
-- Check if contact already exists for this email/phone
SELECT EXISTS (
SELECT 1 FROM contacts
WHERE email = p_email OR phone_number = p_phone_number
) INTO v_contact_exists;
IF NOT v_contact_exists THEN
-- Insert contact
INSERT INTO contacts (
id, salutation, first_name, last_name, email,
phone_number, mobile_number, is_primary,
created_on_utc, created_by
) VALUES (
v_contact_id, 'Mr.', p_proprietor_name, NULL, p_email,
p_phone_number, p_phone_number, TRUE,
NOW(), p_created_by
);
RAISE NOTICE 'Contact created for company % with email %.', p_name, p_email;
ELSE
-- Get existing contact ID
SELECT id INTO v_contact_id
FROM contacts
WHERE email = p_email OR phone_number = p_phone_number
LIMIT 1;
RAISE NOTICE 'Contact with email % or phone % already exists. Using existing contact.', p_email, p_phone_number;
END IF;
-- Check if company-contact relationship exists
SELECT EXISTS (
SELECT 1 FROM company_contacts
WHERE company_id = p_id AND contact_id = v_contact_id
) INTO v_company_contact_exists;
IF NOT v_company_contact_exists THEN
-- Link company and contact
INSERT INTO company_contacts (
id, company_id, contact_id, created_on_utc, created_by
) VALUES (
v_company_contact_id, p_id, v_contact_id, NOW(), p_created_by
);
END IF;
-- Check if user-company relationship exists for main user
SELECT EXISTS (
SELECT 1 FROM company_users
WHERE company_id = p_id AND user_id = p_user_id
) INTO v_user_company_exists;
IF NOT v_user_company_exists THEN
-- Link main user to company
INSERT INTO company_users (
id, company_id, user_id, effective_start_date,
effective_end_date, created_on_utc, created_by
) VALUES (
gen_random_uuid(), p_id, p_user_id, NOW(),
DATE '9999-12-31', NOW(), p_created_by
);
END IF;
-- Check if user-company relationship exists for default user
SELECT EXISTS (
SELECT 1 FROM company_users
WHERE company_id = p_id AND user_id = DEFAULT_USER_ID
) INTO v_default_user_company_exists;
IF NOT v_default_user_company_exists THEN
-- Link default user to company
INSERT INTO company_users (
id, company_id, user_id, effective_start_date,
effective_end_date, created_on_utc, created_by
) VALUES (
gen_random_uuid(), p_id, DEFAULT_USER_ID, NOW(),
DATE '9999-12-31', NOW(), p_created_by
);
END IF;
-- Initialize finance years if company was newly created
IF NOT v_company_exists THEN
CALL public.insert_company_finance_years(p_id);
CALL public.initialize_journal_voucher_header_ids(p_default_company_id, p_id);
END IF;
RAISE NOTICE 'Company initialization completed for % (ID: %)', p_name, p_id;
END;
$procedure$
-- Procedure: initialize_journal_voucher_header_ids
CREATE OR REPLACE PROCEDURE public.initialize_journal_voucher_header_ids(IN old_company_id uuid, IN new_company_id uuid)
LANGUAGE plpgsql
AS $procedure$
BEGIN
-- Insert new records for the new company, skipping duplicates
INSERT INTO journal_voucher_header_id (
company_id,
fin_year,
voucher_prefix,
voucher_length,
last_voucher_id
)
SELECT
new_company_id,
fin_year,
voucher_prefix,
voucher_length,
last_voucher_id
FROM journal_voucher_header_id AS j
WHERE company_id = old_company_id
AND NOT EXISTS (
SELECT 1
FROM journal_voucher_header_id
WHERE company_id = new_company_id
AND fin_year = j.fin_year
);
RAISE NOTICE 'Journal voucher header IDs initialized successfully for new company ID: %', new_company_id;
END;
$procedure$
-- Procedure: initialize_org_coa
CREATE OR REPLACE PROCEDURE public.initialize_org_coa(IN old_org_id uuid, IN new_org_id uuid, IN new_company_ids text, IN p_created_by uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
account_mapping RECORD;
old_organization_account RECORD; -- To store the old organization_accounts record
defualt_company_preference RECORD; -- to store default company preference record
v_company_id_array uuid[];
v_company_id uuid;
v_coa_exists boolean;
DEFAULT_COMPANY_ID CONSTANT uuid := '048000ba-e62c-474b-b5eb-fe629a4dc863';
BEGIN
SELECT EXISTS (
SELECT 1 FROM chart_of_accounts WHERE organization_id = new_org_id
) INTO v_coa_exists;
IF v_coa_exists THEN
RAISE NOTICE 'Chart of accounts already exists for organization %. Skipping initialization.', new_org_id;
RETURN;
END IF;
-- Parse the comma-separated company IDs into an array
v_company_id_array := string_to_array(new_company_ids, ',');
-- Create a temporary table for mapping old account IDs to new account IDs
CREATE TEMP TABLE temp_account_mapping (
old_id UUID,
new_id UUID
);
-- Populate the temporary mapping table with old IDs and their new UUIDs
INSERT INTO temp_account_mapping (old_id, new_id)
SELECT id, gen_random_uuid()
FROM chart_of_accounts
WHERE organization_id = old_org_id;
-- Insert new records into chart_of_accounts for the new organization
INSERT INTO chart_of_accounts (
id,
account_number,
name,
organization_id,
parent_account_id,
description,
account_type_id,
account_group_code,
second_group_code,
alternative_name,
is_ledger_total,
is_show_outs,
is_tds_tcs,
opening_balance,
current_balance,
created_by,
created_on_utc,
is_default_account
)
SELECT
tm.new_id, -- Use the new ID from the mapping table
coa.account_number,
coa.name,
new_org_id, -- Assign to the new organization
(SELECT tm2.new_id FROM temp_account_mapping tm2 WHERE tm2.old_id = coa.parent_account_id), -- Map the parent ID
coa.description,
coa.account_type_id,
coa.account_group_code,
coa.second_group_code,
coa.alternative_name,
coa.is_ledger_total,
coa.is_show_outs,
coa.is_tds_tcs,
0,
0,
p_created_by,
NOW(),
coa.is_default_account
FROM chart_of_accounts coa
INNER JOIN temp_account_mapping tm ON coa.id = tm.old_id;
-- Fetch the old organization_accounts record
SELECT * INTO old_organization_account
FROM organization_accounts
WHERE organization_id = old_org_id;
-- Insert a single entry into organization_accounts for the new organization
INSERT INTO organization_accounts (
id,
organization_id,
accounts_receivable_account_id,
accounts_payable_account_id,
sales_revenue_account_id,
cgst_receivable_account_id,
sgst_receivable_account_id,
igst_receivable_account_id,
cgst_payable_account_id,
sgst_payable_account_id,
igst_payable_account_id,
round_off_gain_account_id,
round_off_loss_account_id,
sales_tax_payable_account_id,
purchase_tax_receivable_account_id,
discounts_given_account_id,
discounts_received_account_id,
interest_income_account_id,
interest_expense_account_id,
depreciation_expense_account_id,
bad_debt_expense_account_id,
bank_charges_account_id,
foreign_exchange_gain_loss_account_id,
created_on_utc,
created_by,
cost_of_goods_sold_account_id,
inventory_account_id,
salary_expense_account_id,
salary_payable_account_id,
tds_receivable_account_id,
penalty_receivable_account_id,
tds_payable_account_id
)
VALUES (
nextval('organization_accounts_id_seq'), -- Generate a new ID for organization_accounts
new_org_id, -- Assign to the new organization
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.accounts_receivable_account_id),
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.accounts_payable_account_id),
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.sales_revenue_account_id),
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.cgst_receivable_account_id),
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.sgst_receivable_account_id),
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.igst_receivable_account_id),
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.cgst_payable_account_id),
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.sgst_payable_account_id),
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.igst_payable_account_id),
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.round_off_gain_account_id),
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.round_off_loss_account_id),
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.sales_tax_payable_account_id),
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.purchase_tax_receivable_account_id),
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.discounts_given_account_id),
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.discounts_received_account_id),
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.interest_income_account_id),
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.interest_expense_account_id),
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.depreciation_expense_account_id),
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.bad_debt_expense_account_id),
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.bank_charges_account_id),
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.foreign_exchange_gain_loss_account_id),
NOW(),
p_created_by,
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.cost_of_goods_sold_account_id),
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.inventory_account_id),
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.salary_expense_account_id),
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.salary_payable_account_id),
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.tds_receivable_account_id),
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.penalty_receivable_account_id),
(SELECT new_id FROM temp_account_mapping WHERE old_id = old_organization_account.tds_payable_account_id)
);
--fetch old company_preference data
SELECT * INTO defualt_company_preference
FROM public.company_preferences
WHERE company_id = DEFAULT_COMPANY_ID;
-- Loop over the company IDs to insert records into company_preferences
FOREACH v_company_id IN ARRAY v_company_id_array LOOP
INSERT INTO public.company_preferences (
id,
company_id,
default_sales_account_id,
default_purchase_account_id,
default_cash_account_id,
default_bank_account_id,
created_on_utc,
created_by
)
VALUES (
gen_random_uuid(),
v_company_id,
(SELECT new_id FROM temp_account_mapping WHERE old_id = defualt_company_preference.default_sales_account_id),
(SELECT new_id FROM temp_account_mapping WHERE old_id = defualt_company_preference.default_purchase_account_id),
(SELECT new_id FROM temp_account_mapping WHERE old_id = defualt_company_preference.default_cash_account_id),
(SELECT new_id FROM temp_account_mapping WHERE old_id = defualt_company_preference.default_bank_account_id),
NOW(),
p_created_by
);
END LOOP;
-- Drop the temporary mapping table
DROP TABLE temp_account_mapping;
UPDATE public.chart_of_accounts
set parent_account_id = '00000000-0000-0000-0000-000000000000'
where organization_id = new_org_id
And account_number in ('1000000','2000000','3000000','4000000','5000000');
-- Log theparent_account_id completion of the procedure
RAISE NOTICE 'Chart of accounts and organization accounts successfully copied from organization % to %.', old_org_id, new_org_id;
END;
$procedure$
-- Procedure: initilize_organization
CREATE OR REPLACE PROCEDURE public.initilize_organization(IN p_id uuid, IN p_name text, IN p_company_guids text, IN p_company_names text, IN p_user_id uuid, IN p_user_first_name text, IN p_user_last_name text, IN p_phone_number character varying, IN p_email character varying, IN p_gstin text, IN p_description text, IN p_tag_line text, IN p_short_name text, IN p_pan text, IN p_tan text, IN p_address_line1 text, IN p_address_line2 text, IN p_country_id uuid, IN p_state_id uuid, IN p_city_name text, IN p_zip_code text, IN p_created_by uuid, IN p_default_company_id uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_city_id uuid;
v_address_id uuid; -- Generate a unique address ID
v_user_id uuid; -- Generate a unique user ID
v_permission_ids uuid[];
v_company_names text[]; -- Array to hold parsed company names
v_company_guids uuid[]; -- Array to hold parsed company GUIDs
v_company_name text; -- Variable for iterating through company names
v_company_guid uuid; -- Variable for iterating through company GUIDs
v_user_company_id uuid;
-- Constants for default organization and company IDs
DEFAULT_ORGANIZATION_ID CONSTANT uuid := '68929d37-b647-4d7b-8c91-7dfe2396c93b';
--DEFAULT_COMPANY_ID CONSTANT uuid := '048000ba-e62c-474b-b5eb-fe629a4dc863';
ADMIN_PERMISSION CONSTANT text := 'Dhanman.Admin';
BEGIN
-- Parse company names and GUIDs into arrays
v_company_names := string_to_array(p_company_names, ',');
v_company_guids := string_to_array(p_company_guids, ',');
-- Get city_id
v_city_id := get_city_id(p_city_name, p_zip_code, p_state_id, p_created_by);
-- Get address_id
v_address_id := get_address_id(p_country_id, p_state_id , v_city_id , p_address_line1, p_zip_code, p_created_by, p_address_line2);
v_user_company_id := v_company_guids[1]; -- Pick the first company ID
-- Create and get user_id
v_user_id := create_user(p_user_id, v_user_company_id, p_email, p_phone_number, p_user_first_name, p_user_last_name, v_address_id, p_created_by);
RAISE NOTICE 'Generated User ID: %',v_user_company_id;
-- Print the generated address_id and fetched state_id and country_id
RAISE NOTICE 'Generated address_id: %, City ID: %, Country ID: %, User ID: %', v_address_id, v_city_id, p_country_id, v_user_id;
-- Check if organization exists by any unique identifier
IF NOT EXISTS (SELECT 1 FROM organizations WHERE id = p_id) THEN
-- Only create organization if no matching record exists
CALL public.create_organization(
p_id,
p_name,
p_description,
p_phone_number,
p_email,
p_address_line1,
p_gstin,
p_tag_line,
v_city_id,
p_short_name,
p_pan,
p_tan,
p_created_by
);
RAISE NOTICE 'Organization ID % successfully inserted.', p_id;
ELSE
RAISE NOTICE 'Organization with ID % already exists. Skipping organization creation.', p_id;
END IF;
-- Insert into organization_users table to link the generated user to the organization
CALL public.insert_organization_user(v_user_id, p_created_by, p_id);
-- Fetch the permission_id for 'Dhanman.Admin'
SELECT array_agg(id) INTO v_permission_ids
FROM permissions
WHERE name = ADMIN_PERMISSION;
CALL public.insert_user_permission(
v_user_id ,
p_id ,
v_permission_ids,
p_created_by
);
-- Assign all default users and default permissions as admin
CALL public.assign_default_users_to_organization(p_id, v_permission_ids, p_created_by);
-- Iterate through the company names and GUIDs
FOR i IN 1..array_length(v_company_names, 1) LOOP
v_company_name := v_company_names[i];
v_company_guid := v_company_guids[i];
-- Call initialize_company for each company
CALL public.initialize_company(
v_company_guid,
p_id,
v_company_name,
p_description,
p_gstin,
p_pan,
p_tan,
'INR',
p_short_name,
p_tag_line,
p_name,
p_phone_number,
p_email,
p_created_by,
v_user_id,
p_default_company_id
);
RAISE NOTICE 'Company % initialized with GUID %.', v_company_name, v_company_guid;
END LOOP;
-- Call initialize_org_coa to copy chart_of_accounts from the old organization to the new organization
CALL public.initialize_org_coa(
DEFAULT_ORGANIZATION_ID, -- Old organization ID
p_id, -- New organization ID
p_company_guids, -- New Company Id
p_created_by
);
-- Print a confirmation after the chart_of_accounts is copied
RAISE NOTICE 'Chart of accounts successfully copied from organization % to %.', DEFAULT_ORGANIZATION_ID, p_id;
END;
$procedure$
-- Procedure: insert_bank_statement_by_excel
CREATE OR REPLACE PROCEDURE public.insert_bank_statement_by_excel(IN p_company_id uuid, IN p_txn_date date, IN p_cheque_number text, IN p_description text, IN p_value_date date, IN p_branch_code text, IN p_debit_amount numeric, IN p_credit_amount numeric, IN p_balance numeric, IN p_bank_id integer, IN p_created_by uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
existing_id INT;
BEGIN
-- Check if a record with same txn_date and description already exists for the company and bank
SELECT id INTO existing_id
FROM public.bank_statements
WHERE company_id = p_company_id
AND bank_id = p_bank_id
AND txn_date = p_txn_date
AND description = p_description
AND is_deleted = false
LIMIT 1;
IF existing_id IS NOT NULL THEN
-- Update the existing record
UPDATE public.bank_statements
SET
cheque_number = p_cheque_number,
value_date = p_value_date,
branch_code = p_branch_code,
debit_amount = p_debit_amount,
credit_amount = p_credit_amount,
balance = p_balance,
modified_by = p_created_by,
modified_on_utc = (CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Kolkata')::timestamp
WHERE id = existing_id;
RAISE NOTICE 'Updated existing bank statement with ID: %', existing_id;
ELSE
-- Insert a new record
INSERT INTO public.bank_statements (
company_id,
txn_date,
cheque_number,
description,
value_date,
branch_code,
debit_amount,
credit_amount,
balance,
bank_id,
created_by,
created_on_utc,
is_deleted
) VALUES (
p_company_id,
p_txn_date,
p_cheque_number,
p_description,
p_value_date,
p_branch_code,
p_debit_amount,
p_credit_amount,
p_balance,
p_bank_id,
p_created_by,
(CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Kolkata')::timestamp,
false
);
RAISE NOTICE 'Inserted new bank statement';
END IF;
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION 'Bank Statement insert/update failed: %', SQLERRM;
END;
$procedure$
-- Procedure: insert_company_finance_years
CREATE OR REPLACE PROCEDURE public.insert_company_finance_years(IN p_company_id uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_finance_year_id INTEGER;
v_prev_finance_year_id INTEGER;
v_next_finance_year_id INTEGER;
BEGIN
-- Get the financial year ID based on the current date
SELECT id INTO v_finance_year_id
FROM finance_year
WHERE start_date <= NOW() AND end_date >= NOW();
-- Get the previous and next year IDs
SELECT id INTO v_prev_finance_year_id FROM finance_year WHERE id = v_finance_year_id - 1;
SELECT id INTO v_next_finance_year_id FROM finance_year WHERE id = v_finance_year_id + 1;
-- Insert for the current, previous, and next years (if not already inserted)
IF v_finance_year_id IS NOT NULL AND NOT EXISTS (
SELECT 1 FROM company_finance_year WHERE company_id = p_company_id AND finance_year_id = v_finance_year_id
) THEN
INSERT INTO company_finance_year (company_id, finance_year_id)
VALUES (p_company_id, v_finance_year_id);
END IF;
IF v_prev_finance_year_id IS NOT NULL AND NOT EXISTS (
SELECT 1 FROM company_finance_year WHERE company_id = p_company_id AND finance_year_id = v_prev_finance_year_id
) THEN
INSERT INTO company_finance_year (company_id, finance_year_id)
VALUES (p_company_id, v_prev_finance_year_id);
END IF;
IF v_next_finance_year_id IS NOT NULL AND NOT EXISTS (
SELECT 1 FROM company_finance_year WHERE company_id = p_company_id AND finance_year_id = v_next_finance_year_id
) THEN
INSERT INTO company_finance_year (company_id, finance_year_id)
VALUES (p_company_id, v_next_finance_year_id);
END IF;
RAISE NOTICE 'Data inserted for company_id: %', p_company_id;
END;
$procedure$
-- Procedure: insert_company_user
CREATE OR REPLACE PROCEDURE public.insert_company_user(IN p_user_id uuid, IN p_created_by uuid, IN p_company_id uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_existing_id uuid;
v_is_deleted boolean;
BEGIN
-- Check if user-company relationship exists (including soft-deleted)
SELECT id, is_deleted
INTO v_existing_id, v_is_deleted
FROM public.company_users
WHERE user_id = p_user_id
AND company_id = p_company_id
LIMIT 1;
-- If exists and active (not deleted), do nothing
IF FOUND AND v_is_deleted = false THEN
RAISE NOTICE 'User % already exists in company % (active), skipping insert.', p_user_id, p_company_id;
-- If exists but soft-deleted, reactivate the record
ELSIF FOUND AND v_is_deleted = true THEN
UPDATE public.company_users
SET is_deleted = false,
modified_on_utc = NOW(),
modified_by = p_created_by,
deleted_on_utc = NULL
WHERE id = v_existing_id;
RAISE NOTICE 'User % re-activated in company % (id=%)', p_user_id, p_company_id, v_existing_id;
-- If no record found, insert new
ELSE
INSERT INTO public.company_users (
id,
user_id,
company_id,
effective_start_date,
effective_end_date,
created_on_utc,
created_by,
is_deleted
) VALUES (
gen_random_uuid(),
p_user_id,
p_company_id,
NOW(),
DATE '9999-12-31',
NOW(),
p_created_by,
false
);
RAISE NOTICE 'Company-user relationship created for user % in company %', p_user_id, p_company_id;
END IF;
END;
$procedure$
-- Procedure: insert_into_manual_journal
CREATE OR REPLACE PROCEDURE public.insert_into_manual_journal(IN p_company_id uuid, IN p_journal_header_id uuid, IN p_date date, IN p_amount numeric, IN p_note text, IN p_description text, IN p_is_debit boolean, IN p_account_id uuid, IN p_created_by uuid, IN p_journal_details_data jsonb, IN p_transaction_header_data jsonb, IN journal_entries_data jsonb)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_voucher_number character varying;
v_voucher_detail JSONB;
v_transaction_record JSONB;
v_transaction_result RECORD;
v_transactions_data JSONB;
BEGIN
v_voucher_number := get_new_voucher_number(p_company_id, p_date);
RAISE NOTICE 'new v_voucher_number: %', v_voucher_number;
-- Insert into journal_voucher_headers
INSERT INTO public.journal_voucher_headers(
id,
company_id,
date,
amount,
note,
is_debit,
account_id,
voucher_number,
created_by,
created_on_utc,
is_deleted
)
VALUES (
p_journal_header_id,
p_company_id,
p_date,
p_amount,
p_note,
p_is_debit,
p_account_id,
v_voucher_number,
p_created_by,
CURRENT_TIMESTAMP,
FALSE
);
RAISE NOTICE 'Inserted into journal_voucher_headers: %', p_journal_header_id;
-- Insert into journal_voucher_details
FOR v_voucher_detail IN SELECT * FROM jsonb_array_elements(p_journal_details_data)
LOOP
INSERT INTO public.journal_voucher_details(
id,
journal_header_id,
credit_account_id,
debit_account_id,
amount,
tds_tcs,
narration,
created_by,
created_on_utc,
is_deleted
)
VALUES (
(v_voucher_detail->>'detail_id')::UUID,
p_journal_header_id,
(v_voucher_detail->>'credit_account_id')::UUID,
(v_voucher_detail->>'debit_account_id')::UUID,
(v_voucher_detail->>'amount')::NUMERIC,
v_voucher_detail->>'tds_tcs',
v_voucher_detail->>'narration',
p_created_by,
CURRENT_TIMESTAMP,
FALSE
);
RAISE NOTICE 'Inserted into journal_voucher_details';
END LOOP;
-- Prepare single transaction record matching insert_multiple_transactions_and_journal_entries input
v_transaction_record := jsonb_build_object(
'company_id', p_transaction_header_data->>'company_id',
'customer_id', p_transaction_header_data->>'customer_id',
'vendor_id', p_transaction_header_data->>'vendor_id',
'employee_id', p_transaction_header_data->>'employee_id',
'transaction_date', p_transaction_header_data->>'transaction_date',
'transaction_source_type', p_transaction_header_data->>'transaction_source_type',
'status_id', p_transaction_header_data->>'status_id',
'document_id', p_transaction_header_data->>'document_id',
'document_number', v_voucher_number,
'description', p_transaction_header_data->>'description',
'user_id', p_created_by,
'journal_entries', journal_entries_data
);
v_transactions_data := jsonb_build_array(v_transaction_record);
-- Call the batch function to insert into transaction_headers and journal_entries (and narration)
FOR v_transaction_result IN
SELECT * FROM public.insert_multiple_transactions_and_journal_entries(v_transactions_data)
LOOP
RAISE NOTICE 'Inserted transaction_id: %, document_id: %',
v_transaction_result.transaction_id, v_transaction_result.document_id;
-- You can use v_transaction_result.transaction_id if you need to reference it for other inserts
END LOOP;
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION 'Error occurred: %', SQLERRM;
END;
$procedure$
-- Procedure: insert_organization_company
CREATE OR REPLACE PROCEDURE public.insert_organization_company(IN p_organization_id uuid, IN p_organization_name text, IN p_description text, IN p_phone_number character varying, IN p_email character varying, IN p_address_line text, IN p_gstin text, IN p_tag_line text, IN p_city_id uuid, IN p_pin text, IN p_short_name text, IN p_pan text, IN p_tan text, IN p_outstanding_limit numeric, IN p_is_non_work boolean, IN p_interest_percentage numeric, IN p_created_by uuid, IN p_companies jsonb)
LANGUAGE plpgsql
AS $procedure$
DECLARE
company RECORD;
v_created_on_utc timestamp without time zone := CURRENT_TIMESTAMP;
BEGIN
-- Insert into the organizations table
CALL public.create_organization(
p_organization_id,
p_organization_name,
p_description,
p_phone_number,
p_email,
p_address_line,
p_gstin,
p_tag_line,
p_city_id,
p_pin,
p_short_name,
p_pan,
p_tan,
p_outstanding_limit,
p_is_non_work,
p_interest_percentage,
p_created_by,
v_created_on_utc
);
RAISE NOTICE 'Organization inserted with ID: %', p_organization_id;
-- Loop through the JSONB array of companies
FOR company IN
SELECT * FROM jsonb_to_recordset(p_companies) AS (
id uuid,
name text,
description text,
gstin text,
pan text,
tan text,
currency text,
short_name text,
tag_line text,
proprietor_name text,
outstanding_limit numeric,
is_non_work boolean,
interest_percentage numeric,
created_by uuid
)
LOOP
-- Insert into the companies table
CALL public.create_company(
company.id,
p_organization_id,
company.name,
company.description,
company.gstin,
company.pan,
company.tan,
company.currency,
company.short_name,
company.tag_line,
company.proprietor_name,
company.outstanding_limit,
company.is_non_work,
company.interest_percentage,
company.created_by,
v_created_on_utc
);
RAISE NOTICE 'Company inserted with ID: %', company.id;
END LOOP;
RAISE NOTICE 'Transaction completed successfully';
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE 'An error occurred: %, transaction rolled back', SQLERRM;
ROLLBACK;
END;
$procedure$
-- Procedure: insert_organization_user
CREATE OR REPLACE PROCEDURE public.insert_organization_user(IN p_user_id uuid, IN p_created_by uuid, IN p_organization_id uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_existing_id uuid;
v_is_deleted boolean;
BEGIN
-- Check if user-organization relationship exists (including soft-deleted)
SELECT id, is_deleted
INTO v_existing_id, v_is_deleted
FROM public.organization_users
WHERE user_id = p_user_id
AND organization_id = p_organization_id
LIMIT 1;
-- If exists and active (not deleted), do nothing
-- If exists and active (not deleted), do nothing
-- If exists and active (not deleted), do nothing
-- If exists and active (not deleted), do nothing
-- If exists and active (not deleted), do nothing
IF FOUND AND v_is_deleted = false THEN
RAISE NOTICE 'User % already exists in organization % (active), skipping insert.', p_user_id, p_organization_id;
-- If exists but soft-deleted, reactivate the record
ELSIF FOUND AND v_is_deleted = true THEN
UPDATE public.organization_users
SET is_deleted = false,
modified_on_utc = NOW(),
modified_by = p_created_by,
deleted_on_utc = NULL
WHERE id = v_existing_id;
RAISE NOTICE 'User % re-activated in organization % (id=%)', p_user_id, p_organization_id, v_existing_id;
-- If no record found, insert new
ELSE
INSERT INTO public.organization_users(
id,
user_id,
organization_id,
effective_start_date,
effective_end_date,
created_on_utc,
created_by,
is_deleted
)
VALUES (
gen_random_uuid(),
p_user_id,
p_organization_id,
NOW(),
DATE '9999-12-31',
NOW(),
p_created_by,
false
);
RAISE NOTICE 'Organization-user relationship created for user % in organization %', p_user_id, p_organization_id;
END IF;
END;
$procedure$
-- Procedure: insert_user_role
CREATE OR REPLACE PROCEDURE public.insert_user_role(IN p_user_id uuid, IN p_role_id integer, IN p_created_by uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_existing_id INT;
v_is_deleted BOOLEAN;
BEGIN
-- Look for existing role assignment for user
SELECT id, is_deleted
INTO v_existing_id, v_is_deleted
FROM public.user_roles
WHERE user_id = p_user_id
AND role_id = p_role_id
LIMIT 1;
-- Case 1: Record exists and active -> do nothing
IF FOUND AND v_is_deleted = false THEN
RAISE NOTICE 'Role % already active for user %, skipping insert', p_role_id, p_user_id;
-- Case 2: Record exists but soft-deleted -> reactivate
ELSIF FOUND AND v_is_deleted = true THEN
UPDATE public.user_roles
SET is_deleted = false,
modified_on_utc = NOW(),
modified_by = p_created_by,
deleted_on_utc = NULL
WHERE id = v_existing_id;
RAISE NOTICE 'Role % re-activated for user % (id=%)', p_role_id, p_user_id, v_existing_id;
-- Case 3: No record - insert new
ELSE
INSERT INTO public.user_roles (
user_id,
role_id,
created_on_utc,
created_by,
is_deleted,
start_date,
end_date
)
VALUES (
p_user_id,
p_role_id,
NOW(),
p_created_by,
false,
NOW(), -- Fixed start_date as now()
'9999-12-31 00:00:00'::timestamp -- Fixed end_date as default far future
);
RAISE NOTICE 'Role % assigned to user % (new)', p_role_id, p_user_id;
END IF;
END;
$procedure$
-- Procedure: insert_user_roles
CREATE OR REPLACE PROCEDURE public.insert_user_roles(IN p_user_id uuid, IN p_role_ids integer[], IN p_organization_id uuid, IN p_created_by uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_role_id integer;
v_existing_id INT;
v_is_deleted BOOLEAN;
BEGIN
FOREACH v_role_id IN ARRAY p_role_ids
LOOP
-- Find matching user-role-org record
SELECT id, is_deleted
INTO v_existing_id, v_is_deleted
FROM public.user_roles
WHERE user_id = p_user_id
AND role_id = v_role_id
AND organization_id = p_organization_id
LIMIT 1;
-- If active record exists -> do nothing
IF FOUND AND v_is_deleted = false THEN
RAISE NOTICE 'Role % already active for user % in organization %, skipping insert', v_role_id, p_user_id, p_organization_id;
-- If found and soft-deleted -> reactivate
ELSIF FOUND AND v_is_deleted = true THEN
UPDATE public.user_roles
SET is_deleted = false,
modified_on_utc = NOW(),
modified_by = p_created_by,
deleted_on_utc = NULL
WHERE id = v_existing_id;
RAISE NOTICE 'Role % re-activated for user % in organization % (id=%)', v_role_id, p_user_id, p_organization_id, v_existing_id;
-- No record exists, insert new
ELSE
INSERT INTO public.user_roles (
user_id,
role_id,
organization_id,
created_on_utc,
created_by,
is_deleted,
start_date,
end_date
)
VALUES (
p_user_id,
v_role_id,
p_organization_id,
NOW(),
p_created_by,
false,
NOW(),
'9999-12-31 00:00:00'::timestamp
);
RAISE NOTICE 'Role % assigned to user % in organization % (new)', v_role_id, p_user_id, p_organization_id;
END IF;
END LOOP;
END;
$procedure$
-- Procedure: migrate_opening_balances_for_organization
CREATE OR REPLACE PROCEDURE public.migrate_opening_balances_for_organization(IN p_organization_id uuid, IN p_finyear_id integer, IN p_created_by uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_status_id integer := 1;
v_transaction_source_type_id integer := 18; -- as per your instruction
v_description_template_id integer := 1;
v_entry_source_id integer := 1;
v_opening_equity uuid;
v_sundry_debtors uuid;
v_sundry_creditors uuid;
v_salary_payable uuid;
rec_company RECORD;
ob_rec RECORD;
v_transaction_id bigint;
v_document_number text;
v_transaction_date timestamp;
v_entry_type text;
v_entity_type text;
v_entity_id uuid;
v_main_account uuid;
v_should_insert boolean;
v_name text;
v_customer_id uuid;
v_vendor_id uuid;
v_employee_id uuid;
v_account_id uuid;
v_year_label text;
BEGIN
-- COA account IDs
SELECT id INTO v_opening_equity
FROM public.chart_of_accounts
WHERE account_type_id = (SELECT account_type_id FROM public.chart_of_accounts WHERE name = 'Opening Balance Equity' LIMIT 1)
AND name = 'Opening Balance Equity'
LIMIT 1;
IF v_opening_equity IS NULL THEN
RAISE EXCEPTION 'Chart of Accounts "Opening Balance Equity" not found!';
END IF;
SELECT accounts_receivable_account_id, accounts_payable_account_id, salary_payable_account_id INTO v_sundry_debtors, v_sundry_creditors, v_salary_payable
FROM public.organization_accounts
WHERE organization_id = p_organization_id;
IF v_sundry_debtors IS NULL THEN
RAISE EXCEPTION 'Chart of Accounts "Sundry Debtors" not found!';
END IF;
IF v_sundry_creditors IS NULL THEN
RAISE EXCEPTION 'Chart of Accounts "Sundry Creditors" not found!';
END IF;
IF v_salary_payable IS NULL THEN
RAISE EXCEPTION 'Chart of Accounts "Salary Payable" not found!';
END IF;
-- Set transaction_date and year label from the finance year
SELECT start_date, "year" INTO v_transaction_date, v_year_label FROM public.finance_year WHERE id = p_finyear_id;
IF v_transaction_date IS NULL OR v_year_label IS NULL THEN
RAISE EXCEPTION 'Finance year with id % not found or year column is NULL!', p_finyear_id;
END IF;
-- Create a temporary table to track inserted COA accounts
CREATE TEMP TABLE IF NOT EXISTS temp_inserted_coa_accounts (account_id uuid PRIMARY KEY);
-- For each company in the organization, with the selected finance year
FOR rec_company IN
SELECT c.id AS company_id
FROM public.companies c
JOIN public.company_finance_year cfy ON c.id = cfy.company_id
WHERE c.organization_id = p_organization_id
AND cfy.finance_year_id = p_finyear_id
LOOP
-- For each opening balance for this organization and year
FOR ob_rec IN
SELECT *
FROM public.opening_balances
WHERE organization_id = p_organization_id
AND finyear_id = p_finyear_id
LOOP
v_should_insert := false;
v_name := null;
v_customer_id := null;
v_vendor_id := null;
v_employee_id := null;
v_account_id := null;
-- Determine main account and entity type, and check mapping
IF ob_rec.customer_id IS NOT NULL THEN
v_entity_type := 'Customer';
v_entity_id := ob_rec.customer_id;
v_main_account := v_sundry_debtors;
-- Check customers.company_id = rec_company.company_id
SELECT c.name INTO v_name
FROM public.customers c
WHERE c.id = ob_rec.customer_id AND c.company_id = rec_company.company_id;
IF v_name IS NOT NULL THEN
v_should_insert := true;
v_document_number := v_name; -- Only the name, not 'OPENBAL-' || v_name
v_customer_id := ob_rec.customer_id;
END IF;
ELSIF ob_rec.vendor_id IS NOT NULL THEN
v_entity_type := 'Vendor';
v_entity_id := ob_rec.vendor_id;
v_main_account := v_sundry_creditors;
SELECT v.name INTO v_name
FROM public.vendors v
WHERE v.id = ob_rec.vendor_id AND v.company_id = rec_company.company_id;
IF v_name IS NOT NULL THEN
v_should_insert := true;
v_document_number := v_name; -- Only the name, not 'OPENBAL-' || v_name
v_vendor_id := ob_rec.vendor_id;
END IF;
ELSIF ob_rec.employee_id IS NOT NULL THEN
v_entity_type := 'Employee';
v_entity_id := ob_rec.employee_id;
v_main_account := v_salary_payable;
SELECT e.first_name INTO v_name
FROM public.employees e
WHERE e.id = ob_rec.employee_id AND e.company_id = rec_company.company_id;
IF v_name IS NOT NULL THEN
v_should_insert := true;
v_document_number := v_name; -- Only the name, not 'OPENBAL-' || v_name
v_employee_id := ob_rec.employee_id;
END IF;
ELSE
-- COA-only
v_entity_type := 'COA';
v_entity_id := ob_rec.account_id;
v_main_account := ob_rec.account_id;
-- Get COA name
SELECT c.name INTO v_name
FROM public.chart_of_accounts c
WHERE c.id = ob_rec.account_id;
IF v_name IS NOT NULL THEN
v_document_number := v_name; -- Only the name, not 'OPENBAL-' || v_name
v_account_id := ob_rec.account_id;
-- insert only if this coa id is not already present
IF NOT EXISTS (SELECT 1 FROM temp_inserted_coa_accounts WHERE account_id = v_account_id) THEN
v_should_insert := true;
INSERT INTO temp_inserted_coa_accounts(account_id) VALUES (v_account_id);
END IF;
END IF;
END IF;
IF v_should_insert THEN
-- Insert into transaction_headers
INSERT INTO public.transaction_headers (
company_id, transaction_date, status_id, document_number, description, created_on_utc, created_by, transaction_source_type,
customer_id, vendor_id, employee_id, document_id
)
VALUES (
rec_company.company_id,
v_transaction_date,
v_status_id,
v_document_number,
'Opening Balance for ' || v_year_label, -- Only the year label from finance_year
v_transaction_date,
p_created_by,
v_transaction_source_type_id,
v_customer_id,
v_vendor_id,
v_employee_id,
v_account_id
)
RETURNING id INTO v_transaction_id;
-- Insert into journal_entries (double-entry)
INSERT INTO public.journal_entries (
transaction_id, transaction_date, amount, entry_type, description_template_id, dynamic_data, entry_source_id, account_id
) VALUES (
v_transaction_id, v_transaction_date, ob_rec.balance, ob_rec.entry_type, v_description_template_id, '{}'::jsonb, v_entry_source_id, v_main_account
);
-- Opening Balance Equity
IF ob_rec.entry_type = 'D' THEN
v_entry_type := 'C';
ELSE
v_entry_type := 'D';
END IF;
INSERT INTO public.journal_entries (
transaction_id, transaction_date, amount, entry_type, description_template_id, dynamic_data, entry_source_id, account_id
) VALUES (
v_transaction_id, v_transaction_date, ob_rec.balance, v_entry_type, v_description_template_id, '{}'::jsonb, v_entry_source_id, v_opening_equity
);
END IF;
END LOOP;
END LOOP;
DROP TABLE IF EXISTS temp_inserted_coa_accounts;
END;
$procedure$
-- Procedure: update_bank_transfers
CREATE OR REPLACE PROCEDURE public.update_bank_transfers(IN p_company_id uuid, IN p_bank_transfer_id uuid, IN p_transfer_date timestamp without time zone, IN p_amount numeric, IN p_description text, IN p_source_account_id uuid, IN p_target_account_id uuid, IN p_mode_of_payment integer, IN p_reference text, IN p_modified_by uuid, IN p_transaction_header_data jsonb, IN p_journal_entries_data jsonb)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_now_utc timestamp := (now() at time zone 'utc');
v_header_id bigint;
v_txn_ts timestamp;
v_txn_date date;
v_desc text;
v_status integer;
v_source_type integer := 12; -- default BankTransfer source type
v_credit_count integer;
v_debit_count integer;
v_credit_id bigint;
v_debit_id bigint;
v_credit_rec jsonb;
v_debit_rec jsonb;
BEGIN
-- 0) Validate the bank transfer exists & active
IF NOT EXISTS (
SELECT 1
FROM public.bank_transfers bt
WHERE bt.id = p_bank_transfer_id
AND bt.company_id = p_company_id
AND bt.is_deleted = FALSE
) THEN
RAISE EXCEPTION 'Bank transfer % not found for company %', p_bank_transfer_id, p_company_id
USING ERRCODE = 'P0002';
END IF;
-- 1) Update bank_transfers (note: description keeps existing if empty passed)
UPDATE public.bank_transfers
SET transfer_date = p_transfer_date::timestamp,
amount = p_amount,
source_account_id = p_source_account_id,
target_account_id = p_target_account_id,
mode_of_payment = p_mode_of_payment,
reference = p_reference,
description = COALESCE(NULLIF(p_description, ''), description),
modified_by = p_modified_by,
modified_on_utc = v_now_utc
WHERE id = p_bank_transfer_id
AND company_id = p_company_id
AND is_deleted = FALSE;
-- 2) Resolve header data from JSON (with safe fallbacks)
v_txn_ts := COALESCE(
NULLIF(p_transaction_header_data ->> 'transaction_date','')::timestamp,
p_transfer_date::timestamp
);
v_txn_date := v_txn_ts::date;
v_desc := COALESCE(
NULLIF(p_transaction_header_data ->> 'description',''),
(SELECT description FROM public.bank_transfers WHERE id = p_bank_transfer_id)
);
v_status := NULLIF(p_transaction_header_data ->> 'status_id','')::int;
v_source_type := COALESCE(NULLIF(p_transaction_header_data ->> 'transaction_source_type','')::int, v_source_type);
-- 3) Locate transaction header
SELECT th.id
INTO v_header_id
FROM public.transaction_headers th
WHERE th.company_id = p_company_id
AND th.document_id = p_bank_transfer_id
AND th.transaction_source_type = v_source_type
AND th.is_deleted = FALSE
LIMIT 1;
IF v_header_id IS NULL THEN
RAISE EXCEPTION 'Transaction header not found for bank transfer %', p_bank_transfer_id
USING ERRCODE = 'P0002';
END IF;
-- 4) Update header (partition key is transaction_date (timestamp))
UPDATE public.transaction_headers
SET transaction_date = v_txn_ts,
description = v_desc,
status_id = COALESCE(v_status, status_id),
modified_by = p_modified_by,
modified_on_utc = v_now_utc
WHERE id = v_header_id;
-- 5) Journal shape check (exactly 1C + 1D among active entries)
SELECT COUNT(*) FILTER (WHERE entry_type = 'C'),
COUNT(*) FILTER (WHERE entry_type = 'D')
INTO v_credit_count, v_debit_count
FROM public.journal_entries je
WHERE je.transaction_id = v_header_id
AND je.is_deleted = FALSE;
IF v_credit_count <> 1 OR v_debit_count <> 1 THEN
-- Soft-delete all active and re-insert from JSON payload
UPDATE public.journal_entries
SET is_deleted = TRUE,
deleted_on_utc = v_now_utc
WHERE transaction_id = v_header_id
AND is_deleted = FALSE;
-- Insert CREDIT entries
FOR v_credit_rec IN
SELECT elem
FROM jsonb_array_elements(p_journal_entries_data) AS elem
WHERE UPPER(elem ->> 'entry_type') = 'C'
LOOP
INSERT INTO public.journal_entries
(transaction_id, transaction_date, amount, entry_type,
description_template_id, dynamic_data, entry_source_id, account_id, is_deleted)
VALUES
(v_header_id,
COALESCE(NULLIF(v_credit_rec ->> 'transaction_date','')::date, v_txn_date),
(v_credit_rec ->> 'amount')::numeric,
'C',
COALESCE(NULLIF(v_credit_rec ->> 'description_template_id','')::int, 1),
COALESCE((v_credit_rec ->> 'dynamic_data')::jsonb, '{}'::jsonb),
COALESCE(NULLIF(v_credit_rec ->> 'entry_source_id','')::int, v_source_type),
(v_credit_rec ->> 'account_id')::uuid,
FALSE);
END LOOP;
-- Insert DEBIT entries
FOR v_debit_rec IN
SELECT elem
FROM jsonb_array_elements(p_journal_entries_data) AS elem
WHERE UPPER(elem ->> 'entry_type') = 'D'
LOOP
INSERT INTO public.journal_entries
(transaction_id, transaction_date, amount, entry_type,
description_template_id, dynamic_data, entry_source_id, account_id, is_deleted)
VALUES
(v_header_id,
COALESCE(NULLIF(v_debit_rec ->> 'transaction_date','')::date, v_txn_date),
(v_debit_rec ->> 'amount')::numeric,
'D',
COALESCE(NULLIF(v_debit_rec ->> 'description_template_id','')::int, 1),
COALESCE((v_debit_rec ->> 'dynamic_data')::jsonb, '{}'::jsonb),
COALESCE(NULLIF(v_debit_rec ->> 'entry_source_id','')::int, v_source_type),
(v_debit_rec ->> 'account_id')::uuid,
FALSE);
END LOOP;
ELSE
-- Update-in-place path
SELECT je.id INTO v_credit_id
FROM public.journal_entries je
WHERE je.transaction_id = v_header_id
AND je.entry_type = 'C'
AND je.is_deleted = FALSE
LIMIT 1;
SELECT je.id INTO v_debit_id
FROM public.journal_entries je
WHERE je.transaction_id = v_header_id
AND je.entry_type = 'D'
AND je.is_deleted = FALSE
LIMIT 1;
SELECT elem INTO v_credit_rec
FROM jsonb_array_elements(p_journal_entries_data) AS elem
WHERE UPPER(elem ->> 'entry_type') = 'C'
LIMIT 1;
SELECT elem INTO v_debit_rec
FROM jsonb_array_elements(p_journal_entries_data) AS elem
WHERE UPPER(elem ->> 'entry_type') = 'D'
LIMIT 1;
-- Fallbacks if payload is missing either side
IF v_credit_rec IS NULL THEN
v_credit_rec := jsonb_build_object(
'account_id', p_source_account_id::text,
'transaction_date', v_txn_date::text,
'amount', p_amount::text,
'entry_type', 'C',
'entry_source_id', v_source_type,
'description_template_id', 1,
'dynamic_data', '{"type":"bank_transfer"}'
);
END IF;
IF v_debit_rec IS NULL THEN
v_debit_rec := jsonb_build_object(
'account_id', p_target_account_id::text,
'transaction_date', v_txn_date::text,
'amount', p_amount::text,
'entry_type', 'D',
'entry_source_id', v_source_type,
'description_template_id', 1,
'dynamic_data', '{"type":"bank_transfer"}'
);
END IF;
UPDATE public.journal_entries
SET account_id = (v_credit_rec ->> 'account_id')::uuid,
transaction_date = COALESCE(NULLIF(v_credit_rec ->> 'transaction_date','')::date, v_txn_date),
amount = (v_credit_rec ->> 'amount')::numeric,
entry_source_id = COALESCE(NULLIF(v_credit_rec ->> 'entry_source_id','')::int, v_source_type),
description_template_id = COALESCE(NULLIF(v_credit_rec ->> 'description_template_id','')::int, 1),
dynamic_data = COALESCE((v_credit_rec ->> 'dynamic_data')::jsonb, '{}'::jsonb)
WHERE id = v_credit_id;
UPDATE public.journal_entries
SET account_id = (v_debit_rec ->> 'account_id')::uuid,
transaction_date = COALESCE(NULLIF(v_debit_rec ->> 'transaction_date','')::date, v_txn_date),
amount = (v_debit_rec ->> 'amount')::numeric,
entry_source_id = COALESCE(NULLIF(v_debit_rec ->> 'entry_source_id','')::int, v_source_type),
description_template_id = COALESCE(NULLIF(v_debit_rec ->> 'description_template_id','')::int, 1),
dynamic_data = COALESCE((v_debit_rec ->> 'dynamic_data')::jsonb, '{}'::jsonb)
WHERE id = v_debit_id;
END IF;
END;
$procedure$
-- Procedure: upsert_user_permissions
CREATE OR REPLACE PROCEDURE public.upsert_user_permissions(IN p_user_id uuid, IN p_permission_ids uuid[], IN p_organization_id uuid, IN p_created_by uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_permission_id uuid;
v_existing_id int;
v_existing_deleted bool;
v_inserted_id int;
BEGIN
FOR v_permission_id IN SELECT unnest(p_permission_ids) LOOP
SELECT id, is_deleted INTO v_existing_id, v_existing_deleted
FROM public.user_permissions
WHERE user_id = p_user_id
AND permission_id = v_permission_id
AND organization_id = p_organization_id
FOR UPDATE;
IF v_existing_id IS NOT NULL AND NOT v_existing_deleted THEN
CONTINUE;
ELSIF v_existing_id IS NOT NULL AND v_existing_deleted THEN
UPDATE public.user_permissions
SET is_deleted = false,
deleted_on_utc = NULL,
modified_by = p_created_by,
modified_on_utc = NOW()
WHERE id = v_existing_id;
ELSE
INSERT INTO public.user_permissions (
user_id,
permission_id,
organization_id,
created_on_utc,
created_by,
is_deleted
) VALUES (
p_user_id,
v_permission_id,
p_organization_id,
NOW(),
p_created_by,
false
) RETURNING id INTO v_inserted_id;
END IF;
END LOOP;
END;
$procedure$
-- Procedure: purge_common_organization_data
CREATE OR REPLACE PROCEDURE public.purge_common_organization_data(IN p_organization_ids uuid[] DEFAULT NULL::uuid[])
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_orgs uuid[];
v_company_ids uuid[];
v_user_ids uuid[];
v_contact_ids uuid[];
v_bank_account_ids uuid[];
v_bank_ids int[]; -- integer PK
v_address_ids uuid[];
BEGIN
-- Determine which organizations to clean
IF p_organization_ids IS NULL OR array_length(p_organization_ids, 1) IS NULL THEN
SELECT array_agg(id)
INTO v_orgs
FROM organizations
WHERE created_on_utc > '2025-05-23'
AND created_on_utc < (NOW() - interval '24 hours');
ELSE
v_orgs := p_organization_ids;
END IF;
IF v_orgs IS NULL OR array_length(v_orgs, 1) IS NULL THEN
RAISE NOTICE 'No organizations found for cleanup.';
RETURN;
END IF;
-- Log organizations to be cleaned up
RAISE NOTICE 'Organizations to be deleted: %', v_orgs;
-- Find all company_ids under these orgs
SELECT array_agg(id) INTO v_company_ids FROM companies WHERE organization_id = ANY(v_orgs);
-- Log companies to be cleaned up
RAISE NOTICE 'Companies to be deleted: %', v_company_ids;
-- Find all users under these companies
SELECT array_agg(id) INTO v_user_ids FROM users WHERE company_id = ANY(v_company_ids)
AND created_on_utc > '2025-05-23'
AND created_on_utc < (NOW() - interval '24 hours');
-- Find all contact_ids for company_contacts
SELECT array_agg(contact_id) INTO v_contact_ids FROM company_contacts WHERE company_id = ANY(v_company_ids);
-- Find all bank_account_ids for company_bank_accounts
SELECT array_agg(bank_account_id) INTO v_bank_account_ids FROM company_bank_accounts WHERE company_id = ANY(v_company_ids);
-- Find all bank_ids for bank_accounts
SELECT array_agg(bank_id) INTO v_bank_ids FROM bank_accounts WHERE id = ANY(v_bank_account_ids);
-- Find all addresses for warehouses (address_id)
SELECT array_agg(address_id) INTO v_address_ids FROM warehouses WHERE company_id = ANY(v_company_ids);
-------------------------------------------------------------------
-- CHILD TABLES: Delete by IDs collected above, in safe order
-------------------------------------------------------------------
-- 1. user_roles (must go before users)
DELETE FROM user_roles WHERE user_id = ANY(v_user_ids);
-- 2. company_contacts (before contacts)
DELETE FROM company_contacts WHERE company_id = ANY(v_company_ids);
-- 3. contacts (after company_contacts)
DELETE FROM contacts WHERE id = ANY(v_contact_ids);
-- 4. company_bank_accounts (before bank_accounts)
DELETE FROM company_bank_accounts WHERE company_id = ANY(v_company_ids);
-- 5. bank_accounts (before banks)
DELETE FROM bank_accounts WHERE id = ANY(v_bank_account_ids);
-- 6. banks (if no more bank_accounts reference them, safe to delete)
DELETE FROM banks WHERE id = ANY(v_bank_ids);
-- 7. warehouses (before addresses)
DELETE FROM warehouses WHERE company_id = ANY(v_company_ids);
-- 8. addresses (only addresses used by warehouses)
DELETE FROM addresses WHERE id = ANY(v_address_ids);
-------------------------------------------------------------------
-- Your original cleanup (core business, already comprehensive)
-------------------------------------------------------------------
-- Vendors & related
DELETE FROM journal_entries
WHERE (transaction_id, transaction_date) IN (
SELECT th.id, th.transaction_date
FROM transaction_headers th
JOIN vendors v ON th.vendor_id = v.id
WHERE v.company_id = ANY(v_company_ids)
);
DELETE FROM opening_balances
WHERE vendor_id IN (
SELECT id FROM vendors WHERE company_id = ANY(v_company_ids)
);
DELETE FROM transaction_headers
WHERE vendor_id IN (
SELECT id FROM vendors WHERE company_id = ANY(v_company_ids)
);
DELETE FROM vendors
WHERE company_id = ANY(v_company_ids);
-- Customers & related
DELETE FROM journal_entries
WHERE (transaction_id, transaction_date) IN (
SELECT th.id, th.transaction_date
FROM transaction_headers th
JOIN customers c ON th.customer_id = c.id
WHERE c.company_id = ANY(v_company_ids)
);
DELETE FROM opening_balances
WHERE customer_id IN (
SELECT id FROM customers WHERE company_id = ANY(v_company_ids)
);
DELETE FROM transaction_headers
WHERE customer_id IN (
SELECT id FROM customers WHERE company_id = ANY(v_company_ids)
);
DELETE FROM customers
WHERE company_id = ANY(v_company_ids);
-- Journal vouchers
DELETE FROM journal_voucher_details
WHERE journal_header_id IN (
SELECT id FROM journal_voucher_headers
WHERE company_id = ANY(v_company_ids)
);
DELETE FROM journal_voucher_headers
WHERE company_id = ANY(v_company_ids);
-- Transactions directly linked to company
DELETE FROM journal_entries
WHERE (transaction_id, transaction_date) IN (
SELECT id, transaction_date
FROM transaction_headers
WHERE company_id = ANY(v_company_ids)
);
DELETE FROM transaction_headers
WHERE company_id = ANY(v_company_ids);
-- Company-related cleanup
DELETE FROM general_ledgers WHERE company_id = ANY(v_company_ids);
DELETE FROM company_finance_year WHERE company_id = ANY(v_company_ids);
DELETE FROM company_preferences WHERE company_id = ANY(v_company_ids);
DELETE FROM company_upis WHERE company_id = ANY(v_company_ids);
DELETE FROM company_users WHERE company_id = ANY(v_company_ids);
-- Chart of accounts (org level)
DELETE FROM journal_entries
WHERE account_id IN (
SELECT id FROM chart_of_accounts WHERE organization_id = ANY(v_orgs)
);
DELETE FROM journal_voucher_header_id WHERE company_id = ANY(v_company_ids);
-- Org-level children
DELETE FROM organization_accounts WHERE organization_id = ANY(v_orgs);
DELETE FROM chart_of_accounts WHERE organization_id = ANY(v_orgs);
DELETE FROM email_templates WHERE organization_id = ANY(v_orgs);
DELETE FROM user_permissions WHERE organization_id = ANY(v_orgs);
DELETE FROM organization_users WHERE organization_id = ANY(v_orgs);
DELETE FROM account_groups WHERE organization_id = ANY(v_orgs);
-- Finally, companies and organizations
DELETE FROM companies WHERE id = ANY(v_company_ids);
DELETE FROM organizations WHERE id = ANY(v_orgs);
DELETE FROM users
WHERE company_id = ANY(v_company_ids)
AND created_on_utc > '2025-05-23'
AND created_on_utc < (NOW() - interval '24 hours');
RAISE NOTICE 'Deleted all common data for organizations: %', v_orgs;
END;
$procedure$
-- Procedure: insert_user_permission
CREATE OR REPLACE PROCEDURE public.insert_user_permission(IN p_user_id uuid, IN p_organization_id uuid, IN p_permission_ids uuid[], IN p_created_by uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_permission_id uuid;
v_existing_id INT;
v_is_deleted BOOLEAN;
BEGIN
RAISE NOTICE '▶️ START insert_user_permission for user_id=%, org_id=%, created_by=%, permissions=%',
p_user_id, p_organization_id, p_created_by, p_permission_ids;
IF p_user_id IS NULL OR p_organization_id IS NULL OR p_created_by IS NULL THEN
RAISE EXCEPTION '❌ One of the input parameters is NULL (user_id=%, org_id=%, created_by=%)',
p_user_id, p_organization_id, p_created_by;
END IF;
FOREACH v_permission_id IN ARRAY p_permission_ids
LOOP
RAISE NOTICE '🔍 Checking permission_id=% for user_id=% in org_id=%',
v_permission_id, p_user_id, p_organization_id;
-- Check if the record exists
SELECT id, is_deleted
INTO v_existing_id, v_is_deleted
FROM public.user_permissions
WHERE user_id = p_user_id
AND organization_id = p_organization_id
AND permission_id = v_permission_id
LIMIT 1;
IF FOUND THEN
RAISE NOTICE '✅ Found existing record with id=%, is_deleted=%', v_existing_id, v_is_deleted;
ELSE
RAISE NOTICE '❌ No existing record found for this combination';
END IF;
IF FOUND AND v_is_deleted = false THEN
RAISE NOTICE '⚠️ Permission % already active for user %, skipping insert',
v_permission_id, p_user_id;
ELSIF FOUND AND v_is_deleted = true THEN
UPDATE public.user_permissions
SET is_deleted = false,
modified_on_utc = NOW(),
modified_by = p_created_by,
deleted_on_utc = NULL
WHERE id = v_existing_id;
RAISE NOTICE '♻️ Permission % re-activated for user % (id=%)',
v_permission_id, p_user_id, v_existing_id;
ELSE
RAISE NOTICE '✅ Inserting Permission % assigned to user % (new) org: %', v_permission_id, p_user_id, p_organization_id;
INSERT INTO public.user_permissions (
user_id,
organization_id,
permission_id,
created_on_utc,
created_by,
is_deleted
)
VALUES (
p_user_id,
p_organization_id,
v_permission_id,
NOW(),
p_created_by,
false
);
END IF;
END LOOP;
RAISE NOTICE '✅ Completed insert_user_permission for user_id=%', p_user_id;
END;
$procedure$
-- Procedure: insert_multiple_bank_statements_by_excel
CREATE OR REPLACE PROCEDURE public.insert_multiple_bank_statements_by_excel(IN p_statements jsonb)
LANGUAGE plpgsql
AS $procedure$
DECLARE
stmt RECORD;
v_created_by UUID;
BEGIN
-- Get 'System' user or fallback
SELECT id INTO v_created_by
FROM public.users
WHERE first_name = 'System'
LIMIT 1;
IF v_created_by IS NULL THEN
RAISE NOTICE '⚠️ No System user found. Using NULL created_by.';
END IF;
-- Loop through input JSON
FOR stmt IN
SELECT * FROM jsonb_to_recordset(p_statements) AS (
company_id UUID,
txn_date TIMESTAMP,
cheque_number TEXT,
description TEXT,
value_date TIMESTAMP,
branch_code TEXT,
debit_amount NUMERIC,
credit_amount NUMERIC,
balance NUMERIC,
bank_id UUID,
created_by UUID
)
LOOP
BEGIN
-- Check for existing record using composite natural key
IF NOT EXISTS (
SELECT 1
FROM public.bank_statements bs
WHERE bs.company_id = stmt.company_id
AND bs.bank_id = stmt.bank_id
AND bs.is_deleted = FALSE
AND (
(stmt.txn_date::time <> '00:00:00'::time AND bs.txn_date = stmt.txn_date)
OR
(stmt.txn_date::time = '00:00:00'::time AND bs.txn_date::date = stmt.txn_date::date)
)
AND COALESCE(bs.debit_amount, 0) = COALESCE(stmt.debit_amount, 0)
AND COALESCE(bs.credit_amount, 0) = COALESCE(stmt.credit_amount, 0)
AND COALESCE(bs.cheque_number, '') = COALESCE(stmt.cheque_number, '')
AND COALESCE(bs.description, '') = COALESCE(stmt.description, '') -- Ensure description is checked
) THEN
-- Insert new record if not exists
INSERT INTO public.bank_statements (
company_id,
txn_date,
cheque_number,
description,
value_date,
branch_code,
debit_amount,
credit_amount,
balance,
bank_id,
created_by,
created_on_utc,
is_deleted,
has_reconciled
)
VALUES (
stmt.company_id,
stmt.txn_date,
stmt.cheque_number,
stmt.description,
stmt.value_date,
stmt.branch_code,
stmt.debit_amount,
stmt.credit_amount,
stmt.balance,
stmt.bank_id,
COALESCE(stmt.created_by, v_created_by),
(CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Kolkata')::timestamp,
FALSE,
FALSE
);
RAISE NOTICE '✅ Inserted: %, %', stmt.txn_date, stmt.description;
ELSE
-- Update existing record if found
UPDATE public.bank_statements
SET
cheque_number = stmt.cheque_number,
value_date = stmt.value_date,
branch_code = stmt.branch_code,
balance = stmt.balance,
modified_by = COALESCE(stmt.created_by, v_created_by),
modified_on_utc = (CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Kolkata')::timestamp
WHERE company_id = stmt.company_id
AND bank_id = stmt.bank_id
AND is_deleted = FALSE
AND (
(stmt.txn_date::time <> '00:00:00'::time AND txn_date = stmt.txn_date)
OR
(stmt.txn_date::time = '00:00:00'::time AND txn_date::date = stmt.txn_date::date)
)
AND COALESCE(debit_amount, 0) = COALESCE(stmt.debit_amount, 0)
AND COALESCE(credit_amount, 0) = COALESCE(stmt.credit_amount, 0)
AND COALESCE(cheque_number, '') = COALESCE(stmt.cheque_number, '')
AND COALESCE(description, '') = COALESCE(stmt.description, ''); -- Ensure description is checked
RAISE NOTICE '🔄 Updated: %, %', stmt.txn_date, stmt.description;
END IF;
EXCEPTION
WHEN OTHERS THEN
RAISE EXCEPTION '❌ Error for txn_date=%, desc=%, Error: %',
stmt.txn_date, stmt.description, SQLERRM;
END;
END LOOP;
RAISE NOTICE '🎉 All bank statements processed successfully.';
END;
$procedure$
-- Procedure: upsert_user_permissions
CREATE OR REPLACE PROCEDURE public.upsert_user_permissions(IN p_user_id uuid, IN p_permission_ids uuid[], IN p_role_ids integer[], IN p_organization_id uuid, IN p_created_by uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_permission_id uuid;
v_role_id int;
v_existing_id int;
v_existing_deleted bool;
v_existing_role_id int;
BEGIN
-- 0️⃣ Soft-delete permissions NOT in the new list
UPDATE public.user_permissions
SET is_deleted = TRUE,
deleted_on_utc = NOW(),
modified_by = p_created_by,
modified_on_utc = NOW()
WHERE user_id = p_user_id
AND organization_id = p_organization_id
AND (p_permission_ids IS NULL OR permission_id <> ALL(p_permission_ids))
AND is_deleted = FALSE;
-- 1️⃣ Upsert (reactivate or insert) listed permissions
FOR v_permission_id IN SELECT unnest(p_permission_ids) LOOP
SELECT id, is_deleted
INTO v_existing_id, v_existing_deleted
FROM public.user_permissions
WHERE user_id = p_user_id
AND permission_id = v_permission_id
AND organization_id = p_organization_id
FOR UPDATE;
IF v_existing_id IS NOT NULL AND NOT v_existing_deleted THEN
CONTINUE;
ELSIF v_existing_id IS NOT NULL AND v_existing_deleted THEN
UPDATE public.user_permissions
SET is_deleted = FALSE,
deleted_on_utc = NULL,
modified_by = p_created_by,
modified_on_utc = NOW()
WHERE id = v_existing_id;
ELSE
INSERT INTO public.user_permissions (
user_id, permission_id, organization_id,
created_on_utc, created_by, is_deleted
) VALUES (
p_user_id, v_permission_id, p_organization_id,
NOW(), p_created_by, false
);
END IF;
END LOOP;
-- 2️⃣ Soft-delete roles NOT in the new list
UPDATE public.user_roles
SET is_deleted = TRUE,
deleted_on_utc = NOW(),
modified_by = p_created_by,
modified_on_utc = NOW()
WHERE user_id = p_user_id
AND organization_id = p_organization_id
AND (p_role_ids IS NULL OR role_id <> ALL(p_role_ids))
AND is_deleted = FALSE;
-- 3️⃣ Upsert (reactivate or insert) listed roles
FOR v_role_id IN SELECT unnest(p_role_ids) LOOP
SELECT id
INTO v_existing_role_id
FROM public.user_roles
WHERE user_id = p_user_id
AND role_id = v_role_id
AND organization_id = p_organization_id;
IF v_existing_role_id IS NULL THEN
INSERT INTO public.user_roles (
user_id, role_id, organization_id,
created_on_utc, created_by, start_date, is_deleted
) VALUES (
p_user_id, v_role_id, p_organization_id,
NOW(), p_created_by, NOW(), FALSE
);
ELSE
UPDATE public.user_roles
SET is_deleted = FALSE,
deleted_on_utc = NULL,
modified_by = p_created_by,
modified_on_utc = NOW()
WHERE id = v_existing_role_id
AND is_deleted = TRUE;
END IF;
END LOOP;
END;
$procedure$
-- Procedure: upsert_user_permission_template
CREATE OR REPLACE PROCEDURE public.upsert_user_permission_template(IN p_user_id uuid, IN p_permission_template_ids integer[], IN p_created_by uuid, IN p_organization_id uuid DEFAULT NULL::uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_permission_id uuid;
v_existing_up_id int;
v_existing_up_deleted boolean;
v_inserted_up_id int;
v_org_user_id uuid;
BEGIN
-- nothing to do if no templates provided
IF p_permission_template_ids IS NULL OR array_length(p_permission_template_ids, 1) IS NULL THEN
RETURN;
END IF;
----------------------------------------------------------------
-- 1) Ensure organization_users row exists for this user + org
-- (Do this before touching user_permissions)
----------------------------------------------------------------
SELECT id
INTO v_org_user_id
FROM public.organization_users
WHERE user_id = p_user_id
AND organization_id IS NOT DISTINCT FROM p_organization_id
FOR UPDATE;
IF v_org_user_id IS NULL THEN
INSERT INTO public.organization_users (
id,
user_id,
effective_start_date,
effective_end_date,
created_on_utc,
is_deleted,
created_by,
organization_id
) VALUES (
gen_random_uuid(), -- requires pgcrypto (or replace with uuid_generate_v4())
p_user_id,
NOW(), -- effective_start_date
NOW() + INTERVAL '1 year', -- effective_end_date
NOW(), -- created_on_utc
false, -- is_deleted
p_created_by,
COALESCE(p_organization_id, '00000000-0000-0000-0000-000000000000'::uuid)
)
RETURNING id INTO v_org_user_id;
END IF;
----------------------------------------------------------------
-- 2) Upsert user_permissions for permission_ids from templates
----------------------------------------------------------------
FOR v_permission_id IN
SELECT DISTINCT ptm.permission_id
FROM public.permission_template_mappings ptm
WHERE ptm.permission_template_id = ANY (p_permission_template_ids)
LOOP
-- Look for an existing user_permissions row for this user, permission and organization.
SELECT id, is_deleted
INTO v_existing_up_id, v_existing_up_deleted
FROM public.user_permissions
WHERE user_id = p_user_id
AND permission_id = v_permission_id
AND organization_id IS NOT DISTINCT FROM p_organization_id
FOR UPDATE;
-- If it exists and is active, nothing to do.
IF v_existing_up_id IS NOT NULL AND NOT v_existing_up_deleted THEN
CONTINUE;
-- If it exists and is marked deleted, reactivate it.
ELSIF v_existing_up_id IS NOT NULL AND v_existing_up_deleted THEN
UPDATE public.user_permissions
SET is_deleted = false,
deleted_on_utc = NULL,
modified_by = p_created_by,
modified_on_utc = NOW()
WHERE id = v_existing_up_id;
-- Otherwise insert a new row.
ELSE
INSERT INTO public.user_permissions (
user_id,
permission_id,
organization_id,
created_on_utc,
created_by,
is_deleted
) VALUES (
p_user_id,
v_permission_id,
p_organization_id,
NOW(),
p_created_by,
false
) RETURNING id INTO v_inserted_up_id;
END IF;
END LOOP;
END;
$procedure$
-- Procedure: insert_into_bank_transfers
CREATE OR REPLACE PROCEDURE public.insert_into_bank_transfers(IN p_company_id uuid, IN p_bank_transfer_id uuid, IN p_transfer_date date, IN p_amount numeric, IN p_description text, IN p_source_account_id uuid, IN p_target_account_id uuid, IN p_mode_of_payment integer, IN p_reference text, IN p_created_by uuid)
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_transfer_number text;
v_transaction_id bigint;
v_narration_id bigint;
BEGIN
-- 1️⃣ Generate transfer number
v_transfer_number :=
get_new_transfer_number(p_company_id, p_transfer_date);
-- 2️⃣ Insert bank transfer
INSERT INTO public.bank_transfers (
id,
company_id,
source_account_id,
target_account_id,
amount,
transfer_date,
mode_of_payment,
reference,
description,
is_bulk,
created_by,
created_on_utc,
is_deleted,
transfer_number
)
VALUES (
p_bank_transfer_id,
p_company_id,
p_source_account_id,
p_target_account_id,
p_amount,
p_transfer_date,
COALESCE(p_mode_of_payment, 0),
NULLIF(p_reference, ''),
p_description,
FALSE,
p_created_by,
CURRENT_TIMESTAMP,
FALSE,
v_transfer_number
);
-- 3️⃣ Insert transaction header
INSERT INTO public.transaction_headers (
company_id,
transaction_date,
transaction_source_type,
status_id,
document_id,
document_number,
description,
created_by,
created_on_utc
)
VALUES (
p_company_id,
p_transfer_date,
19, -- BANK_TRANSFER
1,
p_bank_transfer_id,
v_transfer_number,
p_description,
p_created_by,
CURRENT_TIMESTAMP
)
RETURNING id INTO v_transaction_id;
-- 4️⃣ Create narration ONCE
INSERT INTO public.journal_narrations (
transaction_date,
description_template_id,
dynamic_data,
created_on_utc,
created_by
)
VALUES (
p_transfer_date,
49, -- BANK_TRANSFER narration template
jsonb_build_object(
'type', 'bank_transfer',
'reference', p_reference
)::text,
CURRENT_TIMESTAMP,
p_created_by
)
RETURNING id INTO v_narration_id;
-- 5️⃣ Credit source account
INSERT INTO public.journal_entries (
transaction_id,
transaction_date,
account_id,
amount,
entry_type,
entry_source_id,
journal_narration_id
)
VALUES (
v_transaction_id,
p_transfer_date,
p_source_account_id,
p_amount,
'C',
2,
v_narration_id
);
-- 6️⃣ Debit target account
INSERT INTO public.journal_entries (
transaction_id,
transaction_date,
account_id,
amount,
entry_type,
entry_source_id,
journal_narration_id
)
VALUES (
v_transaction_id,
p_transfer_date,
p_target_account_id,
p_amount,
'D',
2,
v_narration_id
);
END;
$procedure$
-- Procedure: update_existing_user_role_permissions
CREATE OR REPLACE PROCEDURE public.update_existing_user_role_permissions(IN p_user_id uuid, IN p_created_by uuid, IN p_organization_id uuid DEFAULT NULL::uuid, IN p_company_id uuid DEFAULT NULL::uuid, IN p_permission_ids uuid[] DEFAULT NULL::uuid[], IN p_role_ids integer[] DEFAULT NULL::integer[])
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_role_permission_ids uuid[];
v_final_permission_ids uuid[];
BEGIN
RAISE NOTICE 'Procedure started for user_id=%', p_user_id;
-- Insert user to organization
IF p_organization_id IS NOT NULL THEN
RAISE NOTICE 'Inserting user into organization: %', p_organization_id;
CALL public.insert_organization_user(
p_user_id,
p_created_by,
p_organization_id
);
END IF;
-- Insert user to company
IF p_company_id IS NOT NULL THEN
RAISE NOTICE 'Inserting user into company: %', p_company_id;
CALL public.insert_company_user(
p_user_id,
p_created_by,
p_company_id
);
END IF;
/* ---------------------------------------------
1. Fetch permission_ids from role_permissions
--------------------------------------------- */
IF p_role_ids IS NOT NULL THEN
RAISE NOTICE 'Fetching permissions for roles: %', p_role_ids;
SELECT ARRAY_AGG(DISTINCT rp.permission_id)
INTO v_role_permission_ids
FROM public.role_permissions rp
WHERE rp.role_id = ANY (p_role_ids)
AND rp.is_deleted = false;
RAISE NOTICE 'Permissions from roles: %', v_role_permission_ids;
END IF;
/* ---------------------------------------------
2. Merge direct permissions + role permissions
--------------------------------------------- */
v_final_permission_ids :=
ARRAY(
SELECT DISTINCT unnest(
COALESCE(p_permission_ids, '{}') ||
COALESCE(v_role_permission_ids, '{}')
)
);
RAISE NOTICE 'Final permission list: %', v_final_permission_ids;
/* ---------------------------------------------
3. Insert permissions to user
--------------------------------------------- */
IF v_final_permission_ids IS NOT NULL
AND array_length(v_final_permission_ids, 1) > 0
AND p_organization_id IS NOT NULL THEN
RAISE NOTICE 'Inserting permissions for user into organization %', p_organization_id;
CALL public.insert_user_permission(
p_user_id,
p_organization_id,
v_final_permission_ids,
p_created_by
);
ELSE
RAISE NOTICE 'No permissions inserted (empty list or organization missing)';
END IF;
/* ---------------------------------------------
4. Insert user roles
--------------------------------------------- */
IF p_role_ids IS NOT NULL THEN
RAISE NOTICE 'Assigning roles to user: %', p_role_ids;
CALL public.insert_user_roles(
p_user_id,
p_role_ids,
p_organization_id,
p_created_by
);
END IF;
RAISE NOTICE 'Procedure completed successfully for user_id=%', p_user_id;
END;
$procedure$
-- Procedure: update_existing_user_role_permissions
CREATE OR REPLACE PROCEDURE public.update_existing_user_role_permissions(IN p_user_id uuid, IN p_created_by uuid, IN p_organization_id uuid DEFAULT NULL::uuid, IN p_company_id uuid DEFAULT NULL::uuid, IN p_role_ids integer[] DEFAULT NULL::integer[])
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_role_permission_ids uuid[];
BEGIN
RAISE NOTICE 'UPDATE procedure started for user_id=%', p_user_id;
/* ---------------------------------------------
Ensure user-organization & company mapping
--------------------------------------------- */
IF p_organization_id IS NOT NULL THEN
RAISE NOTICE 'Ensuring user exists in organization: %', p_organization_id;
CALL public.insert_organization_user(
p_user_id,
p_created_by,
p_organization_id
);
END IF;
IF p_company_id IS NOT NULL THEN
RAISE NOTICE 'Ensuring user exists in company: %', p_company_id;
CALL public.insert_company_user(
p_user_id,
p_created_by,
p_company_id
);
END IF;
/* ---------------------------------------------
1. Fetch permissions ONLY from roles
--------------------------------------------- */
IF p_role_ids IS NOT NULL THEN
RAISE NOTICE 'Roles received for update: %', p_role_ids;
SELECT ARRAY_AGG(DISTINCT rp.permission_id)
INTO v_role_permission_ids
FROM public.role_permissions rp
WHERE rp.role_id = ANY (p_role_ids)
AND rp.is_deleted = false;
RAISE NOTICE 'Permissions derived from roles: %', v_role_permission_ids;
ELSE
RAISE NOTICE 'No roles provided, skipping permission derivation';
END IF;
/* ---------------------------------------------
2. Update user permissions (role-based only)
--------------------------------------------- */
IF v_role_permission_ids IS NOT NULL
AND array_length(v_role_permission_ids, 1) > 0
AND p_organization_id IS NOT NULL THEN
RAISE NOTICE 'Updating user permissions using role-based permissions';
CALL public.insert_user_permission(
p_user_id,
p_organization_id,
v_role_permission_ids,
p_created_by
);
ELSE
RAISE NOTICE 'No role-based permissions to apply';
END IF;
/* ---------------------------------------------
3. Update user roles
--------------------------------------------- */
IF p_role_ids IS NOT NULL THEN
RAISE NOTICE 'Updating user roles: %', p_role_ids;
CALL public.insert_user_roles(
p_user_id,
p_role_ids,
p_organization_id,
p_created_by
);
END IF;
RAISE NOTICE 'UPDATE procedure completed successfully for user_id=%', p_user_id;
END;
$procedure$
-- Procedure: update_existing_users_role_permissions
CREATE OR REPLACE PROCEDURE public.update_existing_users_role_permissions(IN p_user_ids uuid[], IN p_created_by uuid, IN p_organization_id uuid DEFAULT NULL::uuid, IN p_company_id uuid DEFAULT NULL::uuid, IN p_role_ids integer[] DEFAULT NULL::integer[])
LANGUAGE plpgsql
AS $procedure$
DECLARE
v_user_id uuid;
v_role_permission_ids uuid[];
BEGIN
RAISE NOTICE 'UPDATE procedure started for users=%', p_user_ids;
/* ---------------------------------------------
1. Fetch permissions ONLY from roles (once)
--------------------------------------------- */
IF p_role_ids IS NOT NULL THEN
RAISE NOTICE 'Roles received: %', p_role_ids;
SELECT ARRAY_AGG(DISTINCT rp.permission_id)
INTO v_role_permission_ids
FROM public.role_permissions rp
WHERE rp.role_id = ANY (p_role_ids)
AND rp.is_deleted = false;
RAISE NOTICE 'Permissions derived from roles: %', v_role_permission_ids;
ELSE
RAISE NOTICE 'No roles provided, skipping permission derivation';
END IF;
/* ---------------------------------------------
2. Loop through each user
--------------------------------------------- */
FOREACH v_user_id IN ARRAY p_user_ids
LOOP
RAISE NOTICE 'Processing user_id=%', v_user_id;
-- Ensure user-organization mapping
IF p_organization_id IS NOT NULL THEN
CALL public.insert_organization_user(
v_user_id,
p_created_by,
p_organization_id
);
END IF;
-- Ensure user-company mapping
IF p_company_id IS NOT NULL THEN
CALL public.insert_company_user(
v_user_id,
p_created_by,
p_company_id
);
END IF;
-- Assign role-based permissions
IF v_role_permission_ids IS NOT NULL
AND array_length(v_role_permission_ids, 1) > 0
AND p_organization_id IS NOT NULL THEN
CALL public.insert_user_permission(
v_user_id,
p_organization_id,
v_role_permission_ids,
p_created_by
);
END IF;
-- Assign roles
IF p_role_ids IS NOT NULL THEN
CALL public.insert_user_roles(
v_user_id,
p_role_ids,
p_organization_id,
p_created_by
);
END IF;
RAISE NOTICE 'Completed user_id=%', v_user_id;
END LOOP;
RAISE NOTICE 'UPDATE procedure completed for all users';
END;
$procedure$
-- Procedure: add_existing_user_role_permissions
CREATE OR REPLACE PROCEDURE public.add_existing_user_role_permissions(IN p_user_id uuid, IN p_created_by uuid, IN p_organization_id uuid DEFAULT NULL::uuid, IN p_company_id uuid DEFAULT NULL::uuid, IN p_permission_ids uuid[] DEFAULT NULL::uuid[], IN p_role_ids integer[] DEFAULT NULL::integer[])
LANGUAGE plpgsql
AS $procedure$
BEGIN
-- Insert user to organization if organization_id is provided
IF p_organization_id IS NOT NULL THEN
CALL public.insert_organization_user(p_user_id, p_created_by, p_organization_id);
END IF;
-- Insert user to company if company_id is provided
IF p_company_id IS NOT NULL THEN
CALL public.insert_company_user(p_user_id, p_created_by, p_company_id);
END IF;
-- Insert permissions if both permission_ids array and organization_id are provided
IF p_permission_ids IS NOT NULL AND p_organization_id IS NOT NULL THEN
CALL public.insert_user_permission(p_user_id, p_organization_id, p_permission_ids, p_created_by);
END IF;
-- Insert roles if role_ids array is provided
IF p_role_ids IS NOT NULL THEN
CALL public.insert_user_roles(p_user_id, p_role_ids, p_organization_id, p_created_by);
END IF;
END;
$procedure$
-- View: vw_permission_hierarchy
WITH RECURSIVE permission_tree AS (
SELECT p.id,
p.name,
p.parent_permission_id,
p.description,
1 AS level,
p.id AS root_permission_id
FROM permissions p
UNION ALL
SELECT c.id,
c.name,
c.parent_permission_id,
c.description,
(pt.level + 1) AS level,
pt.root_permission_id
FROM (permissions c
JOIN permission_tree pt ON ((c.parent_permission_id = pt.id)))
)
SELECT root_permission_id,
id AS permission_id,
name AS permission_name,
parent_permission_id,
description,
level
FROM permission_tree;
-- View: vw_organization_summary
SELECT DISTINCT o.id AS organization_id,
o.name AS organization_name,
c.id AS company_id,
c.name AS company_name,
u.id AS user_id,
concat(u.first_name, ' ', u.last_name) AS user_full_name,
u.email AS user_email,
u.phone_number AS user_phone,
'Company User'::text AS user_role_source,
o.created_on_utc AS organization_created_on
FROM (((organizations o
JOIN companies c ON ((c.organization_id = o.id)))
JOIN company_users cu ON ((cu.company_id = c.id)))
JOIN users u ON ((u.id = cu.user_id)))
UNION
SELECT DISTINCT o.id AS organization_id,
o.name AS organization_name,
NULL::uuid AS company_id,
NULL::text AS company_name,
u.id AS user_id,
concat(u.first_name, ' ', u.last_name) AS user_full_name,
u.email AS user_email,
u.phone_number AS user_phone,
'Organization User'::text AS user_role_source,
o.created_on_utc AS organization_created_on
FROM ((organizations o
JOIN organization_users ou ON ((ou.organization_id = o.id)))
JOIN users u ON ((u.id = ou.user_id)));
-- View: transaction_sums_by_party
SELECT th.customer_id,
th.vendor_id,
th.employee_id,
c.name AS customer_name,
v.name AS vendor_name,
sum(
CASE
WHEN ((je.entry_type = 'C'::bpchar) AND (coa.account_type_id = ANY (ARRAY[4, 14, 15, 19, 20]))) THEN je.amount
ELSE (0)::numeric
END) AS total_invoiced,
sum(
CASE
WHEN ((je.entry_type = 'D'::bpchar) AND (coa.account_type_id = ANY (ARRAY[5, 16, 17, 18, 21, 22]))) THEN je.amount
ELSE (0)::numeric
END) AS total_billed,
sum(
CASE
WHEN ((je.entry_type = 'C'::bpchar) AND (coa.account_type_id = ANY (ARRAY[8, 9]))) THEN je.amount
ELSE (0)::numeric
END) AS total_received,
sum(
CASE
WHEN ((je.entry_type = 'D'::bpchar) AND (coa.account_type_id = ANY (ARRAY[8, 9]))) THEN je.amount
ELSE (0)::numeric
END) AS total_paid
FROM ((((transaction_headers th
LEFT JOIN customers c ON ((th.customer_id = c.id)))
LEFT JOIN vendors v ON ((th.vendor_id = v.id)))
JOIN journal_entries je ON ((je.transaction_id = th.id)))
JOIN chart_of_accounts coa ON ((je.account_id = coa.id)))
WHERE ((th.is_deleted = false) AND (je.is_deleted = false) AND (th.company_id = '9891a03a-d80a-430e-ab33-c419f99cffa0'::uuid))
GROUP BY th.customer_id, th.vendor_id, th.employee_id, c.name, v.name
ORDER BY COALESCE(c.name, v.name);
-- View: user_permission_view
SELECT ou.user_id,
(((u.first_name)::text || ' '::text) || (u.last_name)::text) AS user_name,
ou.organization_id,
o.name AS organization_name,
up.permission_id,
p.name AS permission_name,
p.description AS permission_description
FROM ((((organization_users ou
JOIN user_permissions up ON (((ou.user_id = up.user_id) AND (ou.organization_id = up.organization_id))))
JOIN permissions p ON ((up.permission_id = p.id)))
JOIN users u ON ((ou.user_id = u.id)))
JOIN organizations o ON ((ou.organization_id = o.id)))
WHERE ((ou.is_deleted = false) AND (up.is_deleted = false) AND (p.is_deleted = false) AND (u.is_deleted = false) AND (o.is_deleted = false));
-- View: vw_user_permissions_details
SELECT up.id AS user_permission_id,
u.id AS user_id,
u.first_name AS user_name,
p.id AS permission_id,
p.name AS permission_name,
o.id AS organization_id,
o.name AS organization_name,
o.short_name AS organization_short_name,
o.phone_number AS organization_phone_number,
o.email AS organization_email,
o.gstin AS organization_gstin,
up.created_on_utc,
up.created_by,
up.modified_on_utc,
up.modified_by,
up.is_deleted,
p.deleted_on_utc AS permission_deleted_on_utc,
up.deleted_on_utc AS user_permission_deleted_on_utc
FROM (((user_permissions up
JOIN permissions p ON ((up.permission_id = p.id)))
JOIN users u ON ((up.user_id = u.id)))
JOIN organizations o ON ((up.organization_id = o.id)))
WHERE ((up.is_deleted = false) AND (p.is_deleted = false) AND (o.is_deleted = false));
-- View: transaction_party_view
SELECT th.id AS transaction_id,
th.company_id,
th.transaction_date,
th.document_number,
th.description,
th.customer_id,
c.name AS customer_name,
th.vendor_id,
v.name AS vendor_name,
th.employee_id,
je.entry_type,
coa.account_type_id,
coa.name AS account_name,
je.amount,
CASE
WHEN ((je.entry_type = 'C'::bpchar) AND (coa.account_type_id = ANY (ARRAY[4, 14, 15, 19, 20]))) THEN je.amount
ELSE (0)::numeric
END AS invoiced,
CASE
WHEN ((je.entry_type = 'C'::bpchar) AND (coa.account_type_id = ANY (ARRAY[8, 9]))) THEN je.amount
ELSE (0)::numeric
END AS received,
CASE
WHEN ((je.entry_type = 'D'::bpchar) AND (coa.account_type_id = ANY (ARRAY[5, 16, 17, 18, 21, 22]))) THEN je.amount
ELSE (0)::numeric
END AS billed,
CASE
WHEN ((je.entry_type = 'D'::bpchar) AND (coa.account_type_id = ANY (ARRAY[8, 9]))) THEN je.amount
ELSE (0)::numeric
END AS paid
FROM ((((transaction_headers th
LEFT JOIN customers c ON ((th.customer_id = c.id)))
LEFT JOIN vendors v ON ((th.vendor_id = v.id)))
JOIN journal_entries je ON ((je.transaction_id = th.id)))
JOIN chart_of_accounts coa ON ((je.account_id = coa.id)))
WHERE ((th.is_deleted = false) AND (je.is_deleted = false));
-- View: view_permissions
SELECT id,
name,
parent_permission_id,
tree_name,
description
FROM get_permissions() get_permissions(id, name, parent_permission_id, tree_name, description);
-- Trigger: trg_user_permissions_notify
CREATE TRIGGER trg_user_permissions_notify AFTER INSERT OR DELETE OR UPDATE ON user_permissions FOR EACH ROW EXECUTE FUNCTION notify_user_permission_change()